@effect-app/vue 4.0.4 → 4.0.6

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.
@@ -0,0 +1,507 @@
1
+ import { defaultRegistry } from "@effect/atom-vue"
2
+ import { DataDependencies, makeQueryKey } from "effect-app/client"
3
+ import * as Effect from "effect-app/Effect"
4
+ import * as Layer from "effect-app/Layer"
5
+ import * as ManagedRuntime from "effect/ManagedRuntime"
6
+ import { afterEach, describe, expect, it, vi } from "vitest"
7
+ import { awaitAtomResult, buildQueryFamily, invalidateAndAwait, makeAtomClientRuntime, withQueryOptions } from "../src/atomQuery.js"
8
+ import { getQueryReadDependencies } from "../src/dependencyMetadata.js"
9
+ import { acquireLiveQueryFetch, awaitLiveQueryFetch, beginLiveQueryFetch, configureLiveQueryInvalidation, endLiveQueryFetch, type LiveQueryInvalidationSource, liveQueryReady, registerLiveQuery, releaseLiveQueryFetch } from "../src/liveQueryInvalidation.js"
10
+
11
+ type Keys = Array<ReadonlyArray<ReadonlyArray<unknown>>>
12
+
13
+ const repo = DataDependencies.repo("Inventory", ["item-1"])
14
+ const never = () => new Promise<void>(() => {})
15
+ const flush = () => new Promise((resolve) => setTimeout(resolve, 0))
16
+
17
+ const acquire = (key: ReadonlyArray<unknown>) => {
18
+ const fetch = acquireLiveQueryFetch(key)
19
+ if (fetch === undefined) throw new Error("expected a live fetch")
20
+ return fetch
21
+ }
22
+
23
+ const isRegistered = (key: ReadonlyArray<unknown>) => {
24
+ const fetch = acquireLiveQueryFetch(key)
25
+ if (fetch !== undefined) releaseLiveQueryFetch(fetch)
26
+ return fetch !== undefined
27
+ }
28
+
29
+ const makeSource = (ready: () => Promise<void> = () => Promise.resolve()) => {
30
+ const invalidations: Keys = []
31
+ const handlers = { receive: (_writes: DataDependencies.DataDependencies) => {}, reset: () => {} }
32
+ const source: LiveQueryInvalidationSource = {
33
+ ready,
34
+ subscribe: (onWrites, onReset) => {
35
+ handlers.receive = onWrites
36
+ handlers.reset = onReset
37
+ return () => {}
38
+ },
39
+ invalidate: (keys) => invalidations.push(keys)
40
+ }
41
+ return { source, invalidations, handlers }
42
+ }
43
+
44
+ describe("live query lifecycle", () => {
45
+ const cleanups: Array<() => void> = []
46
+
47
+ afterEach(async () => {
48
+ while (cleanups.length > 0) cleanups.pop()?.()
49
+ defaultRegistry.reset()
50
+ vi.useRealTimers()
51
+ await flush()
52
+ })
53
+
54
+ it("keeps a delayed invalidation when the replayed fetch publishes its result", async () => {
55
+ const dependency = DataDependencies.repo("DelayedOrders")
56
+ const { handlers, source } = makeSource()
57
+ let calls = 0
58
+ configureLiveQueryInvalidation({
59
+ ...source,
60
+ invalidate: (keys) => void Effect.runPromise(invalidateAndAwait(keys))
61
+ })
62
+ const mrt = ManagedRuntime.make(Layer.empty)
63
+ cleanups.push(() => void mrt.dispose())
64
+ const rt = makeAtomClientRuntime(() => Layer.empty, mrt.memoMap)
65
+ const query = {
66
+ id: "DelayedOrders.List",
67
+ handler: () =>
68
+ DataDependencies.read(dependency).pipe(Effect.andThen(Effect.sync(() => {
69
+ // the server changes while the initial fetch is still discovering
70
+ if (++calls === 1) handlers.receive(new Set([dependency]))
71
+ return calls
72
+ })))
73
+ }
74
+ const atom = withQueryOptions(
75
+ buildQueryFamily(rt, query)(undefined),
76
+ { live: { maxDelayMs: 10 }, revalidateOnFocus: false },
77
+ [...makeQueryKey(query), undefined]
78
+ )
79
+ cleanups.push(defaultRegistry.mount(atom))
80
+ await Effect.runPromise(awaitAtomResult(defaultRegistry, atom))
81
+ await new Promise((resolve) => setTimeout(resolve, 30))
82
+ await Effect.runPromise(awaitAtomResult(defaultRegistry, atom))
83
+ expect(calls).toBe(2)
84
+ })
85
+
86
+ it("lets settled queries react while another query is still discovering", () => {
87
+ const { handlers, invalidations, source } = makeSource(never)
88
+ configureLiveQueryInvalidation(source)
89
+ const pending = ["$Inventory", "Get", { id: "pending" }]
90
+ const settled = ["$Inventory", "List", undefined]
91
+ cleanups.push(registerLiveQuery(pending, () => DataDependencies.empty(), {}))
92
+ cleanups.push(registerLiveQuery(settled, () => new Set([repo]), {}))
93
+ const fetch = acquire(pending)
94
+ cleanups.push(() => releaseLiveQueryFetch(fetch))
95
+
96
+ handlers.receive(new Set([repo]))
97
+ expect(invalidations).toEqual([[settled]])
98
+ })
99
+
100
+ it("requests readiness again when the same source is configured again", async () => {
101
+ let calls = 0
102
+ let open = () => {}
103
+ const { source } = makeSource(() => ++calls === 1 ? never() : new Promise<void>((resolve) => open = resolve))
104
+ configureLiveQueryInvalidation(source)
105
+ const key = ["$Inventory", "Get", { id: "same" }]
106
+ cleanups.push(registerLiveQuery(key, () => DataDependencies.empty(), {}))
107
+ const fetch = acquire(key)
108
+ cleanups.push(() => releaseLiveQueryFetch(fetch))
109
+ let ready = false
110
+ void awaitLiveQueryFetch(fetch).then(() => ready = true)
111
+
112
+ configureLiveQueryInvalidation(source)
113
+ await flush()
114
+ expect(ready).toBe(false)
115
+ open()
116
+ await flush()
117
+ expect(ready).toBe(true)
118
+ })
119
+
120
+ it("follows a source configured synchronously from ready()", async () => {
121
+ const next = makeSource()
122
+ const { source } = makeSource(() => {
123
+ configureLiveQueryInvalidation(next.source)
124
+ return never()
125
+ })
126
+ configureLiveQueryInvalidation(source)
127
+ const key = ["$Inventory", "Get", { id: "sync" }]
128
+ cleanups.push(registerLiveQuery(key, () => DataDependencies.empty(), {}))
129
+ const fetch = acquire(key)
130
+ cleanups.push(() => releaseLiveQueryFetch(fetch))
131
+ await awaitLiveQueryFetch(fetch)
132
+ })
133
+
134
+ it("ignores a stale rejection from a replaced source", async () => {
135
+ let reject = (_error: unknown) => {}
136
+ const { source } = makeSource(() => new Promise<void>((_, rej) => reject = rej))
137
+ configureLiveQueryInvalidation(source)
138
+ const key = ["$Inventory", "Get", { id: "stale" }]
139
+ cleanups.push(registerLiveQuery(key, () => DataDependencies.empty(), {}))
140
+ const fetch = acquire(key)
141
+ cleanups.push(() => releaseLiveQueryFetch(fetch))
142
+ const waiting = awaitLiveQueryFetch(fetch)
143
+ reject(new Error("old source"))
144
+ configureLiveQueryInvalidation(makeSource().source)
145
+ await expect(waiting).resolves.toBeUndefined()
146
+ })
147
+
148
+ it("invalidates a discovering query when the source resets", async () => {
149
+ const { handlers, invalidations, source } = makeSource()
150
+ configureLiveQueryInvalidation(source)
151
+ const key = ["$Inventory", "Get", { id: "reset" }]
152
+ let reads = DataDependencies.empty()
153
+ cleanups.push(registerLiveQuery(key, () => reads, {}))
154
+ const fetch = acquire(key)
155
+ await awaitLiveQueryFetch(fetch)
156
+ handlers.receive(new Set([repo]))
157
+ handlers.reset()
158
+ reads = new Set([repo])
159
+ releaseLiveQueryFetch(fetch)
160
+ expect(invalidations).toEqual([[key]])
161
+ })
162
+
163
+ it("catches up a query registered without a covering fetch", async () => {
164
+ const { invalidations, source } = makeSource()
165
+ configureLiveQueryInvalidation(source)
166
+ const key = ["$Inventory", "List", "cached"]
167
+ cleanups.push(registerLiveQuery(key, () => new Set([repo]), {}))
168
+ expect(invalidations).toEqual([])
169
+ await flush()
170
+ expect(invalidations).toEqual([[key]])
171
+ })
172
+
173
+ it("does not catch up a query whose fetch covers the subscription", async () => {
174
+ const { invalidations, source } = makeSource()
175
+ configureLiveQueryInvalidation(source)
176
+ const key = ["$Inventory", "List", "fresh"]
177
+ cleanups.push(registerLiveQuery(key, () => new Set([repo]), {}))
178
+ const fetch = acquire(key)
179
+ await awaitLiveQueryFetch(fetch)
180
+ releaseLiveQueryFetch(fetch)
181
+ await flush()
182
+ expect(invalidations).toEqual([])
183
+ })
184
+
185
+ it("refetches a query whose running fetch was only ready for a replaced source", async () => {
186
+ configureLiveQueryInvalidation(makeSource().source)
187
+ const key = ["$Inventory", "List", "switched"]
188
+ cleanups.push(registerLiveQuery(key, () => new Set([repo]), {}))
189
+ const fetch = acquire(key)
190
+ await awaitLiveQueryFetch(fetch)
191
+ const next = makeSource()
192
+ configureLiveQueryInvalidation(next.source)
193
+ await flush()
194
+ expect(next.invalidations).toEqual([])
195
+ releaseLiveQueryFetch(fetch)
196
+ expect(next.invalidations).toEqual([[key]])
197
+ })
198
+
199
+ it("stops waiting for readiness when the fetch is aborted", async () => {
200
+ configureLiveQueryInvalidation(makeSource(never).source)
201
+ const key = ["$Inventory", "Get", { id: "abort" }]
202
+ cleanups.push(registerLiveQuery(key, () => DataDependencies.empty(), {}))
203
+ const fetch = acquire(key)
204
+ const controller = new AbortController()
205
+ const waiting = awaitLiveQueryFetch(fetch, controller.signal)
206
+ controller.abort()
207
+ await expect(waiting).rejects.toBeDefined()
208
+ releaseLiveQueryFetch(fetch)
209
+
210
+ const ready = vi.fn(() => Promise.resolve())
211
+ configureLiveQueryInvalidation({ ...makeSource().source, ready })
212
+ await flush()
213
+ // only the catch-up for the still-registered query asks the new source
214
+ expect(ready).toHaveBeenCalledTimes(1)
215
+ })
216
+
217
+ it("releases the deprecated fetch when readiness rejects", async () => {
218
+ const { handlers, invalidations, source } = makeSource(() => Promise.reject(new Error("offline")))
219
+ configureLiveQueryInvalidation(source)
220
+ const key = ["$Inventory", "List", "legacy"]
221
+ cleanups.push(registerLiveQuery(key, () => new Set([repo]), {}))
222
+ await expect(beginLiveQueryFetch(key)).rejects.toThrow("offline")
223
+ handlers.receive(new Set([repo]))
224
+ expect(invalidations).toEqual([[key]])
225
+ })
226
+
227
+ it("records dependencies of a failed fetch", async () => {
228
+ configureLiveQueryInvalidation(makeSource().source)
229
+ const dependency = DataDependencies.repo("MissingOrders")
230
+ const mrt = ManagedRuntime.make(Layer.empty)
231
+ cleanups.push(() => void mrt.dispose())
232
+ const rt = makeAtomClientRuntime(() => Layer.empty, mrt.memoMap)
233
+ const query = {
234
+ id: "MissingOrders.Get",
235
+ handler: () => DataDependencies.read(dependency).pipe(Effect.andThen(Effect.fail("not found")))
236
+ }
237
+ const key = [...makeQueryKey(query), undefined]
238
+ const atom = withQueryOptions(buildQueryFamily(rt, query)(undefined), { live: true, revalidateOnFocus: false }, key)
239
+ cleanups.push(defaultRegistry.mount(atom))
240
+ await Effect.runPromise(Effect.exit(awaitAtomResult(defaultRegistry, atom)))
241
+ expect(getQueryReadDependencies(key)).toEqual(new Set([dependency]))
242
+ })
243
+
244
+ it("keeps dependencies of projected families apart", async () => {
245
+ const first = DataDependencies.repo("ProjectedA")
246
+ const second = DataDependencies.repo("ProjectedB")
247
+ const mrt = ManagedRuntime.make(Layer.empty)
248
+ cleanups.push(() => void mrt.dispose())
249
+ const rt = makeAtomClientRuntime(() => Layer.empty, mrt.memoMap)
250
+ const make = (queryKeyProjectionHash: string, dependency: DataDependencies.DataDependency) => ({
251
+ id: "Projected.Get",
252
+ queryKeyProjectionHash,
253
+ handler: () => DataDependencies.read(dependency)
254
+ })
255
+ const a = buildQueryFamily(rt, make("a", first))(undefined)
256
+ const b = buildQueryFamily(rt, make("b", second))(undefined)
257
+ cleanups.push(defaultRegistry.mount(a), defaultRegistry.mount(b))
258
+ await Effect.runPromise(awaitAtomResult(defaultRegistry, a))
259
+ await Effect.runPromise(awaitAtomResult(defaultRegistry, b))
260
+ const baseKey = makeQueryKey(make("a", first))
261
+ expect(getQueryReadDependencies([...baseKey, "a", undefined])).toEqual(new Set([first]))
262
+ expect(getQueryReadDependencies([...baseKey, "b", undefined])).toEqual(new Set([second]))
263
+ })
264
+
265
+ it("keeps keys with colliding hashes apart", () => {
266
+ const { handlers, invalidations, source } = makeSource()
267
+ configureLiveQueryInvalidation(source)
268
+ const first = ["$Inventory", "Get", { id: "15019" }]
269
+ const second = ["$Inventory", "Get", { id: "66804" }]
270
+ cleanups.push(registerLiveQuery(first, () => new Set([repo]), {}))
271
+ cleanups.push(registerLiveQuery(second, () => new Set([repo]), {}))
272
+ handlers.receive(new Set([repo]))
273
+ expect(invalidations).toEqual([[first], [second]])
274
+ })
275
+
276
+ it("subscribes once when a synchronous reset replaces the registration", () => {
277
+ configureLiveQueryInvalidation(makeSource().source)
278
+ const key = ["$Inventory", "List", "reentrant"]
279
+ let unregister = registerLiveQuery(key, () => new Set([repo]), {})
280
+ let subscriptions = 0
281
+ let unsubscriptions = 0
282
+ configureLiveQueryInvalidation({
283
+ ready: () => Promise.resolve(),
284
+ subscribe: (_onWrites, onReset) => {
285
+ subscriptions++
286
+ onReset()
287
+ return () => unsubscriptions++
288
+ },
289
+ invalidate: () => {
290
+ unregister()
291
+ unregister = registerLiveQuery(key, () => new Set([repo]), {})
292
+ }
293
+ })
294
+ unregister()
295
+ expect([subscriptions, unsubscriptions]).toEqual([1, 1])
296
+ })
297
+
298
+ it("brings a pending delayed invalidation forward for a stricter observer", () => {
299
+ vi.useFakeTimers()
300
+ const { handlers, invalidations, source } = makeSource()
301
+ configureLiveQueryInvalidation(source)
302
+ const key = ["$Inventory", "List", "strict"]
303
+ cleanups.push(registerLiveQuery(key, () => new Set([repo]), { maxDelayMs: 50 }))
304
+ handlers.receive(new Set([repo]))
305
+ vi.advanceTimersByTime(10)
306
+ cleanups.push(registerLiveQuery(key, () => new Set([repo]), { maxDelayMs: 0 }))
307
+ expect(invalidations).toEqual([[key]])
308
+ })
309
+
310
+ it("releases deprecated fetches together when they end out of order", async () => {
311
+ const { handlers, invalidations, source } = makeSource()
312
+ configureLiveQueryInvalidation(source)
313
+ const first = ["$Inventory", "Get", { id: "legacy-a" }]
314
+ const second = ["$Inventory", "Get", { id: "legacy-b" }]
315
+ let firstReads = DataDependencies.empty()
316
+ cleanups.push(registerLiveQuery(first, () => firstReads, {}))
317
+ cleanups.push(registerLiveQuery(second, () => new Set([repo]), {}))
318
+ const [a, b] = await Promise.all([beginLiveQueryFetch(first), beginLiveQueryFetch(second)])
319
+ handlers.receive(new Set([repo]))
320
+ endLiveQueryFetch(b)
321
+ expect(invalidations).toEqual([])
322
+ firstReads = new Set([repo])
323
+ endLiveQueryFetch(a)
324
+ expect(invalidations).toEqual([[first], [second]])
325
+ })
326
+
327
+ it("rejects an already aborted readiness wait", async () => {
328
+ configureLiveQueryInvalidation(makeSource(never).source)
329
+ const key = ["$Inventory", "Get", { id: "pre-aborted" }]
330
+ cleanups.push(registerLiveQuery(key, () => DataDependencies.empty(), {}))
331
+ const fetch = acquire(key)
332
+ cleanups.push(() => releaseLiveQueryFetch(fetch))
333
+ await expect(awaitLiveQueryFetch(fetch, AbortSignal.abort())).rejects.toBeDefined()
334
+ })
335
+
336
+ it("rejects an aborted wait after following a source switch", async () => {
337
+ configureLiveQueryInvalidation(makeSource(never).source)
338
+ const key = ["$Inventory", "Get", { id: "abort-after-switch" }]
339
+ cleanups.push(registerLiveQuery(key, () => DataDependencies.empty(), {}))
340
+ const fetch = acquire(key)
341
+ cleanups.push(() => releaseLiveQueryFetch(fetch))
342
+ const controller = new AbortController()
343
+ const waiting = awaitLiveQueryFetch(fetch, controller.signal)
344
+ configureLiveQueryInvalidation(makeSource(never).source)
345
+ await flush()
346
+ controller.abort()
347
+ await expect(waiting).rejects.toBeDefined()
348
+ })
349
+
350
+ it("does not ask the source again after an unregistered query's catch-up is cancelled", async () => {
351
+ let open = () => {}
352
+ const ready = vi.fn(() => new Promise<void>((resolve) => open = resolve))
353
+ const { invalidations, source } = makeSource(ready)
354
+ configureLiveQueryInvalidation(source)
355
+ const key = ["$Inventory", "List", "cancelled-catch-up"]
356
+ registerLiveQuery(key, () => new Set([repo]), {})()
357
+ open()
358
+ await flush()
359
+ expect(invalidations).toEqual([])
360
+ })
361
+
362
+ it.each([0, 60_000])("revalidates a remount once with staleTime %i", async (staleTime) => {
363
+ const dependency = DataDependencies.repo("RemountOrders")
364
+ configureLiveQueryInvalidation({
365
+ ...makeSource().source,
366
+ invalidate: (keys) => void Effect.runPromise(invalidateAndAwait(keys))
367
+ })
368
+ const mrt = ManagedRuntime.make(Layer.empty)
369
+ cleanups.push(() => void mrt.dispose())
370
+ const rt = makeAtomClientRuntime(() => Layer.empty, mrt.memoMap)
371
+ let calls = 0
372
+ const query = {
373
+ id: "RemountOrders.List",
374
+ // network latency: SWR's mount task sees the catch-up refetch in flight
375
+ handler: () =>
376
+ DataDependencies.read(dependency).pipe(
377
+ Effect.andThen(Effect.sleep("1 millis")),
378
+ Effect.andThen(Effect.sync(() => ++calls))
379
+ )
380
+ }
381
+ const key = [...makeQueryKey(query), undefined]
382
+ const atom = withQueryOptions(
383
+ buildQueryFamily(rt, query)(undefined),
384
+ { live: { maxDelayMs: 20 }, staleTime, revalidateOnFocus: false },
385
+ key
386
+ )
387
+ const unmount = defaultRegistry.mount(atom)
388
+ await Effect.runPromise(awaitAtomResult(defaultRegistry, atom))
389
+ unmount()
390
+ // the registry disposes asynchronously; a remount before that keeps the registration (nothing missed)
391
+ await vi.waitFor(() => expect(isRegistered(key)).toBe(false))
392
+ cleanups.push(defaultRegistry.mount(atom))
393
+ await new Promise((resolve) => setTimeout(resolve, 50))
394
+ await Effect.runPromise(awaitAtomResult(defaultRegistry, atom))
395
+ expect(calls).toBe(2)
396
+ })
397
+
398
+ it("catches up a mounted stale query when the source is switched", async () => {
399
+ const dependency = DataDependencies.repo("SwitchedOrders")
400
+ const invalidate = (keys: ReadonlyArray<ReadonlyArray<unknown>>) => void Effect.runPromise(invalidateAndAwait(keys))
401
+ configureLiveQueryInvalidation({ ...makeSource().source, invalidate })
402
+ const mrt = ManagedRuntime.make(Layer.empty)
403
+ cleanups.push(() => void mrt.dispose())
404
+ const rt = makeAtomClientRuntime(() => Layer.empty, mrt.memoMap)
405
+ let calls = 0
406
+ const query = {
407
+ id: "SwitchedOrders.List",
408
+ handler: () => DataDependencies.read(dependency).pipe(Effect.andThen(Effect.sync(() => ++calls)))
409
+ }
410
+ const atom = withQueryOptions(
411
+ buildQueryFamily(rt, query)(undefined),
412
+ { live: true, staleTime: 0, revalidateOnFocus: false },
413
+ [...makeQueryKey(query), undefined]
414
+ )
415
+ cleanups.push(defaultRegistry.mount(atom))
416
+ await Effect.runPromise(awaitAtomResult(defaultRegistry, atom))
417
+ await flush()
418
+ expect(calls).toBe(1)
419
+ configureLiveQueryInvalidation({ ...makeSource().source, invalidate })
420
+ await flush()
421
+ await Effect.runPromise(awaitAtomResult(defaultRegistry, atom))
422
+ expect(calls).toBe(2)
423
+ })
424
+
425
+ it("keeps waiting when a switch runs between readiness settling and the wait resuming", async () => {
426
+ let open = () => {}
427
+ const ready = new Promise<void>((resolve) => open = resolve)
428
+ configureLiveQueryInvalidation(makeSource(() => ready).source)
429
+ const key = ["$Inventory", "Get", { id: "overtaken" }]
430
+ cleanups.push(registerLiveQuery(key, () => DataDependencies.empty(), {}))
431
+ const fetch = acquire(key)
432
+ cleanups.push(() => releaseLiveQueryFetch(fetch))
433
+ let settled = false
434
+ void awaitLiveQueryFetch(fetch).then(() => settled = true, () => settled = true)
435
+ void ready.then(() => configureLiveQueryInvalidation(makeSource(never).source))
436
+ open()
437
+ await flush()
438
+ expect(settled).toBe(false)
439
+ })
440
+
441
+ it("rejects when ready() aborts the wait synchronously", async () => {
442
+ const controller = new AbortController()
443
+ configureLiveQueryInvalidation(
444
+ makeSource(() => {
445
+ controller.abort()
446
+ return never()
447
+ })
448
+ .source
449
+ )
450
+ const key = ["$Inventory", "Get", { id: "sync-abort" }]
451
+ cleanups.push(registerLiveQuery(key, () => DataDependencies.empty(), {}))
452
+ const fetch = acquire(key)
453
+ cleanups.push(() => releaseLiveQueryFetch(fetch))
454
+ await expect(awaitLiveQueryFetch(fetch, controller.signal)).rejects.toBeDefined()
455
+ })
456
+
457
+ it.each(["modern", "legacy"] as const)(
458
+ "keeps a %s wait when a switch is queued right after readiness",
459
+ async (api) => {
460
+ let open = () => {}
461
+ const oldReady = new Promise<void>((resolve) => open = resolve)
462
+ configureLiveQueryInvalidation(makeSource(() => oldReady).source)
463
+ const key = ["$Inventory", "Get", { id: `deferred-${api}` }]
464
+ cleanups.push(registerLiveQuery(key, () => DataDependencies.empty(), {}))
465
+ let settled = false
466
+ if (api === "modern") {
467
+ const fetch = acquire(key)
468
+ cleanups.push(() => releaseLiveQueryFetch(fetch))
469
+ void awaitLiveQueryFetch(fetch).then(() => settled = true, () => settled = true)
470
+ } else {
471
+ cleanups.push(() => endLiveQueryFetch(true))
472
+ void beginLiveQueryFetch(key).then(() => settled = true, () => settled = true)
473
+ }
474
+ void oldReady.then(() => queueMicrotask(() => configureLiveQueryInvalidation(makeSource(never).source)))
475
+ open()
476
+ await flush()
477
+ expect(settled).toBe(false)
478
+ }
479
+ )
480
+
481
+ it.each(["modern", "legacy", "ready"] as const)(
482
+ "ignores a %s rejection when a switch is queued right after it",
483
+ async (api) => {
484
+ let rejectOld = (_error: unknown) => {}
485
+ const oldReady = new Promise<void>((_, reject) => rejectOld = reject)
486
+ configureLiveQueryInvalidation(makeSource(() => oldReady).source)
487
+ const key = ["$Inventory", "Get", { id: `deferred-rejection-${api}` }]
488
+ cleanups.push(registerLiveQuery(key, () => DataDependencies.empty(), {}))
489
+ let settled = false
490
+ const done = () => settled = true
491
+ if (api === "modern") {
492
+ const fetch = acquire(key)
493
+ cleanups.push(() => releaseLiveQueryFetch(fetch))
494
+ void awaitLiveQueryFetch(fetch).then(done, done)
495
+ } else if (api === "legacy") {
496
+ cleanups.push(() => endLiveQueryFetch(true))
497
+ void beginLiveQueryFetch(key).then(done, done)
498
+ } else {
499
+ void liveQueryReady().then(done, done)
500
+ }
501
+ void oldReady.catch(() => queueMicrotask(() => configureLiveQueryInvalidation(makeSource(never).source)))
502
+ rejectOld(new Error("departed source"))
503
+ await flush()
504
+ expect(settled).toBe(false)
505
+ }
506
+ )
507
+ })