From 98dab56093c72843a56497acd3874360a9ae1810 Mon Sep 17 00:00:00 2001 From: Omar Azmi <64020006+omar-azmi@users.noreply.github.com> Date: Wed, 4 Oct 2023 00:01:38 -0400 Subject: [PATCH] remove `signal*.ts` submodules and port it over to a separate project repository in order to make it much more featureful and modular the new signal repository (currently private) is at: `https://github.com/omar-azmi/tsignal_ts` add `StaticImplements` in `typedefs.ts`, which allows one to `implement` static class methods (and also static properties) bump minor version to 0.7.0 --- build_npm.ts | 4 +- deno.json | 2 +- src/builtin_aliases.ts | 4 +- src/builtin_aliases_deps.ts | 6 +- src/mod.ts | 1 - src/signal.ts | 260 ----------------- src/signal2.ts | 474 ------------------------------ src/signal3.ts | 558 ------------------------------------ src/typedefs.ts | 44 +++ test/signal.test.ts | 127 -------- 10 files changed, 52 insertions(+), 1428 deletions(-) delete mode 100644 src/signal.ts delete mode 100644 src/signal2.ts delete mode 100644 src/signal3.ts delete mode 100644 test/signal.test.ts diff --git a/build_npm.ts b/build_npm.ts index 0ea205c1..830e1d0f 100644 --- a/build_npm.ts +++ b/build_npm.ts @@ -27,7 +27,6 @@ const sub_entrypoints: string[] = [ "./src/mapper.ts", "./src/numericarray.ts", "./src/numericmethods.ts", - "./src/signal.ts", "./src/stringman.ts", "./src/struct.ts", "./src/typedbuffer.ts", @@ -70,7 +69,6 @@ const typedoc = { "mapper": site_root + "modules/mapper.html", "numericarray": site_root + "modules/numericarray.html", "numericmethods": site_root + "modules/numericmethods.html", - "signal": site_root + "modules/signal.html", "stringman": site_root + "modules/stringman.html", "struct": site_root + "modules/struct.html", "typedbuffer": site_root + "modules/typedbuffer.html", @@ -113,7 +111,7 @@ await build({ }, compilerOptions: deno_package.compilerOptions, typeCheck: false, - declaration: true, + declaration: "inline", esModule: true, scriptModule: false, test: false, diff --git a/deno.json b/deno.json index b1856f7b..56f4fd4d 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "kitchensink_ts", - "version": "0.6.5a", + "version": "0.7.0", "description": "a collection of personal utility functions", "author": "Omar Azmi", "license": "Lulz plz don't steal yet", diff --git a/src/builtin_aliases.ts b/src/builtin_aliases.ts index f9146dc4..ce5dc1bc 100644 --- a/src/builtin_aliases.ts +++ b/src/builtin_aliases.ts @@ -135,11 +135,11 @@ export const { isExtensible: object_isExtensible, isFrozen: object_isFrozen, isSealed: object_isSealed, - keys: object_keys, + //keys: object_keys, preventExtensions: object_preventExtensions, seal: object_seal, setPrototypeOf: object_setPrototypeOf, - values: object_values, + //values: object_values, } = Object export const { diff --git a/src/builtin_aliases_deps.ts b/src/builtin_aliases_deps.ts index 9f6bb31d..1a178c89 100644 --- a/src/builtin_aliases_deps.ts +++ b/src/builtin_aliases_deps.ts @@ -25,7 +25,9 @@ export const { export const { assign: object_assign, - getPrototypeOf: object_getPrototypeOf + keys: object_keys, + getPrototypeOf: object_getPrototypeOf, + values: object_values, } = Object export const date_now = Date.now @@ -39,4 +41,4 @@ export const dom_setTimeout = setTimeout export const dom_clearTimeout = clearTimeout export const dom_setInterval = setInterval export const dom_clearInterval = clearInterval -export const noop: () => void = () => {} +export const noop: () => void = () => { } diff --git a/src/mod.ts b/src/mod.ts index 1de08b69..71fea4d1 100644 --- a/src/mod.ts +++ b/src/mod.ts @@ -19,7 +19,6 @@ export * from "./lambdacalc.ts" export * from "./mapper.ts" export * from "./numericarray.ts" export * from "./numericmethods.ts" -export * from "./signal.ts" export * from "./stringman.ts" export * from "./struct.ts" export * from "./typedbuffer.ts" diff --git a/src/signal.ts b/src/signal.ts deleted file mode 100644 index 2a48cf0c..00000000 --- a/src/signal.ts +++ /dev/null @@ -1,260 +0,0 @@ -/** a sugar-free, fat-free, and low-calorie clone of the popular reactivity library [SolidJS](https://github.com/solidjs/solid).
- * for under a 100 javascript lines, you get: - * - a few core reactivity functions: {@link createSignal}, {@link createMemo}, and {@link createEffect} - * - a few core utility functions: {@link batch}, {@link untrack}, and {@link reliesOn} (similar to `on(...)` in solid-js)
- * but in exchange, you sacrifice: DOM manipulation, scheduler, asynchronicity (promises), infinite loop checks, shortest update path, and much more.
- * but hey, cheer up. cuz youz gonna loze sum wei8 ma8!
- * TODO add usage examples - * @module -*/ - -import { min } from "./numericmethods.ts" - -/** type definition for a computation function. */ -type Computation = () => void - -/** type definition for a computation ID. */ -type ComputationId = number - -/** type definition for a cleanup function. */ -type Cleanup = () => void - -/** type definition for an updater function. */ -type Updater = (prev_value: T) => T - -/** type definition for a signal accessor (value getter) function. */ -export type Accessor = () => T - -/** type definition for a signal value setter function. */ -export type Setter = (value: T | Updater) => void - -/** type definition for an accessor and setter pair, which is what is returned by {@link createSignal} */ -export type AccessorSetter = [Accessor, Setter] - -/** type definition for a memorizable function. to be used as a call parameter for {@link createMemo} */ -export type MemoFn = () => T - -/** type definition for an effect function. to be used as a call parameter for {@link createEffect} */ -export type EffectFn = () => Cleanup | void - -/** type definition for a value equality check function. */ -export type EqualityFn = (prev_value: T, new_value: T) => boolean - -/** type definition for an equality check specification.
- * when `undefined`, javascript's regular `===` equality will be used.
- * 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 = undefined | false | EqualityFn - -/** represents options when creating a signal via {@link createSignal}. */ -export interface CreateSignalOptions { - /** when a signal's value is updated (either through a {@link Setter}, or a change in the value of a dependancy 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`.
- * see {@link EqualityCheck} to see its function signature and default behavior when left `undefined` - */ - equals?: EqualityCheck -} - -/** represents options when creating an effect signal via {@link createEffect}. */ -export interface CreateEffectOptions { - /** when `true`, the effect function {@link EffectFn} will not be evaluated immediately (ie the first execution will be skipped), - * and its execution will be put off until the function returned by {@link createEffect} is called.
- * by default, `defer` is `false`, and effects are immediately executed during initialization.
- * the reason why you might want to defer an effect is because the body of the effect 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.
- */ - defer?: boolean -} - -/** represents options when creating a memo signal via {@link createMemo}. */ -export interface CreateMemoOptions extends CreateSignalOptions { - /** when `true`, the memo function {@link MemoFn} will not be evaluated immediately (ie the first execution will be skipped), - * and its execution will be put off until the first time the memo signal's accessor {@link Accessor} is called.
- * by default, `defer` is `false`, and memos are immediately evaluated for a value during initialization.
- * the reason why you might want to defer a memo's value is because the body of the memo 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.
- * note that defering adds an additional arrow function call. so its better to change your code pattern where performance needs to be higher.
- * also do not fear the first execution for being potentially redundant, because adding a defer call layer will certainly be worse on subsequent calls. - */ - defer?: boolean -} - -let active_computation: ComputationScope | undefined = undefined -let computation_id_counter: ComputationId = 0 -const default_equality = ((v1: T, v2: T) => (v1 === v2)) satisfies EqualityFn -const falsey_equality = ((v1: T, v2: T) => false) satisfies EqualityFn -const noop: () => void = () => (undefined) -let pause_reactivity_stack = 0 -export const pauseReactivity = () => { pause_reactivity_stack++ } -export const resumeReactivity = () => { pause_reactivity_stack = min(pause_reactivity_stack - 1, 0) } -export const resetReactivity = () => { pause_reactivity_stack = 0 } - -/** a reactive signal that holds a value and updates its dependant observers when the value changes. */ -export class Signal { - private observers: Map = new Map() - private equals: EqualityFn - - /** create a new `Signal` instance. - * @param value initial value of the signal. - * @param equals optional equality check function for value comparison. - */ - constructor( - private value: T, - equals?: EqualityCheck, - ) { - this.equals = equals === false ? falsey_equality : (equals ?? default_equality) - } - - /** get the current value of the signal, and also become a dependant observer of this signal.
- * note that this is an arrow function because `getValue` is typically destructured and then passed around with the `Signal`'s `this` context being lost.
- * if this were a regular class method delaration (ie: `getValue() {...}`), then it would be necessary to always call it via `this.getValue()`.
- * using it via `const { getter: getValue } = this; getter()` would result in an error, because `this` is lost from `getter`'s context now.
- * @returns the current value. - */ - getValue: Accessor = () => { - if (active_computation && pause_reactivity_stack === 0) { - this.observers.set(active_computation.id, active_computation.computation) - } - return this.value - } - - /** set the value of the signal, and if the new value is not equal to the old value, notify the dependant observers to rerun.
- * note that this is an arrow function because `setValue` is typically destructured and then passed around with the `Signal`'s `this` context being lost.
- * if this were a regular class method delaration (ie: `setValue(xyz) {...}`), then it would be necessary to always call it via `this.setValue(xyz)`.
- * using it via `const { setter: setValue } = this; setter(xyz)` would result in an error, because `this` is lost from `setter`'s context now.
- * @param value new value or updater function. - */ - setValue: Setter = (value) => { - value = typeof value === "function" ? (value as Updater)(this.value) : value - if (this.equals(this.value, value)) { return } - this.value = value - if (pause_reactivity_stack > 0) { return } - for (const fn of this.observers.values()) { - fn() - } - } -} - -/** represents a computation scope for managing reactive computations. */ -class ComputationScope { - /** create a new computation scope. - * @param computation the computation function to run. - * @param cleanup optional cleanup function to execute after the computation. - * @param id optional computation ID. - */ - constructor( - public computation: Computation, - public cleanup?: Cleanup, - public id: ComputationId = computation_id_counter++, - ) { - this.run() - } - - /** run the computation within this scope. */ - run(): void { - if (this.cleanup) { this.cleanup() } - active_computation = this - this.computation() - active_computation = undefined - } - - /** dispose of the computation scope. */ - dispose(): void { - if (this.cleanup) { this.cleanup() } - } -} - -/** create a reactive signal with an initial value. - * @param initial_value initial value of the signal. - * @param options options for signal creation. see {@link CreateSignalOptions}. - * @returns an accessor and setter pair for the signal. -*/ -export const createSignal = (initial_value: T, options?: CreateSignalOptions): AccessorSetter => { - const signal = new Signal(initial_value, options?.equals) - return [signal.getValue, signal.setValue] -} - -/** create a reactive memo using a memoization function. - * @param fn memoization function. see {@link MemoFn}. - * @param options options for memo creation. - * @returns an accessor for the memoized value. -*/ -export const createMemo = (fn: MemoFn, options?: CreateMemoOptions): Accessor => { - const [getValue, setValue] = createSignal(undefined as T, options) - if (options?.defer) { - let executed = false - return () => { - if (!executed) { - new ComputationScope(() => setValue(fn())) - executed = true - } - return getValue() - } - } - new ComputationScope(() => setValue(fn())) - return getValue -} - -/** create a reactive effect using an effect function. - * @param fn effect function to run. {@link see EffectFn}. -*/ -export const createEffect = (fn: EffectFn, options?: CreateEffectOptions): (() => void) => { - let cleanup: Cleanup | void - const execute_effect = () => { - new ComputationScope( - () => (cleanup = fn()), - () => { if (cleanup) { cleanup() } } - ) - } - if (options?.defer) { - return execute_effect - } - execute_effect() - return noop -} - -/** batch multiple computations together for efficient execution. - * @param fn computation function containing multiple reactive operations. -*/ -export const batch = (fn: Computation): void => { - const prev_active_computation = active_computation - active_computation = undefined - fn() - active_computation = prev_active_computation -} - -/** temporarily disable tracking of reactive dependencies within a function. - * @param fn function containing reactive dependencies. - * @returns the result of the function. -*/ -export const untrack = (fn: MemoFn): T => { - const prev_active_computation = active_computation - active_computation = undefined - const result = fn() - active_computation = prev_active_computation - return result -} - -/** evaluate a function with explicit reactive dependencies. - * @param dependencies list of reactive dependencies to consider. - * @param fn function containing reactive logic with a return value. - * @returns the result of the {@link fn} function. -*/ -export const dependsOn = (dependancies: Iterable>, fn: MemoFn): MemoFn => { - return () => { - for (const dep of dependancies) { dep() } - return untrack(fn) - } -} - -/** create an effect that explicitly depends on specified reactive dependencies. - * @param dependencies list of reactive dependencies to consider for the effect. - * @param fn function containing reactive logic for the effect. - * @returns an effect function that tracks the specified dependency signals. -*/ -export const reliesOn = (dependancies: Iterable>, fn: MemoFn | EffectFn): EffectFn => { - return () => { - for (const dep of dependancies) { dep() } - untrack(fn) - } -} diff --git a/src/signal2.ts b/src/signal2.ts deleted file mode 100644 index 6eaa8927..00000000 --- a/src/signal2.ts +++ /dev/null @@ -1,474 +0,0 @@ -import { - bind_map_clear, - bind_map_delete, - bind_map_forEach, - bind_map_get, - bind_map_set, - bind_set_add, - bind_set_clear, - bind_set_delete, - bind_set_has, -} from "./binder.ts" -import { array_isEmpty, object_assign } from "./builtin_aliases_deps.ts" - -const DEBUG = true as const - -interface InvertibleGraphEdges { - /** forward mapping of directed edges. not intended for direct mutaion, since it will ruin the invertibility with the reverse map if you're not careful. */ - fmap: Map> - - /** reverse mapping of directed edges. not intended for direct mutaion, since it will ruin the invertibility with the forward map if you're not careful. */ - rmap: Map> - - /** at a specific source node `src_id` in the forward map, add an aditional destination node `dst_id`, - * and then also append `src_id` to `dst_id` in the reverse map to maintain invertibility. - */ - fadd: (src_id: FROM, dst_id: TO) => void - - /** at a specific destination node `dst_id` in the reverse map, add an aditional source node `src_id`, - * and then also append `dst_id` to `src_id` in the forward map to maintain invertibility. - */ - radd: (dst_id: TO, src_id: FROM) => void - - /** clear out both forward and reverse maps completely of all their entries */ - clear: () => void - - /** delete an `id` in the forward map, and also remove its mentions from the reverse map's entries.
- * if `keep_key` is optionally set to `true`, we will only clear out the set of items held by the forward map at the key, - * and keep the node id itself intact (along with the original (now mutated and cleared) `Set` which the key refers to).
- * @returns `true` if the node existed in the forward map before deletion, else `false` - */ - fdelete: (src_id: FROM, keep_key?: boolean) => boolean - - /** delete an `id` in the reverse map, and also remove its mentions from the forward map's entries.
- * if `keep_key` is optionally set to `true`, we will only clear out the set of items held by the reverse map at the key, - * and keep the node id itself intact (along with the original (now mutated and cleared) `Set` which the key refers to).
- * @returns `true` if the node existed in the reverse map before deletion, else `false` - */ - rdelete: (dst_id: TO, keep_key?: boolean) => boolean - - /** at a specific `id` in the forward map, remove/delete the list of destination nodes `dst_ids`, - * and then also remove `id` from each of the list of `dst_ids` in the reverse map to maintain invertibility. - */ - fremove: (src_id: FROM, ...dst_ids: TO[]) => void - - /** at a specific `id` in the reverse map, remove/delete the list of source nodes `src_ids`, - * and then also remove `id` from each of the list of `src_ids` in the reverse map to maintain invertibility. - */ - rremove: (dst_id: TO, ...src_ids: FROM[]) => void - - -} - - -const NODE_TYPE_LEN = 4 as const - -type ID = number -type FROM = number -type TO = number - -//const createContext = () => { -let id_counter: number = 0 -const - increment_id_counter = (): number => (id_counter += NODE_TYPE_LEN), - fmap = new Map>(), - rmap = new Map>(), - fmap_get = bind_map_get(fmap), - rmap_get = bind_map_get(rmap), - fmap_set = bind_map_set(fmap), - rmap_set = bind_map_set(rmap), - fmap_delete = bind_map_delete(fmap), - rmap_delete = bind_map_delete(rmap), - fmap_clear = bind_map_clear(fmap), - rmap_clear = bind_map_clear(rmap) - -const fadd = (src_id: FROM, dst_id: TO) => { - const forward_items = fmap_get(src_id) ?? (fmap_set(src_id, new Set()) && fmap_get(src_id)!) - if (!forward_items.has(dst_id)) { - forward_items.add(dst_id) - if (!rmap_get(dst_id)?.add(src_id)) { - rmap_set(dst_id, new Set([src_id])) - } - } -} - -const radd = (dst_id: TO, src_id: FROM) => fadd(src_id, dst_id) - -const - all_signals = new Map>(), - all_signals_get = bind_map_get(all_signals), - all_signals_set = bind_map_set(all_signals) - - -class SignalBase { - rid: TO | 0 - value?: T - - constructor( - public id: ID, - public fn: (observer_id: TO | 0) => T - ) { - this.rid = id as unknown as TO - all_signals_set(id, this) - } - - get = (observer_id?: TO | 0): T => { - let { id, rid, value } = this - if (observer_id) { - // register this.id to observer - fadd(id, observer_id) - } - if (rid || value === undefined) { - this.run() - this.rid = 0 - value = this.value - } - if (DEBUG) { - console.table([["GET_ID", "OBSERVER_ID", "VALUE"], [id, observer_id, value]]) - } - return value as T - } - - run = () => { - this.value = this.fn(this.rid) - } -} -//} - -// scheduler -const - pending: Set = new Set(), - pending_add = bind_set_add(pending), - pending_delete = bind_set_delete(pending), - pending_clear = bind_set_clear(pending), - // count (value) of number of edges going INTO an id (key). (aka the number of dependencies an id has) - deps_count: Map = new Map(), - deps_count_get = bind_map_get(deps_count), - deps_count_set = bind_map_set(deps_count), - deps_count_clear = bind_map_clear(deps_count), - // count (value) of number of rejected edges going INTO an id (key). (aka the number of dependencies, of an id, which have been rejected) - // if the count tops up to the total number of edges going into the `id` (`rejected_deps_count[id] === get(id).size`), - // then we will also have to reject `id`, and propagate its rejection's effect onto its dependants - rejected_deps_count: Map = new Map(), - rejected_deps_count_get = bind_map_get(rejected_deps_count), - rejected_deps_count_set = bind_map_set(rejected_deps_count), - rejected_deps_count_clear = bind_map_clear(rejected_deps_count), - rforEach = bind_map_forEach(rmap) - -const clear = () => { - pending_clear() - deps_count_clear() - rejected_deps_count_clear() - rforEach((src_ids, dst_id) => { - deps_count_set(dst_id, src_ids.size) - }) -} - -const fire = (...src_ids: FROM[]) => { - clear(); - (src_ids as unknown[] as TO[]).forEach(pending_add) -} - -const resolve = (...ids: ID[]): TO[] => { - const next_ids: TO[] = [] - for (const id of ids as TO[]) { - if (pending_delete(id)) { - all_signals_get(id)!.run() - fmap_get(id as unknown as FROM)?.forEach((dst_id) => { - // `deps_count_get(dst_id)` may be undefined due to a dependency that was adder later (after a firing cycle had begun). - // in that case, we look it up from `rmap_get(dst_id).size`, which should contain the updated info. - // but if that too turns out to be undefined, then we fall back to `1 - 1` - const deps_count_of_id = ( - deps_count_get(dst_id) ?? - rmap_get(dst_id)?.size ?? - 1 - ) - 1 - if (deps_count_of_id <= 0) { - // `dst_id` now has no unresolved dependencies left. therefore we can push it `next_ids`, and eventually to `pending` - next_ids.push(dst_id) - } - deps_count_set(dst_id, deps_count_of_id) - }) - } - } - next_ids.forEach(pending_add) - return next_ids -} - -const reject = (...ids: ID[]): TO[] => { - const next_rejected_ids: TO[] = [] - for (const id of ids as TO[]) { - pending_delete(id) - fmap_get(id as unknown as FROM)?.forEach((dst_id) => { - const rejected_deps_count_of_id = (rejected_deps_count_get(dst_id) ?? 0) + 1 - rejected_deps_count_set(dst_id, rejected_deps_count_of_id) - if (rejected_deps_count_of_id >= (rmap_get(dst_id)?.size ?? 0)) { - // `dst_id` now has had all of its dependencies rejected. therefore we must now push it `next_rejected_ids`, and eventually to reject it on the next recursion - next_rejected_ids.push(dst_id) - } - }) - } - return (ids as TO[]).concat( - array_isEmpty(next_rejected_ids) ? - next_rejected_ids : - reject(...next_rejected_ids) - ) -} - - - -const - A = new SignalBase("A", () => 1), - B = new SignalBase("B", () => 2), - C = new SignalBase("C", () => 3), - D = new SignalBase("D", (id) => A.get(id) + 10), - E = new SignalBase("E", (id) => B.get(id) + F.get(id) + D.get(id) + C.get(id)), - F = new SignalBase("F", (id) => C.get(id) + 20), - G = new SignalBase("G", (id) => E.get(id) + 100), - H = new SignalBase("H", (id) => G.get(id) + A.get(id)), - I = new SignalBase("I", (id) => F.get(id) - 100) - -console.log(I.get()) - - - - - -class Context implements InvertibleGraphEdges { - // edge manipulation - declare fmap: Map> - declare rmap: Map> - declare fadd: (src_id: FROM, dst_id: TO) => void - declare radd: (dst_id: TO, src_id: FROM) => void - declare clear_map: () => void - declare fdelete: (src_id: FROM, keep_key?: boolean | undefined) => boolean - declare rdelete: (dst_id: TO, keep_key?: boolean | undefined) => boolean - declare fremove: (src_id: FROM, ...dst_ids: TO[]) => void - declare rremove: (dst_id: TO, ...src_ids: FROM[]) => void - // schedule manipulation - declare pending: Set - declare clear: () => void - declare fire: (...src_ids: FROM[]) => void - declare resolve: (...ids: ID[]) => TO[] - declare reject: (...ids: ID[]) => TO[] - - - constructor() { - let id_counter: number = 0 - const - increment_id_counter = (): number => (id_counter += NODE_TYPE_LEN), - fmap = new Map>(), - rmap = new Map>(), - fmap_get = bind_map_get(fmap), - rmap_get = bind_map_get(rmap), - fmap_set = bind_map_set(fmap), - rmap_set = bind_map_set(rmap), - fmap_delete = bind_map_delete(fmap), - rmap_delete = bind_map_delete(rmap), - fmap_clear = bind_map_clear(fmap), - rmap_clear = bind_map_clear(rmap) - - const fadd: this["fadd"] = (src_id: FROM, dst_id: TO) => { - const forward_items = fmap_get(src_id) ?? (fmap_set(src_id, new Set()) && fmap_get(src_id)!) - if (!forward_items.has(dst_id)) { - forward_items.add(dst_id) - if (!rmap_get(dst_id)?.add(src_id)) { - rmap_set(dst_id, new Set([src_id])) - } - } - } - - const radd: this["radd"] = (dst_id: TO, src_id: FROM) => fadd(src_id, dst_id) - - const clear_map: this["clear_map"] = () => { - fmap_clear() - rmap_clear() - } - - const fdelete: this["fdelete"] = (src_id: FROM, keep_key: boolean = false): boolean => { - const forward_items = fmap_get(src_id) - if (forward_items) { - // first remove all mentions of `id` from the reverse mapping - forward_items.forEach((dst_id) => { - rmap_get(dst_id)!.delete(src_id) - }) - if (keep_key) { forward_items.clear() } - else { keep_key = fmap_delete(src_id) } - } - return keep_key - } - - const rdelete: this["rdelete"] = (dst_id: TO, keep_key: boolean = false): boolean => { - const reverse_items = rmap_get(dst_id) - if (reverse_items) { - // first remove all mentions of `id` from the reverse mapping - reverse_items.forEach((src_id) => { - fmap_get(src_id)!.delete(dst_id) - }) - if (keep_key) { reverse_items.clear() } - else { keep_key = rmap_delete(dst_id) } - } - return keep_key - } - - const fremove: this["fremove"] = (src_id: FROM, ...dst_ids: TO[]) => { - const forward_items = fmap_get(src_id) - if (forward_items) { - const forward_items_delete = bind_set_delete(forward_items) - dst_ids.forEach((dst_id) => { - if (forward_items_delete(dst_id)) { - rmap_get(dst_id)!.delete(src_id) - } - }) - } - } - - const rremove: this["rremove"] = (dst_id: TO, ...src_ids: FROM[]) => { - const reverse_items = rmap_get(dst_id) - if (reverse_items) { - const reverse_items_delete = bind_set_delete(reverse_items) - src_ids.forEach((src_id) => { - if (reverse_items_delete(src_id)) { - fmap_get(src_id)!.delete(dst_id) - } - }) - } - } - - // scheduler - const - pending: Set = new Set(), - pending_add = bind_set_add(pending), - pending_delete = bind_set_delete(pending), - pending_clear = bind_set_clear(pending), - // count (value) of number of edges going INTO an id (key). (aka the number of dependencies an id has) - deps_count: Map = new Map(), - deps_count_get = bind_map_get(deps_count), - deps_count_set = bind_map_set(deps_count), - deps_count_clear = bind_map_clear(deps_count), - // count (value) of number of rejected edges going INTO an id (key). (aka the number of dependencies, of an id, which have been rejected) - // if the count tops up to the total number of edges going into the `id` (`rejected_deps_count[id] === get(id).size`), - // then we will also have to reject `id`, and propagate its rejection's effect onto its dependants - rejected_deps_count: Map = new Map(), - rejected_deps_count_get = bind_map_get(rejected_deps_count), - rejected_deps_count_set = bind_map_set(rejected_deps_count), - rejected_deps_count_clear = bind_map_clear(rejected_deps_count), - rforEach = bind_map_forEach(rmap) - - const clear: this["clear"] = () => { - pending_clear() - deps_count_clear() - rejected_deps_count_clear() - rforEach((src_ids, dst_id) => { - deps_count_set(dst_id, src_ids.size) - }) - } - - const fire: this["fire"] = (...src_ids: FROM[]) => { - clear(); - (src_ids as unknown[] as TO[]).forEach(pending_add) - } - - const resolve: this["resolve"] = (...ids: ID[]): TO[] => { - const next_ids: TO[] = [] - for (const id of ids as TO[]) { - if (pending_delete(id)) { - fmap_get(id as unknown as FROM)?.forEach((dst_id) => { - // `deps_count_get(dst_id)` may be undefined due to a dependency that was adder later (after a firing cycle had begun). - // in that case, we look it up from `rmap_get(dst_id).size`, which should contain the updated info. - // but if that too turns out to be undefined, then we fall back to `1 - 1` - const deps_count_of_id = ( - deps_count_get(dst_id) ?? - rmap_get(dst_id)?.size ?? - 1 - ) - 1 - if (deps_count_of_id <= 0) { - // `dst_id` now has no unresolved dependencies left. therefore we can push it `next_ids`, and eventually to `pending` - next_ids.push(dst_id) - } - deps_count_set(dst_id, deps_count_of_id) - }) - } - } - next_ids.forEach(pending_add) - return next_ids - } - - const reject: this["reject"] = (...ids: ID[]): TO[] => { - const next_rejected_ids: TO[] = [] - for (const id of ids as TO[]) { - pending_delete(id) - fmap_get(id as unknown as FROM)?.forEach((dst_id) => { - const rejected_deps_count_of_id = (rejected_deps_count_get(dst_id) ?? 0) + 1 - rejected_deps_count_set(dst_id, rejected_deps_count_of_id) - if (rejected_deps_count_of_id >= (rmap_get(dst_id)?.size ?? 0)) { - // `dst_id` now has had all of its dependencies rejected. therefore we must now push it `next_rejected_ids`, and eventually to reject it on the next recursion - next_rejected_ids.push(dst_id) - } - }) - } - return (ids as TO[]).concat( - array_isEmpty(next_rejected_ids) ? - next_rejected_ids : - reject(...next_rejected_ids) - ) - } - - - - class SignalBase { - id = increment_id_counter() as ID - declare rid: ID | 0 - - constructor() { - this.rid = this.id - - } - - /** */ - fire = () => { - fire(this.id as FROM) - } - - get = (observer_id: TO | 0) => { - if (observer_id) { - fadd(this.id as FROM, observer_id) - } - console.log("GET: ", this.id, " OBSERVER: ", observer_id) - return - } - - } - - object_assign(this, { pending, clear, fire, resolve, reject }) - - } - -} - - -/* -let A, B, C, D, E, F, G, H, I - -G.fn = (id: number) => { - E(id) - D(id) -} -// add(E, G); add(D, G) -// OR: radd(G, E, D) - -D.fn = (id: number) => { - A(id) -} - -E.fn = (id: number) => { - D(id) - C(id) - F(id) - B(id) -} - -F.fn = (id: number) => { - C(id) -} -*/ diff --git a/src/signal3.ts b/src/signal3.ts deleted file mode 100644 index e2664696..00000000 --- a/src/signal3.ts +++ /dev/null @@ -1,558 +0,0 @@ -import { - bind_array_clear, - bind_array_push, - bind_map_clear, - bind_map_delete, - bind_map_forEach, - bind_map_get, - bind_map_set, - bind_set_add, - bind_set_clear, - bind_set_delete, - bind_set_has, -} from "./binder.ts" -import { THROTTLE_REJECT, throttle } from "./browser.ts" - -const DEBUG = true as const - -type ID = number -type UNTRACKED_ID = 0 -type FROM = ID -type TO = ID -type HASH_IDS = number - -/** type definition for an updater function. */ -type Updater = (prev_value?: T) => T - -/** type definition for a signal accessor (value getter) function. */ -export type Accessor = (observer_id?: TO | UNTRACKED_ID) => T - -/** type definition for a signal value setter function. */ -export type Setter = (new_value: T | Updater) => boolean - -/** type definition for an accessor and setter pair, which is what is returned by {@link createSignal} */ -export type AccessorSetter = [Accessor, Setter] - -/** type definition for a memorizable function. to be used as a call parameter for {@link createMemo} */ -export type MemoFn = (observer_id: TO | UNTRACKED_ID) => T - -/** type definition for an effect function. to be used as a call parameter for {@link createEffect}
- * the return value of the function describes whether or not the signal should propagate.
- * 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 | 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.
- * 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 - -/** type definition for an effect accessor (ie for registering as an observer) and an effect forceful-emitter pair, which is what is returned by {@link createEffect} */ -export type AccessorEmitter = [Accessor, EffectEmitter] - -/** type definition for a value equality check function. */ -export type EqualityFn = (prev_value: T | undefined, new_value: T) => boolean - -/** type definition for an equality check specification.
- * when `undefined`, javascript's regular `===` equality will be used.
- * 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 = undefined | false | EqualityFn - -export interface BaseSignalConfig { - /** give a name to the signal for debuging purposes */ - name?: string - - /** when a signal's value is updated (either through a {@link Setter}, or a change in the value of a dependancy 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`.
- * see {@link EqualityCheck} to see its function signature and default behavior when left `undefined` - */ - equals?: EqualityCheck - - /** when `false`, the computaion/effect function will be be evaluated/run immediately after it is declared.
- * 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.
- * by default, `defer` is `true`, and reactivity is not immediately executed during initialization.
- * 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.
- */ - defer?: boolean - - /** initial value declaration for reactive signals.
- * 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 -} - -const enum SignalUpdateStatus { - PENDING_ASYNC = -1, - NOT_UPDATED = 0, - UPDATED = 1, -} - -const default_equality = ((v1: T, v2: T) => (v1 === v2)) satisfies EqualityFn -const falsey_equality = ((v1: T, v2: T) => false) satisfies EqualityFn -const hash_ids = (ids: ID[]): HASH_IDS => { - const sqrt_len = ids.length ** 0.5 - return ids.reduce((sum, id) => sum + id * (id + sqrt_len), 0) -} - -/** transforms a regular equality check function ({@link BaseSignalConfig.equals}) into a one that throttles when called too frequently.
- * this means that a singal composed of this as its `equals` function will limit propagating itself further, until at least `delta_time_ms` - * amount of time has passed since the last time it was potentially propagated. - * - * @param delta_time_ms the time interval in milliseconds for throttling - * @param base_equals use an optional customized equality checking function. otherwise the default `prev_value === new_value` comparison will be used - * @returns a throttled version of the equality checking function which would prematurely return a `true` if called too frequently (ie within `delta_time_ms` interval since the last actual equality cheking) - * - * @example - * ```ts - * const { createState, createMemo, createEffect } = createContext() - * const [A, setA] = createState(0) - * const B = createMemo((id) => { - * return A(id) ** 0.5 - * }, { equals: throttlingEquals(500) }) // at least 500ms must pass before `B` propagates itself onto `C` - * const [C, fireC] = createEffect((id) => { - * // SOME EXPENSIVE COMPUTATION OR EFFECT // - * console.log("C says Hi!, and B is of the value:", B(id)) - * }, { defer: false }) - * // it is important that you note that `B` will be recomputed each time `setA(...)` is fired. - * // it is only that `B` won't propagate to `C` (even if it changes in value) if at least 500ms have not passed - * setInterval(() => setA(A() + 1), 10) - * ``` -*/ -export const throttlingEquals = (delta_time_ms: number, base_equals?: EqualityCheck): EqualityCheck => { - const - base_equals_fn = base_equals === false ? falsey_equality : (base_equals ?? default_equality), - throttled_equals = throttle(delta_time_ms, base_equals_fn) - return (prev_value: T | undefined, new_value: T) => { - const is_equal = throttled_equals(prev_value, new_value) - return is_equal === THROTTLE_REJECT ? true : is_equal - } -} - -export const createContext = () => { - let - id_counter: number = 0, - batch_nestedness = 0 - - const - fmap = new Map>(), - rmap = new Map>(), - fmap_get = bind_map_get(fmap), - rmap_get = bind_map_get(rmap), - fmap_set = bind_map_set(fmap), - rmap_set = bind_map_set(rmap) - - const fadd = (src_id: FROM, dst_id: TO) => { - const forward_items = fmap_get(src_id) ?? ( - fmap_set(src_id, new Set()) && - fmap_get(src_id)! - ) - if (!forward_items.has(dst_id)) { - forward_items.add(dst_id) - if (!rmap_get(dst_id)?.add(src_id)) { - rmap_set(dst_id, new Set([src_id])) - } - } - } - - const - ids_to_visit_cache = new Map>(), - ids_to_visit_cache_get = bind_map_get(ids_to_visit_cache), - ids_to_visit_cache_set = bind_map_set(ids_to_visit_cache), - ids_to_visit_cache_clear = bind_map_clear(ids_to_visit_cache), - ids_to_visit_cache_create_new_entry = (source_ids: ID[]): Set => { - const - to_visit = new Set(), - to_visit_add = bind_set_add(to_visit), - to_visit_has = bind_set_has(to_visit) - const dfs_visiter = (id: ID) => { - if (!to_visit_has(id)) { - to_visit_add(id) - fmap_get(id)?.forEach(dfs_visiter) - } - } - source_ids.forEach(dfs_visiter) - return to_visit - }, - get_ids_to_visit = (...source_ids: ID[]): Set => { - const hash = hash_ids(source_ids) - return ids_to_visit_cache_get(hash) ?? ( - ids_to_visit_cache_set(hash, ids_to_visit_cache_create_new_entry(source_ids)) && - ids_to_visit_cache_get(hash)! - ) - } - - const - all_signals = new Map>(), - all_signals_get = bind_map_get(all_signals), - all_signals_set = bind_map_set(all_signals) - - const - to_visit_this_cycle = new Set(), - to_visit_this_cycle_add = bind_set_add(to_visit_this_cycle), - to_visit_this_cycle_delete = bind_set_delete(to_visit_this_cycle), - to_visit_this_cycle_clear = bind_set_clear(to_visit_this_cycle), - updated_this_cycle = new Map(), - updated_this_cycle_get = bind_map_get(updated_this_cycle), - updated_this_cycle_set = bind_map_set(updated_this_cycle), - updated_this_cycle_clear = bind_map_clear(updated_this_cycle), - batched_ids: FROM[] = [], - batched_ids_push = bind_array_push(batched_ids), - batched_ids_clear = bind_array_clear(batched_ids), - fireUpdateCycle = (...source_ids: FROM[]) => { - to_visit_this_cycle_clear() - updated_this_cycle_clear() - // clone the ids to visit into the "visit cycle for this update" - get_ids_to_visit(...source_ids).forEach(to_visit_this_cycle_add) - // fire the signal and propagate its reactivity. - // the souce signals are `force`d in order to skip any unresolved dependency check. - // this is needed because although state signals do not have dependencies, effect signals may have one. - // but effect signals are themselves designed to be fired/ran as standalone signals - for (const source_id of source_ids) { - propagateSignalUpdate(source_id, true) - } - }, - startBatching = () => (++batch_nestedness), - endBatching = () => { - if (--batch_nestedness <= 0) { - batch_nestedness = 0 - fireUpdateCycle(...batched_ids_clear()) - } - }, - scopedBatching = ( - fn: (...args: ARGS) => T, - ...args: ARGS - ): T => { - startBatching() - const return_value = fn(...args) - endBatching() - return return_value - } - - const propagateSignalUpdate = (id: ID, force?: true | any) => { - if (to_visit_this_cycle_delete(id)) { - if (DEBUG) { console.log("UPDATE_CYCLE\t", "visiting :\t", all_signals_get(id)?.name) } - // first make sure that all of this signal's dependencies are up to date (should they be among the set of ids to visit this update cycle) - // `any_updated_dependency` is always initially `false`. however, if the signal id was `force`d, then it would be `true`, and skip dependency checking and updating. - // you must use `force === true` for `StateSignal`s (because they are dependency free), or for a independently fired `EffectSignal` - let any_updated_dependency: SignalUpdateStatus = force === true ? SignalUpdateStatus.UPDATED : SignalUpdateStatus.NOT_UPDATED - if (any_updated_dependency <= SignalUpdateStatus.NOT_UPDATED) { - for (const dependency_id of rmap_get(id) ?? []) { - propagateSignalUpdate(dependency_id) - any_updated_dependency |= updated_this_cycle_get(dependency_id) ?? SignalUpdateStatus.NOT_UPDATED - } - } - // now, depending on two AND criterias: - // 1) at least one dependency has updated (or must be free of dependencies via `force === true`) - // 2) AND, this signal's value has changed after the update computation (ie `run()` method) - // if both criterias are met, then this signal should propagate forward towards its observers - const this_signal_update_status: SignalUpdateStatus = any_updated_dependency >= SignalUpdateStatus.UPDATED ? - (all_signals_get(id)?.run() ?? SignalUpdateStatus.NOT_UPDATED) : - any_updated_dependency as (SignalUpdateStatus.NOT_UPDATED | SignalUpdateStatus.PENDING_ASYNC) - updated_this_cycle_set(id, this_signal_update_status) - if (DEBUG) { console.log("UPDATE_CYCLE\t", this_signal_update_status > 0 ? "propagating:\t" : this_signal_update_status < 0 ? "delaying \t" : "blocking :\t", all_signals_get(id)?.name) } - if (this_signal_update_status >= SignalUpdateStatus.UPDATED) { - fmap_get(id)?.forEach(propagateSignalUpdate) - } else if (this_signal_update_status <= SignalUpdateStatus.PENDING_ASYNC) { - batched_ids_push(id) - } - } - } - - const log_get_request = DEBUG ? (observed_id: FROM, observer_id?: TO | UNTRACKED_ID) => { - const - observed_signal = all_signals_get(observed_id)!, - observer_signal = observer_id ? all_signals_get(observer_id)! : { name: "untracked" } - console.log( - "GET:\t", observed_signal.name, - "\tby OBSERVER:\t", observer_signal.name, - "\twith VALUE\t", observed_signal.value, - ) - } : () => { } - - class BaseSignal { - get: Accessor - set: Setter - id: number - name?: string - - constructor( - public value?: T, - { - name, - equals - }: BaseSignalConfig = {}, - ) { - const - id = ++id_counter, - equals_fn = equals === false ? falsey_equality : (equals ?? default_equality) - // register the new signal - all_signals_set(id, this) - // clear the `ids_to_visit_cache`, because the old cache won't include this new signal in any of this signal's dependency pathways. - // the pathway (ie DFS) has to be re-discovered for this new signal to be included in it - ids_to_visit_cache_clear() - this.id = id - this.name = name - - this.get = (observer_id?: TO | UNTRACKED_ID): T => { - if (observer_id) { - // register this.id to observer - fadd(id, observer_id) - } - if (DEBUG) { log_get_request(id, observer_id) } - return this.value as T - } - - this.set = (new_value: T | Updater): boolean => { - const old_value = this.value - return !equals_fn(old_value, ( - this.value = typeof new_value === "function" ? - (new_value as Updater)(old_value) : - new_value - )) - } - } - - public run(): SignalUpdateStatus { - return SignalUpdateStatus.UPDATED - } - } - - class StateSignal extends BaseSignal { - declare value: T - - constructor( - value: T, - config?: BaseSignalConfig, - ) { - super(value, config) - const - id = this.id, - set_value = this.set - - this.set = (new_value: T | Updater): boolean => { - const value_has_changed = set_value(new_value) - if (value_has_changed) { batch_nestedness <= 0 ? fireUpdateCycle(id) : batched_ids_push(id) } - return value_has_changed - } - } - } - - class MemoSignal extends BaseSignal { - constructor( - fn: MemoFn, - config?: BaseSignalConfig, - ) { - super(config?.value, config) - let rid: TO | UNTRACKED_ID = this.id - const get_value = this.get - const set_value = this.set - const run = (): SignalUpdateStatus => { - return set_value(fn(rid)) ? SignalUpdateStatus.UPDATED : SignalUpdateStatus.NOT_UPDATED - } - const get = (observer_id?: TO | UNTRACKED_ID): T => { - if (rid) { - run() - rid = 0 as UNTRACKED_ID - } - return get_value(observer_id) - } - this.get = get - this.run = run - if (config?.defer === false) { get() } - } - } - - class LazySignal extends BaseSignal { - constructor( - fn: MemoFn, - config?: BaseSignalConfig, - ) { - super(config?.value, config) - let - rid: TO | UNTRACKED_ID = this.id, - dirty: 0 | 1 = 1 - const get_value = this.get - const set_value = this.set - const run = (): SignalUpdateStatus.UPDATED => { - return (dirty = 1) - } - const get = (observer_id?: TO | UNTRACKED_ID): T => { - if (rid || dirty) { - set_value(fn(rid)) - dirty = 1 - rid = 0 as UNTRACKED_ID - } - return get_value(observer_id) - } - this.get = get - this.run = run - if (config?.defer === false) { get() } - } - } - - class EffectSignal extends BaseSignal { - constructor( - fn: EffectFn, - config?: BaseSignalConfig, - ) { - super(undefined, config) - const - id = this.id, - get_value = this.get - let rid: TO | UNTRACKED_ID = id - const run = (): SignalUpdateStatus => { - const signal_should_propagate = fn(rid) !== false - if (rid) { rid = 0 as UNTRACKED_ID } - return signal_should_propagate ? SignalUpdateStatus.UPDATED : SignalUpdateStatus.NOT_UPDATED - } - // 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. - const get = (observer_id?: TO | UNTRACKED_ID): void => { - if (observer_id) { - run() - get_value(observer_id) - } - } - const set = () => { - const effect_will_fire_immediately = batch_nestedness <= 0 - effect_will_fire_immediately ? fireUpdateCycle(id) : batched_ids_push(id) - return effect_will_fire_immediately - } - this.get = get - this.run = run - this.set = set - if (config?.defer === false) { set() } - } - } - - class AsyncMemoSignal extends BaseSignal> { - constructor( - fn: MemoFn>, - config?: BaseSignalConfig>, - ) { - super(config?.value, config) - let - current_promise: undefined | Promise, - rid: TO | UNTRACKED_ID = this.id - const get_value = this.get - const set_value = this.set - const run = (): SignalUpdateStatus => { - current_promise ??= fn(rid).then( - (resolved_value: T) => { - current_promise = undefined - return set_value(resolved_value) ? SignalUpdateStatus.UPDATED : SignalUpdateStatus.NOT_UPDATED - } - ) - - - - return set_value(fn(rid)) ? SignalUpdateStatus.UPDATED : SignalUpdateStatus.NOT_UPDATED - } - const get = (observer_id?: TO | UNTRACKED_ID): T => { - if (rid) { - run() - rid = 0 as UNTRACKED_ID - } - return get_value(observer_id) - } - this.get = get - this.run = run - if (config?.defer === false) { get() } - } - } - - const createState = (value: T, config?: BaseSignalConfig): AccessorSetter => { - const new_signal = new StateSignal(value, config) - return [new_signal.get, new_signal.set] - } - - const createMemo = (fn: MemoFn, config?: BaseSignalConfig): Accessor => { - const new_signal = new MemoSignal(fn, config) - return new_signal.get - } - - const createLazy = (fn: MemoFn, config?: BaseSignalConfig): Accessor => { - const new_signal = new LazySignal(fn, config) - return new_signal.get - } - - const createEffect = (fn: EffectFn, config?: BaseSignalConfig): AccessorEmitter => { - const new_signal = new EffectSignal(fn, config) - return [new_signal.get, new_signal.set] - } - - return { - createState, createMemo, createLazy, createEffect, - startBatching, endBatching, scopedBatching, - } -} - -const - { createState, createMemo, createLazy, createEffect } = createContext(), - [A, setA] = createState(1, { name: "A" }), - [B, setB] = createState(2, { name: "B" }), - [C, setC] = createState(3, { name: "C" }), - H = createMemo((id) => (A(id) + G(id)), { name: "H" }), - G = createMemo((id) => (- D(id) + E(id) + 100), { name: "G" }), - D = createMemo((id) => (A(id) * 0 + 10), { name: "D" }), - E = createMemo((id) => (B(id) + F(id) + D(id) + C(id)), { name: "E" }), - F = createMemo((id) => (C(id) + 20), { name: "F" }), - I = createMemo((id) => (F(id) - 100), { name: "I" }), - [K, fireK] = createEffect((id) => { console.log("this is effect K, broadcasting", A(id), B(id), E(id)); J(id) }, { name: "K" }), - [J, fireJ] = createEffect((id) => { console.log("this is effect J, broadcasting", H(id), D(id), E(id)) }, { name: "J" }) - -I() -H() -K() - -let start = performance.now() -for (let A_value = 0; A_value < 100_000; A_value++) { - setA(A_value) -} -let end = performance.now() -console.log("time:\t", end - start, " ms") // takes 220ms to 300ms for updating signal `A` 100_000 times (with `DEBUG` off) (dfs nodes to update/visit are cached after first run) - -setA(10) -setB(10) - -/* TODO: -7) implement a maximum size for `ids_to_visit_cache`, after which it starts deleting the old entries (or most obscure/least called ones). this size should be set when creating a new context -9) implement gradually comuted signal (i.e. diff-able computaion signal based on each dependency signal's individual change) -11) create tests and examples -12) document how it works through a good graphical illustration -15) when caching `ids_to_visit`, only immediately cache single source `ids_to_visit`, and then when a multi source `ids_to_visit` is requested, combine/merge/union the two sources of single source `ids_to_visit`, and then cache this result too -17) design a cleanup mechanism, a deletion mechanism, and a frozen-delete mechanism (whereby a computed signal's value is forever frozen, and becomes non-reactive and non-propagating, and destroys its own `fn` computation function) -18) consider designing a "compress" (or maybe call it "collapse" or "crumple") function for a given context, where you specify your input/tweekable signals, and then specify the set of output signals, - and then the function will collapse all intermediate computations into topologically ordered and executed functions (bare `fn`s). - naturally, this will not be ideal computation wise, as it will bypass caching of all computed signals' values and force a recomputation on every input-signal update. but it may provide a way for deriving signals out of encapuslated contexts (ie composition) -20) consider if there is a need for a DOM specific signal (one that outputs JSX, but does not create a new JSX/DOM element on every update, but rather it modifies it partially so that it gets reflected directly/immediately in the DOM) -21) it would be nice if we could declare the signal classes outside of the `createContext` function, without losing performance (due to potential property accessing required when accessing the context's `fmap`, `rmap`, etc... local variables). maybe consider a higher order signal-class generator function? -23) add option for `cleanup` function in `BaseSignalConfig` when configuring a newly created signal. its purpose would be to get called when the signal is being destroyed -*/ - -/* DONE list: -1) [DONE] implement `batch` state.set and `untrack` state.set -2) [DONE, albeit not too impressive] implement createContext -3) [DONE, but needs work. see 22)] implement effect signal -4) [DONE] implement lazy signal -5) [DONE] remove dependance on id types -6) [DONE, I suppose...] check collision resistance of your hash_ids function -8) [DONE] implement `isEqual` and `defer: false`/`immediate: true` config options when creating signals -10) [DONE] rename `ReactiveSignal` to a `MemoSignal` -13) [UNPLANNED, because it requires pure Kahn's alorithm with BFS, whereas I'm currently using a combination of DFS for observers and BFS of untouched dependencies that must be visited. blocking propagation will be more dificult with Kahn, and furthermore, will require async awaiting + promises within the propagation cycle, which will lead to overall significant slowdown] develop asynchronous signals -14) [DONE] `ids_to_visit_cache` needs to be entirely flushed whenever ANY new signal is created under ANY circumstances (`ids_to_visit_cache_clear` should be executed in `BaseSignal.constructor`) -16) [DONE] batch set should be more primitive than `StateSignal.set`. in fact `StateSignal.set` should utilize batch setting underneath - alternatively -16b) [UNPLANNED] add an aditional parameter `untrack?: boolean = false` to `StateSignal.set` that avoids propagation of the state signal. -19) [DONE, via `throttledEquals` equality function] implement a throttle signal (ie max-polling limiting) -19b) [UNPLANNED, because debouncing will require the current propagation cycle to remain alive, or will require some form of async signals/async propagation cycles/pending singals, which is unplanned] implement debounce signal (awaits a certain amount of time in which update stop occuring, for the signal to eventually fire), and an interval signal (is this one even necessary? perhaps it will be cleaner to use this instead of setInterval) -22) [DONE] EffectSignal.get results in multiple calls to EffectFn. this is not ideal because you will probably want each effect to run only once per cycle, however, the way it currently is, whenever each observer calls EffectSignal.get, the effect function gets called again. - either consider using a counter/boolean to check if the effect has already run in the current cycle, or use EffectSignal.get purely for observer registration purposes (which may fire EffectFn iff the observer is new). - and use EffectSignal.set for independent firing of EffectFn in addition to its propagation. - this will lead to the following design choice: if `[J, fireJ] = createEffect(...)` and `[K, fireK] = createEffect(() => {J(); ...})` then `fireK()` will NOT result in EffectFn of `J` from running. to run `J` and propagate it to `K`, you will need to `fireJ()` -*/ diff --git a/src/typedefs.ts b/src/typedefs.ts index 1ad8f705..16c39992 100644 --- a/src/typedefs.ts +++ b/src/typedefs.ts @@ -46,6 +46,50 @@ export type PrefixProps = { [K in keyof T & string as `${ /** add a postfix (suffix) `POST` to all property names of object `T` */ export type PostfixProps = { [K in keyof T & string as `${K}${POST}`]: T[K] } +/** allows one to declare static interface `CONSTRUCTOR` that must be implemented by a class `CLASS`
+ * it is important that your `CONSTRUCTOR` static interface must contain a constructor method in it. + * although, that constructor could be super generalized too, despite the other static methods being narrowly defined, like: + * ```ts + * // interface for a class that must implement a static `clone` method, which should return a clone of the provided object `obj`, but omit numeric keys + * interface Cloneable { + * constructor(...args: any[]): any + * clone(obj: T): Omit + * } + * ``` + * to use this utility type, you must provide the static interface as the first parameter, + * and then `typeof CLASS_NAME` (which is the name of the class itself) as the second parameter. + * + * @example + * ```ts + * interface Stack { + * push(...items: T[]): void + * pop(): T | undefined + * clear(): T[] + * } + * interface CloneableStack { + * new (...args: any[]): Stack + * // this static method should remove all function objects from the stack + * clone(original_stack: Stack): Stack> + * } + * const stack_class_alias = class MyStack implements StaticImplements { + * arr: T[] + * constructor(first_item?: T) { + * this.arr = first_item === undefined ? [] : [first_item] + * } + * push(...items: T[]): void { this.arr.push(...items) } + * pop(): T | undefined { return this.arr.pop() } + * clear(): T[] { return this.arr.splice(0) } + * static clone(some_stack: Stack) { + * const arr_no_func = (some_stack as MyStack).arr.filter((v) => typeof v !== "function") as Array> + * const new_stack = new this>() + * new_stack.push(...arr_no_func) + * return new_stack + * } + * } + * ``` +*/ +export type StaticImplements any, CLASS extends CONSTRUCTOR> = InstanceType + /** `DecrementNumber[N]` returns `N-1`, for up to `N = 10` */ export type DecrementNumber = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] diff --git a/test/signal.test.ts b/test/signal.test.ts deleted file mode 100644 index b91a6bb5..00000000 --- a/test/signal.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { assert } from "https://deno.land/std/testing/asserts.ts" -import { createSignal, createMemo, createEffect, batch, untrack, dependsOn, reliesOn, } from "../src/signal.ts" - -Deno.test("create a signal and test getter and setter functions", () => { - const [getValue, setValue] = createSignal(0) - assert(getValue() === 0) - setValue(42) - assert(getValue() === 42) -}) - -Deno.test("create a memo and test its memorization and reactivity with signals", () => { - let counter = 0 - const memoFn = createMemo(() => (counter++)) - assert(memoFn() === 0) - assert(memoFn() === 0) // memoized value remains the same - - const [getCounter, setCounter] = createSignal(0) - let times_memo_was_run: number = 0 - const memoFn2 = createMemo(() => { - times_memo_was_run++ - let double = getCounter() * 2 - return double - }) - assert((memoFn2() === 0) && (times_memo_was_run === 1 as number)) - setCounter(5) - assert((memoFn2() === 10) && (times_memo_was_run === 2 as number)) - setCounter(5.0) // same value as before, therefore, signal should NOT notify its `memoFn2` observer to rerun - assert((memoFn2() === 10) && (times_memo_was_run === 2 as number)) -}) - -Deno.test("create and execute an effect", () => { - let times_effect_was_run = 0 - const effectFn = () => { - times_effect_was_run++ - return () => { - times_effect_was_run-- - } - } - createEffect(effectFn) - assert(times_effect_was_run === 1) - - const [getCounter, setCounter] = createSignal(0) - let times_effect_was_run2: number = 0 - const effectFn2 = () => { - times_effect_was_run2++ - let double = getCounter() * 2 - return undefined - } - createEffect(effectFn2) - assert(times_effect_was_run2 === 1 as number) - setCounter(5) - assert(times_effect_was_run2 === 2 as number) - setCounter(5.0) //same value as before, therefore, signal should NOT notify its `createEffect(effectFn2)` observer to rerun - assert(times_effect_was_run2 === 2 as number) - //TODO trigger cleanup and implement async testing -}) - -Deno.test("batch computations", () => { - let counter = 0 - const [getCounter1, setCounter1] = createSignal(0) - const [getCounter2, setCounter2] = createSignal(0) - const [getCounter3, setCounter3] = createSignal(0) - createEffect(() => batch(() => { - // create dependance on `getCounter1`, `getCounter2` `getCounter3`, and become an observer of all the three - const v1 = getCounter1() - const v2 = getCounter2() - const v3 = getCounter3() - counter++ - })) - setCounter1(1) - setCounter2(2) - setCounter3(3) - assert(counter === 1) - assert((getCounter1() === 1) && (getCounter2() === 2) && (getCounter3() === 3)) -}) - -Deno.test("untrack reactive dependencies", () => { - let counter = 0 - const [getCounter1, setCounter1] = createSignal(0) - const [getCounter2, setCounter2] = createSignal(0) - const [getCounter3, setCounter3] = createSignal(0) - createEffect(() => untrack(() => { - // create dependance on `getCounter1`, `getCounter2` `getCounter3`, and become an observer of all the three - const v1 = getCounter1() - const v2 = getCounter2() - const v3 = getCounter3() - setCounter1(1) - setCounter2(2) - setCounter3(3) - counter++ - })) - assert(counter === 1) - assert((getCounter1() === 1) && (getCounter2() === 2) && (getCounter3() === 3)) -}) - -Deno.test("evaluate function with explicit dependencies", () => { - let counter = 0 - const [getA, setA] = createSignal(0) - const [getB, setB] = createSignal(0) - const [getC, setC] = createSignal(0) - const dependencyFn = createMemo(dependsOn([getA, getB, getC], () => { - counter++ - return getA() + getB() - })) - setA(1) - setB(2) - setC(3) - assert(counter === 4) - assert(dependencyFn() === 3) -}) - -Deno.test("create effect with explicit dependencies", () => { - let counter = 0 - const [getA, setA] = createSignal(0) - const [getB, setB] = createSignal(0) - const [getC, setC] = createSignal(0) - createEffect(dependsOn([getA, getB, getC], () => { - counter++ - })) - assert(counter === 1 as number) - setA(1) - assert(counter === 2 as number) - setB(2) - assert(counter === 3 as number) - setC(3) - assert(counter === 4 as number) -})