@effect-app/vue 4.0.0-beta.99 → 4.0.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 (128) hide show
  1. package/CHANGELOG.md +2701 -0
  2. package/dist/atomQuery.d.ts +120 -0
  3. package/dist/atomQuery.d.ts.map +1 -0
  4. package/dist/atomQuery.js +434 -0
  5. package/dist/commander.d.ts +318 -43
  6. package/dist/commander.d.ts.map +1 -1
  7. package/dist/commander.js +593 -81
  8. package/dist/confirm.d.ts +5 -3
  9. package/dist/confirm.d.ts.map +1 -1
  10. package/dist/confirm.js +12 -14
  11. package/dist/dependencyMetadata.d.ts +18 -0
  12. package/dist/dependencyMetadata.d.ts.map +1 -0
  13. package/dist/dependencyMetadata.js +67 -0
  14. package/dist/errorReporter.d.ts +6 -4
  15. package/dist/errorReporter.d.ts.map +1 -1
  16. package/dist/errorReporter.js +14 -19
  17. package/dist/form.d.ts +5 -5
  18. package/dist/form.d.ts.map +1 -1
  19. package/dist/form.js +16 -10
  20. package/dist/internal/tanstackQuery.d.ts +9 -0
  21. package/dist/internal/tanstackQuery.d.ts.map +1 -0
  22. package/dist/internal/tanstackQuery.js +204 -0
  23. package/dist/intl.d.ts +4 -4
  24. package/dist/intl.d.ts.map +1 -1
  25. package/dist/intl.js +2 -2
  26. package/dist/lib.d.ts +8 -10
  27. package/dist/lib.d.ts.map +1 -1
  28. package/dist/lib.js +37 -12
  29. package/dist/liveQueryInvalidation.d.ts +16 -0
  30. package/dist/liveQueryInvalidation.d.ts.map +1 -0
  31. package/dist/liveQueryInvalidation.js +115 -0
  32. package/dist/makeClient.d.ts +232 -109
  33. package/dist/makeClient.d.ts.map +1 -1
  34. package/dist/makeClient.js +416 -84
  35. package/dist/makeContext.d.ts.map +1 -1
  36. package/dist/makeIntl.d.ts.map +1 -1
  37. package/dist/makeUseCommand.d.ts +3 -2
  38. package/dist/makeUseCommand.d.ts.map +1 -1
  39. package/dist/makeUseCommand.js +2 -2
  40. package/dist/mutate.d.ts +110 -41
  41. package/dist/mutate.d.ts.map +1 -1
  42. package/dist/mutate.js +236 -59
  43. package/dist/query.d.ts +161 -44
  44. package/dist/query.d.ts.map +1 -1
  45. package/dist/query.js +382 -113
  46. package/dist/queryLifetime.d.ts +25 -0
  47. package/dist/queryLifetime.d.ts.map +1 -0
  48. package/dist/queryLifetime.js +54 -0
  49. package/dist/routeParams.d.ts +4 -4
  50. package/dist/routeParams.d.ts.map +1 -1
  51. package/dist/routeParams.js +4 -3
  52. package/dist/runtime.d.ts +1 -17
  53. package/dist/runtime.d.ts.map +1 -1
  54. package/dist/runtime.js +2 -38
  55. package/dist/suspense.d.ts +19 -0
  56. package/dist/suspense.d.ts.map +1 -0
  57. package/dist/suspense.js +38 -0
  58. package/dist/toast.d.ts +1 -45
  59. package/dist/toast.d.ts.map +1 -1
  60. package/dist/toast.js +2 -32
  61. package/dist/withToast.d.ts +1 -24
  62. package/dist/withToast.d.ts.map +1 -1
  63. package/dist/withToast.js +2 -45
  64. package/docs/atom-query-api-redesign.md +207 -0
  65. package/docs/mutation-command-atoms.md +40 -0
  66. package/docs/query-key-invalidation.md +107 -0
  67. package/examples/streamMutation.ts +72 -0
  68. package/package.json +35 -94
  69. package/src/atomQuery.ts +600 -0
  70. package/src/commander.ts +1064 -116
  71. package/src/confirm.ts +12 -14
  72. package/src/dependencyMetadata.ts +84 -0
  73. package/src/errorReporter.ts +65 -75
  74. package/src/form.ts +22 -15
  75. package/src/index.ts +7 -7
  76. package/src/internal/tanstackQuery.ts +308 -0
  77. package/src/intl.ts +2 -2
  78. package/src/lib.ts +51 -23
  79. package/src/liveQueryInvalidation.ts +138 -0
  80. package/src/makeClient.ts +972 -263
  81. package/src/makeIntl.ts +2 -2
  82. package/src/makeUseCommand.ts +8 -5
  83. package/src/mutate.ts +407 -154
  84. package/src/query.ts +758 -242
  85. package/src/queryLifetime.ts +74 -0
  86. package/src/routeParams.ts +7 -7
  87. package/src/runtime.ts +1 -54
  88. package/src/suspense.ts +43 -0
  89. package/src/toast.ts +1 -52
  90. package/src/withToast.ts +1 -99
  91. package/test/Mutation.test.ts +104 -17
  92. package/test/dependencyInvalidation.test.ts +444 -0
  93. package/test/dist/dependencyInvalidation.test.d.ts.map +1 -0
  94. package/test/dist/form.test.d.ts.map +1 -1
  95. package/test/dist/inactiveInvalidation.test.d.ts.map +1 -0
  96. package/test/dist/interrupt-refetch-repro.test.d.ts.map +1 -0
  97. package/test/dist/lib.test.d.ts.map +1 -0
  98. package/test/dist/liveQueryInvalidation.test.d.ts.map +1 -0
  99. package/test/dist/query-span.test.d.ts.map +1 -0
  100. package/test/dist/queryOptions.test.d.ts.map +1 -0
  101. package/test/dist/streamFinal.test.d.ts.map +1 -0
  102. package/test/dist/streamFn.test.d.ts.map +1 -0
  103. package/test/dist/stubs.d.ts +3681 -256
  104. package/test/dist/stubs.d.ts.map +1 -1
  105. package/test/dist/stubs.js +166 -25
  106. package/test/dist/suspense-regression.test.d.ts.map +1 -0
  107. package/test/dist/suspense.test.d.ts.map +1 -0
  108. package/test/form-validation-errors.test.ts +2 -1
  109. package/test/form.test.ts +2 -1
  110. package/test/inactiveInvalidation.test.ts +172 -0
  111. package/test/interrupt-refetch-repro.test.ts +625 -0
  112. package/test/lib.test.ts +240 -0
  113. package/test/liveQueryInvalidation.test.ts +108 -0
  114. package/test/makeClient.test.ts +414 -32
  115. package/test/query-span.test.ts +92 -0
  116. package/test/queryOptions.test.ts +88 -0
  117. package/test/streamFinal.test.ts +64 -0
  118. package/test/streamFn.test.ts +457 -0
  119. package/test/stubs.ts +194 -36
  120. package/test/suspense-regression.test.ts +168 -0
  121. package/test/suspense.test.ts +160 -0
  122. package/tsconfig.examples.json +20 -0
  123. package/tsconfig.json +9 -1
  124. package/tsconfig.src.json +34 -34
  125. package/tsconfig.test.json +2 -2
  126. package/vitest.config.ts +5 -5
  127. package/eslint.config.mjs +0 -24
  128. package/tsconfig.json.bak +0 -12
@@ -0,0 +1,444 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { defaultRegistry } from "@effect/atom-vue"
3
+ import { expect, it } from "@effect/vitest"
4
+ import { QueryClient, VueQueryPlugin } from "@tanstack/vue-query"
5
+ import { DataDependencies, type InvalidationKey, InvalidationKeysFromServer, makeQueryKey } from "effect-app/client"
6
+ import * as Context from "effect-app/Context"
7
+ import * as Effect from "effect-app/Effect"
8
+ import * as Deferred from "effect/Deferred"
9
+ import * as Fiber from "effect/Fiber"
10
+ import * as Layer from "effect/Layer"
11
+ import * as ManagedRuntime from "effect/ManagedRuntime"
12
+ import * as Reactivity from "effect/reactivity/Reactivity"
13
+ import * as Stream from "effect/Stream"
14
+ import { TestClock } from "effect/testing"
15
+ import { createApp, effectScope, ref } from "vue"
16
+ import { awaitAtomResult, buildQueryFamily, invalidateAndAwait, makeAtomClientRuntime } from "../src/atomQuery.js"
17
+ import { clearQueryReadDependencies, getDerivedInvalidationKeys, partitionInvalidationKeys, registerQueryInvalidationMode, setQueryReadDependencies } from "../src/dependencyMetadata.js"
18
+ import { makeTanstackQuery, makeTanstackQueryInvalidator } from "../src/internal/tanstackQuery.js"
19
+ import { invalidateQueries, makeStreamMutation2, type MutationOptionsBase } from "../src/mutate.js"
20
+
21
+ const repo = DataDependencies.repo("FrontendRepo")
22
+ const otherRepo = DataDependencies.repo("OtherRepo")
23
+
24
+ it.live("stream mutations accumulate repeated server keys and invalidate once when settled", () =>
25
+ Effect.gen(function*() {
26
+ const key: InvalidationKey = ["$PickList", "List"]
27
+ const calls: Array<ReadonlyArray<ReadonlyArray<unknown>>> = []
28
+ const queryInvalidator = {
29
+ invalidateAndAwait: (keys: ReadonlyArray<ReadonlyArray<unknown>>) => Effect.sync(() => calls.push(keys))
30
+ }
31
+ const mutation = makeStreamMutation2(queryInvalidator)({
32
+ id: "PickList.StartBatchPrint",
33
+ handler: () =>
34
+ Stream.fromIterable([1, 2, 3]).pipe(
35
+ Stream.tap(() => InvalidationKeysFromServer.use((service) => service.add(key)))
36
+ )
37
+ })
38
+
39
+ yield* mutation(undefined).pipe(Stream.runDrain)
40
+
41
+ expect(calls).toEqual([[key]])
42
+ }))
43
+
44
+ it.live("stream mutations flush write-deps once when they first arrive, then again on settle", () =>
45
+ Effect.gen(function*() {
46
+ const jobKey: InvalidationKey = ["$PickList", "GetActiveJob"]
47
+ const listKey: InvalidationKey = ["$PickList", "List"]
48
+ const jobRepo = DataDependencies.repo("PickJob")
49
+ const itemRepo = DataDependencies.repo("PickItem")
50
+ setQueryReadDependencies(jobKey, new Set([jobRepo]))
51
+ setQueryReadDependencies(listKey, new Set([itemRepo]))
52
+
53
+ const calls: Array<ReadonlyArray<ReadonlyArray<unknown>>> = []
54
+ const firstFlush = yield* Deferred.make<void>()
55
+ const gate = yield* Deferred.make<void>()
56
+ const queryInvalidator = {
57
+ invalidateAndAwait: (keys: ReadonlyArray<ReadonlyArray<unknown>>) =>
58
+ Effect
59
+ .sync(() => {
60
+ calls.push(keys)
61
+ })
62
+ .pipe(
63
+ Effect.flatMap(() => calls.length === 1 ? Deferred.succeed(firstFlush, undefined) : Effect.void)
64
+ )
65
+ }
66
+ const mutation = makeStreamMutation2(queryInvalidator)({
67
+ id: "PickList.StartBatchPrint",
68
+ handler: () =>
69
+ Stream.make(1).pipe(
70
+ Stream.tap(() => DataDependencies.write(jobRepo)),
71
+ Stream.concat(Stream.fromEffect(Deferred.await(gate).pipe(Effect.as(2)))),
72
+ Stream.tap((n) => n === 2 ? DataDependencies.write(itemRepo) : Effect.void)
73
+ )
74
+ })
75
+
76
+ try {
77
+ const fiber = yield* Effect.forkChild(Stream.runDrain(mutation(undefined)))
78
+ yield* Deferred.await(firstFlush)
79
+ expect(calls).toEqual([[jobKey]])
80
+
81
+ yield* Deferred.succeed(gate, undefined)
82
+ yield* Fiber.join(fiber)
83
+ expect(calls).toEqual([[jobKey], [jobKey, listKey]])
84
+ } finally {
85
+ clearQueryReadDependencies(jobKey)
86
+ clearQueryReadDependencies(listKey)
87
+ }
88
+ }))
89
+
90
+ // --- shared registry + derivation logic --------------------------------------------------------
91
+
92
+ it("getDerivedInvalidationKeys returns keys of queries whose reads intersect the writes", () => {
93
+ const inventoryKey = ["$Inventory", "List", undefined]
94
+ const ordersKey = ["$Orders", "List", undefined]
95
+ setQueryReadDependencies(inventoryKey, new Set([repo]))
96
+ setQueryReadDependencies(ordersKey, new Set([otherRepo]))
97
+
98
+ try {
99
+ expect(getDerivedInvalidationKeys(new Set([repo]))).toEqual([inventoryKey])
100
+ expect(getDerivedInvalidationKeys(new Set([repo, otherRepo]))).toEqual([inventoryKey, ordersKey])
101
+ expect(getDerivedInvalidationKeys(new Set())).toEqual([])
102
+ } finally {
103
+ clearQueryReadDependencies(inventoryKey)
104
+ clearQueryReadDependencies(ordersKey)
105
+ }
106
+ })
107
+
108
+ it("uses the strictest active query invalidation mode", () => {
109
+ const overviewKey = ["$Overview", "$List", undefined]
110
+ const pickListKey = ["$PickList", "$List", undefined]
111
+
112
+ const unregisterSoft = registerQueryInvalidationMode(overviewKey, "soft")
113
+ const unregisterAwait = registerQueryInvalidationMode(pickListKey, "await")
114
+ const unregisterStrictOverview = registerQueryInvalidationMode(overviewKey, "await")
115
+ try {
116
+ expect(partitionInvalidationKeys([overviewKey, pickListKey])).toEqual({
117
+ awaitKeys: [overviewKey, pickListKey],
118
+ softKeys: []
119
+ })
120
+ unregisterStrictOverview()
121
+ expect(partitionInvalidationKeys([overviewKey, pickListKey])).toEqual({
122
+ awaitKeys: [pickListKey],
123
+ softKeys: [overviewKey]
124
+ })
125
+ } finally {
126
+ unregisterStrictOverview()
127
+ unregisterSoft()
128
+ unregisterAwait()
129
+ }
130
+ })
131
+
132
+ it("clearing read dependencies drops the query from derivation", () => {
133
+ const inventoryKey = ["$Inventory", "List", undefined]
134
+ setQueryReadDependencies(inventoryKey, new Set([repo]))
135
+ clearQueryReadDependencies(inventoryKey)
136
+ expect(getDerivedInvalidationKeys(new Set([repo]))).toEqual([])
137
+ })
138
+
139
+ it("derivation matches on both dependency type and name (repo vs signal)", () => {
140
+ const key = ["$Live", "Feed", undefined]
141
+ setQueryReadDependencies(key, new Set([DataDependencies.signal("Feed")]))
142
+ try {
143
+ // Same name but different type (repo "Feed") must NOT intersect a signal read.
144
+ expect(getDerivedInvalidationKeys(new Set([DataDependencies.repo("Feed")]))).toEqual([])
145
+ // Same type and name does intersect.
146
+ expect(getDerivedInvalidationKeys(new Set([DataDependencies.signal("Feed")]))).toEqual([key])
147
+ } finally {
148
+ clearQueryReadDependencies(key)
149
+ }
150
+ })
151
+
152
+ it("derivation intersects when any one of multiple reads matches a write", () => {
153
+ const key = ["$Mixed", "List", undefined]
154
+ setQueryReadDependencies(key, new Set([DataDependencies.repo("A"), DataDependencies.repo("B")]))
155
+ try {
156
+ expect(getDerivedInvalidationKeys(new Set([DataDependencies.repo("B")]))).toEqual([key])
157
+ expect(getDerivedInvalidationKeys(new Set([DataDependencies.repo("C")]))).toEqual([])
158
+ } finally {
159
+ clearQueryReadDependencies(key)
160
+ }
161
+ })
162
+
163
+ it.effect("a command's write deps invalidate active queries whose recorded reads intersect", () =>
164
+ Effect.gen(function*() {
165
+ const inventoryKey = ["$Inventory", "List", undefined]
166
+ setQueryReadDependencies(inventoryKey, new Set([repo]))
167
+
168
+ const recorded: Array<ReadonlyArray<unknown>> = []
169
+ const queryInvalidator = {
170
+ invalidateAndAwait: (keys: ReadonlyArray<ReadonlyArray<unknown>>) =>
171
+ Effect.sync(() => keys.forEach((key) => recorded.push(key)))
172
+ }
173
+
174
+ // The command handler records a write to the same repo the query read from.
175
+ const mutate = invalidateQueries({ id: "Admin.Save" }, undefined, queryInvalidator)
176
+ const command = DataDependencies.write(repo).pipe(Effect.as(123))
177
+
178
+ const fiber = yield* Effect.forkChild(mutate(command, { id: "abc" }))
179
+ yield* TestClock.adjust("1 millis")
180
+ const result = yield* Fiber.join(fiber)
181
+
182
+ clearQueryReadDependencies(inventoryKey)
183
+
184
+ expect(result).toBe(123)
185
+ expect(recorded).toContainEqual(inventoryKey)
186
+ }))
187
+
188
+ // --- atom engine: recording wires through buildQueryFamily ---------------------------------------
189
+
190
+ it("atom engine: a query records its read deps so a command's writes derive it", async () => {
191
+ const atomRepo = DataDependencies.repo("AtomRepo")
192
+ const mrt = ManagedRuntime.make(Layer.empty)
193
+ const baseContext = mrt.runSync(Effect.context<never>())
194
+ const rt = makeAtomClientRuntime(
195
+ () => Layer.succeedContext(baseContext) as Layer.Layer<any, never, never>,
196
+ mrt.memoMap
197
+ )
198
+
199
+ let runs = 0
200
+ const self = {
201
+ id: "AtomInv.List",
202
+ handler: () => DataDependencies.read(atomRepo).pipe(Effect.as(++runs))
203
+ }
204
+ const family = buildQueryFamily(rt as any, self as any)
205
+ const atom = family(undefined)
206
+
207
+ const unmount = defaultRegistry.mount(atom)
208
+ try {
209
+ await Effect.runPromise(awaitAtomResult(defaultRegistry, atom))
210
+ expect(runs).toBe(1)
211
+
212
+ const fullKey = [...makeQueryKey(self), undefined]
213
+ expect(getDerivedInvalidationKeys(new Set([atomRepo]))).toContainEqual(fullKey)
214
+ expect(getDerivedInvalidationKeys(new Set([repo]))).not.toContainEqual(fullKey)
215
+ } finally {
216
+ unmount()
217
+ clearQueryReadDependencies([...makeQueryKey(self), undefined])
218
+ }
219
+ })
220
+
221
+ // --- legacy tanstack engine: recording + cache-removal cleanup -----------------------------------
222
+
223
+ const makeTanstackContext = (queryClient: QueryClient) => {
224
+ const app = createApp({ render: () => null })
225
+ app.use(VueQueryPlugin, { queryClient })
226
+ const scope = effectScope(true)
227
+ const run = <T>(fn: () => T): T => {
228
+ let out!: T
229
+ app.runWithContext(() => {
230
+ scope.run(() => {
231
+ out = fn()
232
+ })
233
+ })
234
+ return out
235
+ }
236
+ return { run, dispose: () => scope.stop() }
237
+ }
238
+
239
+ it("tanstack engine: a query records reads, and a command's writes refetch it", async () => {
240
+ const tsRepo = DataDependencies.repo("TanstackRepo")
241
+ const queryClient = new QueryClient({
242
+ defaultOptions: { queries: { retry: false, gcTime: Infinity, staleTime: 0 } }
243
+ })
244
+ const useQuery = makeTanstackQuery(() => Context.empty(), queryClient)
245
+
246
+ let runs = 0
247
+ const self = { id: "TanstackInv.List", handler: () => DataDependencies.read(tsRepo).pipe(Effect.as(++runs)) }
248
+ const ctx = makeTanstackContext(queryClient)
249
+ const [, , , handle] = ctx.run(() => useQuery(self as any)(ref(undefined) as any, {} as any)) as any
250
+
251
+ try {
252
+ await Effect.runPromise(handle.refetch())
253
+ expect(runs).toBe(1)
254
+
255
+ const fullKey = [...makeQueryKey(self), undefined]
256
+ const derived = getDerivedInvalidationKeys(new Set([tsRepo]))
257
+ expect(derived).toContainEqual(fullKey)
258
+
259
+ // Invalidating those derived keys via the tanstack invalidator refetches the active query.
260
+ await Effect.runPromise(makeTanstackQueryInvalidator(queryClient).invalidateAndAwait(derived))
261
+ expect(runs).toBe(2)
262
+ } finally {
263
+ ctx.dispose()
264
+ }
265
+ })
266
+
267
+ it("tanstack engine: evicting a query from the cache clears its recorded reads", () => {
268
+ const tsRepo = DataDependencies.repo("TanstackEvictRepo")
269
+ const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
270
+ // Installs the cache-removal subscription that clears the registry on eviction.
271
+ makeTanstackQuery(() => Context.empty(), queryClient)
272
+
273
+ const key = ["$TanstackEvict", "List", undefined]
274
+ const cache = queryClient.getQueryCache()
275
+ cache.build(queryClient, { queryKey: key, queryFn: () => Promise.resolve(1) })
276
+ setQueryReadDependencies(key, new Set([tsRepo]))
277
+ expect(getDerivedInvalidationKeys(new Set([tsRepo]))).toContainEqual(key)
278
+
279
+ cache.clear()
280
+ expect(getDerivedInvalidationKeys(new Set([tsRepo]))).not.toContainEqual(key)
281
+ })
282
+
283
+ // --- atom engine: GC finalizer clears recorded reads --------------------------------------------
284
+
285
+ it("atom engine: disposing the query atom clears its recorded reads", async () => {
286
+ const atomRepo = DataDependencies.repo("AtomGcRepo")
287
+ const mrt = ManagedRuntime.make(Layer.empty)
288
+ const baseContext = mrt.runSync(Effect.context<never>())
289
+ const rt = makeAtomClientRuntime(
290
+ () => Layer.succeedContext(baseContext) as Layer.Layer<any, never, never>,
291
+ mrt.memoMap
292
+ )
293
+ const self = { id: "AtomGc.List", handler: () => DataDependencies.read(atomRepo).pipe(Effect.as(1)) }
294
+ const atom = buildQueryFamily(rt as any, self as any)(undefined)
295
+ const fullKey = [...makeQueryKey(self), undefined]
296
+
297
+ const unmount = defaultRegistry.mount(atom)
298
+ await Effect.runPromise(awaitAtomResult(defaultRegistry, atom))
299
+ expect(getDerivedInvalidationKeys(new Set([atomRepo]))).toContainEqual(fullKey)
300
+
301
+ // Disposing the registry runs the atom's finalizers, including `trackReadDependencies`.
302
+ unmount()
303
+ defaultRegistry.reset()
304
+ expect(getDerivedInvalidationKeys(new Set([atomRepo]))).not.toContainEqual(fullKey)
305
+ })
306
+
307
+ // --- full engine e2e matrix: every invalidation source, on each engine ---------------------------
308
+ //
309
+ // For each engine, a live query records a repo read; then commands sharing the query's namespace are
310
+ // run through the real mutate path, covering every invalidation source. The command namespace matches
311
+ // the query's, so the "no refetch" cases also confirm there is NO implicit namespace invalidation.
312
+
313
+ interface EngineHarness {
314
+ readonly queryFullKey: ReadonlyArray<unknown>
315
+ readonly serverInvalidationKey: InvalidationKey
316
+ readonly fetchInitial: () => Promise<unknown>
317
+ readonly runs: () => number
318
+ readonly runCommand: <A, E>(
319
+ options: MutationOptionsBase | undefined,
320
+ command: Effect.Effect<A, E>
321
+ ) => Promise<unknown>
322
+ readonly dispose: () => void
323
+ }
324
+
325
+ const makeTanstackHarness = (queryRepo: DataDependencies.DataDependency): EngineHarness => {
326
+ const queryClient = new QueryClient({
327
+ defaultOptions: { queries: { retry: false, gcTime: Infinity, staleTime: 0 } }
328
+ })
329
+ const useQuery = makeTanstackQuery(() => Context.empty(), queryClient)
330
+ const invalidator = makeTanstackQueryInvalidator(queryClient)
331
+ let runs = 0
332
+ const self = { id: "MatrixTs.List", handler: () => DataDependencies.read(queryRepo).pipe(Effect.as(++runs)) }
333
+ const ctx = makeTanstackContext(queryClient)
334
+ const [, , , handle] = ctx.run(() => useQuery(self as any)(ref(undefined) as any, {} as any)) as any
335
+ return {
336
+ queryFullKey: [...makeQueryKey(self), undefined],
337
+ serverInvalidationKey: makeQueryKey(self),
338
+ fetchInitial: () => Effect.runPromise(handle.refetch()),
339
+ runs: () => runs,
340
+ runCommand: (options, command) =>
341
+ Effect.runPromise(invalidateQueries({ id: "MatrixTs.Save" }, options, invalidator)(command, { id: "x" })),
342
+ dispose: () => ctx.dispose()
343
+ }
344
+ }
345
+
346
+ const makeAtomHarness = (queryRepo: DataDependencies.DataDependency): EngineHarness => {
347
+ const mrt = ManagedRuntime.make(Reactivity.layer)
348
+ const baseContext = mrt.runSync(Effect.context<Reactivity.Reactivity>())
349
+ const reactivity = Context.get(baseContext, Reactivity.Reactivity)
350
+ const rt = makeAtomClientRuntime(
351
+ () => Layer.succeedContext(baseContext) as Layer.Layer<any, never, never>,
352
+ mrt.memoMap
353
+ )
354
+ const invalidator = {
355
+ invalidateAndAwait: (keys: ReadonlyArray<ReadonlyArray<unknown>>) =>
356
+ invalidateAndAwait(keys).pipe(Effect.provideService(Reactivity.Reactivity, reactivity))
357
+ }
358
+ let runs = 0
359
+ const self = { id: "MatrixAtom.List", handler: () => DataDependencies.read(queryRepo).pipe(Effect.as(++runs)) }
360
+ const atom = buildQueryFamily(rt as any, self as any)(undefined)
361
+ const unmount = defaultRegistry.mount(atom)
362
+ return {
363
+ queryFullKey: [...makeQueryKey(self), undefined],
364
+ serverInvalidationKey: makeQueryKey(self),
365
+ fetchInitial: () => Effect.runPromise(awaitAtomResult(defaultRegistry, atom)),
366
+ runs: () => runs,
367
+ runCommand: (options, command) =>
368
+ Effect.runPromise(
369
+ invalidateQueries({ id: "MatrixAtom.Save" }, options, invalidator)(command, { id: "x" })
370
+ .pipe(Effect.andThen(awaitAtomResult(defaultRegistry, atom).pipe(Effect.exit)))
371
+ ),
372
+ dispose: () => {
373
+ unmount()
374
+ clearQueryReadDependencies([...makeQueryKey(self), undefined])
375
+ }
376
+ }
377
+ }
378
+
379
+ const runInvalidationMatrix = (engine: string, makeHarness: (repo: DataDependencies.DataDependency) => EngineHarness) =>
380
+ it(`${engine} engine e2e: invalidation source matrix`, async () => {
381
+ const queryRepo = DataDependencies.repo(`${engine}MatrixRepo`)
382
+ const otherRepo = DataDependencies.repo(`${engine}MatrixOther`)
383
+ const h = makeHarness(queryRepo)
384
+ const noop = Effect.succeed(undefined)
385
+
386
+ const expectRefetch = async <A, E>(
387
+ label: string,
388
+ options: MutationOptionsBase | undefined,
389
+ command: Effect.Effect<A, E>
390
+ ) => {
391
+ const before = h.runs()
392
+ await h.runCommand(options, command)
393
+ expect(h.runs(), `${label} should refetch`).toBeGreaterThan(before)
394
+ }
395
+ const expectNoRefetch = async <A, E>(
396
+ label: string,
397
+ options: MutationOptionsBase | undefined,
398
+ command: Effect.Effect<A, E>
399
+ ) => {
400
+ const before = h.runs()
401
+ await h.runCommand(options, command)
402
+ expect(h.runs(), `${label} must not refetch`).toBe(before)
403
+ }
404
+
405
+ try {
406
+ await h.fetchInitial()
407
+ expect(h.runs()).toBeGreaterThanOrEqual(1)
408
+
409
+ // 1. write-dependency derivation: command writes the repo the query read.
410
+ await expectRefetch(
411
+ "intersecting write dep",
412
+ undefined,
413
+ DataDependencies.write(queryRepo).pipe(Effect.as(undefined))
414
+ )
415
+
416
+ // 2. write-dependency derivation: command writes an UNRELATED repo (same namespace).
417
+ await expectNoRefetch(
418
+ "unrelated write dep",
419
+ undefined,
420
+ DataDependencies.write(otherRepo).pipe(Effect.as(undefined))
421
+ )
422
+
423
+ // 3. explicit `queryInvalidation` config targeting the query.
424
+ await expectRefetch("explicit config", { queryInvalidation: () => [h.queryFullKey as any] }, noop)
425
+
426
+ // 4. explicit config targeting an UNRELATED key.
427
+ await expectNoRefetch("unrelated explicit config", { queryInvalidation: () => [["$NotAQuery"]] }, noop)
428
+
429
+ // 5. server-sent invalidation key targeting the query.
430
+ await expectRefetch(
431
+ "server key",
432
+ undefined,
433
+ Effect.flatMap(InvalidationKeysFromServer, (svc) => svc.add(h.serverInvalidationKey)).pipe(Effect.as(undefined))
434
+ )
435
+
436
+ // 6. nothing — no config, no deps, no server keys, same namespace (no implicit invalidation).
437
+ await expectNoRefetch("empty command", undefined, noop)
438
+ } finally {
439
+ h.dispose()
440
+ }
441
+ })
442
+
443
+ runInvalidationMatrix("tanstack", makeTanstackHarness)
444
+ runInvalidationMatrix("atom", makeAtomHarness)
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dependencyInvalidation.test.d.ts","sourceRoot":"","sources":["../dependencyInvalidation.test.ts"],"names":[],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"form.test.d.ts","sourceRoot":"","sources":["../form.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,CAAC,EAAE,MAAM,YAAY,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAGtC,qBAAa,YAAa,SAAQ,iBAahC;CAAG;;;;AAEL,qBAAa,mBAAoB,SAAQ,wBAEvC;CAAG;;;;;;;;;;;;AAEL,qBAAa,WAAY,SAAQ,gBAK/B;CAAG;;;;;;;;AAEL,cAAM,MAAO,SAAQ,WAEnB;CAAG;;;;;;;;AAEL,cAAM,MAAO,SAAQ,WAEnB;CAAG;;;;;;;;;;;;AAEL,cAAM,QAAS,SAAQ,aAGrB;CAAG;;;;;;;;;;;;;;;;;;;;;;;;AAqBL,qBAAa,cAAe,SAAQ,mBAGlC;CAAG"}
1
+ {"version":3,"file":"form.test.d.ts","sourceRoot":"","sources":["../form.test.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,CAAC,MAAM,mBAAmB,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAGtC,qBAAa,YAAa,SAAQ,iBAahC;CAAG;;;;AAEL,qBAAa,mBAAoB,SAAQ,wBAEvC;CAAG;;;;;;;;;;;;AAEL,qBAAa,WAAY,SAAQ,gBAK/B;CAAG;;;;;;AAEL,cAAM,MAAO,SAAQ,WAEnB;CAAG;;;;;;AAEL,cAAM,MAAO,SAAQ,WAEnB;CAAG;;;;;;;;;;AAEL,cAAM,QAAS,SAAQ,aAGrB;CAAG;;;;;;;;;;;;;;;;;;AAqBL,qBAAa,cAAe,SAAQ,mBAGlC;CAAG"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inactiveInvalidation.test.d.ts","sourceRoot":"","sources":["../inactiveInvalidation.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interrupt-refetch-repro.test.d.ts","sourceRoot":"","sources":["../interrupt-refetch-repro.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lib.test.d.ts","sourceRoot":"","sources":["../lib.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"liveQueryInvalidation.test.d.ts","sourceRoot":"","sources":["../liveQueryInvalidation.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-span.test.d.ts","sourceRoot":"","sources":["../query-span.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queryOptions.test.d.ts","sourceRoot":"","sources":["../queryOptions.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streamFinal.test.d.ts","sourceRoot":"","sources":["../streamFinal.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streamFn.test.d.ts","sourceRoot":"","sources":["../streamFn.test.ts"],"names":[],"mappings":""}