@zerolith/state 1.1.0 → 1.3.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/history.d.ts CHANGED
@@ -32,7 +32,11 @@ export interface History<T> {
32
32
  * the undone value back. A new checkpoint after an undo starts a new
33
33
  * branch, as in most editors, so what was undone is gone. `limit`
34
34
  * checkpoints are kept, 50 by default. Each move writes the signal and
35
- * both flags in one batch, so an effect over them runs once.
35
+ * both flags in one batch, so an effect over them runs once. Over a
36
+ * `stored` signal, a write from another tab makes the history forget its
37
+ * checkpoints, since going back to one would take that change back too;
38
+ * created inside an owner, the history stops listening for those writes
39
+ * when the owner is disposed.
36
40
  */
37
41
  export declare function history<T>(source: Signal<T>, options?: HistoryOptions): History<T>;
38
42
  //# sourceMappingURL=history.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"history.d.ts","sourceRoot":"","sources":["../src/history.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,MAAM,EAA4B,MAAM,UAAU,CAAC;AAEtF,6BAA6B;AAC7B,MAAM,WAAW,cAAc;IAE3B,8IAA8I;IAC9I,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,uCAAuC;AACvC,MAAM,WAAW,OAAO,CAAC,CAAC;IAEtB,4HAA4H;IAC5H,UAAU,IAAI,IAAI,CAAC;IACnB,8HAA8H;IAC9H,IAAI,IAAI,OAAO,CAAC;IAChB,6FAA6F;IAC7F,IAAI,IAAI,OAAO,CAAC;IAChB,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC;IAC1C,+CAA+C;IAC/C,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC;IAC1C,sGAAsG;IACtG,OAAO,IAAI,IAAI,CAAC;IAChB,oCAAoC;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;CAC9B;AAED;;;;;;;;GAQG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,CAAC,CAAC,CA+DtF"}
1
+ {"version":3,"file":"history.d.ts","sourceRoot":"","sources":["../src/history.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,MAAM,EAAiD,MAAM,UAAU,CAAC;AAG3G,6BAA6B;AAC7B,MAAM,WAAW,cAAc;IAE3B,8IAA8I;IAC9I,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,uCAAuC;AACvC,MAAM,WAAW,OAAO,CAAC,CAAC;IAEtB,4HAA4H;IAC5H,UAAU,IAAI,IAAI,CAAC;IACnB,8HAA8H;IAC9H,IAAI,IAAI,OAAO,CAAC;IAChB,6FAA6F;IAC7F,IAAI,IAAI,OAAO,CAAC;IAChB,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC;IAC1C,+CAA+C;IAC/C,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC,OAAO,CAAC,CAAC;IAC1C,sGAAsG;IACtG,OAAO,IAAI,IAAI,CAAC;IAChB,oCAAoC;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;CAC9B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,CAAC,CAAC,CAqEtF"}
package/dist/history.js CHANGED
@@ -2,7 +2,8 @@
2
2
  * Undo and redo for a signal, from explicit checkpoints, as an editor
3
3
  * keeps them.
4
4
  */
5
- import { batch, signal, untracked } from "zerolith";
5
+ import { batch, hasOwner, onDispose, signal, untracked } from "zerolith";
6
+ import { onOutsideWrite } from "./stored.js";
6
7
  /**
7
8
  * Undo and redo for `source` from explicit checkpoints: call
8
9
  * `checkpoint()` before a change worth undoing, as an editor does before
@@ -10,7 +11,11 @@ import { batch, signal, untracked } from "zerolith";
10
11
  * the undone value back. A new checkpoint after an undo starts a new
11
12
  * branch, as in most editors, so what was undone is gone. `limit`
12
13
  * checkpoints are kept, 50 by default. Each move writes the signal and
13
- * both flags in one batch, so an effect over them runs once.
14
+ * both flags in one batch, so an effect over them runs once. Over a
15
+ * `stored` signal, a write from another tab makes the history forget its
16
+ * checkpoints, since going back to one would take that change back too;
17
+ * created inside an owner, the history stops listening for those writes
18
+ * when the owner is disposed.
14
19
  */
15
20
  export function history(source, options = {}) {
16
21
  const limit = Number.isFinite(options.limit) ? Math.max(1, Math.floor(options.limit)) : 50;
@@ -22,6 +27,15 @@ export function history(source, options = {}) {
22
27
  canUndo.set(past.length > 0);
23
28
  canRedo.set(future.length > 0);
24
29
  };
30
+ const forget = () => {
31
+ past.length = 0;
32
+ future.length = 0;
33
+ batch(update);
34
+ };
35
+ const stop = onOutsideWrite(source, forget);
36
+ if (hasOwner()) {
37
+ onDispose(stop);
38
+ }
25
39
  return {
26
40
  source,
27
41
  canUndo,
@@ -58,11 +72,7 @@ export function history(source, options = {}) {
58
72
  });
59
73
  return true;
60
74
  },
61
- dispose() {
62
- past.length = 0;
63
- future.length = 0;
64
- batch(update);
65
- }
75
+ dispose: forget
66
76
  };
67
77
  }
68
78
  //# sourceMappingURL=history.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"history.js","sourceRoot":"","sources":["../src/history.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAoC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AA4BtF;;;;;;;;GAQG;AACH,MAAM,UAAU,OAAO,CAAI,MAAiB,EAAE,OAAO,GAAmB,EAAE;IAEtE,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAe,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACrG,MAAM,IAAI,GAAQ,EAAE,CAAC;IACrB,MAAM,MAAM,GAAQ,EAAE,CAAC;IACvB,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC9B,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC9B,MAAM,MAAM,GAAG,GAAS,EAAE;QAEtB,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAC7B,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACnC,CAAC,CAAC;IACF,OAAO;QACH,MAAM;QACN,OAAO;QACP,OAAO;QACP,UAAU;YAEN,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;YACzC,IAAI,IAAI,CAAC,MAAM,GAAG,KAAK,EACvB,CAAC;gBACG,IAAI,CAAC,KAAK,EAAE,CAAC;YACjB,CAAC;YACD,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;YAClB,KAAK,CAAC,MAAM,CAAC,CAAC;QAClB,CAAC;QACD,IAAI;YAEA,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EACrB,CAAC;gBACG,OAAO,KAAK,CAAC;YACjB,CAAC;YACD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAO,CAAC;YACjC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;YAC3C,KAAK,CAAC,GAAG,EAAE;gBAEP,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;gBACrB,MAAM,EAAE,CAAC;YACb,CAAC,CAAC,CAAC;YACH,OAAO,IAAI,CAAC;QAChB,CAAC;QACD,IAAI;YAEA,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EACvB,CAAC;gBACG,OAAO,KAAK,CAAC;YACjB,CAAC;YACD,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,EAAO,CAAC;YAC/B,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;YACzC,KAAK,CAAC,GAAG,EAAE;gBAEP,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;gBACjB,MAAM,EAAE,CAAC;YACb,CAAC,CAAC,CAAC;YACH,OAAO,IAAI,CAAC;QAChB,CAAC;QACD,OAAO;YAEH,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;YAChB,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;YAClB,KAAK,CAAC,MAAM,CAAC,CAAC;QAClB,CAAC;KACJ,CAAC;AACN,CAAC"}
1
+ {"version":3,"file":"history.js","sourceRoot":"","sources":["../src/history.ts"],"names":[],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{batch as e,signal as t,untracked as n}from"zerolith";export function history(r,i={}){let a=Number.isFinite(i.limit)?Math.max(1,Math.floor(i.limit)):50,o=[],s=[],c=t(!1),l=t(!1),u=()=>{c.set(o.length>0),l.set(s.length>0)};return{source:r,canUndo:c,canRedo:l,checkpoint(){o.push(n(()=>r.get())),o.length>a&&o.shift(),s.length=0,e(u)},undo(){if(o.length===0)return!1;let t=o.pop();return s.push(n(()=>r.get())),e(()=>{r.set(t),u()}),!0},redo(){if(s.length===0)return!1;let t=s.pop();return o.push(n(()=>r.get())),e(()=>{r.set(t),u()}),!0},dispose(){o.length=0,s.length=0,e(u)}}}
1
+ import{batch as e,hasOwner as t,onDispose as n,signal as r,untracked as i}from"zerolith";import{onOutsideWrite as a}from"./stored.js";export function history(o,s={}){let c=Number.isFinite(s.limit)?Math.max(1,Math.floor(s.limit)):50,l=[],u=[],d=r(!1),f=r(!1),p=()=>{d.set(l.length>0),f.set(u.length>0)},m=()=>{l.length=0,u.length=0,e(p)},h=a(o,m);return t()&&n(h),{source:o,canUndo:d,canRedo:f,checkpoint(){l.push(i(()=>o.get())),l.length>c&&l.shift(),u.length=0,e(p)},undo(){if(l.length===0)return!1;let t=l.pop();return u.push(i(()=>o.get())),e(()=>{o.set(t),p()}),!0},redo(){if(u.length===0)return!1;let t=u.pop();return l.push(i(()=>o.get())),e(()=>{o.set(t),p()}),!0},dispose:m}}
2
2
  //# sourceMappingURL=history.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"history.js","sources":["../../src/history.ts"],"sourcesContent":["/**\n * Undo and redo for a signal, from explicit checkpoints, as an editor\n * keeps them.\n */\nimport { type ReadableSignal, type Signal, batch, signal, untracked } from \"zerolith\";\n\n/** Options for `history`. */\nexport interface HistoryOptions\n{\n /** 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. */\n limit?: number;\n}\n\n/** The undo and redo of one signal. */\nexport interface History<T>\n{\n /** Saves the signal's current value as the point `undo()` goes back to, and drops what `redo()` could have brought back. */\n checkpoint(): void;\n /** Sets the signal back to the last checkpoint, keeping the current value for `redo()`; false when there is no checkpoint. */\n undo(): boolean;\n /** Sets the signal forward to the value the last `undo()` left; false when there is none. */\n redo(): boolean;\n /** True while `undo()` has somewhere to go, for a button to follow. */\n readonly canUndo: ReadableSignal<boolean>;\n /** True while `redo()` has somewhere to go. */\n readonly canRedo: ReadableSignal<boolean>;\n /** Forgets every checkpoint and every value `redo()` could bring back; the signal keeps its value. */\n dispose(): void;\n /** The signal the history is of. */\n readonly source: Signal<T>;\n}\n\n/**\n * Undo and redo for `source` from explicit checkpoints: call\n * `checkpoint()` before a change worth undoing, as an editor does before\n * each edit, and `undo()` sets the signal back to it while `redo()` brings\n * the undone value back. A new checkpoint after an undo starts a new\n * branch, as in most editors, so what was undone is gone. `limit`\n * checkpoints are kept, 50 by default. Each move writes the signal and\n * both flags in one batch, so an effect over them runs once.\n */\nexport function history<T>(source: Signal<T>, options: HistoryOptions = {}): History<T>\n{\n const limit = Number.isFinite(options.limit) ? Math.max(1, Math.floor(options.limit as number)) : 50;\n const past: T[] = [];\n const future: T[] = [];\n const canUndo = signal(false);\n const canRedo = signal(false);\n const update = (): void =>\n {\n canUndo.set(past.length > 0);\n canRedo.set(future.length > 0);\n };\n return {\n source,\n canUndo,\n canRedo,\n checkpoint(): void\n {\n past.push(untracked(() => source.get()));\n if (past.length > limit)\n {\n past.shift();\n }\n future.length = 0;\n batch(update);\n },\n undo(): boolean\n {\n if (past.length === 0)\n {\n return false;\n }\n const previous = past.pop() as T;\n future.push(untracked(() => source.get()));\n batch(() =>\n {\n source.set(previous);\n update();\n });\n return true;\n },\n redo(): boolean\n {\n if (future.length === 0)\n {\n return false;\n }\n const next = future.pop() as T;\n past.push(untracked(() => source.get()));\n batch(() =>\n {\n source.set(next);\n update();\n });\n return true;\n },\n dispose(): void\n {\n past.length = 0;\n future.length = 0;\n batch(update);\n }\n };\n}\n"],"names":["limit","options","past","future","canUndo","signal","canRedo","update","untracked","source","batch","previous","next"],"mappings":"AAIA,OAA2C,WAAO,YAAQ,mBAAiB,WAqC3E,OAAM,SAAU,QAAW,EAAmB,EAA0B,CAAA,EAAE,CAEtE,IAAMA,EAAQ,OAAO,SAASC,EAAQ,KAAK,EAAI,KAAK,IAAI,EAAG,KAAK,MAAMA,EAAQ,KAAe,CAAC,EAAI,GAC5FC,EAAY,CAAA,EACZC,EAAc,CAAA,EACdC,EAAUC,EAAO,EAAK,EACtBC,EAAUD,EAAO,EAAK,EACtBE,MAAoB,CAEtBH,EAAQ,IAAIF,EAAK,OAAS,CAAC,EAC3BI,EAAQ,IAAIH,EAAO,OAAS,CAAC,CACjC,EACA,MAAO,CACH,SACA,UACA,UACA,YAAU,CAEND,EAAK,KAAKM,MAAgBC,EAAO,IAAG,CAAE,CAAC,EACnCP,EAAK,OAASF,GAEdE,EAAK,MAAK,EAEd,EAAO,OAAS,EAChBQ,EAAMH,CAAM,CAChB,EACA,MAAI,CAEA,GAAIL,EAAK,SAAW,EAEhB,MAAO,GAEX,IAAMS,EAAWT,EAAK,IAAG,EAOzB,OANAC,EAAO,KAAKK,MAAgBC,EAAO,IAAG,CAAE,CAAC,EACzCC,MAAW,CAEPD,EAAO,IAAIE,CAAQ,EACnBJ,EAAM,CACV,CAAC,EACM,EACX,EACA,MAAI,CAEA,GAAIJ,EAAO,SAAW,EAElB,MAAO,GAEX,IAAMS,EAAOT,EAAO,IAAG,EAOvB,OANAD,EAAK,KAAKM,MAAgBC,EAAO,IAAG,CAAE,CAAC,EACvCC,MAAW,CAEPD,EAAO,IAAIG,CAAI,EACfL,EAAM,CACV,CAAC,EACM,EACX,EACA,SAAO,CAEH,EAAK,OAAS,EACd,EAAO,OAAS,EAChBG,EAAMH,CAAM,CAChB,CACH,CACL"}
1
+ {"version":3,"file":"history.js","sources":["../../src/history.ts"],"sourcesContent":["/**\n * Undo and redo for a signal, from explicit checkpoints, as an editor\n * keeps them.\n */\nimport { type ReadableSignal, type Signal, batch, hasOwner, onDispose, signal, untracked } from \"zerolith\";\nimport { onOutsideWrite } from \"./stored.js\";\n\n/** Options for `history`. */\nexport interface HistoryOptions\n{\n /** 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. */\n limit?: number;\n}\n\n/** The undo and redo of one signal. */\nexport interface History<T>\n{\n /** Saves the signal's current value as the point `undo()` goes back to, and drops what `redo()` could have brought back. */\n checkpoint(): void;\n /** Sets the signal back to the last checkpoint, keeping the current value for `redo()`; false when there is no checkpoint. */\n undo(): boolean;\n /** Sets the signal forward to the value the last `undo()` left; false when there is none. */\n redo(): boolean;\n /** True while `undo()` has somewhere to go, for a button to follow. */\n readonly canUndo: ReadableSignal<boolean>;\n /** True while `redo()` has somewhere to go. */\n readonly canRedo: ReadableSignal<boolean>;\n /** Forgets every checkpoint and every value `redo()` could bring back; the signal keeps its value. */\n dispose(): void;\n /** The signal the history is of. */\n readonly source: Signal<T>;\n}\n\n/**\n * Undo and redo for `source` from explicit checkpoints: call\n * `checkpoint()` before a change worth undoing, as an editor does before\n * each edit, and `undo()` sets the signal back to it while `redo()` brings\n * the undone value back. A new checkpoint after an undo starts a new\n * branch, as in most editors, so what was undone is gone. `limit`\n * checkpoints are kept, 50 by default. Each move writes the signal and\n * both flags in one batch, so an effect over them runs once. Over a\n * `stored` signal, a write from another tab makes the history forget its\n * checkpoints, since going back to one would take that change back too;\n * created inside an owner, the history stops listening for those writes\n * when the owner is disposed.\n */\nexport function history<T>(source: Signal<T>, options: HistoryOptions = {}): History<T>\n{\n const limit = Number.isFinite(options.limit) ? Math.max(1, Math.floor(options.limit as number)) : 50;\n const past: T[] = [];\n const future: T[] = [];\n const canUndo = signal(false);\n const canRedo = signal(false);\n const update = (): void =>\n {\n canUndo.set(past.length > 0);\n canRedo.set(future.length > 0);\n };\n const forget = (): void =>\n {\n past.length = 0;\n future.length = 0;\n batch(update);\n };\n const stop = onOutsideWrite(source as Signal<unknown>, forget);\n if (hasOwner())\n {\n onDispose(stop);\n }\n return {\n source,\n canUndo,\n canRedo,\n checkpoint(): void\n {\n past.push(untracked(() => source.get()));\n if (past.length > limit)\n {\n past.shift();\n }\n future.length = 0;\n batch(update);\n },\n undo(): boolean\n {\n if (past.length === 0)\n {\n return false;\n }\n const previous = past.pop() as T;\n future.push(untracked(() => source.get()));\n batch(() =>\n {\n source.set(previous);\n update();\n });\n return true;\n },\n redo(): boolean\n {\n if (future.length === 0)\n {\n return false;\n }\n const next = future.pop() as T;\n past.push(untracked(() => source.get()));\n batch(() =>\n {\n source.set(next);\n update();\n });\n return true;\n },\n dispose: forget\n };\n}\n"],"names":["limit","options","past","future","canUndo","signal","canRedo","update","forget","batch","stop","onOutsideWrite","source","hasOwner","onDispose","untracked","previous","next"],"mappings":"AAIA,OAA2C,WAAO,cAAU,eAAW,YAAQ,mBAAiB,WAChG,OAAS,wBAAsB,cAyC/B,OAAM,SAAU,QAAW,EAAmB,EAA0B,CAAA,EAAE,CAEtE,IAAMA,EAAQ,OAAO,SAASC,EAAQ,KAAK,EAAI,KAAK,IAAI,EAAG,KAAK,MAAMA,EAAQ,KAAe,CAAC,EAAI,GAC5FC,EAAY,CAAA,EACZC,EAAc,CAAA,EACdC,EAAUC,EAAO,EAAK,EACtBC,EAAUD,EAAO,EAAK,EACtBE,MAAoB,CAEtBH,EAAQ,IAAIF,EAAK,OAAS,CAAC,EAC3BI,EAAQ,IAAIH,EAAO,OAAS,CAAC,CACjC,EACMK,MAAoB,CAEtB,EAAK,OAAS,EACd,EAAO,OAAS,EAChBC,EAAMF,CAAM,CAChB,EACMG,EAAOC,EAAeC,EAA2BJ,CAAM,EAK7D,OAJIK,EAAQ,GAERC,EAAUJ,CAAI,EAEX,CACH,SACA,UACA,UACA,YAAU,CAENR,EAAK,KAAKa,MAAgBH,EAAO,IAAG,CAAE,CAAC,EACnCV,EAAK,OAASF,GAEdE,EAAK,MAAK,EAEd,EAAO,OAAS,EAChBO,EAAMF,CAAM,CAChB,EACA,MAAI,CAEA,GAAIL,EAAK,SAAW,EAEhB,MAAO,GAEX,IAAMc,EAAWd,EAAK,IAAG,EAOzB,OANAC,EAAO,KAAKY,MAAgBH,EAAO,IAAG,CAAE,CAAC,EACzCH,MAAW,CAEPG,EAAO,IAAII,CAAQ,EACnBT,EAAM,CACV,CAAC,EACM,EACX,EACA,MAAI,CAEA,GAAIJ,EAAO,SAAW,EAElB,MAAO,GAEX,IAAMc,EAAOd,EAAO,IAAG,EAOvB,OANAD,EAAK,KAAKa,MAAgBH,EAAO,IAAG,CAAE,CAAC,EACvCH,MAAW,CAEPG,EAAO,IAAIK,CAAI,EACfV,EAAM,CACV,CAAC,EACM,EACX,EACA,QAASC,CACZ,CACL"}
@@ -1,2 +1,2 @@
1
- import{diagnostic as e,effect as t,hasOwner as n,onDispose as r,report as i,signal as a}from"zerolith";const o=new Set,s=new Map;function c(t,n){try{let e=t===`session`?globalThis.sessionStorage:globalThis.localStorage;return e.getItem(``),e}catch{o.has(t)||(o.add(t),i(e(`state_storage_unavailable`,`warning`,{storage:t,key:n})));return}}function l(t,n,r){if(t===null)return r;try{return JSON.parse(t)}catch{return i(e(`state_storage_unreadable`,`warning`,{key:n})),r}}function u(e){try{return JSON.stringify(e)}catch{return}}export function stored(o,d,f={}){let p=f.storage??`local`,m=c(p,o),h=a(m===void 0?d:l(m.getItem(o),o,d),f.equals===void 0?void 0:{equals:f.equals});if(m===void 0)return h;let g=`${p}:${o}`,_=(s.get(g)??0)+1;s.set(g,_),_>1&&i(e(`state_key_duplicate`,`warning`,{storage:p,key:o}));let v=!0,y=!1;t(()=>{let t=h.get();if(v||y){v=!1;return}let n=u(t);if(n===void 0&&t!==void 0)i(e(`state_value_not_json`,`warning`,{key:o}));else try{n===void 0?m.removeItem(o):m.setItem(o,n)}catch{i(e(`state_storage_unavailable`,`warning`,{storage:p,key:o}))}});let b=e=>{if(!(e.storageArea!==m||e.key!==null&&e.key!==o)){y=!0;try{h.set(l(e.newValue,o,d))}finally{y=!1}}};return addEventListener(`storage`,b),n()&&r(()=>{removeEventListener(`storage`,b);let e=(s.get(g)??1)-1;e>0?s.set(g,e):s.delete(g)}),h}
1
+ import{diagnostic as e,effect as t,hasOwner as n,onDispose as r,report as i,signal as a}from"zerolith";const o=new Set,s=new Map,c=new WeakMap;export function onOutsideWrite(e,t){let n=c.get(e);return n===void 0&&(n=new Set,c.set(e,n)),n.add(t),()=>{n.delete(t)}}function l(t,n){try{let e=t===`session`?globalThis.sessionStorage:globalThis.localStorage;return e.getItem(``),e}catch{o.has(t)||(o.add(t),i(e(`state_storage_unavailable`,`warning`,{storage:t,key:n})));return}}function u(t,n,r,a){if(t===null)return r;let o,s;try{o=JSON.parse(t),s=a===void 0||a(o)}catch{s=!1}return s?o:(i(e(`state_storage_unreadable`,`warning`,{key:n})),r)}function d(e){try{return JSON.stringify(e)}catch{return}}export function stored(o,f,p={}){let m=p.storage??`local`,h=l(m,o),g=a(h===void 0?f:u(h.getItem(o),o,f,p.validate),p.equals===void 0?void 0:{equals:p.equals});if(h===void 0)return g;let _=`${m}:${o}`,v=(s.get(_)??0)+1;s.set(_,v),v>1&&i(e(`state_key_duplicate`,`warning`,{storage:m,key:o}));let y=!0,b=!1;t(()=>{let t=g.get();if(y||b){y=!1;return}let n=d(t);if(n===void 0&&t!==void 0)i(e(`state_value_not_json`,`warning`,{key:o}));else try{n===void 0?h.removeItem(o):h.setItem(o,n)}catch{i(e(`state_storage_unavailable`,`warning`,{storage:m,key:o}))}});let x=e=>{if(!(e.storageArea!==h||e.key!==null&&e.key!==o)){b=!0;try{g.set(u(e.newValue,o,f,p.validate))}finally{b=!1}for(let e of c.get(g)??[])e()}};return addEventListener(`storage`,x),n()&&r(()=>{removeEventListener(`storage`,x);let e=(s.get(_)??1)-1;e>0?s.set(_,e):s.delete(_)}),g}
2
2
  //# sourceMappingURL=stored.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"stored.js","sources":["../../src/stored.ts"],"sourcesContent":["/**\n * A signal kept in the browser's storage under a key, as JSON, and in step\n * across tabs through the `storage` event.\n */\nimport { type Signal, diagnostic, effect, hasOwner, onDispose, report, signal } from \"zerolith\";\n\n/** Options for `stored`. */\nexport interface StoredOptions<T>\n{\n /** Which storage keeps the value: `localStorage`, shared across tabs and kept across sessions, or `sessionStorage`, this tab's alone. Defaults to local. */\n storage?: \"local\" | \"session\";\n /** When two values count as the same, so a set of an equal value changes nothing; the signal's own option. */\n equals?: (previous: T, next: T) => boolean;\n}\n\n/** The storage kinds reported unavailable on this page, each once. */\nconst unavailable = new Set<string>();\n\n/** How many stored signals are live for each key of each storage, so a second one for a key is reported. */\nconst live = new Map<string, number>();\n\n/** 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. */\nfunction storageOf(kind: \"local\" | \"session\", key: string): Storage | undefined\n{\n try\n {\n const store = kind === \"session\" ? globalThis.sessionStorage : globalThis.localStorage;\n store.getItem(\"\");\n return store;\n }\n catch\n {\n if (!unavailable.has(kind))\n {\n unavailable.add(kind);\n report(diagnostic(\"state_storage_unavailable\", \"warning\", { storage: kind, key }));\n }\n return undefined;\n }\n}\n\n/** The value kept as `raw` under `key`, or `initial` where nothing is kept or what is kept does not parse, which is reported. */\nfunction parse<T>(raw: string | null, key: string, initial: T): T\n{\n if (raw === null)\n {\n return initial;\n }\n try\n {\n return JSON.parse(raw) as T;\n }\n catch\n {\n report(diagnostic(\"state_storage_unreadable\", \"warning\", { key }));\n return initial;\n }\n}\n\n/** 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. */\nfunction toJson(next: unknown): string | undefined\n{\n try\n {\n return JSON.stringify(next);\n }\n catch\n {\n return undefined;\n }\n}\n\n/**\n * A writable signal kept in storage under `key` as JSON: read back on\n * creation, written on every set, and set again when another tab writes\n * the key, so every tab shows the same value. A missing value takes\n * `initial` quietly; one that does not parse takes `initial` and reports\n * `state_storage_unreadable`. Setting `undefined` removes the key, so it\n * reads back as `initial`; a value JSON cannot hold, such as a BigInt,\n * stays in memory and reports `state_value_not_json`. Where the page has\n * no storage, as on the server, the signal is a plain one and\n * `state_storage_unavailable` is reported once, as it is for each write\n * the storage refuses. Create each key once and share the signal: two\n * stored signals for one key in the same tab do not hear each other,\n * since the storage event tells other tabs alone, and the second reports\n * `state_key_duplicate`. The write and the listener for other tabs are\n * disposed with the enclosing owner when there is one, after which the\n * signal keeps its value in memory alone; created in plain code, they\n * live for the page.\n */\nexport function stored<T>(key: string, initial: T, options: StoredOptions<T> = {}): Signal<T>\n{\n const kind = options.storage ?? \"local\";\n const store = storageOf(kind, key);\n const value = signal<T>(store === undefined ? initial : parse(store.getItem(key), key, initial), options.equals === undefined ? undefined : { equals: options.equals });\n if (store === undefined)\n {\n return value;\n }\n const id = `${kind}:${key}`;\n const count = (live.get(id) ?? 0) + 1;\n live.set(id, count);\n if (count > 1)\n {\n report(diagnostic(\"state_key_duplicate\", \"warning\", { storage: kind, key }));\n }\n let first = true;\n let echo = false;\n effect(() =>\n {\n const next = value.get();\n if (first || echo)\n {\n first = false;\n return;\n }\n const json = toJson(next);\n if (json === undefined && next !== undefined)\n {\n report(diagnostic(\"state_value_not_json\", \"warning\", { key }));\n return;\n }\n try\n {\n if (json === undefined)\n {\n store.removeItem(key);\n }\n else\n {\n store.setItem(key, json);\n }\n }\n catch\n {\n report(diagnostic(\"state_storage_unavailable\", \"warning\", { storage: kind, key }));\n }\n });\n const onStorage = (event: StorageEvent): void =>\n {\n if (event.storageArea !== store || (event.key !== null && event.key !== key))\n {\n return;\n }\n echo = true;\n try\n {\n value.set(parse(event.newValue, key, initial));\n }\n finally\n {\n echo = false;\n }\n };\n addEventListener(\"storage\", onStorage);\n if (hasOwner())\n {\n onDispose(() =>\n {\n removeEventListener(\"storage\", onStorage);\n const left = (live.get(id) ?? 1) - 1;\n if (left > 0)\n {\n live.set(id, left);\n }\n else\n {\n live.delete(id);\n }\n });\n }\n return value;\n}\n"],"names":["unavailable","live","storageOf","store","kind","report","diagnostic","parse","raw","initial","toJson","next","options","key","value","signal","id","count","first","echo","effect","json","onStorage","event","hasOwner","onDispose","left"],"mappings":"AAIA,OAAsB,gBAAY,YAAQ,cAAU,eAAW,YAAQ,gBAAc,WAYrF,MAAMA,EAAc,IAAI,IAGlBC,EAAO,IAAI,IAGjB,SAASC,EAAU,EAA2B,EAAW,CAErD,GACA,CACI,IAAMC,EAAQC,IAAS,UAAY,WAAW,eAAiB,WAAW,aAE1E,OADAD,EAAM,QAAQ,EAAE,EACTA,CACX,MAEA,CACSH,EAAY,IAAII,CAAI,IAErBJ,EAAY,IAAII,CAAI,EACpBC,EAAOC,EAAW,4BAA6B,UAAW,CAAE,QAASF,EAAM,KAAG,CAAE,CAAC,GAErF,MACJ,CACJ,CAGA,SAASG,EAAS,EAAoB,EAAa,EAAU,CAEzD,GAAIC,IAAQ,KAER,OAAOC,EAEX,GACA,CACI,OAAO,KAAK,MAAMD,CAAG,CACzB,MAEA,CAEI,OADAH,EAAOC,EAAW,2BAA4B,UAAW,CAAE,KAAG,CAAE,CAAC,EAC1DG,CACX,CACJ,CAGA,SAASC,EAAO,EAAa,CAEzB,GACA,CACI,OAAO,KAAK,UAAUC,CAAI,CAC9B,MAEA,CACI,MACJ,CACJ,CAoBA,OAAM,SAAU,OAAU,EAAa,EAAY,EAA4B,CAAA,EAAE,CAE7E,IAAMP,EAAOQ,EAAQ,SAAW,QAC1BT,EAAQD,EAAUE,EAAMS,CAAG,EAC3BC,EAAQC,EAAUZ,IAAU,OAAYM,EAAUF,EAAMJ,EAAM,QAAQU,CAAG,EAAGA,EAAKJ,CAAO,EAAGG,EAAQ,SAAW,OAAY,OAAY,CAAE,OAAQA,EAAQ,MAAM,CAAE,EACtK,GAAIT,IAAU,OAEV,OAAOW,EAEX,IAAME,EAAK,GAAGZ,EAAI,GAAIS,IAChBI,GAAShB,EAAK,IAAIe,CAAE,GAAK,GAAK,EACpCf,EAAK,IAAIe,EAAIC,CAAK,EACdA,EAAQ,GAERZ,EAAOC,EAAW,sBAAuB,UAAW,CAAE,QAASF,EAAM,KAAG,CAAE,CAAC,EAE/E,IAAIc,EAAQ,GACRC,EAAO,GACXC,MAAY,CAER,IAAMT,EAAOG,EAAM,IAAG,EACtB,GAAII,GAASC,EACb,CACI,EAAQ,GACR,MACJ,CACA,IAAME,EAAOX,EAAOC,CAAI,EACxB,GAAIU,IAAS,QAAaV,IAAS,OAE/BN,EAAOC,EAAW,uBAAwB,UAAW,CAAE,KAAG,CAAE,CAAC,OAGjE,GACA,CACQe,IAAS,OAETlB,EAAM,WAAWU,CAAG,EAIpBV,EAAM,QAAQU,EAAKQ,CAAI,CAE/B,MAEA,CACIhB,EAAOC,EAAW,4BAA6B,UAAW,CAAE,QAASF,EAAM,KAAG,CAAE,CAAC,CACrF,CACJ,CAAC,EACD,IAAMkB,EAAa,GAA6B,CAE5C,GAAI,IAAM,cAAgBnB,GAAUoB,EAAM,MAAQ,MAAQA,EAAM,MAAQV,GAIxE,GAAO,GACP,GACA,CACIC,EAAM,IAAIP,EAAMgB,EAAM,SAAUV,EAAKJ,CAAO,CAAC,CACjD,QAEA,CACI,EAAO,EACX,CARW,CASf,EAkBA,OAjBA,iBAAiB,UAAWa,CAAS,EACjCE,EAAQ,GAERC,MAAe,CAEX,oBAAoB,UAAWH,CAAS,EACxC,IAAMI,GAAQzB,EAAK,IAAIe,CAAE,GAAK,GAAK,EAC/BU,EAAO,EAEPzB,EAAK,IAAIe,EAAIU,CAAI,EAIjBzB,EAAK,OAAOe,CAAE,CAEtB,CAAC,EAEEF,CACX"}
1
+ {"version":3,"file":"stored.js","sources":["../../src/stored.ts"],"sourcesContent":["/**\n * A signal kept in the browser's storage under a key, as JSON, and in step\n * across tabs through the `storage` event.\n */\nimport { type Signal, diagnostic, effect, hasOwner, onDispose, report, signal } from \"zerolith\";\n\n/** Options for `stored`. */\nexport interface StoredOptions<T>\n{\n /** Which storage keeps the value: `localStorage`, shared across tabs and kept across sessions, or `sessionStorage`, this tab's alone. Defaults to local. */\n storage?: \"local\" | \"session\";\n /** When two values count as the same, so a set of an equal value changes nothing; the signal's own option. */\n equals?: (previous: T, next: T) => boolean;\n /**\n * Whether a kept value, once parsed, is one the signal can hold. A value\n * it refuses, or throws on, is read as `initial` and reported as\n * `state_storage_unreadable`, on creation and when another tab writes the\n * key, so data of another shape, such as another app's under the same\n * key, never reaches the page.\n */\n validate?: (value: unknown) => boolean;\n}\n\n/** The storage kinds reported unavailable on this page, each once. */\nconst unavailable = new Set<string>();\n\n/** How many stored signals are live for each key of each storage, so a second one for a key is reported. */\nconst live = new Map<string, number>();\n\n/** What runs after a stored signal takes a value another tab wrote, keyed by the signal. */\nconst outside = new WeakMap<Signal<unknown>, Set<() => void>>();\n\n/**\n * Runs `listener` each time `source` takes a value another tab wrote, which\n * only a stored signal does, and returns the function that stops it. The\n * history uses it; the package does not export it.\n */\nexport function onOutsideWrite(source: Signal<unknown>, listener: () => void): () => void\n{\n let listeners = outside.get(source);\n if (listeners === undefined)\n {\n listeners = new Set();\n outside.set(source, listeners);\n }\n listeners.add(listener);\n return () =>\n {\n listeners.delete(listener);\n };\n}\n\n/** 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. */\nfunction storageOf(kind: \"local\" | \"session\", key: string): Storage | undefined\n{\n try\n {\n const store = kind === \"session\" ? globalThis.sessionStorage : globalThis.localStorage;\n store.getItem(\"\");\n return store;\n }\n catch\n {\n if (!unavailable.has(kind))\n {\n unavailable.add(kind);\n report(diagnostic(\"state_storage_unavailable\", \"warning\", { storage: kind, key }));\n }\n return undefined;\n }\n}\n\n/** 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. */\nfunction parse<T>(raw: string | null, key: string, initial: T, validate: ((value: unknown) => boolean) | undefined): T\n{\n if (raw === null)\n {\n return initial;\n }\n let value: unknown;\n let accepted: boolean;\n try\n {\n value = JSON.parse(raw);\n accepted = validate === undefined || validate(value);\n }\n catch\n {\n accepted = false;\n }\n if (!accepted)\n {\n report(diagnostic(\"state_storage_unreadable\", \"warning\", { key }));\n return initial;\n }\n return value as T;\n}\n\n/** 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. */\nfunction toJson(next: unknown): string | undefined\n{\n try\n {\n return JSON.stringify(next);\n }\n catch\n {\n return undefined;\n }\n}\n\n/**\n * A writable signal kept in storage under `key` as JSON: read back on\n * creation, written on every set, and set again when another tab writes\n * the key, so every tab shows the same value. A missing value takes\n * `initial` quietly; one that does not parse, or that the `validate`\n * option refuses, takes `initial` and reports `state_storage_unreadable`. Setting `undefined` removes the key, so it\n * reads back as `initial`; a value JSON cannot hold, such as a BigInt,\n * stays in memory and reports `state_value_not_json`. Where the page has\n * no storage, as on the server, the signal is a plain one and\n * `state_storage_unavailable` is reported once, as it is for each write\n * the storage refuses. Create each key once and share the signal: two\n * stored signals for one key in the same tab do not hear each other,\n * since the storage event tells other tabs alone, and the second reports\n * `state_key_duplicate`. The write and the listener for other tabs are\n * disposed with the enclosing owner when there is one, after which the\n * signal keeps its value in memory alone; created in plain code, they\n * live for the page.\n */\nexport function stored<T>(key: string, initial: T, options: StoredOptions<T> = {}): Signal<T>\n{\n const kind = options.storage ?? \"local\";\n const store = storageOf(kind, key);\n const value = signal<T>(store === undefined ? initial : parse(store.getItem(key), key, initial, options.validate), options.equals === undefined ? undefined : { equals: options.equals });\n if (store === undefined)\n {\n return value;\n }\n const id = `${kind}:${key}`;\n const count = (live.get(id) ?? 0) + 1;\n live.set(id, count);\n if (count > 1)\n {\n report(diagnostic(\"state_key_duplicate\", \"warning\", { storage: kind, key }));\n }\n let first = true;\n let echo = false;\n effect(() =>\n {\n const next = value.get();\n if (first || echo)\n {\n first = false;\n return;\n }\n const json = toJson(next);\n if (json === undefined && next !== undefined)\n {\n report(diagnostic(\"state_value_not_json\", \"warning\", { key }));\n return;\n }\n try\n {\n if (json === undefined)\n {\n store.removeItem(key);\n }\n else\n {\n store.setItem(key, json);\n }\n }\n catch\n {\n report(diagnostic(\"state_storage_unavailable\", \"warning\", { storage: kind, key }));\n }\n });\n const onStorage = (event: StorageEvent): void =>\n {\n if (event.storageArea !== store || (event.key !== null && event.key !== key))\n {\n return;\n }\n echo = true;\n try\n {\n value.set(parse(event.newValue, key, initial, options.validate));\n }\n finally\n {\n echo = false;\n }\n for (const listener of outside.get(value as Signal<unknown>) ?? [])\n {\n listener();\n }\n };\n addEventListener(\"storage\", onStorage);\n if (hasOwner())\n {\n onDispose(() =>\n {\n removeEventListener(\"storage\", onStorage);\n const left = (live.get(id) ?? 1) - 1;\n if (left > 0)\n {\n live.set(id, left);\n }\n else\n {\n live.delete(id);\n }\n });\n }\n return value;\n}\n"],"names":["unavailable","live","outside","listeners","source","listener","storageOf","store","kind","report","diagnostic","parse","raw","initial","value","accepted","validate","toJson","next","options","key","signal","id","count","first","echo","effect","json","onStorage","event","hasOwner","onDispose","left"],"mappings":"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"}
package/dist/stored.d.ts CHANGED
@@ -9,13 +9,27 @@ export interface StoredOptions<T> {
9
9
  storage?: "local" | "session";
10
10
  /** When two values count as the same, so a set of an equal value changes nothing; the signal's own option. */
11
11
  equals?: (previous: T, next: T) => boolean;
12
+ /**
13
+ * Whether a kept value, once parsed, is one the signal can hold. A value
14
+ * it refuses, or throws on, is read as `initial` and reported as
15
+ * `state_storage_unreadable`, on creation and when another tab writes the
16
+ * key, so data of another shape, such as another app's under the same
17
+ * key, never reaches the page.
18
+ */
19
+ validate?: (value: unknown) => boolean;
12
20
  }
21
+ /**
22
+ * Runs `listener` each time `source` takes a value another tab wrote, which
23
+ * only a stored signal does, and returns the function that stops it. The
24
+ * history uses it; the package does not export it.
25
+ */
26
+ export declare function onOutsideWrite(source: Signal<unknown>, listener: () => void): () => void;
13
27
  /**
14
28
  * A writable signal kept in storage under `key` as JSON: read back on
15
29
  * creation, written on every set, and set again when another tab writes
16
30
  * the key, so every tab shows the same value. A missing value takes
17
- * `initial` quietly; one that does not parse takes `initial` and reports
18
- * `state_storage_unreadable`. Setting `undefined` removes the key, so it
31
+ * `initial` quietly; one that does not parse, or that the `validate`
32
+ * option refuses, takes `initial` and reports `state_storage_unreadable`. Setting `undefined` removes the key, so it
19
33
  * reads back as `initial`; a value JSON cannot hold, such as a BigInt,
20
34
  * stays in memory and reports `state_value_not_json`. Where the page has
21
35
  * no storage, as on the server, the signal is a plain one and
@@ -1 +1 @@
1
- {"version":3,"file":"stored.d.ts","sourceRoot":"","sources":["../src/stored.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,KAAK,MAAM,EAA2D,MAAM,UAAU,CAAC;AAEhG,4BAA4B;AAC5B,MAAM,WAAW,aAAa,CAAC,CAAC;IAE5B,4JAA4J;IAC5J,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,8GAA8G;IAC9G,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC;CAC9C;AA2DD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAE,aAAa,CAAC,CAAC,CAAM,GAAG,MAAM,CAAC,CAAC,CAAC,CAkF5F"}
1
+ {"version":3,"file":"stored.d.ts","sourceRoot":"","sources":["../src/stored.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,KAAK,MAAM,EAA2D,MAAM,UAAU,CAAC;AAEhG,4BAA4B;AAC5B,MAAM,WAAW,aAAa,CAAC,CAAC;IAE5B,4JAA4J;IAC5J,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,8GAA8G;IAC9G,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC;IAC3C;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;CAC1C;AAWD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAaxF;AA6DD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAE,aAAa,CAAC,CAAC,CAAM,GAAG,MAAM,CAAC,CAAC,CAAC,CAsF5F"}
package/dist/stored.js CHANGED
@@ -7,6 +7,24 @@ import { diagnostic, effect, hasOwner, onDispose, report, signal } from "zerolit
7
7
  const unavailable = new Set();
8
8
  /** How many stored signals are live for each key of each storage, so a second one for a key is reported. */
9
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
+ }
10
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. */
11
29
  function storageOf(kind, key) {
12
30
  try {
@@ -22,18 +40,25 @@ function storageOf(kind, key) {
22
40
  return undefined;
23
41
  }
24
42
  }
25
- /** The value kept as `raw` under `key`, or `initial` where nothing is kept or what is kept does not parse, which is reported. */
26
- function parse(raw, key, initial) {
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) {
27
45
  if (raw === null) {
28
46
  return initial;
29
47
  }
48
+ let value;
49
+ let accepted;
30
50
  try {
31
- return JSON.parse(raw);
51
+ value = JSON.parse(raw);
52
+ accepted = validate === undefined || validate(value);
32
53
  }
33
54
  catch {
55
+ accepted = false;
56
+ }
57
+ if (!accepted) {
34
58
  report(diagnostic("state_storage_unreadable", "warning", { key }));
35
59
  return initial;
36
60
  }
61
+ return value;
37
62
  }
38
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. */
39
64
  function toJson(next) {
@@ -48,8 +73,8 @@ function toJson(next) {
48
73
  * A writable signal kept in storage under `key` as JSON: read back on
49
74
  * creation, written on every set, and set again when another tab writes
50
75
  * the key, so every tab shows the same value. A missing value takes
51
- * `initial` quietly; one that does not parse takes `initial` and reports
52
- * `state_storage_unreadable`. Setting `undefined` removes the key, so it
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
53
78
  * reads back as `initial`; a value JSON cannot hold, such as a BigInt,
54
79
  * stays in memory and reports `state_value_not_json`. Where the page has
55
80
  * no storage, as on the server, the signal is a plain one and
@@ -65,7 +90,7 @@ function toJson(next) {
65
90
  export function stored(key, initial, options = {}) {
66
91
  const kind = options.storage ?? "local";
67
92
  const store = storageOf(kind, key);
68
- const value = signal(store === undefined ? initial : parse(store.getItem(key), key, initial), options.equals === undefined ? undefined : { equals: options.equals });
93
+ const value = signal(store === undefined ? initial : parse(store.getItem(key), key, initial, options.validate), options.equals === undefined ? undefined : { equals: options.equals });
69
94
  if (store === undefined) {
70
95
  return value;
71
96
  }
@@ -106,11 +131,14 @@ export function stored(key, initial, options = {}) {
106
131
  }
107
132
  echo = true;
108
133
  try {
109
- value.set(parse(event.newValue, key, initial));
134
+ value.set(parse(event.newValue, key, initial, options.validate));
110
135
  }
111
136
  finally {
112
137
  echo = false;
113
138
  }
139
+ for (const listener of outside.get(value) ?? []) {
140
+ listener();
141
+ }
114
142
  };
115
143
  addEventListener("storage", onStorage);
116
144
  if (hasOwner()) {
@@ -1 +1 @@
1
- {"version":3,"file":"stored.js","sourceRoot":"","sources":["../src/stored.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"stored.js","sourceRoot":"","sources":["../src/stored.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerolith/state",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
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
5
  "license": "Apache-2.0",
6
6
  "author": "molexxxx",
@@ -44,9 +44,9 @@
44
44
  "build": "tsc -p tsconfig.build.json && node ../../scripts/production.ts"
45
45
  },
46
46
  "peerDependencies": {
47
- "zerolith": "^1.1.0"
47
+ "zerolith": "^1.3.0"
48
48
  },
49
49
  "devDependencies": {
50
- "zerolith": "^1.1.0"
50
+ "zerolith": "^1.3.0"
51
51
  }
52
52
  }
package/src/history.ts CHANGED
@@ -2,7 +2,8 @@
2
2
  * Undo and redo for a signal, from explicit checkpoints, as an editor
3
3
  * keeps them.
4
4
  */
5
- import { type ReadableSignal, type Signal, batch, signal, untracked } from "zerolith";
5
+ import { type ReadableSignal, type Signal, batch, hasOwner, onDispose, signal, untracked } from "zerolith";
6
+ import { onOutsideWrite } from "./stored.js";
6
7
 
7
8
  /** Options for `history`. */
8
9
  export interface HistoryOptions
@@ -37,7 +38,11 @@ export interface History<T>
37
38
  * the undone value back. A new checkpoint after an undo starts a new
38
39
  * branch, as in most editors, so what was undone is gone. `limit`
39
40
  * checkpoints are kept, 50 by default. Each move writes the signal and
40
- * both flags in one batch, so an effect over them runs once.
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.
41
46
  */
42
47
  export function history<T>(source: Signal<T>, options: HistoryOptions = {}): History<T>
43
48
  {
@@ -51,6 +56,17 @@ export function history<T>(source: Signal<T>, options: HistoryOptions = {}): His
51
56
  canUndo.set(past.length > 0);
52
57
  canRedo.set(future.length > 0);
53
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
+ }
54
70
  return {
55
71
  source,
56
72
  canUndo,
@@ -95,11 +111,6 @@ export function history<T>(source: Signal<T>, options: HistoryOptions = {}): His
95
111
  });
96
112
  return true;
97
113
  },
98
- dispose(): void
99
- {
100
- past.length = 0;
101
- future.length = 0;
102
- batch(update);
103
- }
114
+ dispose: forget
104
115
  };
105
116
  }
package/src/stored.ts CHANGED
@@ -11,6 +11,14 @@ export interface StoredOptions<T>
11
11
  storage?: "local" | "session";
12
12
  /** When two values count as the same, so a set of an equal value changes nothing; the signal's own option. */
13
13
  equals?: (previous: T, next: T) => boolean;
14
+ /**
15
+ * Whether a kept value, once parsed, is one the signal can hold. A value
16
+ * it refuses, or throws on, is read as `initial` and reported as
17
+ * `state_storage_unreadable`, on creation and when another tab writes the
18
+ * key, so data of another shape, such as another app's under the same
19
+ * key, never reaches the page.
20
+ */
21
+ validate?: (value: unknown) => boolean;
14
22
  }
15
23
 
16
24
  /** The storage kinds reported unavailable on this page, each once. */
@@ -19,6 +27,29 @@ const unavailable = new Set<string>();
19
27
  /** How many stored signals are live for each key of each storage, so a second one for a key is reported. */
20
28
  const live = new Map<string, number>();
21
29
 
30
+ /** What runs after a stored signal takes a value another tab wrote, keyed by the signal. */
31
+ const outside = new WeakMap<Signal<unknown>, Set<() => void>>();
32
+
33
+ /**
34
+ * Runs `listener` each time `source` takes a value another tab wrote, which
35
+ * only a stored signal does, and returns the function that stops it. The
36
+ * history uses it; the package does not export it.
37
+ */
38
+ export function onOutsideWrite(source: Signal<unknown>, listener: () => void): () => void
39
+ {
40
+ let listeners = outside.get(source);
41
+ if (listeners === undefined)
42
+ {
43
+ listeners = new Set();
44
+ outside.set(source, listeners);
45
+ }
46
+ listeners.add(listener);
47
+ return () =>
48
+ {
49
+ listeners.delete(listener);
50
+ };
51
+ }
52
+
22
53
  /** 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. */
23
54
  function storageOf(kind: "local" | "session", key: string): Storage | undefined
24
55
  {
@@ -39,22 +70,30 @@ function storageOf(kind: "local" | "session", key: string): Storage | undefined
39
70
  }
40
71
  }
41
72
 
42
- /** The value kept as `raw` under `key`, or `initial` where nothing is kept or what is kept does not parse, which is reported. */
43
- function parse<T>(raw: string | null, key: string, initial: T): T
73
+ /** 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. */
74
+ function parse<T>(raw: string | null, key: string, initial: T, validate: ((value: unknown) => boolean) | undefined): T
44
75
  {
45
76
  if (raw === null)
46
77
  {
47
78
  return initial;
48
79
  }
80
+ let value: unknown;
81
+ let accepted: boolean;
49
82
  try
50
83
  {
51
- return JSON.parse(raw) as T;
84
+ value = JSON.parse(raw);
85
+ accepted = validate === undefined || validate(value);
52
86
  }
53
87
  catch
88
+ {
89
+ accepted = false;
90
+ }
91
+ if (!accepted)
54
92
  {
55
93
  report(diagnostic("state_storage_unreadable", "warning", { key }));
56
94
  return initial;
57
95
  }
96
+ return value as T;
58
97
  }
59
98
 
60
99
  /** 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. */
@@ -74,8 +113,8 @@ function toJson(next: unknown): string | undefined
74
113
  * A writable signal kept in storage under `key` as JSON: read back on
75
114
  * creation, written on every set, and set again when another tab writes
76
115
  * the key, so every tab shows the same value. A missing value takes
77
- * `initial` quietly; one that does not parse takes `initial` and reports
78
- * `state_storage_unreadable`. Setting `undefined` removes the key, so it
116
+ * `initial` quietly; one that does not parse, or that the `validate`
117
+ * option refuses, takes `initial` and reports `state_storage_unreadable`. Setting `undefined` removes the key, so it
79
118
  * reads back as `initial`; a value JSON cannot hold, such as a BigInt,
80
119
  * stays in memory and reports `state_value_not_json`. Where the page has
81
120
  * no storage, as on the server, the signal is a plain one and
@@ -92,7 +131,7 @@ export function stored<T>(key: string, initial: T, options: StoredOptions<T> = {
92
131
  {
93
132
  const kind = options.storage ?? "local";
94
133
  const store = storageOf(kind, key);
95
- const value = signal<T>(store === undefined ? initial : parse(store.getItem(key), key, initial), options.equals === undefined ? undefined : { equals: options.equals });
134
+ const value = signal<T>(store === undefined ? initial : parse(store.getItem(key), key, initial, options.validate), options.equals === undefined ? undefined : { equals: options.equals });
96
135
  if (store === undefined)
97
136
  {
98
137
  return value;
@@ -145,12 +184,16 @@ export function stored<T>(key: string, initial: T, options: StoredOptions<T> = {
145
184
  echo = true;
146
185
  try
147
186
  {
148
- value.set(parse(event.newValue, key, initial));
187
+ value.set(parse(event.newValue, key, initial, options.validate));
149
188
  }
150
189
  finally
151
190
  {
152
191
  echo = false;
153
192
  }
193
+ for (const listener of outside.get(value as Signal<unknown>) ?? [])
194
+ {
195
+ listener();
196
+ }
154
197
  };
155
198
  addEventListener("storage", onStorage);
156
199
  if (hasOwner())