@praxisjs/core 2.2.0 → 2.3.1

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.
Files changed (116) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/component/internals.d.ts +12 -2
  3. package/dist/component/internals.d.ts.map +1 -1
  4. package/dist/component/internals.js +25 -7
  5. package/dist/component/internals.js.map +1 -1
  6. package/dist/internal.d.ts +3 -2
  7. package/dist/internal.d.ts.map +1 -1
  8. package/dist/internal.js +3 -2
  9. package/dist/internal.js.map +1 -1
  10. package/dist/signal/batch.d.ts +1 -3
  11. package/dist/signal/batch.d.ts.map +1 -1
  12. package/dist/signal/batch.js +4 -30
  13. package/dist/signal/batch.js.map +1 -1
  14. package/dist/signal/computed.d.ts.map +1 -1
  15. package/dist/signal/computed.js +13 -66
  16. package/dist/signal/computed.js.map +1 -1
  17. package/dist/signal/effect.d.ts +6 -14
  18. package/dist/signal/effect.d.ts.map +1 -1
  19. package/dist/signal/effect.js +11 -83
  20. package/dist/signal/effect.js.map +1 -1
  21. package/dist/signal/graph.d.ts +111 -0
  22. package/dist/signal/graph.d.ts.map +1 -0
  23. package/dist/signal/graph.js +513 -0
  24. package/dist/signal/graph.js.map +1 -0
  25. package/dist/signal/index.d.ts +1 -0
  26. package/dist/signal/index.d.ts.map +1 -1
  27. package/dist/signal/index.js +1 -0
  28. package/dist/signal/index.js.map +1 -1
  29. package/dist/signal/peek.d.ts.map +1 -1
  30. package/dist/signal/peek.js +2 -9
  31. package/dist/signal/peek.js.map +1 -1
  32. package/dist/signal/selector.d.ts +5 -0
  33. package/dist/signal/selector.d.ts.map +1 -0
  34. package/dist/signal/selector.js +128 -0
  35. package/dist/signal/selector.js.map +1 -0
  36. package/dist/signal/signal.d.ts +0 -5
  37. package/dist/signal/signal.d.ts.map +1 -1
  38. package/dist/signal/signal.js +11 -70
  39. package/dist/signal/signal.js.map +1 -1
  40. package/package.json +13 -4
  41. package/src/component/internals.ts +35 -10
  42. package/src/internal.ts +26 -0
  43. package/src/signal/batch.ts +5 -33
  44. package/src/signal/computed.ts +13 -66
  45. package/src/signal/effect.ts +12 -92
  46. package/src/signal/graph.ts +562 -0
  47. package/src/signal/index.ts +1 -0
  48. package/src/signal/peek.ts +2 -8
  49. package/src/signal/selector.ts +140 -0
  50. package/src/signal/signal.ts +11 -74
  51. package/dist/__tests__/batch.test.d.ts +0 -2
  52. package/dist/__tests__/batch.test.d.ts.map +0 -1
  53. package/dist/__tests__/batch.test.js +0 -124
  54. package/dist/__tests__/batch.test.js.map +0 -1
  55. package/dist/__tests__/component.test.d.ts +0 -2
  56. package/dist/__tests__/component.test.d.ts.map +0 -1
  57. package/dist/__tests__/component.test.js +0 -124
  58. package/dist/__tests__/component.test.js.map +0 -1
  59. package/dist/__tests__/computed.test.d.ts +0 -2
  60. package/dist/__tests__/computed.test.d.ts.map +0 -1
  61. package/dist/__tests__/computed.test.js +0 -289
  62. package/dist/__tests__/computed.test.js.map +0 -1
  63. package/dist/__tests__/effect.test.d.ts +0 -2
  64. package/dist/__tests__/effect.test.d.ts.map +0 -1
  65. package/dist/__tests__/effect.test.js +0 -200
  66. package/dist/__tests__/effect.test.js.map +0 -1
  67. package/dist/__tests__/peek.test.d.ts +0 -2
  68. package/dist/__tests__/peek.test.d.ts.map +0 -1
  69. package/dist/__tests__/peek.test.js +0 -26
  70. package/dist/__tests__/peek.test.js.map +0 -1
  71. package/dist/__tests__/persisted-signal.test.d.ts +0 -2
  72. package/dist/__tests__/persisted-signal.test.d.ts.map +0 -1
  73. package/dist/__tests__/persisted-signal.test.js +0 -252
  74. package/dist/__tests__/persisted-signal.test.js.map +0 -1
  75. package/dist/__tests__/reactive.test.d.ts +0 -2
  76. package/dist/__tests__/reactive.test.d.ts.map +0 -1
  77. package/dist/__tests__/reactive.test.js +0 -299
  78. package/dist/__tests__/reactive.test.js.map +0 -1
  79. package/dist/__tests__/resource-focus.test.d.ts +0 -2
  80. package/dist/__tests__/resource-focus.test.d.ts.map +0 -1
  81. package/dist/__tests__/resource-focus.test.js +0 -50
  82. package/dist/__tests__/resource-focus.test.js.map +0 -1
  83. package/dist/__tests__/resource-server-pass.test.d.ts +0 -2
  84. package/dist/__tests__/resource-server-pass.test.d.ts.map +0 -1
  85. package/dist/__tests__/resource-server-pass.test.js +0 -78
  86. package/dist/__tests__/resource-server-pass.test.js.map +0 -1
  87. package/dist/__tests__/resource.test.d.ts +0 -2
  88. package/dist/__tests__/resource.test.d.ts.map +0 -1
  89. package/dist/__tests__/resource.test.js +0 -336
  90. package/dist/__tests__/resource.test.js.map +0 -1
  91. package/dist/__tests__/signal.test.d.ts +0 -2
  92. package/dist/__tests__/signal.test.d.ts.map +0 -1
  93. package/dist/__tests__/signal.test.js +0 -254
  94. package/dist/__tests__/signal.test.js.map +0 -1
  95. package/dist/__tests__/synced.test.d.ts +0 -2
  96. package/dist/__tests__/synced.test.d.ts.map +0 -1
  97. package/dist/__tests__/synced.test.js +0 -142
  98. package/dist/__tests__/synced.test.js.map +0 -1
  99. package/dist/__tests__/track.test.d.ts +0 -2
  100. package/dist/__tests__/track.test.d.ts.map +0 -1
  101. package/dist/__tests__/track.test.js +0 -54
  102. package/dist/__tests__/track.test.js.map +0 -1
  103. package/src/__tests__/batch.test.ts +0 -138
  104. package/src/__tests__/component.test.ts +0 -165
  105. package/src/__tests__/computed.test.ts +0 -333
  106. package/src/__tests__/effect.test.ts +0 -241
  107. package/src/__tests__/peek.test.ts +0 -31
  108. package/src/__tests__/persisted-signal.test.ts +0 -305
  109. package/src/__tests__/reactive.test.ts +0 -343
  110. package/src/__tests__/resource-focus.test.ts +0 -61
  111. package/src/__tests__/resource-server-pass.test.ts +0 -90
  112. package/src/__tests__/resource.test.ts +0 -421
  113. package/src/__tests__/signal.test.ts +0 -291
  114. package/src/__tests__/synced.test.ts +0 -173
  115. package/src/__tests__/track.test.ts +0 -61
  116. package/tsconfig.json +0 -8
@@ -1,241 +0,0 @@
1
- import { describe, it, expect, vi } from "vitest";
2
-
3
- import {
4
- effect,
5
- track,
6
- activeEffect,
7
- cleanupEffectDeps,
8
- recordDependency,
9
- type SubscriberHolder,
10
- } from "../signal/effect";
11
- import { batch } from "../signal/batch";
12
- import { signal } from "../signal/signal";
13
-
14
- describe("effect", () => {
15
- it("runs immediately on creation", () => {
16
- const ran = vi.fn();
17
- effect(ran);
18
- expect(ran).toHaveBeenCalledOnce();
19
- });
20
-
21
- it("re-runs when a tracked signal changes", () => {
22
- const s = signal(1);
23
- const values: number[] = [];
24
- effect(() => {
25
- values.push(s());
26
- });
27
- s.set(2);
28
- s.set(3);
29
- expect(values).toEqual([1, 2, 3]);
30
- });
31
-
32
- it("stop() calls the cleanup function", () => {
33
- const cleanupFn = vi.fn();
34
- const stop = effect(() => cleanupFn);
35
- stop();
36
- expect(cleanupFn).toHaveBeenCalledOnce();
37
- });
38
-
39
- it("calls cleanup function before re-run", () => {
40
- const s = signal(0);
41
- const order: string[] = [];
42
- effect(() => {
43
- const v = s();
44
- order.push(`run:${v}`);
45
- return () => order.push(`cleanup`);
46
- });
47
- s.set(1);
48
- expect(order).toEqual(["run:0", "cleanup", "run:1"]);
49
- });
50
-
51
- it("tracks multiple signals", () => {
52
- const a = signal(1);
53
- const b = signal(10);
54
- const sums: number[] = [];
55
- effect(() => {
56
- sums.push(a() + b());
57
- });
58
- a.set(2);
59
- b.set(20);
60
- expect(sums).toEqual([11, 12, 22]);
61
- });
62
-
63
- it("stop() prevents future re-runs", () => {
64
- const s = signal(0);
65
- const values: number[] = [];
66
- const stop = effect(() => {
67
- values.push(s());
68
- });
69
- stop();
70
- s.set(99);
71
- expect(values).toEqual([0]); // only initial run
72
- });
73
-
74
- it("drops dependencies that are no longer read", () => {
75
- const useA = signal(true);
76
- const a = signal(1);
77
- const b = signal(10);
78
- const values: number[] = [];
79
-
80
- effect(() => {
81
- values.push(useA() ? a() : b());
82
- });
83
-
84
- useA.set(false);
85
- expect(values).toEqual([1, 10]);
86
-
87
- a.set(2);
88
- expect(values).toEqual([1, 10]);
89
-
90
- b.set(20);
91
- expect(values).toEqual([1, 10, 20]);
92
- });
93
-
94
- it("does not skip other subscribers when one effect changes subscriptions while notifying", () => {
95
- const useOther = signal(false);
96
- const source = signal(0);
97
- const other = signal(100);
98
- const first = vi.fn();
99
- const second = vi.fn();
100
-
101
- effect(() => {
102
- first(useOther() ? other() : source());
103
- });
104
- effect(() => {
105
- second(source());
106
- });
107
-
108
- useOther.set(true);
109
- first.mockClear();
110
- second.mockClear();
111
-
112
- source.set(1);
113
-
114
- expect(first).not.toHaveBeenCalled();
115
- expect(second).toHaveBeenCalledWith(1);
116
- });
117
-
118
- it("effect with no reactive dependencies runs once only", () => {
119
- const ran = vi.fn();
120
- effect(() => {
121
- ran(); // no signal reads
122
- });
123
- expect(ran).toHaveBeenCalledTimes(1);
124
- // No signal changes possible — just verify it doesn't re-run spontaneously
125
- expect(ran).toHaveBeenCalledTimes(1);
126
- });
127
-
128
- it("stop() with no cleanup function does not throw", () => {
129
- const stop = effect(() => { /* no return value */ });
130
- expect(() => stop()).not.toThrow();
131
- });
132
-
133
- it("cleanup is re-invoked before each re-run", () => {
134
- const s = signal(0);
135
- const cleanupCount = { n: 0 };
136
- effect(() => {
137
- s(); // track
138
- return () => { cleanupCount.n++; };
139
- });
140
- s.set(1);
141
- s.set(2);
142
- // cleanup called once before second run, once before third run
143
- expect(cleanupCount.n).toBe(2);
144
- });
145
-
146
- it("stop() called multiple times does not crash or double-clean", () => {
147
- const cleanupFn = vi.fn();
148
- const stop = effect(() => cleanupFn);
149
- expect(() => {
150
- stop();
151
- stop();
152
- stop();
153
- }).not.toThrow();
154
- // cleanup is only called once (on first stop)
155
- expect(cleanupFn).toHaveBeenCalledTimes(1);
156
- });
157
-
158
- it("ignores a queued run after the effect was stopped", () => {
159
- const s = signal(0);
160
- const values: number[] = [];
161
- const stop = effect(() => {
162
- values.push(s());
163
- });
164
-
165
- batch(() => {
166
- s.set(1);
167
- stop();
168
- });
169
-
170
- expect(values).toEqual([0]);
171
- });
172
-
173
- it("cleanupEffectDeps handles empty, single, and array dependency holders", () => {
174
- const tracked = vi.fn();
175
- const other = vi.fn();
176
- const emptyHolder: SubscriberHolder = { subs: null };
177
- const singleHolder: SubscriberHolder = { subs: tracked };
178
- const arrayHolder: SubscriberHolder = { subs: [tracked, other] };
179
- const unmatchedSingleHolder: SubscriberHolder = { subs: other };
180
- const unmatchedArrayHolder: SubscriberHolder = { subs: [other] };
181
-
182
- recordDependency(tracked, emptyHolder);
183
- recordDependency(tracked, singleHolder);
184
- recordDependency(tracked, arrayHolder);
185
- recordDependency(tracked, unmatchedSingleHolder);
186
- recordDependency(tracked, unmatchedArrayHolder);
187
-
188
- cleanupEffectDeps(tracked);
189
-
190
- expect(emptyHolder.subs).toBeNull();
191
- expect(singleHolder.subs).toBeNull();
192
- expect(arrayHolder.subs).toEqual([other]);
193
- expect(unmatchedSingleHolder.subs).toBe(other);
194
- expect(unmatchedArrayHolder.subs).toEqual([other]);
195
- });
196
-
197
- it("cleanupEffectDeps handles Set dependency holders, emptying them to null", () => {
198
- const tracked = vi.fn();
199
- const other = vi.fn();
200
- const sharedHolder: SubscriberHolder = { subs: new Set([tracked, other]) };
201
- const soleHolder: SubscriberHolder = { subs: new Set([tracked]) };
202
-
203
- recordDependency(tracked, sharedHolder);
204
- recordDependency(tracked, soleHolder);
205
-
206
- cleanupEffectDeps(tracked);
207
-
208
- expect([...(sharedHolder.subs as Set<() => void>)]).toEqual([other]);
209
- expect(soleHolder.subs).toBeNull();
210
- });
211
-
212
- it("exception inside nested track() restores outer activeEffect", () => {
213
- const s = signal(0);
214
- const outer = vi.fn();
215
- let outerEffectRef: unknown;
216
-
217
- effect(() => {
218
- outer();
219
- s(); // track
220
- outerEffectRef = activeEffect;
221
- });
222
-
223
- // Now do a track that throws — outer activeEffect must be restored
224
- try {
225
- track(() => { throw new Error("inner throws"); });
226
- } catch {
227
- // expected
228
- }
229
-
230
- expect(activeEffect).toBeNull();
231
- });
232
-
233
- it("activeEffect is null after a track() that throws", () => {
234
- try {
235
- track(() => { throw new Error("boom"); });
236
- } catch {
237
- // expected
238
- }
239
- expect(activeEffect).toBeNull();
240
- });
241
- });
@@ -1,31 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
-
3
- import { computed } from "../signal/computed";
4
- import { effect } from "../signal/effect";
5
- import { peek } from "../signal/peek";
6
- import { signal } from "../signal/signal";
7
-
8
- describe("peek", () => {
9
- it("returns the current value without tracking", () => {
10
- const s = signal(42);
11
- expect(peek(s)).toBe(42);
12
- });
13
-
14
- it("does not subscribe the active effect when peeked", () => {
15
- const s = signal(1);
16
- const runs: number[] = [];
17
-
18
- effect(() => {
19
- runs.push(peek(s)); // peek — should not subscribe
20
- });
21
-
22
- s.set(99); // should NOT trigger re-run
23
- expect(runs).toEqual([1]);
24
- });
25
-
26
- it("works with computed values", () => {
27
- const s = signal(5);
28
- const c = computed(() => s() * 2);
29
- expect(peek(c)).toBe(10);
30
- });
31
- });
@@ -1,305 +0,0 @@
1
- // @vitest-environment jsdom
2
- import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
3
-
4
- import { persistedSignal } from "../signal/persisted";
5
-
6
- beforeEach(() => {
7
- localStorage.clear();
8
- });
9
-
10
- describe("persistedSignal", () => {
11
- it("returns initialValue when nothing is stored", () => {
12
- const s = persistedSignal("key1", 42);
13
- expect(s()).toBe(42);
14
- });
15
-
16
- it("reads existing value from localStorage", () => {
17
- localStorage.setItem("key2", "99");
18
- const s = persistedSignal("key2", 0);
19
- expect(s()).toBe(99);
20
- });
21
-
22
- it("persists set() to localStorage", () => {
23
- const s = persistedSignal("key3", 0);
24
- s.set(7);
25
- expect(localStorage.getItem("key3")).toBe("7");
26
- expect(s()).toBe(7);
27
- });
28
-
29
- it("persists update() to localStorage", () => {
30
- const s = persistedSignal("key4", 10);
31
- s.update((v) => v + 5);
32
- expect(localStorage.getItem("key4")).toBe("15");
33
- expect(s()).toBe(15);
34
- });
35
-
36
- it("removes the key when set to null", () => {
37
- const s = persistedSignal<string | null>("key5", "hello");
38
- s.set(null);
39
- expect(localStorage.getItem("key5")).toBeNull();
40
- });
41
-
42
- it("marks __isSignal = true", () => {
43
- const s = persistedSignal("key6", 0);
44
- expect(s.__isSignal).toBe(true);
45
- });
46
-
47
- it("supports custom serialize/deserialize", () => {
48
- const s = persistedSignal("key7", { x: 1 }, {
49
- serialize: (v) => JSON.stringify(v),
50
- deserialize: (raw) => JSON.parse(raw) as { x: number },
51
- });
52
- s.set({ x: 99 });
53
- expect(localStorage.getItem("key7")).toBe('{"x":99}');
54
- const s2 = persistedSignal("key7", { x: 0 }, {
55
- serialize: JSON.stringify,
56
- deserialize: (raw) => JSON.parse(raw) as { x: number },
57
- });
58
- expect(s2().x).toBe(99);
59
- });
60
-
61
- it("falls back to initialValue when deserialization fails", () => {
62
- const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
63
- localStorage.setItem("key8", "{ bad json");
64
- const s = persistedSignal("key8", 42);
65
- expect(s()).toBe(42);
66
- warn.mockRestore();
67
- });
68
-
69
- it("syncs from storage event on other tab", () => {
70
- const s = persistedSignal("key9", 1);
71
- window.dispatchEvent(
72
- new StorageEvent("storage", {
73
- key: "key9",
74
- newValue: "55",
75
- storageArea: localStorage,
76
- }),
77
- );
78
- expect(s()).toBe(55);
79
- });
80
-
81
- it("syncTabs=false does not add storage listener", () => {
82
- const s = persistedSignal("key10", 1, { syncTabs: false });
83
- s.set(5);
84
- // Just verify it works without errors
85
- expect(s()).toBe(5);
86
- });
87
-
88
- it("warns and falls back to initialValue when serialize throws on set", () => {
89
- const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
90
- const s = persistedSignal("key11", { x: 1 }, {
91
- serialize: () => { throw new Error("serialize fail"); },
92
- deserialize: JSON.parse as (v: string) => { x: number },
93
- });
94
- expect(() => s.set({ x: 2 })).not.toThrow();
95
- expect(warn).toHaveBeenCalledWith(expect.stringContaining("Failed to serialize"), expect.any(Error));
96
- warn.mockRestore();
97
- });
98
-
99
- it("syncs to initialValue when storage event has newValue=null (key removed)", () => {
100
- const s = persistedSignal("key12", 42);
101
- s.set(99);
102
-
103
- window.dispatchEvent(
104
- new StorageEvent("storage", {
105
- key: "key12",
106
- newValue: null, // key removed in another tab
107
- storageArea: localStorage,
108
- }),
109
- );
110
- expect(s()).toBe(42); // falls back to initialValue
111
- });
112
-
113
- it("warns and falls back to initialValue when storage event deserialization fails", () => {
114
- const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
115
- const s = persistedSignal("key13", 0, {
116
- deserialize: () => { throw new Error("bad"); },
117
- });
118
-
119
- window.dispatchEvent(
120
- new StorageEvent("storage", {
121
- key: "key13",
122
- newValue: "invalid",
123
- storageArea: localStorage,
124
- }),
125
- );
126
- expect(s()).toBe(0);
127
- expect(warn).toHaveBeenCalledWith(
128
- expect.stringContaining("Failed to deserialize"),
129
- expect.any(Error),
130
- );
131
- warn.mockRestore();
132
- });
133
-
134
- it("ignores storage events from other keys", () => {
135
- const s = persistedSignal("key14", 1);
136
- window.dispatchEvent(
137
- new StorageEvent("storage", {
138
- key: "other-key",
139
- newValue: "999",
140
- storageArea: localStorage,
141
- }),
142
- );
143
- expect(s()).toBe(1);
144
- });
145
-
146
- it("set(undefined) removes the key from localStorage", () => {
147
- const s = persistedSignal<number | undefined>("key15", 5);
148
- s.set(undefined);
149
- expect(localStorage.getItem("key15")).toBeNull();
150
- });
151
-
152
- it("localStorage.setItem throws QuotaExceededError — signal state is updated but no crash", () => {
153
- const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
154
- const setItemSpy = vi.spyOn(Storage.prototype, "setItem").mockImplementation(() => {
155
- const err = new DOMException("QuotaExceededError", "QuotaExceededError");
156
- throw err;
157
- });
158
-
159
- const s = persistedSignal("key16", 0);
160
- expect(() => s.set(42)).not.toThrow();
161
- // The in-memory signal value is still updated despite storage failure
162
- expect(s()).toBe(42);
163
-
164
- setItemSpy.mockRestore();
165
- warn.mockRestore();
166
- });
167
-
168
- it("syncTabs=true — storage event with empty string newValue is handled", () => {
169
- const s = persistedSignal("key17", 99);
170
- window.dispatchEvent(
171
- new StorageEvent("storage", {
172
- key: "key17",
173
- newValue: "",
174
- storageArea: localStorage,
175
- }),
176
- );
177
- // Empty string is falsy, so falls back to initialValue
178
- expect(s()).toBe(99);
179
- });
180
-
181
- it("syncTabs=true — storage event from sessionStorage is ignored", () => {
182
- const s = persistedSignal("key18", 10);
183
- s.set(20);
184
- window.dispatchEvent(
185
- new StorageEvent("storage", {
186
- key: "key18",
187
- newValue: "999",
188
- storageArea: sessionStorage,
189
- }),
190
- );
191
- // Should stay at 20 since the event is from sessionStorage, not localStorage
192
- expect(s()).toBe(20);
193
- });
194
-
195
- it("close() stops storage-event syncing but keeps the signal working locally", () => {
196
- const s = persistedSignal("close-key", 0);
197
- s.close();
198
- window.dispatchEvent(
199
- new StorageEvent("storage", { key: "close-key", newValue: "5", storageArea: localStorage }),
200
- );
201
- expect(s()).toBe(0);
202
- s.set(3);
203
- expect(s()).toBe(3);
204
- expect(localStorage.getItem("close-key")).toBe("3");
205
- });
206
-
207
- it("close() removes the window listener and is safe to call twice", () => {
208
- const remove = vi.spyOn(window, "removeEventListener");
209
- const s = persistedSignal("close-twice", 0);
210
- s.close();
211
- s.close();
212
- expect(remove.mock.calls.filter(([type]) => type === "storage")).toHaveLength(1);
213
- remove.mockRestore();
214
- });
215
-
216
- it("close() is a no-op when syncTabs is false", () => {
217
- const remove = vi.spyOn(window, "removeEventListener");
218
- const s = persistedSignal("close-nosync", 0, { syncTabs: false });
219
- expect(() => { s.close(); }).not.toThrow();
220
- expect(remove.mock.calls.filter(([type]) => type === "storage")).toHaveLength(0);
221
- remove.mockRestore();
222
- });
223
- });
224
-
225
- describe("persistedSignal writeDelay", () => {
226
- afterEach(() => {
227
- vi.useRealTimers();
228
- });
229
-
230
- it("writes immediately by default", () => {
231
- const s = persistedSignal("wd-default", 0);
232
- s.set(1);
233
- expect(localStorage.getItem("wd-default")).toBe("1");
234
- });
235
-
236
- it("holds writes back and flushes the latest value once the delay elapses", () => {
237
- vi.useFakeTimers();
238
- const setItem = vi.spyOn(Storage.prototype, "setItem");
239
- const s = persistedSignal("wd-burst", 0, { writeDelay: 100 });
240
- s.set(1);
241
- s.set(2);
242
- s.update((n) => n + 1);
243
- expect(s()).toBe(3);
244
- expect(localStorage.getItem("wd-burst")).toBeNull();
245
- vi.advanceTimersByTime(100);
246
- expect(localStorage.getItem("wd-burst")).toBe("3");
247
- expect(setItem.mock.calls.filter(([k]) => k === "wd-burst")).toHaveLength(1);
248
- setItem.mockRestore();
249
- });
250
-
251
- it("starts a new window for writes after a flush", () => {
252
- vi.useFakeTimers();
253
- const s = persistedSignal("wd-next", 0, { writeDelay: 50 });
254
- s.set(1);
255
- vi.advanceTimersByTime(50);
256
- s.set(2);
257
- expect(localStorage.getItem("wd-next")).toBe("1");
258
- vi.advanceTimersByTime(50);
259
- expect(localStorage.getItem("wd-next")).toBe("2");
260
- });
261
-
262
- it("flushes pending data on pagehide", () => {
263
- vi.useFakeTimers();
264
- const s = persistedSignal("wd-hide", 0, { writeDelay: 1000 });
265
- s.set(5);
266
- window.dispatchEvent(new Event("pagehide"));
267
- expect(localStorage.getItem("wd-hide")).toBe("5");
268
- vi.advanceTimersByTime(1000);
269
- expect(localStorage.getItem("wd-hide")).toBe("5");
270
- });
271
-
272
- it("close() flushes pending data and stops listening for pagehide", () => {
273
- vi.useFakeTimers();
274
- const s = persistedSignal("wd-close", 0, { writeDelay: 1000 });
275
- s.set(7);
276
- s.close();
277
- expect(localStorage.getItem("wd-close")).toBe("7");
278
- s.set(8);
279
- window.dispatchEvent(new Event("pagehide"));
280
- expect(localStorage.getItem("wd-close")).toBe("7");
281
- });
282
-
283
- it("a value arriving from another tab discards the pending local write", () => {
284
- vi.useFakeTimers();
285
- const s = persistedSignal("wd-remote", 0, { writeDelay: 100 });
286
- s.set(1);
287
- localStorage.setItem("wd-remote", "9");
288
- window.dispatchEvent(
289
- new StorageEvent("storage", { key: "wd-remote", newValue: "9", storageArea: localStorage }),
290
- );
291
- vi.advanceTimersByTime(100);
292
- expect(s()).toBe(9);
293
- expect(localStorage.getItem("wd-remote")).toBe("9");
294
- });
295
-
296
- it("removes null and undefined values when the write is flushed", () => {
297
- vi.useFakeTimers();
298
- const s = persistedSignal<number | null>("wd-null", 1, { writeDelay: 10 });
299
- s.set(2);
300
- vi.advanceTimersByTime(10);
301
- s.set(null);
302
- vi.advanceTimersByTime(10);
303
- expect(localStorage.getItem("wd-null")).toBeNull();
304
- });
305
- });