@zerolith/state 0.0.0-stage → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/stored.js ADDED
@@ -0,0 +1,158 @@
1
+ /**
2
+ * A signal kept in the browser's storage under a key, as JSON, and in step
3
+ * across tabs through the `storage` event.
4
+ */
5
+ import { diagnostic, effect, hasOwner, onDispose, report, signal } from "zerolith";
6
+ /** The storage kinds reported unavailable on this page, each once. */
7
+ const unavailable = new Set();
8
+ /** How many stored signals are live for each key of each storage, so a second one for a key is reported. */
9
+ const live = new Map();
10
+ /** What runs after a stored signal takes a value another tab wrote, keyed by the signal. */
11
+ const outside = new WeakMap();
12
+ /**
13
+ * Runs `listener` each time `source` takes a value another tab wrote, which
14
+ * only a stored signal does, and returns the function that stops it. The
15
+ * history uses it; the package does not export it.
16
+ */
17
+ export function onOutsideWrite(source, listener) {
18
+ let listeners = outside.get(source);
19
+ if (listeners === undefined) {
20
+ listeners = new Set();
21
+ outside.set(source, listeners);
22
+ }
23
+ listeners.add(listener);
24
+ return () => {
25
+ listeners.delete(listener);
26
+ };
27
+ }
28
+ /** The storage of a kind, or undefined where the page has none or refuses it, which is reported once per kind with the first key asked for. */
29
+ function storageOf(kind, key) {
30
+ try {
31
+ const store = kind === "session" ? globalThis.sessionStorage : globalThis.localStorage;
32
+ store.getItem("");
33
+ return store;
34
+ }
35
+ catch {
36
+ if (!unavailable.has(kind)) {
37
+ unavailable.add(kind);
38
+ report(diagnostic("state_storage_unavailable", "warning", { storage: kind, key }));
39
+ }
40
+ return undefined;
41
+ }
42
+ }
43
+ /** The value kept as `raw` under `key`, or `initial` where nothing is kept, or where what is kept does not parse or `validate` refuses it, which is reported. */
44
+ function parse(raw, key, initial, validate) {
45
+ if (raw === null) {
46
+ return initial;
47
+ }
48
+ let value;
49
+ let accepted;
50
+ try {
51
+ value = JSON.parse(raw);
52
+ accepted = validate === undefined || validate(value);
53
+ }
54
+ catch {
55
+ accepted = false;
56
+ }
57
+ if (!accepted) {
58
+ report(diagnostic("state_storage_unreadable", "warning", { key }));
59
+ return initial;
60
+ }
61
+ return value;
62
+ }
63
+ /** The JSON for `next`, or undefined for a value JSON cannot hold: a BigInt or a cycle, which throws, and a function or a symbol, which it leaves out. */
64
+ function toJson(next) {
65
+ try {
66
+ return JSON.stringify(next);
67
+ }
68
+ catch {
69
+ return undefined;
70
+ }
71
+ }
72
+ /**
73
+ * A writable signal kept in storage under `key` as JSON: read back on
74
+ * creation, written on every set, and set again when another tab writes
75
+ * the key, so every tab shows the same value. A missing value takes
76
+ * `initial` quietly; one that does not parse, or that the `validate`
77
+ * option refuses, takes `initial` and reports `state_storage_unreadable`. Setting `undefined` removes the key, so it
78
+ * reads back as `initial`; a value JSON cannot hold, such as a BigInt,
79
+ * stays in memory and reports `state_value_not_json`. Where the page has
80
+ * no storage, as on the server, the signal is a plain one and
81
+ * `state_storage_unavailable` is reported once, as it is for each write
82
+ * the storage refuses. Create each key once and share the signal: two
83
+ * stored signals for one key in the same tab do not hear each other,
84
+ * since the storage event tells other tabs alone, and the second reports
85
+ * `state_key_duplicate`. The write and the listener for other tabs are
86
+ * disposed with the enclosing owner when there is one, after which the
87
+ * signal keeps its value in memory alone; created in plain code, they
88
+ * live for the page.
89
+ */
90
+ export function stored(key, initial, options = {}) {
91
+ const kind = options.storage ?? "local";
92
+ const store = storageOf(kind, key);
93
+ const value = signal(store === undefined ? initial : parse(store.getItem(key), key, initial, options.validate), options.equals === undefined ? undefined : { equals: options.equals });
94
+ if (store === undefined) {
95
+ return value;
96
+ }
97
+ const id = `${kind}:${key}`;
98
+ const count = (live.get(id) ?? 0) + 1;
99
+ live.set(id, count);
100
+ if (count > 1) {
101
+ report(diagnostic("state_key_duplicate", "warning", { storage: kind, key }));
102
+ }
103
+ let first = true;
104
+ let echo = false;
105
+ effect(() => {
106
+ const next = value.get();
107
+ if (first || echo) {
108
+ first = false;
109
+ return;
110
+ }
111
+ const json = toJson(next);
112
+ if (json === undefined && next !== undefined) {
113
+ report(diagnostic("state_value_not_json", "warning", { key }));
114
+ return;
115
+ }
116
+ try {
117
+ if (json === undefined) {
118
+ store.removeItem(key);
119
+ }
120
+ else {
121
+ store.setItem(key, json);
122
+ }
123
+ }
124
+ catch {
125
+ report(diagnostic("state_storage_unavailable", "warning", { storage: kind, key }));
126
+ }
127
+ });
128
+ const onStorage = (event) => {
129
+ if (event.storageArea !== store || (event.key !== null && event.key !== key)) {
130
+ return;
131
+ }
132
+ echo = true;
133
+ try {
134
+ value.set(parse(event.newValue, key, initial, options.validate));
135
+ }
136
+ finally {
137
+ echo = false;
138
+ }
139
+ for (const listener of outside.get(value) ?? []) {
140
+ listener();
141
+ }
142
+ };
143
+ addEventListener("storage", onStorage);
144
+ if (hasOwner()) {
145
+ onDispose(() => {
146
+ removeEventListener("storage", onStorage);
147
+ const left = (live.get(id) ?? 1) - 1;
148
+ if (left > 0) {
149
+ live.set(id, left);
150
+ }
151
+ else {
152
+ live.delete(id);
153
+ }
154
+ });
155
+ }
156
+ return value;
157
+ }
158
+ //# sourceMappingURL=stored.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stored.js","sourceRoot":"","sources":["../src/stored.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Signals that follow another in their own time: after a quiet spell, or
3
+ * at most so often.
4
+ */
5
+ import { type ReadableSignal } from "zerolith";
6
+ /**
7
+ * A signal that follows `source` once it has been quiet for `ms`
8
+ * milliseconds, starting at its current value, so a search runs for the
9
+ * text a reader stopped typing rather than for every keystroke. The
10
+ * pending change is dropped with the enclosing owner when there is one.
11
+ */
12
+ export declare function debounced<T>(source: ReadableSignal<T>, ms: number): ReadableSignal<T>;
13
+ /**
14
+ * A signal that follows `source` at most once per `ms` milliseconds: the
15
+ * first change in a window shows at once, and the last one in it shows
16
+ * when the window ends, so a scroll position drives work at a steady rate.
17
+ * A change still pending is dropped when a newer one shows after the
18
+ * window, and with the enclosing owner when there is one.
19
+ */
20
+ export declare function throttled<T>(source: ReadableSignal<T>, ms: number): ReadableSignal<T>;
21
+ //# sourceMappingURL=timing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timing.d.ts","sourceRoot":"","sources":["../src/timing.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,KAAK,cAAc,EAAkD,MAAM,UAAU,CAAC;AAE/F;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,GAAG,cAAc,CAAC,CAAC,CAAC,CAgBrF;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,GAAG,cAAc,CAAC,CAAC,CAAC,CA4CrF"}
package/dist/timing.js ADDED
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Signals that follow another in their own time: after a quiet spell, or
3
+ * at most so often.
4
+ */
5
+ import { effect, hasOwner, onDispose, signal, untracked } from "zerolith";
6
+ /**
7
+ * A signal that follows `source` once it has been quiet for `ms`
8
+ * milliseconds, starting at its current value, so a search runs for the
9
+ * text a reader stopped typing rather than for every keystroke. The
10
+ * pending change is dropped with the enclosing owner when there is one.
11
+ */
12
+ export function debounced(source, ms) {
13
+ const value = signal(untracked(() => source.get()));
14
+ let first = true;
15
+ effect(() => {
16
+ const next = source.get();
17
+ if (first) {
18
+ first = false;
19
+ return undefined;
20
+ }
21
+ const timer = setTimeout(() => value.set(next), ms);
22
+ return () => clearTimeout(timer);
23
+ });
24
+ return value;
25
+ }
26
+ /**
27
+ * A signal that follows `source` at most once per `ms` milliseconds: the
28
+ * first change in a window shows at once, and the last one in it shows
29
+ * when the window ends, so a scroll position drives work at a steady rate.
30
+ * A change still pending is dropped when a newer one shows after the
31
+ * window, and with the enclosing owner when there is one.
32
+ */
33
+ export function throttled(source, ms) {
34
+ const value = signal(untracked(() => source.get()));
35
+ let last = Number.NEGATIVE_INFINITY;
36
+ let timer;
37
+ let pending;
38
+ let first = true;
39
+ const clear = () => {
40
+ if (timer !== undefined) {
41
+ clearTimeout(timer);
42
+ timer = undefined;
43
+ }
44
+ };
45
+ effect(() => {
46
+ const next = source.get();
47
+ if (first) {
48
+ first = false;
49
+ return;
50
+ }
51
+ const now = Date.now();
52
+ if (now - last >= ms) {
53
+ clear();
54
+ last = now;
55
+ value.set(next);
56
+ return;
57
+ }
58
+ pending = next;
59
+ timer ??= setTimeout(() => {
60
+ timer = undefined;
61
+ last = Date.now();
62
+ value.set(pending);
63
+ }, ms - (now - last));
64
+ });
65
+ if (hasOwner()) {
66
+ onDispose(clear);
67
+ }
68
+ return value;
69
+ }
70
+ //# sourceMappingURL=timing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timing.js","sourceRoot":"","sources":["../src/timing.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAuB,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAE/F;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CAAI,MAAyB,EAAE,EAAU;IAE9D,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IACpD,IAAI,KAAK,GAAG,IAAI,CAAC;IACjB,MAAM,CAAC,GAAG,EAAE;QAER,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,EAAE,CAAC;QAC1B,IAAI,KAAK,EACT,CAAC;YACG,KAAK,GAAG,KAAK,CAAC;YACd,OAAO,SAAS,CAAC;QACrB,CAAC;QACD,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;QACpD,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;IACH,OAAO,KAAK,CAAC;AACjB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,SAAS,CAAI,MAAyB,EAAE,EAAU;IAE9D,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IACpD,IAAI,IAAI,GAAG,MAAM,CAAC,iBAAiB,CAAC;IACpC,IAAI,KAAgD,CAAC;IACrD,IAAI,OAAU,CAAC;IACf,IAAI,KAAK,GAAG,IAAI,CAAC;IACjB,MAAM,KAAK,GAAG,GAAS,EAAE;QAErB,IAAI,KAAK,KAAK,SAAS,EACvB,CAAC;YACG,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,KAAK,GAAG,SAAS,CAAC;QACtB,CAAC;IACL,CAAC,CAAC;IACF,MAAM,CAAC,GAAG,EAAE;QAER,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,EAAE,CAAC;QAC1B,IAAI,KAAK,EACT,CAAC;YACG,KAAK,GAAG,KAAK,CAAC;YACd,OAAO;QACX,CAAC;QACD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,IAAI,GAAG,GAAG,IAAI,IAAI,EAAE,EACpB,CAAC;YACG,KAAK,EAAE,CAAC;YACR,IAAI,GAAG,GAAG,CAAC;YACX,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAChB,OAAO;QACX,CAAC;QACD,OAAO,GAAG,IAAI,CAAC;QACf,KAAK,KAAK,UAAU,CAAC,GAAG,EAAE;YAEtB,KAAK,GAAG,SAAS,CAAC;YAClB,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YAClB,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACvB,CAAC,EAAE,EAAE,GAAG,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC;IAC1B,CAAC,CAAC,CAAC;IACH,IAAI,QAAQ,EAAE,EACd,CAAC;QACG,SAAS,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;IACD,OAAO,KAAK,CAAC;AACjB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,52 @@
1
1
  {
2
- "name": "@zerolith/state",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@zerolith/state",
3
+ "version": "1.2.0",
4
+ "description": "Signals with a memory for zerolith: a signal kept in storage and across tabs, undo and redo, debounced and throttled signals, and a row selector",
5
+ "license": "Apache-2.0",
6
+ "author": "molexxxx",
7
+ "homepage": "https://zerolith.dev/",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/molexxxx/zerolith.git",
11
+ "directory": "packages/state"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/molexxxx/zerolith/issues"
15
+ },
16
+ "keywords": [
17
+ "zerolith",
18
+ "signals",
19
+ "state",
20
+ "storage",
21
+ "undo",
22
+ "debounce"
23
+ ],
24
+ "type": "module",
25
+ "sideEffects": false,
26
+ "engines": {
27
+ "node": ">=24.0.0"
28
+ },
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/index.d.ts",
32
+ "development": "./dist/index.js",
33
+ "default": "./dist/production/index.js"
34
+ },
35
+ "./package.json": "./package.json"
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "src",
40
+ "LICENSE",
41
+ "README.md"
42
+ ],
43
+ "scripts": {
44
+ "build": "tsc -p tsconfig.build.json && node ../../scripts/production.ts"
45
+ },
46
+ "peerDependencies": {
47
+ "zerolith": "^1.2.0"
48
+ },
49
+ "devDependencies": {
50
+ "zerolith": "^1.2.0"
51
+ }
52
+ }
package/src/history.ts ADDED
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Undo and redo for a signal, from explicit checkpoints, as an editor
3
+ * keeps them.
4
+ */
5
+ import { type ReadableSignal, type Signal, batch, hasOwner, onDispose, signal, untracked } from "zerolith";
6
+ import { onOutsideWrite } from "./stored.js";
7
+
8
+ /** Options for `history`. */
9
+ export interface HistoryOptions
10
+ {
11
+ /** How many checkpoints are kept; the oldest goes when one more is saved. Defaults to 50, which a value that is not a finite number keeps. */
12
+ limit?: number;
13
+ }
14
+
15
+ /** The undo and redo of one signal. */
16
+ export interface History<T>
17
+ {
18
+ /** Saves the signal's current value as the point `undo()` goes back to, and drops what `redo()` could have brought back. */
19
+ checkpoint(): void;
20
+ /** Sets the signal back to the last checkpoint, keeping the current value for `redo()`; false when there is no checkpoint. */
21
+ undo(): boolean;
22
+ /** Sets the signal forward to the value the last `undo()` left; false when there is none. */
23
+ redo(): boolean;
24
+ /** True while `undo()` has somewhere to go, for a button to follow. */
25
+ readonly canUndo: ReadableSignal<boolean>;
26
+ /** True while `redo()` has somewhere to go. */
27
+ readonly canRedo: ReadableSignal<boolean>;
28
+ /** Forgets every checkpoint and every value `redo()` could bring back; the signal keeps its value. */
29
+ dispose(): void;
30
+ /** The signal the history is of. */
31
+ readonly source: Signal<T>;
32
+ }
33
+
34
+ /**
35
+ * Undo and redo for `source` from explicit checkpoints: call
36
+ * `checkpoint()` before a change worth undoing, as an editor does before
37
+ * each edit, and `undo()` sets the signal back to it while `redo()` brings
38
+ * the undone value back. A new checkpoint after an undo starts a new
39
+ * branch, as in most editors, so what was undone is gone. `limit`
40
+ * checkpoints are kept, 50 by default. Each move writes the signal and
41
+ * both flags in one batch, so an effect over them runs once. Over a
42
+ * `stored` signal, a write from another tab makes the history forget its
43
+ * checkpoints, since going back to one would take that change back too;
44
+ * created inside an owner, the history stops listening for those writes
45
+ * when the owner is disposed.
46
+ */
47
+ export function history<T>(source: Signal<T>, options: HistoryOptions = {}): History<T>
48
+ {
49
+ const limit = Number.isFinite(options.limit) ? Math.max(1, Math.floor(options.limit as number)) : 50;
50
+ const past: T[] = [];
51
+ const future: T[] = [];
52
+ const canUndo = signal(false);
53
+ const canRedo = signal(false);
54
+ const update = (): void =>
55
+ {
56
+ canUndo.set(past.length > 0);
57
+ canRedo.set(future.length > 0);
58
+ };
59
+ const forget = (): void =>
60
+ {
61
+ past.length = 0;
62
+ future.length = 0;
63
+ batch(update);
64
+ };
65
+ const stop = onOutsideWrite(source as Signal<unknown>, forget);
66
+ if (hasOwner())
67
+ {
68
+ onDispose(stop);
69
+ }
70
+ return {
71
+ source,
72
+ canUndo,
73
+ canRedo,
74
+ checkpoint(): void
75
+ {
76
+ past.push(untracked(() => source.get()));
77
+ if (past.length > limit)
78
+ {
79
+ past.shift();
80
+ }
81
+ future.length = 0;
82
+ batch(update);
83
+ },
84
+ undo(): boolean
85
+ {
86
+ if (past.length === 0)
87
+ {
88
+ return false;
89
+ }
90
+ const previous = past.pop() as T;
91
+ future.push(untracked(() => source.get()));
92
+ batch(() =>
93
+ {
94
+ source.set(previous);
95
+ update();
96
+ });
97
+ return true;
98
+ },
99
+ redo(): boolean
100
+ {
101
+ if (future.length === 0)
102
+ {
103
+ return false;
104
+ }
105
+ const next = future.pop() as T;
106
+ past.push(untracked(() => source.get()));
107
+ batch(() =>
108
+ {
109
+ source.set(next);
110
+ update();
111
+ });
112
+ return true;
113
+ },
114
+ dispose: forget
115
+ };
116
+ }
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @zerolith/state: signals with a memory, over the core alone. A signal
3
+ * kept in storage and in step across tabs, undo and redo for a signal,
4
+ * signals that follow another after a quiet spell or at most so often, and
5
+ * a selector that tells each row whether it is the selected one. The public
6
+ * surface is assembled here.
7
+ */
8
+ export type { StoredOptions } from "./stored.js";
9
+ export { stored } from "./stored.js";
10
+
11
+ export type { History, HistoryOptions } from "./history.js";
12
+ export { history } from "./history.js";
13
+
14
+ export { debounced, throttled } from "./timing.js";
15
+
16
+ export { selector } from "./selector.js";
@@ -0,0 +1,57 @@
1
+ /**
2
+ * A selector over a signal: one boolean signal per key, true for the key
3
+ * the source holds, so a selection among many rows updates two rows
4
+ * rather than every row's own comparison.
5
+ */
6
+ import { type ReadableSignal, type Signal, effect, signal, untracked } from "zerolith";
7
+
8
+ /** True when `Map` would treat `a` and `b` as one key. */
9
+ function same(a: unknown, b: unknown): boolean
10
+ {
11
+ return a === b || (a !== a && b !== b);
12
+ }
13
+
14
+ /**
15
+ * A per-key view of `source`: `selected(key)` is a signal that is true
16
+ * while `source` holds `key`, and a change of `source` updates the two
17
+ * keys concerned alone, so a list of a thousand rows that each bind
18
+ * `selected(row.id)` runs two bindings on a selection, not a thousand
19
+ * comparisons. Keys are compared as `Map` keys are. A key's signal is
20
+ * shared by everyone who asks for it, and the selector keeps no hold of
21
+ * its own: the signal lives while a view reads it and goes with the row
22
+ * that did.
23
+ */
24
+ export function selector<T>(source: ReadableSignal<T>): (key: T) => ReadableSignal<boolean>
25
+ {
26
+ const flags = new Map<T, WeakRef<Signal<boolean>>>();
27
+ const gone = new FinalizationRegistry<T>((key) =>
28
+ {
29
+ if (flags.get(key)?.deref() === undefined)
30
+ {
31
+ flags.delete(key);
32
+ }
33
+ });
34
+ let current = untracked(() => source.get());
35
+ effect(() =>
36
+ {
37
+ const next = source.get();
38
+ if (same(next, current))
39
+ {
40
+ return;
41
+ }
42
+ flags.get(current)?.deref()?.set(false);
43
+ flags.get(next)?.deref()?.set(true);
44
+ current = next;
45
+ });
46
+ return (key) =>
47
+ {
48
+ let flag = flags.get(key)?.deref();
49
+ if (flag === undefined)
50
+ {
51
+ flag = signal(same(key, current));
52
+ flags.set(key, new WeakRef(flag));
53
+ gone.register(flag, key);
54
+ }
55
+ return flag;
56
+ };
57
+ }