From 1aafe6511561360250f32d055dd144d54db17076 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Mon, 4 Mar 2024 11:52:51 +0000 Subject: [PATCH] Remove example-a --- examples/example-a/index.html | 11 - examples/example-a/src/effects.ts | 97 ------ examples/example-a/src/example.test.ts | 356 ---------------------- examples/example-a/src/example.ts | 51 ---- examples/example-a/src/signals.ts | 406 ------------------------- examples/example-a/start.sh | 3 - examples/example-a/vite.config.js | 5 - 7 files changed, 929 deletions(-) delete mode 100644 examples/example-a/index.html delete mode 100644 examples/example-a/src/effects.ts delete mode 100644 examples/example-a/src/example.test.ts delete mode 100644 examples/example-a/src/example.ts delete mode 100644 examples/example-a/src/signals.ts delete mode 100755 examples/example-a/start.sh delete mode 100644 examples/example-a/vite.config.js diff --git a/examples/example-a/index.html b/examples/example-a/index.html deleted file mode 100644 index 923b78e..0000000 --- a/examples/example-a/index.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - Signals Example - - - -

Signals Example

- - - diff --git a/examples/example-a/src/effects.ts b/examples/example-a/src/effects.ts deleted file mode 100644 index 112098d..0000000 --- a/examples/example-a/src/effects.ts +++ /dev/null @@ -1,97 +0,0 @@ -import type { Effect } from "./signals"; -import * as Signal from "./signals"; - -let queue: null | Effect[] = null; -let flush_count = 0; -let current_effect: null | ExampleFrameworkEffect = null; - -function pushChild( - target: ExampleFrameworkEffect, - child: ExampleFrameworkEffect -): void { - const children = target.children; - if (children === null) { - target.children = [child]; - } else { - children.push(child); - } -} - -function destroyEffectChildren(signal: ExampleFrameworkEffect): void { - const children = signal.children; - signal.children = null; - if (children !== null) { - for (let i = 0; i < children.length; i++) { - const child = children[i]; - child[Symbol.dispose](); - } - } -} - -export class ExampleFrameworkEffect extends Signal.Effect { - dispose: void | (() => void); - children: null | ExampleFrameworkEffect[]; - - constructor(callback: () => void, notify: (effect: Effect) => void) { - super( - () => { - destroyEffectChildren(this); - if (typeof this.dispose === "function") { - this.dispose(); - } - const previous_effect = current_effect; - try { - current_effect = this; - this.dispose = callback(); - } finally { - current_effect = previous_effect; - } - }, - { - cleanup: () => { - destroyEffectChildren(this); - if (typeof this.dispose === "function") { - this.dispose(); - } - }, - notify, - } - ); - this.dispose = undefined; - this.children = null; - if (current_effect !== null) { - pushChild(current_effect, this); - } - } -} - -function flushQueue() { - const effects = queue as Effect[]; - queue = null; - if (flush_count === 0) { - setTimeout(() => { - flush_count = 0; - }); - } else if (flush_count > 1000) { - throw new Error("Possible infinite loop detected."); - } - flush_count++; - for (let i = 0; i < effects.length; i++) { - const e = effects[i]; - e.get(); - } -} - -function enqueueSignal(signal: Effect): void { - if (queue === null) { - queue = []; - queueMicrotask(flushQueue); - } - queue.push(signal); -} - -export function effect(cb: () => void | (() => void)) { - let e = new ExampleFrameworkEffect(cb, enqueueSignal); - e.get(); - return () => e[Symbol.dispose](); -} diff --git a/examples/example-a/src/example.test.ts b/examples/example-a/src/example.test.ts deleted file mode 100644 index 2f95599..0000000 --- a/examples/example-a/src/example.test.ts +++ /dev/null @@ -1,356 +0,0 @@ -import { expect, test } from "vitest"; -import * as Signal from "./signals"; -import { ExampleFrameworkEffect } from "./effects"; - -test("state signal can be written and read from", () => { - const a = new Signal.State(0); - - expect(a.get()).toBe(0); - - a.set(1); - - expect(a.get()).toBe(1); -}); - -test("computed signal can be read from", () => { - const a = new Signal.State(0); - const b = new Signal.Computed(() => a.get() * 2); - - expect(a.get()).toBe(0); - expect(b.get()).toBe(0); - - a.set(1); - - expect(a.get()).toBe(1); - expect(b.get()).toBe(2); -}); - -test("effect signal can be read from", () => { - const a = new Signal.State(0); - const b = new Signal.Computed(() => a.get() * 2); - const c = new Signal.Effect(() => b.get()); - - expect(a.get()).toBe(0); - expect(b.get()).toBe(0); - expect(c.get()).toBe(0); - - a.set(1); - - expect(a.get()).toBe(1); - expect(b.get()).toBe(2); - expect(c.get()).toBe(2); -}); - -test("effect signal can notify changes", () => { - let is_dirty = false; - - const a = new Signal.State(0); - const b = new Signal.Computed(() => a.get() * 0); - const c = new Signal.Effect(() => b.get(), { - notify: () => (is_dirty = true), - }); - - c.get(); - - a.set(1); - - c[Symbol.dispose](); - - expect(is_dirty).toBe(true); - - is_dirty = false; - - a.set(1); - - // State hasn't changed - expect(is_dirty).toBe(false); - - a.set(2); - - // Computed hasn't changed - expect(is_dirty).toBe(false); -}); - -test("example framework effect signals can be nested", () => { - let log: string[] = []; - - const a = new Signal.State(0); - const b = new ExampleFrameworkEffect( - () => { - log.push("b update " + a.get()); - const c = new ExampleFrameworkEffect( - () => { - log.push("c create " + a.get()); - - return () => { - log.push("c cleanup " + a.get()); - }; - }, - () => {} - ); - c.get(); - - return () => { - log.push("b cleanup " + a.get()); - }; - }, - () => {} - ); - - b.get(); - - a.set(1); - - b.get(); - - a.set(2); - - b[Symbol.dispose](); - - expect(log).toEqual([ - "b update 0", - "c create 0", - "c cleanup 1", - "b cleanup 1", - "b update 1", - "c create 1", - "c cleanup 2", - "b cleanup 2", - ]); -}); - -test("effect signal should trigger oncleanup and correctly disconnect from graph", () => { - let cleanups: string[] = []; - - const a = new Signal.State(0); - const b = new Signal.Computed(() => a.get() * 0); - const c = new Signal.Effect(() => b.get(), { - notify: () => {}, - }); - - c.cleanup = () => { - cleanups.push("c"); - }; - - expect(cleanups).toEqual([]); - - c.get(); - - expect(a.sinks?.length).toBe(1); - expect(b.sinks?.length).toBe(1); - expect(cleanups).toEqual([]); - - cleanups = []; - - c[Symbol.dispose](); - - expect(a.sinks).toBe(null); - expect(b.sinks).toBe(null); - expect(cleanups).toEqual(["c"]); -}); - -test("effect signal should propogate correctly with computed signals", () => { - let log: string[] = []; - let effects: Signal.Effect[] = []; - - const queueEffect = (signal) => { - effects.push(signal); - }; - - const flush = () => { - effects.forEach((e) => e.get()); - effects = []; - }; - - const count = new Signal.State(0); - const double = new Signal.Computed(() => count.get() * 2); - const triple = new Signal.Computed(() => count.get() * 3); - const quintuple = new Signal.Computed(() => double.get() + triple.get()); - - const a = new Signal.Effect( - () => { - log.push("four"); - log.push( - `${count.get()}:${double.get()}:${triple.get()}:${quintuple.get()}` - ); - }, - { - notify: queueEffect, - } - ); - a.get(); - const b = new Signal.Effect( - () => { - log.push("three"); - log.push(`${double.get()}:${triple.get()}:${quintuple.get()}`); - }, - { - notify: queueEffect, - } - ); - b.get(); - const c = new Signal.Effect( - () => { - log.push("two"); - log.push(`${count.get()}:${double.get()}`); - }, - { - notify: queueEffect, - } - ); - c.get(); - const d = new Signal.Effect( - () => { - log.push("one"); - log.push(`${double.get()}`); - }, - { - notify: queueEffect, - } - ); - d.get(); - - expect(log).toEqual([ - "four", - "0:0:0:0", - "three", - "0:0:0", - "two", - "0:0", - "one", - "0", - ]); - - log = []; - - count.set(1); - flush(); - - expect(log).toEqual([ - "four", - "1:2:3:5", - "three", - "2:3:5", - "two", - "1:2", - "one", - "2", - ]); - - a[Symbol.dispose](); - b[Symbol.dispose](); - c[Symbol.dispose](); - d[Symbol.dispose](); -}); - -test("effect signal should notify only once", () => { - let log: string[] = []; - - const a = new Signal.State(0); - const b = new Signal.Computed(() => a.get() * 2); - const c = new Signal.Effect( - () => { - a.get(); - b.get(); - log.push("effect ran"); - }, - { - notify: () => { - log.push("notified"); - }, - } - ); - - expect(log).toEqual([]); - - c.get(); - - expect(log).toEqual(["effect ran"]); - - a.set(1); - c.get(); - - expect(log).toEqual(["effect ran", "notified", "effect ran"]); - - c[Symbol.dispose](); -}); - -test("https://perf.js.hyoo.ru/#!bench=9h2as6_u0mfnn", () => { - let res: number[] = []; - let effects: Signal.Effect[] = []; - - const queueEffect = (signal) => { - effects.push(signal); - }; - - const flush = () => { - effects.forEach((e) => e.get()); - effects = []; - }; - - const numbers = Array.from({ length: 2 }, (_, i) => i); - const fib = (n: number): number => (n < 2 ? 1 : fib(n - 1) + fib(n - 2)); - const hard = (n: number, l: string) => n + fib(16); - - const A = new Signal.State(0); - const B = new Signal.State(0); - const C = new Signal.Computed(() => (A.get() % 2) + (B.get() % 2)); - const D = new Signal.Computed( - () => numbers.map((i) => i + (A.get() % 2) - (B.get() % 2)), - { equals: (l, r) => l.length === r.length && l.every((v, i) => v === r[i]) } - ); - const E = new Signal.Computed(() => - hard(C.get() + A.get() + D.get()[0]!, "E") - ); - const F = new Signal.Computed(() => hard(D.get()[0]! && B.get(), "F")); - const G = new Signal.Computed( - () => C.get() + (C.get() || E.get() % 2) + D.get()[0]! + F.get() - ); - let H = new Signal.Effect( - () => { - res.push(hard(G.get(), "H")); - }, - { - notify: queueEffect, - } - ); - let I = new Signal.Effect( - () => { - res.push(G.get()); - }, - { - notify: queueEffect, - } - ); - let J = new Signal.Effect( - () => { - res.push(hard(F.get(), "J")); - }, - { - notify: queueEffect, - } - ); - - H.get(); - I.get(); - J.get(); - - let i = 2; - while (--i) { - res.length = 0; - B.set(1); - A.set(1 + i * 2); - flush(); - - A.set(2 + i * 2); - B.set(2); - flush(); - - expect(res.length).toBe(4); - expect(res).toEqual([3198, 1601, 3195, 1598]); - } - - H[Symbol.dispose](); - I[Symbol.dispose](); - J[Symbol.dispose](); -}); diff --git a/examples/example-a/src/example.ts b/examples/example-a/src/example.ts deleted file mode 100644 index 9edc0e0..0000000 --- a/examples/example-a/src/example.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { effect } from "./effects"; -import * as Signal from "./signals"; - -// Setup UI - -const container = document.createElement("div"); -const element = document.createElement("div"); -const button = document.createElement("button"); -button.innerText = "Increment number"; -container.appendChild(element); -container.appendChild(button); -document.body.appendChild(container); - -// Example taken from README.md - -const counter = new Signal.State(0); -const isEven = new Signal.Computed(() => (counter.get() & 1) == 0); -const parity = new Signal.Computed(() => (isEven.get() ? "even" : "odd")); - -effect(() => { - element.innerText = `Counter: ${counter.get()}\nParity: ${parity.get()}`; -}); - -effect(() => { - Signal.unsafe.untrack(() => { - counter.set(counter.get() + 1); - }); -}); - -// Expanded example showing how effects work - -effect(() => { - console.log("update main effect", counter.get()); - - // Nested effects - effect(() => { - console.log("create nested effect"); - - return () => { - console.log("cleanup nested effect"); - }; - }); - - return () => { - console.log("cleanup main effect"); - }; -}); - -button.addEventListener("click", () => { - counter.set(counter.get() + 1); -}); diff --git a/examples/example-a/src/signals.ts b/examples/example-a/src/signals.ts deleted file mode 100644 index 34c68bb..0000000 --- a/examples/example-a/src/signals.ts +++ /dev/null @@ -1,406 +0,0 @@ -const CLEAN = 0; -const MAYBE_DIRTY = 1; -const DIRTY = 2; -const DESTROYED = 3; - -const UNINITIALIZED = Symbol(); -const MAX_SAFE_INT = Number.MAX_SAFE_INTEGER; - -let current_sink: null | Computed | Effect = null; -let current_effect: null | Effect = null; -let current_sources: null | Signal[] = null; -let current_sources_index = 0; -let current_untracking = false; -// To prevent leaking when accumulating sinks during computed.get(), -// we can track unowned computed signals and skip their sinks -// accordingly in the cases where there's no current parent effect -// when reading the computed via computed.get(). -let current_skip_sink = false; -// Used to prevent over-subscribing dependencies on a sink -let current_sink_read_clock = 1; -let current_read_clock = 1; -let current_write_clock = 1; - -type SignalStatus = - | typeof DIRTY - | typeof MAYBE_DIRTY - | typeof CLEAN - | typeof DESTROYED; - -export type StateOptions = { - equals?: (a: T, b: T) => boolean; -}; - -export type ComputedOptions = { - equals?: (a: T, b: T) => boolean; -}; - -export type EffectOptions = { - notify?: (signal: Effect) => void; - cleanup?: (signal: Effect) => void; -}; - -function markSignalSinks( - signal: Signal, - toStatus: SignalStatus, - forceNotify: boolean -): void { - const sinks = signal.sinks; - if (sinks !== null) { - for (const sink of sinks) { - const status = sink.status; - const isEffect = sink instanceof Effect; - if ( - status === DIRTY || - (isEffect && !forceNotify && sink === current_effect) - ) { - continue; - } - sink.status = toStatus; - if (status === CLEAN) { - if (isEffect) { - notifyEffect(sink); - } else { - markSignalSinks(sink, MAYBE_DIRTY, forceNotify); - } - } - } - } -} - -function updateEffectSignal(signal: Effect): void { - const previous_effect = current_effect; - current_effect = signal; - try { - signal.value = executeSignalCallback(signal); - } finally { - current_effect = previous_effect; - } -} - -function isSignalDirty(signal: Computed | Effect): boolean { - const status = signal.status; - if (status === DIRTY) { - return true; - } - if (status === MAYBE_DIRTY) { - const sources = signal.sources; - if (sources !== null) { - for (const source of sources) { - const sourceStatus = source.status; - - if (source instanceof State) { - if (sourceStatus === DIRTY) { - source.status = CLEAN; - return true; - } - continue; - } - if ( - sourceStatus === MAYBE_DIRTY && - !isSignalDirty(source as Computed) - ) { - source.status = CLEAN; - continue; - } - if (source.status === DIRTY) { - updateComputedSignal(source as Computed, true); - // Might have been mutated from above get. - if (signal.status === DIRTY) { - return true; - } - } - } - } - } - return false; -} - -function removeSink( - signal: Computed | Effect, - startIndex: number, - removeUnowned: boolean -): void { - const sources = signal.sources; - if (sources !== null) { - for (let i = startIndex; i < sources.length; i++) { - const source = sources[i]; - const sinks = source.sinks; - let sinksSize = 0; - if (sinks !== null) { - sinksSize = sinks.length - 1; - if (sinksSize === 0) { - source.sinks = null; - } else { - const index = sinks.indexOf(signal); - // Swap with last element and then remove. - sinks[index] = sinks[sinksSize]; - sinks.pop(); - } - } - if ( - removeUnowned && - sinksSize === 0 && - source instanceof Computed && - source.unowned - ) { - removeSink(source, 0, true); - } - } - } -} - -function destroySignal(signal: Signal): void { - const value = signal.value; - signal.status = DESTROYED; - signal.value = UNINITIALIZED as T; - signal.equals = Object.is; - signal.sinks = null; - if (signal instanceof Effect) { - removeSink(signal, 0, true); - signal.sources = null; - const cleanup = signal.cleanup; - if (value !== UNINITIALIZED && typeof cleanup === "function") { - cleanup.call(signal); - } - signal.cleanup = null; - signal.notify = null; - } else if (signal instanceof Computed) { - removeSink(signal, 0, true); - signal.sources = null; - } -} - -function updateComputedSignal( - signal: Computed, - forceNotify: boolean -): void { - signal.status = - current_skip_sink || (current_effect === null && signal.unowned) - ? DIRTY - : CLEAN; - const value = executeSignalCallback(signal); - const equals = signal.equals!; - if (!equals.call(signal, signal.value, value)) { - signal.value = value; - markSignalSinks(signal, DIRTY, forceNotify); - } -} - -function executeSignalCallback(signal: Computed | Effect): T { - const previous_sources = current_sources; - const previous_sink = current_sink; - const previous_sources_index = current_sources_index; - const previous_skip_sink = current_skip_sink; - const previous_sink_read_clock = current_sink_read_clock; - const is_unowned = signal instanceof Computed && signal.unowned; - current_sources = null as null | Signal[]; - current_sources_index = 0; - current_sink = signal; - current_skip_sink = current_effect === null && is_unowned; - if (current_read_clock === MAX_SAFE_INT) { - current_read_clock = 1; - } else { - current_read_clock++; - } - current_sink_read_clock = current_read_clock; - - try { - const value = signal.callback(); - let sources = signal.sources; - - if (current_sources !== null) { - removeSink(signal, current_sources_index, false); - - if (sources !== null && current_sources_index > 0) { - sources.length = current_sources_index + current_sources.length; - for (let i = 0; i < current_sources.length; i++) { - sources[current_sources_index + i] = current_sources[i]; - } - } else { - signal.sources = sources = current_sources; - } - - if (!current_skip_sink) { - for (let i = current_sources_index; i < sources.length; i++) { - const source = sources[i]; - - if (source.sinks === null) { - source.sinks = [signal]; - } else { - source.sinks.push(signal); - } - } - } - } else if (sources !== null && current_sources_index < sources.length) { - removeSink(signal, current_sources_index, false); - sources.length = current_sources_index; - } - - return value as T; - } finally { - current_sources = previous_sources; - current_sources_index = previous_sources_index; - current_sink = previous_sink; - current_skip_sink = previous_skip_sink; - current_sink_read_clock = previous_sink_read_clock; - } -} - -function updateSignalSources(signal: Signal): void { - if (signal.status === DESTROYED) { - return; - } - // Register the source on the current sink signal. - if (current_sink !== null && !current_untracking) { - const sources = current_sink.sources; - const unowned = current_sink instanceof Computed && current_sink.unowned; - if ( - current_sources === null && - sources !== null && - sources[current_sources_index] === signal && - !(unowned && current_effect) - ) { - current_sources_index++; - } else if (current_sources === null) { - current_sources = [signal]; - } else if (signal.readClock !== current_sink_read_clock) { - current_sources.push(signal); - } - if (!unowned) { - signal.readClock = current_sink_read_clock; - } - } -} - -function notifyEffect(signal: Effect): void { - const notify = signal.notify; - if (notify !== null) { - notify.call(signal, signal); - } -} - -class Signal { - sinks: null | Array | Effect>; - equals: (a: T, b: T) => boolean; - status: SignalStatus; - value: T; - readClock: number; - - constructor(value: T, options?: StateOptions) { - this.sinks = null; - this.equals = options?.equals || Object.is; - this.status = DIRTY; - this.value = value; - this.readClock = 0; - } - - get(): T { - updateSignalSources(this); - return this.value; - } -} - -export class State extends Signal { - set(value: T): void { - if (!current_untracking && current_sink instanceof Computed) { - throw new Error( - "Writing to state signals during the computation phase (from within computed signals) is not permitted." - ); - } - if (!this.equals.call(this, this.value, value)) { - this.value = value; - if (current_write_clock === MAX_SAFE_INT) { - current_write_clock = 1; - } else { - current_write_clock++; - } - if ( - current_effect !== null && - current_effect.sinks === null && - current_effect.status === CLEAN && - current_sources !== null && - current_sources.includes(this) - ) { - current_effect.status = DIRTY; - notifyEffect(current_effect); - } - markSignalSinks(this, DIRTY, true); - } - } -} - -export class Computed extends Signal { - callback: () => T; - sources: null | Signal[]; - unowned: boolean; - - constructor(callback: () => T, options?: ComputedOptions) { - super(UNINITIALIZED as T, options); - const unowned = current_effect === null; - this.callback = callback; - this.sources = null; - this.unowned = unowned; - } - - get(): T { - updateSignalSources(this); - if (isSignalDirty(this)) { - updateComputedSignal(this, false); - } - return this.value; - } -} - -export class Effect extends Signal { - callback: () => void; - sources: null | Signal[]; - notify: null | ((signal: Effect) => void); - cleanup: null | ((signal: Effect) => void); - - constructor(callback: () => void, options?: EffectOptions) { - super(UNINITIALIZED as T); - this.callback = callback; - this.sources = null; - this.notify = options?.notify ?? null; - this.cleanup = options?.cleanup ?? null; - } - - [Symbol.dispose]() { - if (this.status !== DESTROYED) { - destroySignal(this); - } - } - - get(): T { - if (current_sink instanceof Computed) { - throw new Error( - "Reading effect signals during the computation phase (from within computed signals) is not permitted." - ); - } - if (this.status === DESTROYED) { - throw new Error( - "Cannot call get() on effect signals that have already been disposed." - ); - } - if (isSignalDirty(this)) { - this.status = CLEAN; - updateEffectSignal(this); - } - return this.value; - } -} - -function untrack(fn: () => T): T { - const previous_untracking = current_untracking; - try { - current_untracking = true; - return fn(); - } finally { - current_untracking = previous_untracking; - } -} - -export const unsafe = { - untrack, -}; diff --git a/examples/example-a/start.sh b/examples/example-a/start.sh deleted file mode 100755 index 563a5a9..0000000 --- a/examples/example-a/start.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -npx vite diff --git a/examples/example-a/vite.config.js b/examples/example-a/vite.config.js deleted file mode 100644 index 2e63ffc..0000000 --- a/examples/example-a/vite.config.js +++ /dev/null @@ -1,5 +0,0 @@ -import { defineConfig } from 'vite'; - -export default defineConfig({ - // config here -})