@zerolith/state 1.6.0 → 1.7.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.
@@ -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,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}
1
+ import{batch as e,diagnostic as t,effect as n,hasOwner as r,onDispose as i,report as a,signal as o}from"zerolith";const s=new Set,c=new Map,l=new WeakMap;export function onOutsideWrite(e,t){let n=l.get(e);return n===void 0&&(n=new Set,l.set(e,n)),n.add(t),()=>{n.delete(t)}}function u(e,n){try{if(typeof window>`u`)throw TypeError(`no window`);let t=e===`session`?window.sessionStorage:window.localStorage;return t.getItem(``),t}catch{s.has(e)||(s.add(e),a(t(`state_storage_unavailable`,`warning`,{storage:e,key:n})));return}}function d(e,n,r,i){if(e===null)return r;let o,s;try{o=JSON.parse(e),s=i===void 0||i(o)}catch{s=!1}return s?o:(a(t(`state_storage_unreadable`,`warning`,{key:n})),r)}function f(e){try{return JSON.stringify(e)}catch{return}}export function stored(s,p,m={}){let h=m.storage??`local`,g=u(h,s),_=o(g===void 0?p:d(g.getItem(s),s,p,m.validate),m.equals===void 0?void 0:{equals:m.equals});if(g===void 0)return _;let v=`${h}:${s}`,y=(c.get(v)??0)+1;c.set(v,y),y>1&&a(t(`state_key_duplicate`,`warning`,{storage:h,key:s}));let b=!0,x;n(()=>{let e=_.get();if(b){b=!1;return}if(x!==void 0&&Object.is(e,x.value)){x=void 0;return}let n=f(e);if(n===void 0&&e!==void 0)a(t(`state_value_not_json`,`warning`,{key:s}));else try{n===void 0?g.removeItem(s):g.setItem(s,n)}catch{a(t(`state_storage_unavailable`,`warning`,{storage:h,key:s}))}});let S=t=>{if(t.storageArea!==g||t.key!==null&&t.key!==s)return;let n=d(t.newValue,s,p,m.validate);x={value:n};try{e(()=>{try{_.set(n)}finally{for(let e of l.get(_)??[])e()}})}finally{x=void 0}};return window.addEventListener(`storage`,S),r()&&i(()=>{window.removeEventListener(`storage`,S);let e=(c.get(v)??1)-1;e>0?c.set(v,e):c.delete(v)}),_}
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 * 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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"}
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, batch, 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/**\n * The storage of a kind, or undefined where the page has none or refuses it,\n * which is reported once per kind with the first key asked for. A runtime\n * with no window has none, even with Web Storage globals, as Node has from\n * 25: there the storage is one store for the whole process, so a value one\n * request set would be read back by the next.\n */\nfunction storageOf(kind: \"local\" | \"session\", key: string): Storage | undefined\n{\n try\n {\n if (typeof window === \"undefined\")\n {\n throw new TypeError(\"no window\");\n }\n const store = kind === \"session\" ? window.sessionStorage : window.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 incoming: { value: T } | undefined;\n effect(() =>\n {\n const next = value.get();\n if (first)\n {\n first = false;\n return;\n }\n if (incoming !== undefined && Object.is(next, incoming.value))\n {\n incoming = undefined;\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 const next = parse(event.newValue, key, initial, options.validate);\n incoming = { value: next };\n try\n {\n batch(() =>\n {\n try\n {\n value.set(next);\n }\n finally\n {\n for (const listener of outside.get(value as Signal<unknown>) ?? [])\n {\n listener();\n }\n }\n });\n }\n finally\n {\n incoming = undefined;\n }\n };\n window.addEventListener(\"storage\", onStorage);\n if (hasOwner())\n {\n onDispose(() =>\n {\n window.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","incoming","effect","json","onStorage","event","batch","hasOwner","onDispose","left"],"mappings":"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"}
@@ -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;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"}
1
+ {"version":3,"file":"stored.d.ts","sourceRoot":"","sources":["../src/stored.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,KAAK,MAAM,EAAkE,MAAM,UAAU,CAAC;AAEvG,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;AAuED;;;;;;;;;;;;;;;;;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,CAqG5F"}
package/dist/stored.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * A signal kept in the browser's storage under a key, as JSON, and in step
3
3
  * across tabs through the `storage` event.
4
4
  */
5
- import { diagnostic, effect, hasOwner, onDispose, report, signal } from "zerolith";
5
+ import { batch, diagnostic, effect, hasOwner, onDispose, report, signal } from "zerolith";
6
6
  /** The storage kinds reported unavailable on this page, each once. */
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. */
@@ -25,10 +25,19 @@ export function onOutsideWrite(source, listener) {
25
25
  listeners.delete(listener);
26
26
  };
27
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. */
28
+ /**
29
+ * The storage of a kind, or undefined where the page has none or refuses it,
30
+ * which is reported once per kind with the first key asked for. A runtime
31
+ * with no window has none, even with Web Storage globals, as Node has from
32
+ * 25: there the storage is one store for the whole process, so a value one
33
+ * request set would be read back by the next.
34
+ */
29
35
  function storageOf(kind, key) {
30
36
  try {
31
- const store = kind === "session" ? globalThis.sessionStorage : globalThis.localStorage;
37
+ if (typeof window === "undefined") {
38
+ throw new TypeError("no window");
39
+ }
40
+ const store = kind === "session" ? window.sessionStorage : window.localStorage;
32
41
  store.getItem("");
33
42
  return store;
34
43
  }
@@ -101,13 +110,17 @@ export function stored(key, initial, options = {}) {
101
110
  report(diagnostic("state_key_duplicate", "warning", { storage: kind, key }));
102
111
  }
103
112
  let first = true;
104
- let echo = false;
113
+ let incoming;
105
114
  effect(() => {
106
115
  const next = value.get();
107
- if (first || echo) {
116
+ if (first) {
108
117
  first = false;
109
118
  return;
110
119
  }
120
+ if (incoming !== undefined && Object.is(next, incoming.value)) {
121
+ incoming = undefined;
122
+ return;
123
+ }
111
124
  const json = toJson(next);
112
125
  if (json === undefined && next !== undefined) {
113
126
  report(diagnostic("state_value_not_json", "warning", { key }));
@@ -129,21 +142,28 @@ export function stored(key, initial, options = {}) {
129
142
  if (event.storageArea !== store || (event.key !== null && event.key !== key)) {
130
143
  return;
131
144
  }
132
- echo = true;
145
+ const next = parse(event.newValue, key, initial, options.validate);
146
+ incoming = { value: next };
133
147
  try {
134
- value.set(parse(event.newValue, key, initial, options.validate));
148
+ batch(() => {
149
+ try {
150
+ value.set(next);
151
+ }
152
+ finally {
153
+ for (const listener of outside.get(value) ?? []) {
154
+ listener();
155
+ }
156
+ }
157
+ });
135
158
  }
136
159
  finally {
137
- echo = false;
138
- }
139
- for (const listener of outside.get(value) ?? []) {
140
- listener();
160
+ incoming = undefined;
141
161
  }
142
162
  };
143
- addEventListener("storage", onStorage);
163
+ window.addEventListener("storage", onStorage);
144
164
  if (hasOwner()) {
145
165
  onDispose(() => {
146
- removeEventListener("storage", onStorage);
166
+ window.removeEventListener("storage", onStorage);
147
167
  const left = (live.get(id) ?? 1) - 1;
148
168
  if (left > 0) {
149
169
  live.set(id, left);
@@ -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.6.0",
3
+ "version": "1.7.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.6.0"
47
+ "zerolith": "^1.7.0"
48
48
  },
49
49
  "devDependencies": {
50
- "zerolith": "^1.6.0"
50
+ "zerolith": "^1.7.0"
51
51
  }
52
52
  }
package/src/stored.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * A signal kept in the browser's storage under a key, as JSON, and in step
3
3
  * across tabs through the `storage` event.
4
4
  */
5
- import { type Signal, diagnostic, effect, hasOwner, onDispose, report, signal } from "zerolith";
5
+ import { type Signal, batch, diagnostic, effect, hasOwner, onDispose, report, signal } from "zerolith";
6
6
 
7
7
  /** Options for `stored`. */
8
8
  export interface StoredOptions<T>
@@ -50,12 +50,22 @@ export function onOutsideWrite(source: Signal<unknown>, listener: () => void): (
50
50
  };
51
51
  }
52
52
 
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. */
53
+ /**
54
+ * The storage of a kind, or undefined where the page has none or refuses it,
55
+ * which is reported once per kind with the first key asked for. A runtime
56
+ * with no window has none, even with Web Storage globals, as Node has from
57
+ * 25: there the storage is one store for the whole process, so a value one
58
+ * request set would be read back by the next.
59
+ */
54
60
  function storageOf(kind: "local" | "session", key: string): Storage | undefined
55
61
  {
56
62
  try
57
63
  {
58
- const store = kind === "session" ? globalThis.sessionStorage : globalThis.localStorage;
64
+ if (typeof window === "undefined")
65
+ {
66
+ throw new TypeError("no window");
67
+ }
68
+ const store = kind === "session" ? window.sessionStorage : window.localStorage;
59
69
  store.getItem("");
60
70
  return store;
61
71
  }
@@ -144,15 +154,20 @@ export function stored<T>(key: string, initial: T, options: StoredOptions<T> = {
144
154
  report(diagnostic("state_key_duplicate", "warning", { storage: kind, key }));
145
155
  }
146
156
  let first = true;
147
- let echo = false;
157
+ let incoming: { value: T } | undefined;
148
158
  effect(() =>
149
159
  {
150
160
  const next = value.get();
151
- if (first || echo)
161
+ if (first)
152
162
  {
153
163
  first = false;
154
164
  return;
155
165
  }
166
+ if (incoming !== undefined && Object.is(next, incoming.value))
167
+ {
168
+ incoming = undefined;
169
+ return;
170
+ }
156
171
  const json = toJson(next);
157
172
  if (json === undefined && next !== undefined)
158
173
  {
@@ -181,26 +196,36 @@ export function stored<T>(key: string, initial: T, options: StoredOptions<T> = {
181
196
  {
182
197
  return;
183
198
  }
184
- echo = true;
199
+ const next = parse(event.newValue, key, initial, options.validate);
200
+ incoming = { value: next };
185
201
  try
186
202
  {
187
- value.set(parse(event.newValue, key, initial, options.validate));
203
+ batch(() =>
204
+ {
205
+ try
206
+ {
207
+ value.set(next);
208
+ }
209
+ finally
210
+ {
211
+ for (const listener of outside.get(value as Signal<unknown>) ?? [])
212
+ {
213
+ listener();
214
+ }
215
+ }
216
+ });
188
217
  }
189
218
  finally
190
219
  {
191
- echo = false;
192
- }
193
- for (const listener of outside.get(value as Signal<unknown>) ?? [])
194
- {
195
- listener();
220
+ incoming = undefined;
196
221
  }
197
222
  };
198
- addEventListener("storage", onStorage);
223
+ window.addEventListener("storage", onStorage);
199
224
  if (hasOwner())
200
225
  {
201
226
  onDispose(() =>
202
227
  {
203
- removeEventListener("storage", onStorage);
228
+ window.removeEventListener("storage", onStorage);
204
229
  const left = (live.get(id) ?? 1) - 1;
205
230
  if (left > 0)
206
231
  {