@effect-app/vue 4.0.0-beta.98 → 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.
- package/CHANGELOG.md +2707 -0
- package/dist/atomQuery.d.ts +120 -0
- package/dist/atomQuery.d.ts.map +1 -0
- package/dist/atomQuery.js +434 -0
- package/dist/commander.d.ts +318 -43
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +593 -81
- package/dist/confirm.d.ts +5 -3
- package/dist/confirm.d.ts.map +1 -1
- package/dist/confirm.js +12 -14
- package/dist/dependencyMetadata.d.ts +18 -0
- package/dist/dependencyMetadata.d.ts.map +1 -0
- package/dist/dependencyMetadata.js +67 -0
- package/dist/errorReporter.d.ts +6 -4
- package/dist/errorReporter.d.ts.map +1 -1
- package/dist/errorReporter.js +14 -19
- package/dist/form.d.ts +5 -5
- package/dist/form.d.ts.map +1 -1
- package/dist/form.js +16 -10
- package/dist/internal/tanstackQuery.d.ts +9 -0
- package/dist/internal/tanstackQuery.d.ts.map +1 -0
- package/dist/internal/tanstackQuery.js +204 -0
- package/dist/intl.d.ts +4 -4
- package/dist/intl.d.ts.map +1 -1
- package/dist/intl.js +2 -2
- package/dist/lib.d.ts +8 -10
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +37 -12
- package/dist/liveQueryInvalidation.d.ts +16 -0
- package/dist/liveQueryInvalidation.d.ts.map +1 -0
- package/dist/liveQueryInvalidation.js +115 -0
- package/dist/makeClient.d.ts +232 -109
- package/dist/makeClient.d.ts.map +1 -1
- package/dist/makeClient.js +416 -84
- package/dist/makeContext.d.ts.map +1 -1
- package/dist/makeIntl.d.ts.map +1 -1
- package/dist/makeUseCommand.d.ts +3 -2
- package/dist/makeUseCommand.d.ts.map +1 -1
- package/dist/makeUseCommand.js +2 -2
- package/dist/mutate.d.ts +110 -41
- package/dist/mutate.d.ts.map +1 -1
- package/dist/mutate.js +236 -59
- package/dist/query.d.ts +161 -44
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +382 -113
- package/dist/queryLifetime.d.ts +25 -0
- package/dist/queryLifetime.d.ts.map +1 -0
- package/dist/queryLifetime.js +54 -0
- package/dist/routeParams.d.ts +4 -4
- package/dist/routeParams.d.ts.map +1 -1
- package/dist/routeParams.js +4 -3
- package/dist/runtime.d.ts +1 -17
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +2 -38
- package/dist/suspense.d.ts +19 -0
- package/dist/suspense.d.ts.map +1 -0
- package/dist/suspense.js +38 -0
- package/dist/toast.d.ts +1 -45
- package/dist/toast.d.ts.map +1 -1
- package/dist/toast.js +2 -32
- package/dist/withToast.d.ts +1 -24
- package/dist/withToast.d.ts.map +1 -1
- package/dist/withToast.js +2 -45
- package/docs/atom-query-api-redesign.md +207 -0
- package/docs/mutation-command-atoms.md +40 -0
- package/docs/query-key-invalidation.md +107 -0
- package/examples/streamMutation.ts +72 -0
- package/package.json +35 -94
- package/src/atomQuery.ts +600 -0
- package/src/commander.ts +1064 -116
- package/src/confirm.ts +12 -14
- package/src/dependencyMetadata.ts +84 -0
- package/src/errorReporter.ts +65 -75
- package/src/form.ts +22 -15
- package/src/index.ts +7 -7
- package/src/internal/tanstackQuery.ts +308 -0
- package/src/intl.ts +2 -2
- package/src/lib.ts +51 -23
- package/src/liveQueryInvalidation.ts +138 -0
- package/src/makeClient.ts +972 -263
- package/src/makeIntl.ts +2 -2
- package/src/makeUseCommand.ts +8 -5
- package/src/mutate.ts +407 -154
- package/src/query.ts +758 -242
- package/src/queryLifetime.ts +74 -0
- package/src/routeParams.ts +7 -7
- package/src/runtime.ts +1 -54
- package/src/suspense.ts +43 -0
- package/src/toast.ts +1 -52
- package/src/withToast.ts +1 -99
- package/test/Mutation.test.ts +104 -17
- package/test/dependencyInvalidation.test.ts +444 -0
- package/test/dist/dependencyInvalidation.test.d.ts.map +1 -0
- package/test/dist/form.test.d.ts.map +1 -1
- package/test/dist/inactiveInvalidation.test.d.ts.map +1 -0
- package/test/dist/interrupt-refetch-repro.test.d.ts.map +1 -0
- package/test/dist/lib.test.d.ts.map +1 -0
- package/test/dist/liveQueryInvalidation.test.d.ts.map +1 -0
- package/test/dist/query-span.test.d.ts.map +1 -0
- package/test/dist/queryOptions.test.d.ts.map +1 -0
- package/test/dist/streamFinal.test.d.ts.map +1 -0
- package/test/dist/streamFn.test.d.ts.map +1 -0
- package/test/dist/stubs.d.ts +3681 -256
- package/test/dist/stubs.d.ts.map +1 -1
- package/test/dist/stubs.js +166 -25
- package/test/dist/suspense-regression.test.d.ts.map +1 -0
- package/test/dist/suspense.test.d.ts.map +1 -0
- package/test/form-validation-errors.test.ts +2 -1
- package/test/form.test.ts +2 -1
- package/test/inactiveInvalidation.test.ts +172 -0
- package/test/interrupt-refetch-repro.test.ts +625 -0
- package/test/lib.test.ts +240 -0
- package/test/liveQueryInvalidation.test.ts +108 -0
- package/test/makeClient.test.ts +414 -32
- package/test/query-span.test.ts +92 -0
- package/test/queryOptions.test.ts +88 -0
- package/test/streamFinal.test.ts +64 -0
- package/test/streamFn.test.ts +457 -0
- package/test/stubs.ts +194 -36
- package/test/suspense-regression.test.ts +168 -0
- package/test/suspense.test.ts +160 -0
- package/tsconfig.examples.json +20 -0
- package/tsconfig.json +9 -1
- package/tsconfig.src.json +34 -34
- package/tsconfig.test.json +2 -2
- package/vitest.config.ts +5 -5
- package/eslint.config.mjs +0 -24
- 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":"
|
|
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":""}
|