UNPKG

@oazmi/tsignal

Version:

a topological order respecting signals library inspired by SolidJS

162 lines (161 loc) 10.6 kB
/** base type definitions <br> * @module */ /** type definition for a value equality check function. */ export type EqualityFn<T> = (prev_value: T | undefined, new_value: T) => boolean; /** type definition for an equality check specification. <br> * when `undefined`, javascript's regular `===` equality will be used. <br> * when `false`, equality will always be evaluated to false, meaning that setting any value will always fire a signal, even if it's equal. */ export type EqualityCheck<T> = undefined | false | EqualityFn<T>; /** the {@link Signal.id | `id`} of a signal is simply a number. */ export type ID = number; /** another way of saying the dependency (or source) signal id. */ export type FROM_ID = ID; /** another way of saying the observer (or destination) signal id. */ export type TO_ID = ID; /** the {@link Signal.rid | runtime id (`rid`)} of a signal becomes zero after its first run (because it has captured all of its dependencies). */ export type UNTRACKED_ID = 0; /** the hash value generated by the dfs-signal-dag-traversal hash function. */ export type HASHED_IDS = number; /** type definition for an incremental updater function. */ export type Updater<T> = (prev_value?: T) => T; /** type definition for a signal accessor (value getter) function. */ export type Accessor<T> = ((observer_id?: TO_ID | UNTRACKED_ID) => T); /** type definition for a signal value setter function. */ export type Setter<T> = ((new_value: T | Updater<T>) => boolean); /** type definition for an async signal value setter function. <br> * TODO: should an `AsyncSetter<T>` return a `Promise<T>` ? or should it return a `Promise<boolean>`, which should tell whether or not the value has changed (i.e. `!signal.equal(old_value, new_value)`) */ export type AsyncSetter<T> = (new_value: (T | Promise<T | Updater<T>>) | Updater<T | Promise<T | Updater<T>>>, rejectable?: boolean) => Promise<T>; /** type definition for when a signal's _update_ function `run` is called by the signal update propagator `propagateSignalUpdate` inside of {@link context!Context}. <br> * the return value should indicate whether this signal has: * - updated ({@link SignalUpdateStatus.UPDATED} === 1), and therefore propagate to its observers to also run * - unchanged ({@link SignalUpdateStatus.UNCHANGED} === 0), and therefore not propagate to its observers * - aborted ({@link SignalUpdateStatus.ABORTED} === -1), and therefore force each of its observers to also become non propagating and inherit the {@link SignalUpdateStatus.ABORTED} status */ export type Runner = () => SignalUpdateStatus; /** the abstraction that defines what a signal is. */ export interface Signal<T> { /** id of this signal in the {@link context!Context} in which it exists. */ id: number; /** runtime-id of this signal. <br> * it equals to the {@link id} on the first, so that the dependencies of this signal can be notified of being observed. * but once all dependencies have been notified after the first run, this runtime-id should become `0` ({@link UNTRACKED_ID}), * so that the dependiencies do not have to re-register this signal as an observer. */ rid: ID | UNTRACKED_ID; /** give a name to this signal for debugging purposes */ name?: string; /** get the value of this signal, and handle any observing signal's id ({@link observer_id}). <br> * typically, when `observer_id` is non-zero, this signal should handle it by registering it as an * observer through the use of the context's {@link context!Context.addEdge} method. * * @example * ```ts * const MySignalClass_Factory = (ctx: Context) => { * const addEdge = ctx.addEdge * return class MySignal<T> implements Signal<T> { * declare value: T * // ... * get(observer_id?: TO_ID | UNTRACKED_ID): T { * // register this.id to observer (if non-zero) in the dependency graph as a directed edge * if (observer_id) { addEdge(this.id, observer_id) } * return this.value * } * // ... * } * } * ``` */ get(observer_id?: TO_ID | UNTRACKED_ID): T; /** set the value of this signal. <br> * the meaning of setting a signal's value greatly varies from signal to signal, which is why it is so abstracted. <br> * however, the returning value must always be a `boolean` describing whether or not this signal's value has changed compared to its previous value. <br> * what makes use of the returned value again greatly varies from signal to signal. * but it is typically used by the {@link run} method to decide whether or not this signal should propagate. <br> * another purpose of the set method is typically to _initiate_ the ignition of an update cycle in a context, bu using {@link context!Context.runId}. * this is how {@link signal!StateSignal}s and {@link signal!EffectSignal}s begin an update cycle when their values have changed from the prior value. * * @example * ```ts * const MySignalClass_Factory = (ctx: Context) => { * const runId = ctx.runId * return class MySignal<T> implements Signal<T> { * declare value: T * // ... * set(new_value: T): boolean { * const value_has_changed = new_value !== this.value * if (value_has_changed) { * runId(this.id) * return true * } * return false * } * // ... * } * } * ``` */ set?(...args: any[]): boolean; /** specify actions that need to be taken __before__ an update cycle has even begun propagating. <br> * TODO: CURRENTLY NOT IMPLEMENTED. * ISSUE: what should the order in which prepruns run be? we do know the FULL set of signal ids that will be visited. * but we do not know the subset of ids that WILL BE affected and ran, not until run time of the signal propagation. * should ids that _might_ be affected also have their preruns ran? and in what order? because we cannot know the order until propagation runtime. */ prerun?(): void; /** run the actions taken by a signal when it is informed that its dependency signals have been modified/changed. <br> * the return value should be of the numertic enum kind {@link SignalUpdateStatus}, which specifies that this signal has been either been: * - ` 1`: updated, and therefore this signal's observers should be notified (i.e. _ran_ via their `run` method) * - ` 0`: unchanged, and therefore this signal's observers should be notified if this is their only active dependency * - `-1`: aborted, and therefore this signal's observer should also abort their `run` method's execution if they were queued * * this method may also accept an optional `forced` parameter, which tells the signal that it is * being _forced_ to run, even though none of its dependency signals have been executed or changed. * this information is useful when coding for signals that _can_ be fired independently, such as {@link signal!StateSignal} or {@link signal!EffectSignal}. * * @param forced was this signal _forced_ to run independently? * @returns the update status of this signal, specifying whether or not it has changed, or if it has been aborted */ run(forced?: boolean): SignalUpdateStatus; /** specify actions that need to be taken __after__ an update cycle has fully propagated till the end. <br> * the order in which `postrun`s will be executed will be in the reverse order in which they were first encountered (i.e: last in, last out). * meaning that if all three signals `A`, `B`, and `C`, had `postrun` methods on them, and the order of execution was: * `A -> B -> C`, then all `postrun`s will run in the order: `[C.postrun, B.postrun, A.postrun]` (similar to a stack popping). */ postrun?(): void; /** a utility method defined in {@link signal!SimpleSignal}, which allows one to bind a certain method (by name) to _this_ instance of a signal, * and therefore make that method freeable/seperable from _this_ signal. <br> * this method is used by all signal classes's static {@link SignalClass.create} method, which is supposed to construct a signal in * a fashion similar to SolidJS, and return an array containing important control functions of the created signal. * most, if not all, of these control function are generally plain old signal methods that have been bounded to the created signal instance. */ bindMethod<M extends keyof this>(method_name: M): this[M]; } /** the abstraction that defines the static methods which must exist in a signal generating class */ export interface SignalClass { new (...args: any[]): Signal<any>; create(...args: any[]): [id: ID, ...any[]]; } /** the numbers used for relaying the status of a signal after it has been _ran_ via its {@link Signal.run | `run method`}. <br> * these numbers convey the following instructions to the context's topological update cycle {@link context!Context.propagateSignalUpdate}: * - ` 1`: this signal's value has been updated, and therefore its observers should be updated too. * - ` 0`: this signal's value has not changed, and therefore its observers should be _not_ be updated. * do note that an observer signal will still run if some _other_ of its dependency signal did update this cycle (i.e. had a status value of `1`) * - `-1`: this signal has been aborted, and therefore its observers must abort execution as well. * the observers will abort _even_ if they had a dependency that _did_ update (had a status value of `1`) * * to sum up, given a signal `D`, with dependencies: `A`, `B`, and `C` (all of which are mutually independent of each other). * then the status of `D` will be as follows in the order of highest conditional priority to lowest: * | status of D | status of D as enum | condition | * |-----------------------|:-------------------------------------:|:-----------------------------------------------------------------------------:| * | `status(D) = -1` | `ABORTED` | `∃X ∈ [A, B, C] such that status(X) === -1` | * | `status(D) = D.run()` | `CHANGED` or `UNCHANGED` or `ABORTED` | `∃X ∈ [A, B, C] such that status(X) === 1` | * | `status(D) = 0` | `UNCHANGED` | `∀X ∈ [A, B, C], status(X) === 0` | */ export declare const enum SignalUpdateStatus { ABORTED = -1, UNCHANGED = 0, UPDATED = 1 }