@zerolith/state 0.0.0-canary.20261107.g95d90a0
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/LICENSE +201 -0
- package/README.md +32 -0
- package/dist/history.d.ts +42 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +78 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/production/history.js +2 -0
- package/dist/production/history.js.map +1 -0
- package/dist/production/index.js +2 -0
- package/dist/production/index.js.map +1 -0
- package/dist/production/selector.js +2 -0
- package/dist/production/selector.js.map +1 -0
- package/dist/production/stored.js +2 -0
- package/dist/production/stored.js.map +1 -0
- package/dist/production/timing.js +2 -0
- package/dist/production/timing.js.map +1 -0
- package/dist/selector.d.ts +18 -0
- package/dist/selector.d.ts.map +1 -0
- package/dist/selector.js +48 -0
- package/dist/selector.js.map +1 -0
- package/dist/stored.d.ts +46 -0
- package/dist/stored.d.ts.map +1 -0
- package/dist/stored.js +158 -0
- package/dist/stored.js.map +1 -0
- package/dist/timing.d.ts +21 -0
- package/dist/timing.d.ts.map +1 -0
- package/dist/timing.js +70 -0
- package/dist/timing.js.map +1 -0
- package/package.json +52 -0
- package/src/history.ts +116 -0
- package/src/index.ts +16 -0
- package/src/selector.ts +57 -0
- package/src/stored.ts +216 -0
- package/src/timing.ts +82 -0
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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"}
|
package/dist/timing.d.ts
ADDED
|
@@ -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
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zerolith/state",
|
|
3
|
+
"version": "0.0.0-canary.20261107.g95d90a0",
|
|
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": "^0.0.0-canary.20261107.g95d90a0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"zerolith": "^0.0.0-canary.20261107.g95d90a0"
|
|
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";
|
package/src/selector.ts
ADDED
|
@@ -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
|
+
}
|