@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,421 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
-
3
- import { resource, createResource } from "../async/resource";
4
- import { invalidateResource, _clearCache } from "../async/resource-cache";
5
- import { signal } from "../signal/signal";
6
-
7
- describe("resource", () => {
8
- it("starts as pending and resolves to success", async () => {
9
- const r = resource(() => Promise.resolve("hello"));
10
- expect(r.status()).toBe("pending");
11
- await vi.waitFor(() => r.status() === "success");
12
- expect(r.data()).toBe("hello");
13
- expect(r.pending()).toBe(false);
14
- expect(r.error()).toBeNull();
15
- });
16
-
17
- it("transitions to error on rejection", async () => {
18
- const r = resource(() => Promise.reject(new Error("oops")));
19
- await new Promise((res) => setTimeout(res, 0));
20
- expect(r.status()).toBe("error");
21
- expect((r.error() as Error).message).toBe("oops");
22
- expect(r.data()).toBeNull();
23
- });
24
-
25
- it("wraps non-Error rejections in Error", async () => {
26
- const r = resource(() => Promise.reject("plain string"));
27
- await new Promise((res) => setTimeout(res, 0));
28
- expect(r.status()).toBe("error");
29
- expect(r.error()).toBeInstanceOf(Error);
30
- });
31
-
32
- it("immediate=false does not fetch on creation", () => {
33
- const fetcher = vi.fn(() => Promise.resolve(1));
34
- const r = resource(fetcher, { immediate: false });
35
- expect(fetcher).not.toHaveBeenCalled();
36
- expect(r.status()).toBe("idle");
37
- });
38
-
39
- it("refetch() re-runs the fetcher", async () => {
40
- let count = 0;
41
- const r = resource(() => Promise.resolve(++count));
42
- await vi.waitFor(() => r.status() === "success");
43
- expect(r.data()).toBe(1);
44
- r.refetch();
45
- await vi.waitFor(() => r.data() === 2);
46
- expect(r.data()).toBe(2);
47
- });
48
-
49
- it("cancel() sets status back to idle and ignores in-flight result", async () => {
50
- let resolve!: (v: number) => void;
51
- const r = resource(() => new Promise<number>((res) => { resolve = res; }));
52
- r.cancel();
53
- expect(r.status()).toBe("idle");
54
- resolve(99);
55
- await new Promise((r) => setTimeout(r, 10));
56
- expect(r.data()).toBeNull(); // stale result ignored
57
- });
58
-
59
- it("mutate() sets data directly", async () => {
60
- const r = resource(() => Promise.resolve(1));
61
- await vi.waitFor(() => r.status() === "success");
62
- r.mutate(999);
63
- expect(r.data()).toBe(999);
64
- expect(r.status()).toBe("success");
65
- expect(r.error()).toBeNull();
66
- });
67
-
68
- it("initialData is visible when immediate=false", () => {
69
- const r = resource(
70
- () => new Promise<string>(() => {}),
71
- { initialData: "cached", immediate: false },
72
- );
73
- expect(r.data()).toBe("cached");
74
- expect(r.status()).toBe("idle");
75
- });
76
-
77
- it("cancel() before rejection ignores the stale error (catch stale path)", async () => {
78
- let reject!: (e: Error) => void;
79
- const r = resource(
80
- () => new Promise<number>((_res, rej) => { reject = rej; }),
81
- );
82
- r.cancel();
83
- expect(r.status()).toBe("idle");
84
- reject(new Error("cancelled error"));
85
- await new Promise((res) => setTimeout(res, 10));
86
- expect(r.status()).toBe("idle");
87
- expect(r.error()).toBeNull();
88
- });
89
-
90
- it("keepPreviousData=true preserves old data during refetch", async () => {
91
- let call = 0;
92
- const r = resource(() => Promise.resolve(++call), { keepPreviousData: true });
93
- await vi.waitFor(() => r.data() === 1);
94
- r.refetch();
95
- // data should still be 1 while pending
96
- expect(r.data()).toBe(1);
97
- await vi.waitFor(() => r.data() === 2);
98
- });
99
- });
100
-
101
- describe("createResource", () => {
102
- it("re-fetches when the param signal changes", async () => {
103
- const id = signal(1);
104
- const fetcher = vi.fn((n: number) => Promise.resolve(n * 10));
105
- const r = createResource(id, fetcher);
106
- await vi.waitFor(() => r.data() === 10);
107
- id.set(2);
108
- await vi.waitFor(() => r.data() === 20);
109
- expect(fetcher).toHaveBeenCalledTimes(2);
110
- });
111
- });
112
-
113
- describe("resource — execute() sync throw via refetch()", () => {
114
- it("refetch() captures a synchronous throw from the fetcher", async () => {
115
- const r = resource(() => Promise.resolve(1), { immediate: false });
116
- let calls = 0;
117
- const r2 = resource(
118
- () => {
119
- calls++;
120
- if (calls >= 2) throw new Error("sync in refetch");
121
- return Promise.resolve(1);
122
- },
123
- { immediate: false },
124
- );
125
- r2.refetch(); // first call succeeds
126
- await vi.waitFor(() => r2.status() === "success");
127
- r2.refetch(); // second call throws synchronously
128
- await vi.waitFor(() => r2.status() === "error");
129
- expect((r2.error() as Error).message).toBe("sync in refetch");
130
- void r;
131
- });
132
-
133
- it("refetch() wraps non-Error sync throws in Error", async () => {
134
- let calls = 0;
135
- const r = resource(
136
- () => {
137
- calls++;
138
- if (calls >= 2) {
139
- // eslint-disable-next-line @typescript-eslint/only-throw-error
140
- throw "plain string sync";
141
- }
142
- return Promise.resolve(1);
143
- },
144
- { immediate: false },
145
- );
146
- r.refetch();
147
- await vi.waitFor(() => r.status() === "success");
148
- r.refetch();
149
- await vi.waitFor(() => r.status() === "error");
150
- expect(r.error()).toBeInstanceOf(Error);
151
- expect((r.error() as Error).message).toBe("plain string sync");
152
- });
153
- });
154
-
155
- describe("resource — destroy()", () => {
156
- it("destroy() stops the reactive effect — signal changes no longer trigger refetch", async () => {
157
- const id = signal(1);
158
- const fetcher = vi.fn((n: number) => Promise.resolve(n * 10));
159
- const r = resource(() => fetcher(id()), { immediate: true });
160
- await vi.waitFor(() => r.status() === "success");
161
- expect(fetcher).toHaveBeenCalledTimes(1);
162
-
163
- r.destroy();
164
- id.set(2);
165
- await new Promise((res) => setTimeout(res, 10));
166
- expect(fetcher).toHaveBeenCalledTimes(1); // no re-fetch after destroy
167
- });
168
-
169
- it("destroy() is idempotent", async () => {
170
- const r = resource(() => Promise.resolve(1));
171
- await vi.waitFor(() => r.status() === "success");
172
- expect(() => { r.destroy(); r.destroy(); }).not.toThrow();
173
- });
174
- });
175
-
176
- describe("resource — cache + SWR (key, staleTime)", () => {
177
- beforeEach(() => { _clearCache(); });
178
-
179
- it("key: populates data from cache on creation (SWR)", async () => {
180
- // First resource fills the cache
181
- const r1 = resource(() => Promise.resolve("cached-value"), { key: "swr-test" });
182
- await vi.waitFor(() => r1.status() === "success");
183
-
184
- // Second resource with same key starts with cached data immediately
185
- const r2 = resource(() => Promise.resolve("new-value"), { key: "swr-test" });
186
- expect(r2.data()).toBe("cached-value"); // stale data shown immediately
187
- expect(r2.status()).toBe("pending"); // fetching fresh data in background
188
- await vi.waitFor(() => r2.data() === "new-value");
189
- r1.destroy();
190
- r2.destroy();
191
- });
192
-
193
- it("staleTime: skips fetch when cache is fresh", async () => {
194
- const fetcher = vi.fn(() => Promise.resolve("fresh"));
195
- const r1 = resource(fetcher, { key: "fresh-test", staleTime: 60_000 });
196
- await vi.waitFor(() => r1.status() === "success");
197
- expect(fetcher).toHaveBeenCalledTimes(1);
198
-
199
- // Second resource within staleTime — no fetch
200
- const r2 = resource(fetcher, { key: "fresh-test", staleTime: 60_000 });
201
- expect(r2.data()).toBe("fresh");
202
- expect(r2.status()).toBe("success");
203
- await new Promise((res) => setTimeout(res, 10));
204
- expect(fetcher).toHaveBeenCalledTimes(1); // no second fetch
205
- r1.destroy();
206
- r2.destroy();
207
- });
208
-
209
- it("staleTime: 0 (default) always refetches even with cached data", async () => {
210
- const fetcher = vi.fn(() => Promise.resolve("v1"));
211
- const r1 = resource(fetcher, { key: "always-stale" });
212
- await vi.waitFor(() => r1.status() === "success");
213
-
214
- const r2 = resource(fetcher, { key: "always-stale", staleTime: 0 });
215
- expect(r2.data()).toBe("v1"); // shows stale immediately
216
- await vi.waitFor(() => r2.status() === "success");
217
- expect(fetcher).toHaveBeenCalledTimes(2); // did refetch
218
- r1.destroy();
219
- r2.destroy();
220
- });
221
-
222
- it("key: mutate() updates the shared cache for future resources", async () => {
223
- const fetcher = vi.fn(() => Promise.resolve("server-value"));
224
- const r1 = resource(fetcher, { key: "mutate-cache", staleTime: 60_000 });
225
- await vi.waitFor(() => r1.status() === "success");
226
-
227
- r1.mutate("optimistic-value");
228
-
229
- const r2 = resource(fetcher, { key: "mutate-cache", staleTime: 60_000 });
230
- expect(r2.data()).toBe("optimistic-value");
231
- expect(r2.status()).toBe("success");
232
- await new Promise((res) => setTimeout(res, 10));
233
- expect(fetcher).toHaveBeenCalledTimes(1);
234
- r1.destroy();
235
- r2.destroy();
236
- });
237
- });
238
-
239
- describe("resource — deduplication", () => {
240
- beforeEach(() => { _clearCache(); });
241
-
242
- it("two resources with the same key share a single in-flight request", async () => {
243
- let resolveShared!: (v: string) => void;
244
- const fetcher = vi.fn(
245
- () => new Promise<string>((res) => { resolveShared = res; }),
246
- );
247
-
248
- const r1 = resource(fetcher, { key: "dedup-test" });
249
- const r2 = resource(fetcher, { key: "dedup-test" });
250
-
251
- expect(fetcher).toHaveBeenCalledTimes(1); // only one fetch started
252
-
253
- resolveShared("shared-result");
254
- await vi.waitFor(() => r1.status() === "success");
255
- await vi.waitFor(() => r2.status() === "success");
256
-
257
- expect(r1.data()).toBe("shared-result");
258
- expect(r2.data()).toBe("shared-result");
259
- r1.destroy();
260
- r2.destroy();
261
- });
262
-
263
- it("key: cancel() clears the settled in-flight request even when the local result is stale", async () => {
264
- let resolveFirst!: (v: string) => void;
265
- const fetcher = vi.fn(
266
- () => new Promise<string>((res) => { resolveFirst = res; }),
267
- );
268
-
269
- const r1 = resource(fetcher, { key: "cancel-inflight" });
270
- r1.cancel();
271
- resolveFirst("stale");
272
- await new Promise((res) => setTimeout(res, 0));
273
-
274
- const r2 = resource(() => Promise.resolve("fresh"), { key: "cancel-inflight" });
275
- await vi.waitFor(() => r2.data() === "fresh");
276
-
277
- expect(fetcher).toHaveBeenCalledTimes(1);
278
- expect(r1.data()).toBeNull();
279
- expect(r2.data()).toBe("fresh");
280
- r1.destroy();
281
- r2.destroy();
282
- });
283
- });
284
-
285
- describe("resource — key + non-Error sync throw in effect", () => {
286
- beforeEach(() => { _clearCache(); });
287
-
288
- it("key: wraps non-Error sync throw from fetcher inside effect in Error", async () => {
289
- const r = resource(
290
- () => {
291
- // eslint-disable-next-line @typescript-eslint/only-throw-error
292
- throw "raw string error in keyed effect";
293
- },
294
- { key: "non-error-keyed" },
295
- );
296
- await vi.waitFor(() => r.status() === "error");
297
- expect(r.error()).toBeInstanceOf(Error);
298
- expect((r.error() as Error).message).toBe("raw string error in keyed effect");
299
- r.destroy();
300
- });
301
- });
302
-
303
- describe("resource — key + rejection / sync-throw", () => {
304
- beforeEach(() => { _clearCache(); });
305
-
306
- it("key: clears in-flight entry on rejection (catch path)", async () => {
307
- const r = resource(
308
- () => Promise.reject(new Error("keyed failure")),
309
- { key: "reject-test" },
310
- );
311
- await new Promise((res) => setTimeout(res, 0));
312
- expect(r.status()).toBe("error");
313
- expect((r.error() as Error).message).toBe("keyed failure");
314
- r.destroy();
315
- });
316
-
317
- it("key: sync throw inside effect is captured as error", async () => {
318
- const r = resource(
319
- () => { throw new Error("sync in keyed effect"); },
320
- { key: "sync-throw-keyed" },
321
- );
322
- await vi.waitFor(() => r.status() === "error");
323
- expect((r.error() as Error).message).toBe("sync in keyed effect");
324
- r.destroy();
325
- });
326
- });
327
-
328
- describe("resource — invalidateResource()", () => {
329
- beforeEach(() => { _clearCache(); });
330
-
331
- it("clears cache and triggers refetch on all registered resources", async () => {
332
- let call = 0;
333
- const fetcher = vi.fn(() => Promise.resolve(++call));
334
-
335
- const r1 = resource(fetcher, { key: "inv-test" });
336
- const r2 = resource(fetcher, { key: "inv-test" });
337
- await vi.waitFor(() => r1.status() === "success");
338
- await vi.waitFor(() => r2.status() === "success");
339
-
340
- const callsBefore = fetcher.mock.calls.length;
341
- invalidateResource("inv-test");
342
-
343
- await vi.waitFor(() => r1.status() === "success" && r1.data() !== callsBefore);
344
- expect(fetcher.mock.calls.length).toBeGreaterThan(callsBefore);
345
- r1.destroy();
346
- r2.destroy();
347
- });
348
-
349
- it("no-op when key has no registered resources", () => {
350
- expect(() => { invalidateResource("nonexistent-key"); }).not.toThrow();
351
- });
352
-
353
- it("unregisters resource on destroy — invalidate no longer triggers it", async () => {
354
- let call = 0;
355
- const fetcher = vi.fn(() => Promise.resolve(++call));
356
- const r = resource(fetcher, { key: "unreg-test" });
357
- await vi.waitFor(() => r.status() === "success");
358
-
359
- r.destroy();
360
- const callsBefore = fetcher.mock.calls.length;
361
-
362
- invalidateResource("unreg-test");
363
- await new Promise((res) => setTimeout(res, 10));
364
- expect(fetcher.mock.calls.length).toBe(callsBefore); // no refetch after destroy
365
- });
366
- });
367
-
368
- describe("resource — additional cases", () => {
369
- it("two concurrent refetch() calls — last result wins, stale result is discarded", async () => {
370
- let resolveFirst!: (v: string) => void;
371
- let resolveSecond!: (v: string) => void;
372
-
373
- let call = 0;
374
- const r = resource(
375
- () => {
376
- call++;
377
- if (call === 1) return new Promise<string>((res) => { resolveFirst = res; });
378
- return new Promise<string>((res) => { resolveSecond = res; });
379
- },
380
- { immediate: false },
381
- );
382
-
383
- r.refetch(); // call 1
384
- r.refetch(); // call 2 — supersedes call 1
385
-
386
- resolveSecond("second");
387
- resolveFirst("first"); // stale
388
-
389
- await vi.waitFor(() => r.status() === "success");
390
- expect(r.data()).toBe("second");
391
- });
392
-
393
- it("fetcher() throws synchronously — error is captured, does not crash", async () => {
394
- const r = resource(() => {
395
- throw new Error("sync throw");
396
- });
397
- await vi.waitFor(() => r.status() === "error");
398
- expect((r.error() as Error).message).toBe("sync throw");
399
- });
400
-
401
- it("keepPreviousData: true — data is preserved while refetching", async () => {
402
- let call = 0;
403
- let resolve!: (v: number) => void;
404
- const r = resource(
405
- () => {
406
- call++;
407
- if (call === 1) return Promise.resolve(1);
408
- return new Promise<number>((res) => { resolve = res; });
409
- },
410
- { keepPreviousData: true },
411
- );
412
-
413
- await vi.waitFor(() => r.data() === 1);
414
- r.refetch();
415
- // During refetch with keepPreviousData, old data is preserved
416
- expect(r.data()).toBe(1);
417
- expect(r.status()).toBe("pending");
418
- resolve(2);
419
- await vi.waitFor(() => r.data() === 2);
420
- });
421
- });