@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.
- package/CHANGELOG.md +24 -0
- package/dist/component/internals.d.ts +12 -2
- package/dist/component/internals.d.ts.map +1 -1
- package/dist/component/internals.js +25 -7
- package/dist/component/internals.js.map +1 -1
- package/dist/internal.d.ts +3 -2
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +3 -2
- package/dist/internal.js.map +1 -1
- package/dist/signal/batch.d.ts +1 -3
- package/dist/signal/batch.d.ts.map +1 -1
- package/dist/signal/batch.js +4 -30
- package/dist/signal/batch.js.map +1 -1
- package/dist/signal/computed.d.ts.map +1 -1
- package/dist/signal/computed.js +13 -66
- package/dist/signal/computed.js.map +1 -1
- package/dist/signal/effect.d.ts +6 -14
- package/dist/signal/effect.d.ts.map +1 -1
- package/dist/signal/effect.js +11 -83
- package/dist/signal/effect.js.map +1 -1
- package/dist/signal/graph.d.ts +111 -0
- package/dist/signal/graph.d.ts.map +1 -0
- package/dist/signal/graph.js +513 -0
- package/dist/signal/graph.js.map +1 -0
- package/dist/signal/index.d.ts +1 -0
- package/dist/signal/index.d.ts.map +1 -1
- package/dist/signal/index.js +1 -0
- package/dist/signal/index.js.map +1 -1
- package/dist/signal/peek.d.ts.map +1 -1
- package/dist/signal/peek.js +2 -9
- package/dist/signal/peek.js.map +1 -1
- package/dist/signal/selector.d.ts +5 -0
- package/dist/signal/selector.d.ts.map +1 -0
- package/dist/signal/selector.js +128 -0
- package/dist/signal/selector.js.map +1 -0
- package/dist/signal/signal.d.ts +0 -5
- package/dist/signal/signal.d.ts.map +1 -1
- package/dist/signal/signal.js +11 -70
- package/dist/signal/signal.js.map +1 -1
- package/package.json +13 -4
- package/src/component/internals.ts +35 -10
- package/src/internal.ts +26 -0
- package/src/signal/batch.ts +5 -33
- package/src/signal/computed.ts +13 -66
- package/src/signal/effect.ts +12 -92
- package/src/signal/graph.ts +562 -0
- package/src/signal/index.ts +1 -0
- package/src/signal/peek.ts +2 -8
- package/src/signal/selector.ts +140 -0
- package/src/signal/signal.ts +11 -74
- package/dist/__tests__/batch.test.d.ts +0 -2
- package/dist/__tests__/batch.test.d.ts.map +0 -1
- package/dist/__tests__/batch.test.js +0 -124
- package/dist/__tests__/batch.test.js.map +0 -1
- package/dist/__tests__/component.test.d.ts +0 -2
- package/dist/__tests__/component.test.d.ts.map +0 -1
- package/dist/__tests__/component.test.js +0 -124
- package/dist/__tests__/component.test.js.map +0 -1
- package/dist/__tests__/computed.test.d.ts +0 -2
- package/dist/__tests__/computed.test.d.ts.map +0 -1
- package/dist/__tests__/computed.test.js +0 -289
- package/dist/__tests__/computed.test.js.map +0 -1
- package/dist/__tests__/effect.test.d.ts +0 -2
- package/dist/__tests__/effect.test.d.ts.map +0 -1
- package/dist/__tests__/effect.test.js +0 -200
- package/dist/__tests__/effect.test.js.map +0 -1
- package/dist/__tests__/peek.test.d.ts +0 -2
- package/dist/__tests__/peek.test.d.ts.map +0 -1
- package/dist/__tests__/peek.test.js +0 -26
- package/dist/__tests__/peek.test.js.map +0 -1
- package/dist/__tests__/persisted-signal.test.d.ts +0 -2
- package/dist/__tests__/persisted-signal.test.d.ts.map +0 -1
- package/dist/__tests__/persisted-signal.test.js +0 -252
- package/dist/__tests__/persisted-signal.test.js.map +0 -1
- package/dist/__tests__/reactive.test.d.ts +0 -2
- package/dist/__tests__/reactive.test.d.ts.map +0 -1
- package/dist/__tests__/reactive.test.js +0 -299
- package/dist/__tests__/reactive.test.js.map +0 -1
- package/dist/__tests__/resource-focus.test.d.ts +0 -2
- package/dist/__tests__/resource-focus.test.d.ts.map +0 -1
- package/dist/__tests__/resource-focus.test.js +0 -50
- package/dist/__tests__/resource-focus.test.js.map +0 -1
- package/dist/__tests__/resource-server-pass.test.d.ts +0 -2
- package/dist/__tests__/resource-server-pass.test.d.ts.map +0 -1
- package/dist/__tests__/resource-server-pass.test.js +0 -78
- package/dist/__tests__/resource-server-pass.test.js.map +0 -1
- package/dist/__tests__/resource.test.d.ts +0 -2
- package/dist/__tests__/resource.test.d.ts.map +0 -1
- package/dist/__tests__/resource.test.js +0 -336
- package/dist/__tests__/resource.test.js.map +0 -1
- package/dist/__tests__/signal.test.d.ts +0 -2
- package/dist/__tests__/signal.test.d.ts.map +0 -1
- package/dist/__tests__/signal.test.js +0 -254
- package/dist/__tests__/signal.test.js.map +0 -1
- package/dist/__tests__/synced.test.d.ts +0 -2
- package/dist/__tests__/synced.test.d.ts.map +0 -1
- package/dist/__tests__/synced.test.js +0 -142
- package/dist/__tests__/synced.test.js.map +0 -1
- package/dist/__tests__/track.test.d.ts +0 -2
- package/dist/__tests__/track.test.d.ts.map +0 -1
- package/dist/__tests__/track.test.js +0 -54
- package/dist/__tests__/track.test.js.map +0 -1
- package/src/__tests__/batch.test.ts +0 -138
- package/src/__tests__/component.test.ts +0 -165
- package/src/__tests__/computed.test.ts +0 -333
- package/src/__tests__/effect.test.ts +0 -241
- package/src/__tests__/peek.test.ts +0 -31
- package/src/__tests__/persisted-signal.test.ts +0 -305
- package/src/__tests__/reactive.test.ts +0 -343
- package/src/__tests__/resource-focus.test.ts +0 -61
- package/src/__tests__/resource-server-pass.test.ts +0 -90
- package/src/__tests__/resource.test.ts +0 -421
- package/src/__tests__/signal.test.ts +0 -291
- package/src/__tests__/synced.test.ts +0 -173
- package/src/__tests__/track.test.ts +0 -61
- 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
|
-
});
|