1.1.1 • Published 6 months ago
@qyu/signal-core v1.1.1
@qyu/signal-core
Definition and implementation of signals + some utility functions
Signal definition
Package defines three types of signals:
- Event Signal: does not hold any value, but allows adding and removing subsciber callbacks
 - Output Signal: extends Event Signal, but also holds value, wich is readonly
 - Normal Signal: extends Output Signal, but also allows sending messages
 
type Signal_Sub = VoidFunction
type Signal_SubConfig = {
    // will ignore batching
    instant?: boolean
}
interface ESignal {
    // remove subscriber from signal
    rmsub(sub: Signal_Sub): void
    // add subscriber to signal
    addsub(sub: Signal_Sub, config?: Signal_SubConfig): void
}
interface OSignal<O> extends ESignal {
    // get signal value
    output(): O
}
interface Signal<I, O> extends OSignal {
    // input a message
    input(message: I): void
}Normally your root signal will be created with signal_new_value
const root = signal_new_value(0)
const root_sub = () => {
    console.log(root.output())
}
root.addsub(root_sub)
// prints 10
root.input(10)
// prints 20
root.input(20)
root.rmsub(root_sub)Another common scenario is using esignal_new_manual
const [esignal, esignal_emit] = esignal_new_manual()
esignal.addsub(() => {
    console.log("Event")
})
// prints Event
esignal_emit()Package also provides some utility functions to transform signals
Fallback for signal value
// signal of number | null
const root_1 = signal_new_value(Math.random() > 0.5 ? null : 0)
// signal of number
const signal_fallback = signal_new_fallback(root_1, () => 0)Merge Signals
const root_1 = signal_new_value(0)
const root_2 = signal_new_value(0)
const root_3 = signal_new_value(0)
// signal of [number, number, number, null, undefined]
const signal_merged = signal_new_merge([root_1, root_2, root_3, null, undefined] as const)
// signal of [number, "HELLOWORLD"]
const signal_merged_pick = signal_new_merge_pick([{ pick: true, value: root_1 }, { pick: false, value: "HELLO WORLD" }] as const)Merge Map of Signals
const root_1 = signal_new_value(0)
const root_2 = signal_new_value(0)
const root_3 = signal_new_value(0)
// signal of { root_1: number, root_2: number, root_3: number, root_4: null, root_5: undefined }
const signal_mergedmap = signal_new_mergemap({
    root_1,
    root_2,
    root_3,
    root_4: null,
    root_5: undefined 
})
// signal of { root_1: number, root_2: "HELLOWORLD" }
const signal_mergedmap = signal_new_mergemap({
    root_1: {
        pick: true,
        value: root_1 
    },
    root_2: {
        pick: false,
        value: "HELLOWORLD" 
    } 
})Transform Signal
const root_1 = signal_new_value(0)
const root_2 = signal_new_value(0)
const root_3 = signal_new_value(0)
// transform output of signal
// also has variants of pipei to transform input and pipe to transform both
// signal of undefined or signal of number
const signal_opipe = signal_new_pipeo(root_1, d => {
    if (d > 50) {
        return root_2
    }
    if (d < 0) {
        return root_3
    }
    return undefined
})Flat nested Signals
const root_1 = signal_new_value(0)
const root_2 = signal_new_value(0)
const signal_opipe = signal_new_pipeo(root_1, d => {
    if (d > 100) {
        return undefined
    }
    return root_2
})
const signal_opipe_pick = signal_new_pipeo(root_1, d => {
    if (d > 100) {
        return {
            pick: false,
            value: "HELLOWORLD"
        }
    }
    return {
        pick: true,
        value: root_2
    }
})
// signal of number or undefined, will update based on current target, input will be redirected to current target
const signal_flat = signal_new_flat(signal_opipe)
// this one will be signal of number | "HELLOWORLD"
const signal_flat_pick = signal_new_flat_pick(signal_opipe_pick)Prevent unnecessary updates
const root_1 = signal_new_value(0)
// do not update if value is the same as it was
const signal_memoed = signal_new_memo(root_1, Object.is)Osignal and ESignal also have their own transformers
const root_1 = signal_new_value(0)
const root_2 = signal_new_value(0)
const root_3 = signal_new_value(0)
// osignal and esignal also have those function variants where it makes sense
const osignal_pipe = osignal_new_pipe(root_1, d => d * 2)In some cases you would need signal_listen utility function
const root = signal_new_value(0)
const unlisten = signal_listen({
    target: root,
    // optional
    config: {
        // false by default
        emit: true
    },
    listener: target => {
        console.log("effect", target.output())
        return () => {
            console.log("cleanup", target.output())
        }
    }
})You may need to create custom signals for specific needs
Package exports some API for doing so
const emitter = emitter_new()
// will attach with provided function when there are subs listening
// will deattach when there is none
const attachment = attachment_new_lazy(
    () => {
        console.log("Attachment activation")
    },
    () => {
        console.log("Attachment cleanup")
    }
)
interface Emitter {
    emit(): void
    submap(): Map<Signal_Sub, Signal_SubConfig>
    rmsub(sub: Signal_Sub): void
    addsub(sub: Signal_Sub, config?: Signal_SubConfig): void
}
interface Attachment {
    emit(): void
    active(): boolean
    rmsub(sub: Signal_Sub): void
    addsub(sub: Signal_Sub, config?: Signal_SubConfig): void
}// special signal that adds lazy sub to source
const osignal_new_special = function <O>(src: OSignal<O>): OSignal<O> {
    const src_sub = () => {
        attachment.emit()
    }
    const src_lazysub = () => {
        console.log("lazysub")
    }
    const attachment = attachment_new_lazy(
        () => {
            // instant: true to prevent batching. Expected for all child signals to do that
            src.addsub(src_sub, { instant: true })
            src.addsub(src_lazysub)
        },
        () => {
            src.rmsub(src_sub)
            src.rmsub(src_lazysub)
        }
    )
    return {
        addsub(sub, config) {
            attachment.addsub(sub, config)
        },
        
        rmsub(sub) {
            attachment.rmsub(sub)
        },
        output() {
            return src.output()
        }
    }
}Updates are batched to prevent unnecessary calls
const root_1 = signal_new_value(0)
const root_2 = signal_new_value(0)
const merged_1 = signal_new_merge([root_1, root_2] as const)
const merged_2 = signal_new_merge([root_1, merged_1] as const)
const merged_sub = () => {
    console.log("Batch Test")
}
merged_1.addsub(merged_sub)
merged_2.addsub(merged_sub)
// Batch Test should only be printed once
root_1.input(5)Batcher itself is accessable through batcher import
import { batcher } from "@qyu/signal-core"
interface Batcher {
    // emits callback in batch mode, emits queue syncronously
    batch_sync(callback: VoidFunction): void
    // schedules callback to microtask (Promise.resolve().then())
    // on microtask - emit all schedulled callback in batch_sync
    // calling batch_microtask(() => signal.input(1)) will not update signal.output() immediately
    batch_microtask(callback: VoidFunction): void
    // if in batch mode - add callback to queue, else - emit callback
    schedule(callback: VoidFunction): void
}