UNPKG

@oazmi/tsignal

Version:

a topological order respecting signals library inspired by SolidJS

167 lines (166 loc) 9.77 kB
/** a equal-calorie clone of the popular reactivity library [SolidJS](https://github.com/solidjs/solid). <br> * @module */ import { Context } from "./context.js"; import { Accessor, EqualityCheck, EqualityFn, ID, Setter, SignalUpdateStatus, TO_ID, UNTRACKED_ID, Updater } from "./typedefs.js"; /** the configuration options used by most signal constructors, and especially the basic/primitive ones. */ export interface SimpleSignalConfig<T> { /** give a name to the signal for debugging purposes */ name?: string; /** when a signal's value is updated (either through a {@link Setter}, or a change in the value of a dependency signal in the case of a memo), * then the dependants/observers of THIS signal will only be notified if the equality check function evaluates to a `false`. <br> * see {@link EqualityCheck} to see its function signature and default behavior when left `undefined` */ equals?: EqualityCheck<T>; /** when `false`, the computaion/effect function will be be evaluated/run immediately after it is declared. <br> * however, if left `undefined`, or `true`, the function's execution will be put off until the reactive signal returned by the createXYZ is called/accessed. <br> * by default, `defer` is `true`, and reactivity is not immediately executed during initialization. <br> * the reason why you might want to defer a reactive function is because the body of the reactive function may contain symbols/variables * that have not been defined yet, in which case an error will be raised, unless you choose to defer the first execution. <br> */ defer?: boolean; } /** the configuration options used by most primitive derived/computed signal constructors. */ export interface MemoSignalConfig<T> extends SimpleSignalConfig<T> { /** initial value declaration for reactive signals. <br> * its purpose is only to be used as a previous value (`prev_value`) for the optional `equals` equality function, * so that you don't get an `undefined` as the `prev_value` on the very first comparison. */ value?: T; } /** an arbitrary instance of a simple/primitive signal. */ export type SimpleSignalInstance = InstanceType<ReturnType<typeof SimpleSignal_Factory>>; /** the base signal class inherited by most other signal classes. <br> * its only function is to: * - when {@link Signal.get | read}, it return its `this.value`, and register any new observers (those with a nonzero runtime-id {@link Signal.rid | `Signal.rid`}) * - if {@link Signal.set | set} to a new value, compare it to its previous value through its `this.equals` function, * and return a boolean specifying whether or not the old and new values are the same. * - when {@link Signal.run | ran}, it will always return `0` (unchanged), unless it is forced, in which case it will return a `1`. */ export declare const SimpleSignal_Factory: (ctx: Context) => { new <T>(value?: T | undefined, { name, equals, }?: SimpleSignalConfig<T>): { id: ID; rid: ID | UNTRACKED_ID; name?: string | undefined; value?: T | undefined; equals: EqualityFn<T>; fn?: ((observer_id: TO_ID | UNTRACKED_ID) => any) | undefined; prerun?(): any; postrun?(): any; get(observer_id?: TO_ID | UNTRACKED_ID): T; set(new_value: T | Updater<T>): boolean; run(forced?: boolean): SignalUpdateStatus; bindMethod<M extends keyof any>(method_name: M): any[M]; }; create<T_1>(...args: any[]): [id: ID, ...any[]]; }; /** creates state signals, which when {@link Signal.set | set} to a changed value, it will fire an update to all of its dependent/observer signals. */ export declare const StateSignal_Factory: (ctx: Context) => { new <T>(value: T, config?: SimpleSignalConfig<T> | undefined): { value: T; fn: never; prerun: never; postrun: never; set(new_value: T | Updater<T>): boolean; id: ID; rid: ID | UNTRACKED_ID; name?: string | undefined; equals: EqualityFn<T>; get(observer_id?: TO_ID | UNTRACKED_ID): T; run(forced?: boolean): SignalUpdateStatus; bindMethod<M extends keyof any>(method_name: M): any[M]; }; create<T_1>(value: T_1, config?: SimpleSignalConfig<T_1> | undefined): [idState: number, getState: Accessor<T_1>, setState: Setter<T_1>]; }; /** type definition for a memorizable function. to be used as a call parameter for {@link createMemo} */ export type MemoFn<T> = (observer_id: TO_ID | UNTRACKED_ID) => T | Updater<T>; /** creates a computational/derived signal that only fires again if at least one of its dependencies has fired, * and after the {@link SimpleSignalInstance.fn | recomputation} (`this.fn`), the new computed value is different from the old one (according to `this.equals`). */ export declare const MemoSignal_Factory: (ctx: Context) => { new <T>(fn: MemoFn<T>, config?: MemoSignalConfig<T> | undefined): { fn: MemoFn<T>; prerun: never; postrun: never; get(observer_id?: TO_ID | UNTRACKED_ID): T; run(forced?: boolean): SignalUpdateStatus; id: ID; rid: ID | UNTRACKED_ID; name?: string | undefined; value?: T | undefined; equals: EqualityFn<T>; set(new_value: T | Updater<T>): boolean; bindMethod<M extends keyof any>(method_name: M): any[M]; }; create<T_1>(fn: MemoFn<T_1>, config?: MemoSignalConfig<T_1> | undefined): [idMemo: number, getMemo: Accessor<T_1>]; }; /** similar to {@link MemoSignal_Factory | `MemoSignal`}, creates a computed/derived signal, but it only recomputes if: * - it is dirty (`this.dirty = 1`) * - AND some signal/observer/caller calls this signal to {@link Signal.get | get} its value. * * this signal becomes dirty when at least one of its dependencies has fired an update. <br> * after which, it will remain dirty unless some caller requests its value, after which it will become not-dirty again. * * this signal also always fires an update when at least one of its dependencies has fired an update. * and it abandons checking for equality all together, since it only recomputes after a get request, * by which it is too late to signal no update in the value (because its observer is already running). * * this signal becomes pointless (in terms of efficiency) once a {@link MemoSignal_Factory | `MemoSignal`} depends on it. * but it is increadibly useful (i.e. lazy) when other {@link LazySignal_Factory | `LazySignal`s} depend on one another. */ export declare const LazySignal_Factory: (ctx: Context) => { new <T>(fn: MemoFn<T>, config?: MemoSignalConfig<T> | undefined): { fn: MemoFn<T>; dirty: 0 | 1; prerun: never; postrun: never; run(forced?: boolean): SignalUpdateStatus.UPDATED; get(observer_id?: TO_ID | UNTRACKED_ID): T; id: ID; rid: ID | UNTRACKED_ID; name?: string | undefined; value?: T | undefined; equals: EqualityFn<T>; set(new_value: T | Updater<T>): boolean; bindMethod<M extends keyof any>(method_name: M): any[M]; }; create<T_1>(fn: MemoFn<T_1>, config?: MemoSignalConfig<T_1> | undefined): [idLazy: number, getLazy: Accessor<T_1>]; }; /** type definition for an effect function. to be used as a call parameter for {@link createEffect} <br> * the return value of the function describes whether or not the signal should propagate. <br> * if `undefined` or `true` (or truethy), then the effect signal will propagate onto its observer signals, * otherwise if it is explicitly `false`, then it won't propagate. */ export type EffectFn = (observer_id: TO_ID | UNTRACKED_ID) => void | undefined | boolean; /** a function that forcefully runs the {@link EffectFn} of an effect signal, and then propagates towards the observers of that effect signal. <br> * the return value is `true` if the effect is ran and propagated immediately, * or `false` if it did not fire immediately because of some form of batching stopped it from doing so. */ export type EffectEmitter = () => boolean; /** extremely similar to {@link MemoSignal_Factory | `MemoSignal`}, but without a value to output, and also has the ability to fire on its own. * TODO-DOC: explain more */ export declare const EffectSignal_Factory: (ctx: Context) => { new (fn: EffectFn, config?: SimpleSignalConfig<void>): { fn: EffectFn; prerun: never; postrun: never; /** a non-untracked observer (which is what all new observers are) depending on an effect signal will result in the triggering of effect function. * this is an intentional design choice so that effects can be scaffolded on top of other effects. * TODO: reconsider, because you can also check for `this.rid !== 0` to determine that `this.fn` effect function has never run before, thus it must run at least once if the observer is not untracked_id * is it really necessary for us to rerun `this.fn` effect function for every new observer? it seems to create chaos rather than reducing it. * UPDATE: decided NOT to re-run on every new observer * TODO: cleanup this messy doc and redeclare how createEffect works */ get(observer_id?: TO_ID | UNTRACKED_ID): void; set(): boolean; run(forced?: boolean): SignalUpdateStatus; id: ID; rid: ID | UNTRACKED_ID; name?: string | undefined; value?: void | undefined; equals: EqualityFn<void>; bindMethod<M extends keyof any>(method_name: M): any[M]; }; create(fn: EffectFn, config?: SimpleSignalConfig<void>): [idEffect: ID, dependOnEffect: Accessor<void>, fireEffect: EffectEmitter]; };