@praxisjs/core 2.2.0 → 2.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.
Files changed (116) hide show
  1. package/CHANGELOG.md +18 -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,343 +0,0 @@
1
- import { describe, it, expect, vi } from "vitest";
2
-
3
- import { history } from "../reactive/history";
4
- import { when, until, debounced } from "../reactive/reactive";
5
- import { computed } from "../signal/computed";
6
- import { signal } from "../signal/signal";
7
-
8
- // ---------- when ----------
9
-
10
- describe("when", () => {
11
- it("calls fn immediately when source is already truthy", () => {
12
- const s = signal<number>(5);
13
- const fn = vi.fn();
14
- when(s, fn);
15
- expect(fn).toHaveBeenCalledWith(5);
16
- });
17
-
18
- it("calls fn once the source becomes truthy", () => {
19
- const s = signal<number>(0);
20
- const fn = vi.fn();
21
- when(s, fn);
22
- expect(fn).not.toHaveBeenCalled();
23
- s.set(1);
24
- expect(fn).toHaveBeenCalledWith(1);
25
- });
26
-
27
- it("only fires once, even if source changes again", () => {
28
- const s = signal(0);
29
- const fn = vi.fn();
30
- when(s, fn);
31
- s.set(1);
32
- s.set(2);
33
- expect(fn).toHaveBeenCalledTimes(1);
34
- });
35
-
36
- it("does not re-fire after immediate truthy source changes again", () => {
37
- // Regression: when source is truthy on first run, stop?.() was a no-op
38
- // because `stop` hadn't been assigned yet. The effect must be cancelled
39
- // after firing so further source changes do not re-trigger fn.
40
- const s = signal(1); // immediately truthy
41
- const fn = vi.fn();
42
- when(s, fn);
43
- expect(fn).toHaveBeenCalledTimes(1);
44
- s.set(2);
45
- s.set(3);
46
- expect(fn).toHaveBeenCalledTimes(1); // must still be 1
47
- });
48
-
49
- it("does not call fn when source stays falsy", () => {
50
- const s = signal<number>(0);
51
- const fn = vi.fn();
52
- when(s, fn);
53
- s.set(0);
54
- expect(fn).not.toHaveBeenCalled();
55
- });
56
-
57
- it("with source that fires immediately on subscription does not crash", () => {
58
- const s = signal(1); // immediately truthy
59
- const fn = vi.fn();
60
- expect(() => when(s, fn)).not.toThrow();
61
- expect(fn).toHaveBeenCalledWith(1);
62
- });
63
- });
64
-
65
- // ---------- until ----------
66
-
67
- describe("until", () => {
68
- it("resolves immediately when source is already truthy", async () => {
69
- const s = signal(42);
70
- const value = await until(s);
71
- expect(value).toBe(42);
72
- });
73
-
74
- it("resolves when source becomes truthy", async () => {
75
- const s = signal<number>(0);
76
- const promise = until(s);
77
- s.set(7);
78
- expect(await promise).toBe(7);
79
- });
80
-
81
- it("the promise remains pending if source is always falsy (no-timeout behavior)", async () => {
82
- const s = signal<number>(0);
83
- let resolved = false;
84
- until(s).then(() => { resolved = true; });
85
- // Never set a truthy value
86
- await new Promise((res) => setTimeout(res, 10));
87
- expect(resolved).toBe(false);
88
- });
89
- });
90
-
91
- // ---------- debounced ----------
92
-
93
- describe("debounced", () => {
94
- it("returns initial value immediately", () => {
95
- vi.useFakeTimers();
96
- const s = signal(1);
97
- const d = debounced(s, 100);
98
- expect(d()).toBe(1);
99
- vi.useRealTimers();
100
- });
101
-
102
- it("debounces rapid updates", () => {
103
- vi.useFakeTimers();
104
- const s = signal(1);
105
- const d = debounced(s, 100);
106
-
107
- s.set(2);
108
- s.set(3);
109
- s.set(4);
110
-
111
- expect(d()).toBe(1); // not yet updated
112
-
113
- vi.advanceTimersByTime(100);
114
- expect(d()).toBe(4); // settled on last value
115
- vi.useRealTimers();
116
- });
117
-
118
- it("emits each value when updates are spaced apart", () => {
119
- vi.useFakeTimers();
120
- const s = signal(0);
121
- const d = debounced(s, 50);
122
-
123
- s.set(1);
124
- vi.advanceTimersByTime(50);
125
- expect(d()).toBe(1);
126
-
127
- s.set(2);
128
- vi.advanceTimersByTime(50);
129
- expect(d()).toBe(2);
130
- vi.useRealTimers();
131
- });
132
-
133
- it("delay=0 fires synchronously after setTimeout(0)", () => {
134
- vi.useFakeTimers();
135
- const s = signal("a");
136
- const d = debounced(s, 0);
137
-
138
- s.set("b");
139
- expect(d()).toBe("a"); // not yet
140
- vi.advanceTimersByTime(0);
141
- expect(d()).toBe("b");
142
- vi.useRealTimers();
143
- });
144
-
145
- it("the inner effect is cleaned up when stop is called (no leak)", () => {
146
- vi.useFakeTimers();
147
- const s = signal(0);
148
- const d = debounced(s, 100);
149
-
150
- s.set(1);
151
- // Stop the debounced effect before the timer fires
152
- d.stop();
153
-
154
- // Advance time — the timer should not fire and update the signal
155
- vi.advanceTimersByTime(200);
156
- expect(d()).toBe(0); // still initial value, effect was stopped
157
-
158
- vi.useRealTimers();
159
- });
160
- });
161
-
162
- // ---------- history ----------
163
-
164
- describe("history", () => {
165
- it("current reflects signal value", () => {
166
- const s = signal("a");
167
- const h = history(s);
168
- expect(h.current()).toBe("a");
169
- });
170
-
171
- it("canUndo is false initially", () => {
172
- const s = signal(0);
173
- const h = history(s);
174
- expect(h.canUndo()).toBe(false);
175
- });
176
-
177
- it("canUndo becomes true after a change", () => {
178
- const s = signal(0);
179
- const h = history(s);
180
- s.set(1);
181
- expect(h.canUndo()).toBe(true);
182
- });
183
-
184
- it("undo reverts to previous value", () => {
185
- const s = signal(0);
186
- const h = history(s);
187
- s.set(1);
188
- s.set(2);
189
- h.undo();
190
- expect(h.current()).toBe(1);
191
- expect(s()).toBe(1);
192
- });
193
-
194
- it("redo re-applies reverted value", () => {
195
- const s = signal(0);
196
- const h = history(s);
197
- s.set(1);
198
- h.undo();
199
- expect(h.canRedo()).toBe(true);
200
- h.redo();
201
- expect(h.current()).toBe(1);
202
- expect(s()).toBe(1);
203
- });
204
-
205
- it("values() returns full timeline", () => {
206
- const s = signal("a");
207
- const h = history(s);
208
- s.set("b");
209
- s.set("c");
210
- expect(h.values()).toEqual(["a", "b", "c"]);
211
- });
212
-
213
- it("clear removes past and future", () => {
214
- const s = signal(0);
215
- const h = history(s);
216
- s.set(1);
217
- s.set(2);
218
- h.clear();
219
- expect(h.canUndo()).toBe(false);
220
- expect(h.canRedo()).toBe(false);
221
- });
222
-
223
- it("undo does nothing when there is no history", () => {
224
- const s = signal(5);
225
- const h = history(s);
226
- h.undo();
227
- expect(h.current()).toBe(5);
228
- });
229
-
230
- it("redo does nothing when there is no future", () => {
231
- const s = signal(5);
232
- const h = history(s);
233
- s.set(10);
234
- h.redo();
235
- expect(h.current()).toBe(10);
236
- });
237
-
238
- it("works with computed source (read-only — no signal.set)", async () => {
239
- const s = signal(1);
240
- const c = computed(() => s() * 2);
241
- const h = history(c);
242
- s.set(2);
243
- await Promise.resolve();
244
- expect(h.current()).toBe(4);
245
- expect(h.canUndo()).toBe(true);
246
- });
247
-
248
- it("respects limit — oldest entries are dropped", () => {
249
- const s = signal(0);
250
- const h = history(s, 3);
251
- for (let i = 1; i <= 5; i++) s.set(i);
252
- // past holds at most 3, plus current = 4 values total
253
- expect(h.values().length).toBeLessThanOrEqual(4);
254
- });
255
-
256
- it("limit=1 allows only one past entry", () => {
257
- const s = signal(0);
258
- const h = history(s, 1);
259
- s.set(1);
260
- s.set(2);
261
- s.set(3);
262
- // values = [past(1 entry max), current] → at most 2 entries
263
- expect(h.values().length).toBeLessThanOrEqual(2);
264
- expect(h.current()).toBe(3);
265
- });
266
-
267
- it("clear() after undo removes both past and future", () => {
268
- const s = signal(0);
269
- const h = history(s);
270
- s.set(1);
271
- s.set(2);
272
- h.undo();
273
- expect(h.canRedo()).toBe(true);
274
- h.clear();
275
- expect(h.canUndo()).toBe(false);
276
- expect(h.canRedo()).toBe(false);
277
- });
278
-
279
- it("undo() called more times than history entries — state stays consistent", () => {
280
- const s = signal(0);
281
- const h = history(s);
282
- s.set(1);
283
- h.undo();
284
- // No more history — undo is a no-op
285
- h.undo();
286
- h.undo();
287
- expect(h.current()).toBe(0);
288
- expect(h.canUndo()).toBe(false);
289
- });
290
-
291
- it("values() returns a snapshot — each call after source change yields a fresh array", () => {
292
- const s = signal("a");
293
- const h = history(s);
294
- s.set("b");
295
- s.set("c");
296
- const snapshot1 = h.values();
297
- expect(snapshot1).toEqual(["a", "b", "c"]);
298
- // Trigger a change so the computed re-evaluates on next read
299
- s.set("d");
300
- const snapshot2 = h.values();
301
- expect(snapshot2).toEqual(["a", "b", "c", "d"]);
302
- // snapshot1 and snapshot2 are different array instances
303
- expect(snapshot1).not.toBe(snapshot2);
304
- });
305
- });
306
-
307
- describe("history.destroy", () => {
308
- it("stops recording changes but keeps what was recorded", () => {
309
- const s = signal(0);
310
- const h = history(s);
311
- s.set(1);
312
- h.destroy();
313
- s.set(2);
314
- s.set(3);
315
- expect(h.values()).toEqual([0, 1]);
316
- expect(h.current()).toBe(1);
317
- });
318
-
319
- it("can still undo recorded entries after destroy", () => {
320
- const s = signal(0);
321
- const h = history(s);
322
- s.set(1);
323
- h.destroy();
324
- h.undo();
325
- expect(s()).toBe(0);
326
- });
327
- });
328
-
329
- describe("history with a computed source", () => {
330
- it("undo and redo move the tracked value without writing back to the read-only source", async () => {
331
- const s = signal(0);
332
- const source = computed(() => s());
333
- const h = history(source);
334
- s.set(1);
335
- await Promise.resolve();
336
- h.undo();
337
- expect(h.current()).toBe(0);
338
- expect(s()).toBe(1);
339
- h.redo();
340
- expect(h.current()).toBe(1);
341
- expect(s()).toBe(1);
342
- });
343
- });
@@ -1,61 +0,0 @@
1
- // @vitest-environment jsdom
2
- import { describe, it, expect, vi, beforeEach } from "vitest";
3
-
4
- import { resource } from "../async/resource";
5
- import { _clearCache } from "../async/resource-cache";
6
-
7
- describe("resource — refetchOnFocus", () => {
8
- beforeEach(() => { _clearCache(); });
9
-
10
- it("refetches when document becomes visible", async () => {
11
- let call = 0;
12
- const fetcher = vi.fn(() => Promise.resolve(++call));
13
- const r = resource(fetcher, { refetchOnFocus: true });
14
- await vi.waitFor(() => r.status() === "success");
15
- expect(fetcher).toHaveBeenCalledTimes(1);
16
-
17
- Object.defineProperty(document, "visibilityState", {
18
- configurable: true,
19
- get: () => "visible",
20
- });
21
- document.dispatchEvent(new Event("visibilitychange"));
22
-
23
- await vi.waitFor(() => r.data() === 2);
24
- expect(fetcher).toHaveBeenCalledTimes(2);
25
- r.destroy();
26
- });
27
-
28
- it("does not refetch when document becomes hidden", async () => {
29
- const fetcher = vi.fn(() => Promise.resolve(1));
30
- const r = resource(fetcher, { refetchOnFocus: true });
31
- await vi.waitFor(() => r.status() === "success");
32
-
33
- Object.defineProperty(document, "visibilityState", {
34
- configurable: true,
35
- get: () => "hidden",
36
- });
37
- document.dispatchEvent(new Event("visibilitychange"));
38
- await new Promise((res) => setTimeout(res, 10));
39
-
40
- expect(fetcher).toHaveBeenCalledTimes(1);
41
- r.destroy();
42
- });
43
-
44
- it("destroy() removes the visibilitychange listener", async () => {
45
- let call = 0;
46
- const fetcher = vi.fn(() => Promise.resolve(++call));
47
- const r = resource(fetcher, { refetchOnFocus: true });
48
- await vi.waitFor(() => r.status() === "success");
49
-
50
- r.destroy();
51
-
52
- Object.defineProperty(document, "visibilityState", {
53
- configurable: true,
54
- get: () => "visible",
55
- });
56
- document.dispatchEvent(new Event("visibilitychange"));
57
- await new Promise((res) => setTimeout(res, 10));
58
-
59
- expect(fetcher).toHaveBeenCalledTimes(1); // no refetch after destroy
60
- });
61
- });
@@ -1,90 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
-
3
- import { flushPendingResources, resource } from "../async/resource";
4
- import { _clearCache, getPendingResources } from "../async/resource-cache";
5
- import { setServerRenderPass, isServerRenderPass } from "../async/server-mode";
6
-
7
- describe("server render pass — flushPendingResources", () => {
8
- beforeEach(() => { _clearCache(); });
9
- afterEach(() => { setServerRenderPass(false); });
10
-
11
- it("is a no-op when no server pass is active", async () => {
12
- setServerRenderPass(false);
13
- const r = resource(() => new Promise<string>(() => {}));
14
- expect(getPendingResources().size).toBe(0);
15
- await flushPendingResources();
16
- // Untouched — resource() never settled and flush didn't wait for it.
17
- expect(r.status()).toBe("pending");
18
- });
19
-
20
- it("resolves immediately when nothing is pending", async () => {
21
- setServerRenderPass(true);
22
- await expect(flushPendingResources()).resolves.toBeUndefined();
23
- });
24
-
25
- it("tracks isServerRenderPass() state via the setter", () => {
26
- expect(isServerRenderPass()).toBe(false);
27
- setServerRenderPass(true);
28
- expect(isServerRenderPass()).toBe(true);
29
- });
30
-
31
- it("awaits an unkeyed resource() (e.g. @Collection) before returning", async () => {
32
- setServerRenderPass(true);
33
- const r = resource(() => new Promise<string>((res) => { setTimeout(() => { res("content"); }, 5); }));
34
- await flushPendingResources();
35
- expect(r.status()).toBe("success");
36
- expect(r.data()).toBe("content");
37
- });
38
-
39
- it("awaits a keyed resource() too", async () => {
40
- setServerRenderPass(true);
41
- const r = resource(() => Promise.resolve("keyed-value"), { key: "ssg-key" });
42
- await flushPendingResources();
43
- expect(r.status()).toBe("success");
44
- expect(r.data()).toBe("keyed-value");
45
- });
46
-
47
- it("awaits multiple concurrently pending resources", async () => {
48
- setServerRenderPass(true);
49
- const r1 = resource(() => new Promise<number>((res) => { setTimeout(() => { res(1); }, 5); }));
50
- const r2 = resource(() => new Promise<number>((res) => { setTimeout(() => { res(2); }, 10); }));
51
- await flushPendingResources();
52
- expect(r1.data()).toBe(1);
53
- expect(r2.data()).toBe(2);
54
- });
55
-
56
- it("fixed-point loop: awaits a resource created while an earlier one settles", async () => {
57
- setServerRenderPass(true);
58
- let second: ReturnType<typeof resource> | undefined;
59
-
60
- // Simulates a component that, upon its first resource resolving, creates a
61
- // second resource (e.g. a detail page fetching related content) — the new
62
- // promise must still be picked up even though it didn't exist yet when
63
- // flushPendingResources() started its first pass.
64
- const first = resource(() => new Promise<string>((res) => {
65
- setTimeout(() => {
66
- res("first");
67
- second = resource(() => new Promise<string>((res2) => { setTimeout(() => { res2("second"); }, 5); }));
68
- }, 5);
69
- }));
70
-
71
- await flushPendingResources();
72
- expect(first.status()).toBe("success");
73
- expect(second?.status()).toBe("success");
74
- expect(second?.data()).toBe("second");
75
- });
76
-
77
- it("does not leave stale entries after resources settle", async () => {
78
- setServerRenderPass(true);
79
- resource(() => Promise.resolve("done"));
80
- await flushPendingResources();
81
- expect(getPendingResources().size).toBe(0);
82
- });
83
-
84
- it("a rejected resource does not hang the flush", async () => {
85
- setServerRenderPass(true);
86
- const r = resource(() => Promise.reject(new Error("boom")));
87
- await expect(flushPendingResources()).resolves.toBeUndefined();
88
- expect(r.status()).toBe("error");
89
- });
90
- });