@effect-app/vue 4.0.0-beta.26 → 4.0.0-beta.260
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 +1923 -0
- package/dist/commander.d.ts +634 -0
- package/dist/commander.d.ts.map +1 -0
- package/dist/commander.js +1070 -0
- package/dist/confirm.d.ts +21 -0
- package/dist/confirm.d.ts.map +1 -0
- package/dist/confirm.js +26 -0
- package/dist/errorReporter.d.ts +7 -5
- package/dist/errorReporter.d.ts.map +1 -1
- package/dist/errorReporter.js +14 -19
- package/dist/form.d.ts +15 -6
- package/dist/form.d.ts.map +1 -1
- package/dist/form.js +46 -13
- package/dist/index.d.ts +1 -1
- package/dist/intl.d.ts +15 -0
- package/dist/intl.d.ts.map +1 -0
- package/dist/intl.js +9 -0
- package/dist/lib.d.ts +8 -10
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +35 -10
- package/dist/makeClient.d.ts +157 -343
- package/dist/makeClient.d.ts.map +1 -1
- package/dist/makeClient.js +216 -376
- package/dist/makeContext.d.ts +1 -1
- package/dist/makeContext.d.ts.map +1 -1
- package/dist/makeIntl.d.ts +1 -1
- package/dist/makeIntl.d.ts.map +1 -1
- package/dist/makeUseCommand.d.ts +9 -0
- package/dist/makeUseCommand.d.ts.map +1 -0
- package/dist/makeUseCommand.js +13 -0
- package/dist/mutate.d.ts +97 -39
- package/dist/mutate.d.ts.map +1 -1
- package/dist/mutate.js +177 -49
- package/dist/query.d.ts +24 -39
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +156 -78
- package/dist/routeParams.d.ts +5 -5
- package/dist/routeParams.d.ts.map +1 -1
- package/dist/routeParams.js +4 -3
- package/dist/runtime.d.ts +2 -15
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +2 -26
- package/dist/toast.d.ts +2 -0
- package/dist/toast.d.ts.map +1 -0
- package/dist/toast.js +2 -0
- package/dist/withToast.d.ts +2 -0
- package/dist/withToast.d.ts.map +1 -0
- package/dist/withToast.js +2 -0
- package/examples/streamMutation.ts +72 -0
- package/package.json +29 -90
- package/src/commander.ts +3406 -0
- package/src/{experimental/confirm.ts → confirm.ts} +12 -14
- package/src/errorReporter.ts +65 -75
- package/src/form.ts +61 -18
- package/src/intl.ts +12 -0
- package/src/lib.ts +48 -20
- package/src/makeClient.ts +581 -1138
- package/src/{experimental/makeUseCommand.ts → makeUseCommand.ts} +8 -5
- package/src/mutate.ts +335 -134
- package/src/query.ts +241 -183
- package/src/routeParams.ts +7 -7
- package/src/runtime.ts +1 -31
- package/src/toast.ts +1 -0
- package/src/withToast.ts +1 -0
- package/test/Mutation.test.ts +181 -24
- package/test/dist/form.test.d.ts.map +1 -1
- package/test/dist/lib.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 +3527 -122
- package/test/dist/stubs.d.ts.map +1 -1
- package/test/dist/stubs.js +187 -32
- package/test/form-validation-errors.test.ts +25 -20
- package/test/form.test.ts +22 -3
- package/test/lib.test.ts +240 -0
- package/test/makeClient.test.ts +327 -38
- package/test/streamFinal.test.ts +64 -0
- package/test/streamFn.test.ts +457 -0
- package/test/stubs.ts +223 -43
- package/tsconfig.examples.json +20 -0
- package/tsconfig.json +2 -1
- package/tsconfig.json.bak +5 -2
- package/tsconfig.src.json +34 -34
- package/tsconfig.test.json +2 -2
- package/vitest.config.ts +5 -5
- package/dist/experimental/commander.d.ts +0 -359
- package/dist/experimental/commander.d.ts.map +0 -1
- package/dist/experimental/commander.js +0 -557
- package/dist/experimental/confirm.d.ts +0 -19
- package/dist/experimental/confirm.d.ts.map +0 -1
- package/dist/experimental/confirm.js +0 -28
- package/dist/experimental/intl.d.ts +0 -16
- package/dist/experimental/intl.d.ts.map +0 -1
- package/dist/experimental/intl.js +0 -5
- package/dist/experimental/makeUseCommand.d.ts +0 -8
- package/dist/experimental/makeUseCommand.d.ts.map +0 -1
- package/dist/experimental/makeUseCommand.js +0 -13
- package/dist/experimental/toast.d.ts +0 -47
- package/dist/experimental/toast.d.ts.map +0 -1
- package/dist/experimental/toast.js +0 -41
- package/dist/experimental/withToast.d.ts +0 -25
- package/dist/experimental/withToast.d.ts.map +0 -1
- package/dist/experimental/withToast.js +0 -45
- package/eslint.config.mjs +0 -24
- package/src/experimental/commander.ts +0 -1835
- package/src/experimental/intl.ts +0 -9
- package/src/experimental/toast.ts +0 -66
- package/src/experimental/withToast.ts +0 -99
|
@@ -1,26 +1,29 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as Effect from "effect-app/Effect"
|
|
2
|
+
import type * as Layer from "effect-app/Layer"
|
|
2
3
|
import { Commander, type CommanderImpl, CommanderStatic } from "./commander.js"
|
|
3
4
|
|
|
4
5
|
type X<X> = X
|
|
5
6
|
|
|
6
7
|
// helps retain JSDoc
|
|
7
8
|
export interface CommanderResolved<RT, RTHooks>
|
|
8
|
-
extends
|
|
9
|
+
extends
|
|
10
|
+
X<typeof CommanderStatic>,
|
|
11
|
+
Pick<CommanderImpl<RT, RTHooks>, "fn" | "wrap" | "streamWrap" | "streamFn" | "alt" | "alt2">
|
|
9
12
|
{
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
export const makeUseCommand = Effect.fnUntraced(
|
|
13
16
|
function*<R = never, RTHooks = never>(rtHooks: Layer.Layer<RTHooks, never, R>) {
|
|
14
17
|
const cmndr = yield* Commander
|
|
15
|
-
const runtime = yield* Effect.
|
|
18
|
+
const runtime = yield* Effect.context<R>()
|
|
16
19
|
|
|
17
20
|
const comm = cmndr(runtime, rtHooks)
|
|
18
21
|
|
|
19
|
-
const command = {
|
|
22
|
+
const command: CommanderResolved<R, RTHooks> = {
|
|
20
23
|
...comm,
|
|
21
24
|
...CommanderStatic
|
|
22
25
|
}
|
|
23
26
|
|
|
24
|
-
return command
|
|
27
|
+
return command
|
|
25
28
|
}
|
|
26
29
|
)
|
package/src/mutate.ts
CHANGED
|
@@ -1,23 +1,58 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { matchQuery } from "@tanstack/query-core"
|
|
2
3
|
import { type InvalidateOptions, type InvalidateQueryFilters, type QueryClient, useQueryClient } from "@tanstack/vue-query"
|
|
3
|
-
import { type
|
|
4
|
-
import {
|
|
5
|
-
import type {
|
|
4
|
+
import { type InvalidationKey, InvalidationKeysFromServer, makeInvalidationKeysService, makeQueryKey, type Req } from "effect-app/client"
|
|
5
|
+
import type { ClientForOptions, RequestHandlerWithInput } from "effect-app/client/clientFor"
|
|
6
|
+
import type { InvalidateQueryInstruction } from "effect-app/client/makeClient"
|
|
7
|
+
import * as Effect from "effect-app/Effect"
|
|
6
8
|
import { tuple } from "effect-app/Function"
|
|
9
|
+
import * as Option from "effect-app/Option"
|
|
10
|
+
import type * as Cause from "effect/Cause"
|
|
11
|
+
import * as Exit from "effect/Exit"
|
|
12
|
+
import * as Ref from "effect/Ref"
|
|
13
|
+
import * as Stream from "effect/Stream"
|
|
7
14
|
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
|
|
8
15
|
import { computed, type ComputedRef, shallowRef } from "vue"
|
|
9
|
-
import { makeQueryKey } from "./lib.js"
|
|
10
16
|
|
|
11
|
-
export
|
|
17
|
+
export type GetQueryKey = (h: { id: string; options?: ClientForOptions }) => string[]
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Default heuristic: invalidate the parent namespace of the action.
|
|
21
|
+
* e.g. `$project/$configuration.get` -> `["$project"]`
|
|
22
|
+
* e.g. `$project/$configuration/$something.get` -> `["$project","$configuration"]`
|
|
23
|
+
*/
|
|
24
|
+
export const defaultGetQueryKey: GetQueryKey = (h) => {
|
|
12
25
|
const key = makeQueryKey(h)
|
|
13
26
|
const ns = key.filter((_) => _.startsWith("$"))
|
|
14
|
-
// we invalidate the parent namespace e.g $project/$configuration.get, we invalidate $project
|
|
15
|
-
// for $project/$configuration/$something.get, we invalidate $project/$configuration
|
|
16
27
|
const k = ns.length ? ns.length > 1 ? ns.slice(0, ns.length - 1) : ns : undefined
|
|
17
28
|
if (!k) throw new Error("empty query key for: " + h.id)
|
|
18
29
|
return k
|
|
19
30
|
}
|
|
20
31
|
|
|
32
|
+
let activeGetQueryKey: GetQueryKey = defaultGetQueryKey
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Override the default query-key heuristic used by mutations for cache
|
|
36
|
+
* invalidation. Call once at app bootstrap. Pass `undefined` to restore the
|
|
37
|
+
* built-in default.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* // invalidate the full namespace of the action (no parent collapse)
|
|
42
|
+
* setDefaultGetQueryKey((h) => {
|
|
43
|
+
* const key = makeQueryKey(h)
|
|
44
|
+
* const ns = key.filter((_) => _.startsWith("$"))
|
|
45
|
+
* if (!ns.length) throw new Error("empty query key for: " + h.id)
|
|
46
|
+
* return ns
|
|
47
|
+
* })
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export const setDefaultGetQueryKey = (fn: GetQueryKey | undefined) => {
|
|
51
|
+
activeGetQueryKey = fn ?? defaultGetQueryKey
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const getQueryKey: GetQueryKey = (h) => activeGetQueryKey(h)
|
|
55
|
+
|
|
21
56
|
export function mutationResultToVue<A, E>(
|
|
22
57
|
mutationResult: AsyncResult.AsyncResult<A, E>
|
|
23
58
|
): Res<A, E> {
|
|
@@ -67,87 +102,135 @@ export function make<A, E, R>(self: Effect.Effect<A, E, R>) {
|
|
|
67
102
|
return tuple(result, latestSuccess, execute)
|
|
68
103
|
}
|
|
69
104
|
|
|
70
|
-
|
|
105
|
+
/**
|
|
106
|
+
* An entry for `queryInvalidation`. Narrowed alias of effect-app's
|
|
107
|
+
* `InvalidateQueryInstruction` with tanstack-query's `InvalidateQueryFilters`
|
|
108
|
+
* and `InvalidateOptions` substituted for the structural defaults.
|
|
109
|
+
*/
|
|
110
|
+
export type InvalidationEntry = InvalidateQueryInstruction<InvalidateQueryFilters, InvalidateOptions>
|
|
111
|
+
|
|
112
|
+
export interface MutationOptionsBase<A = unknown, B = A, E2 = never, R2 = never> {
|
|
71
113
|
/**
|
|
72
114
|
* By default we invalidate one level of the query key, e.g $project/$configuration.get, we invalidate $project.
|
|
73
|
-
* This can be overridden by providing a function that returns an array of filters and options
|
|
115
|
+
* This can be overridden by providing a function that returns an array of filters and options,
|
|
116
|
+
* or RPC handlers directly (their query keys are derived automatically).
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```ts
|
|
120
|
+
* queryInvalidation: (queryKey) => [
|
|
121
|
+
* { filters: { queryKey } },
|
|
122
|
+
* GetMe,
|
|
123
|
+
* PackListIndex
|
|
124
|
+
* ]
|
|
125
|
+
* ```
|
|
74
126
|
*/
|
|
75
|
-
queryInvalidation?: (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
/** @deprecated prefer more basic @see MutationOptionsBase and separate useMutation from Command.fn */
|
|
82
|
-
export interface MutationOptions<A, E, R, A2 = A, E2 = E, R2 = R, I = void> extends MutationOptionsBase {
|
|
127
|
+
queryInvalidation?: (
|
|
128
|
+
defaultKey: string[],
|
|
129
|
+
name: string,
|
|
130
|
+
input?: unknown,
|
|
131
|
+
output?: Exit.Exit<unknown, unknown>
|
|
132
|
+
) => InvalidationEntry[]
|
|
83
133
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
134
|
+
* Run an additional Effect after the mutation succeeds. Its output becomes the
|
|
135
|
+
* final result returned to the caller. Query cache is invalidated once on
|
|
136
|
+
* mutation exit and again after this Effect completes. Useful for long-running
|
|
137
|
+
* operations (e.g. polling a background job) where you want the caller to
|
|
138
|
+
* receive the downstream result and the cache to refresh once it is ready.
|
|
86
139
|
*
|
|
87
|
-
* @
|
|
140
|
+
* @example
|
|
141
|
+
* ```ts
|
|
142
|
+
* useMutation(startExportCommand, {
|
|
143
|
+
* select: (result) => pollUntilDone(result.jobId)
|
|
144
|
+
* // caller receives the pollUntilDone output, not the original result
|
|
145
|
+
* })
|
|
146
|
+
* ```
|
|
88
147
|
*/
|
|
89
|
-
|
|
148
|
+
select?: (result: A) => Effect.Effect<B, E2, R2>
|
|
90
149
|
}
|
|
91
150
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
// for (let i = 0; i < ns.length; i++) {
|
|
99
|
-
// nses.push(ns.slice(0, i + 1).join("/"))
|
|
100
|
-
// }
|
|
101
|
-
*/
|
|
102
|
-
|
|
103
|
-
export const asResult: {
|
|
104
|
-
<A, E, R>(
|
|
105
|
-
handler: Effect.Effect<A, E, R>
|
|
106
|
-
): readonly [ComputedRef<AsyncResult.AsyncResult<A, E>>, Effect.Effect<Exit.Exit<A, E>, never, R>]
|
|
107
|
-
<Args extends readonly any[], A, E, R>(
|
|
108
|
-
handler: (...args: Args) => Effect.Effect<A, E, R>
|
|
109
|
-
): readonly [ComputedRef<AsyncResult.AsyncResult<A, E>>, (...args: Args) => Effect.Effect<Exit.Exit<A, E>, never, R>]
|
|
110
|
-
} = <Args extends readonly any[], A, E, R>(
|
|
111
|
-
handler: Effect.Effect<A, E, R> | ((...args: Args) => Effect.Effect<A, E, R>)
|
|
112
|
-
) => {
|
|
151
|
+
export const asResult = <Args extends readonly any[], A, E, R>(
|
|
152
|
+
handler: (...args: Args) => Effect.Effect<A, E, R>
|
|
153
|
+
): readonly [
|
|
154
|
+
ComputedRef<AsyncResult.AsyncResult<A, E>>,
|
|
155
|
+
(...args: Args) => Effect.Effect<Exit.Exit<A, E>, never, R>
|
|
156
|
+
] => {
|
|
113
157
|
const state = shallowRef<AsyncResult.AsyncResult<A, E>>(AsyncResult.initial())
|
|
114
158
|
|
|
115
|
-
const act =
|
|
116
|
-
|
|
159
|
+
const act = (...args: Args) =>
|
|
160
|
+
Effect
|
|
117
161
|
.sync(() => {
|
|
118
162
|
state.value = AsyncResult.initial(true)
|
|
119
163
|
})
|
|
120
164
|
.pipe(
|
|
121
165
|
Effect.andThen(Effect.suspend(() =>
|
|
122
|
-
handler.pipe(
|
|
166
|
+
handler(...args).pipe(
|
|
123
167
|
Effect.exit,
|
|
124
168
|
Effect.tap((exit) => Effect.sync(() => (state.value = AsyncResult.fromExit(exit))))
|
|
125
169
|
)
|
|
126
170
|
))
|
|
127
171
|
)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
172
|
+
|
|
173
|
+
return tuple(computed(() => state.value), act) as any
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Like `asResult`, but for streams. The ref is updated with each emitted value
|
|
178
|
+
* (keeping `waiting: true`) and is finalised (with `waiting: false`) once the
|
|
179
|
+
* stream terminates successfully. Errors are surfaced as `AsyncResult.failure`.
|
|
180
|
+
*/
|
|
181
|
+
export const asStreamResult = <Args extends readonly any[], A, E, R>(
|
|
182
|
+
handler: (...args: Args) => Stream.Stream<A, E, R>
|
|
183
|
+
): readonly [ComputedRef<AsyncResult.AsyncResult<A, E>>, (...args: Args) => Effect.Effect<void, never, R>] => {
|
|
184
|
+
const state = shallowRef<AsyncResult.AsyncResult<A, E>>(AsyncResult.initial())
|
|
185
|
+
|
|
186
|
+
const runStream = (stream: Stream.Stream<A, E, R>): Effect.Effect<void, never, R> =>
|
|
187
|
+
Effect
|
|
188
|
+
.sync(() => {
|
|
189
|
+
state.value = AsyncResult.initial(true)
|
|
190
|
+
})
|
|
191
|
+
.pipe(
|
|
192
|
+
Effect.andThen(
|
|
193
|
+
stream.pipe(
|
|
194
|
+
Stream.runForEach((value) =>
|
|
195
|
+
Effect.sync(() => {
|
|
196
|
+
state.value = AsyncResult.success(value, { waiting: true })
|
|
197
|
+
})
|
|
198
|
+
),
|
|
199
|
+
Effect.exit,
|
|
200
|
+
Effect.flatMap((exit) =>
|
|
201
|
+
Effect.sync(() => {
|
|
202
|
+
if (exit._tag === "Success") {
|
|
203
|
+
const current = state.value
|
|
204
|
+
if (AsyncResult.isSuccess(current)) {
|
|
205
|
+
state.value = AsyncResult.success(current.value, { waiting: false })
|
|
206
|
+
} else {
|
|
207
|
+
state.value = AsyncResult.initial(false)
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
state.value = AsyncResult.failure(exit.cause)
|
|
211
|
+
}
|
|
212
|
+
})
|
|
138
213
|
)
|
|
139
|
-
)
|
|
214
|
+
)
|
|
140
215
|
)
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
const act = (...args: Args) => runStream(handler(...args))
|
|
141
219
|
|
|
142
220
|
return tuple(computed(() => state.value), act) as any
|
|
143
221
|
}
|
|
144
222
|
|
|
145
|
-
|
|
223
|
+
const buildInvalidateCache = (
|
|
146
224
|
queryClient: QueryClient,
|
|
147
225
|
self: { id: string; options?: ClientForOptions },
|
|
148
|
-
|
|
226
|
+
queryInvalidation?: MutationOptionsBase["queryInvalidation"]
|
|
149
227
|
) => {
|
|
150
|
-
|
|
228
|
+
type InvalidationTarget = {
|
|
229
|
+
readonly filters: InvalidateQueryFilters | undefined
|
|
230
|
+
readonly options: InvalidateOptions | undefined
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const invalidateQueriesFn = (
|
|
151
234
|
filters?: InvalidateQueryFilters,
|
|
152
235
|
options?: InvalidateOptions
|
|
153
236
|
) =>
|
|
@@ -158,71 +241,160 @@ export const invalidateQueries = (
|
|
|
158
241
|
)
|
|
159
242
|
)
|
|
160
243
|
|
|
161
|
-
const
|
|
244
|
+
const getClientInvalidationTargets = (
|
|
245
|
+
input: unknown,
|
|
246
|
+
output: Exit.Exit<unknown, unknown>
|
|
247
|
+
): ReadonlyArray<InvalidationTarget> => {
|
|
162
248
|
const queryKey = getQueryKey(self)
|
|
163
249
|
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
250
|
+
if (queryInvalidation) {
|
|
251
|
+
return queryInvalidation(queryKey, self.id, input, output).map((entry): InvalidationTarget => {
|
|
252
|
+
if (Array.isArray(entry)) {
|
|
253
|
+
return { filters: { queryKey: entry }, options: undefined }
|
|
254
|
+
}
|
|
255
|
+
const obj = entry as Exclude<InvalidationEntry, ReadonlyArray<string>>
|
|
256
|
+
if ("id" in obj) {
|
|
257
|
+
return {
|
|
258
|
+
filters: {
|
|
259
|
+
queryKey: makeQueryKey(obj.options ? { id: obj.id, options: obj.options } : { id: obj.id })
|
|
260
|
+
},
|
|
261
|
+
options: undefined
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return { filters: obj.filters, options: obj.options }
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (!queryKey) {
|
|
269
|
+
return []
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return [{ filters: { queryKey }, options: undefined }]
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const invalidateCache = (
|
|
276
|
+
input: unknown,
|
|
277
|
+
output: Exit.Exit<unknown, unknown>,
|
|
278
|
+
serverKeys: ReadonlyArray<InvalidationKey>
|
|
279
|
+
) =>
|
|
280
|
+
Effect.suspend(() => {
|
|
281
|
+
const clientTargets = getClientInvalidationTargets(input, output)
|
|
282
|
+
const serverTargets: ReadonlyArray<InvalidationTarget> = serverKeys.map((queryKey) => ({
|
|
283
|
+
filters: { queryKey },
|
|
284
|
+
options: undefined
|
|
285
|
+
}))
|
|
286
|
+
const allTargets: ReadonlyArray<InvalidationTarget> = [...clientTargets, ...serverTargets]
|
|
287
|
+
|
|
288
|
+
if (!allTargets.length) return Effect.void
|
|
289
|
+
|
|
290
|
+
// Group targets by refetchType + options so each group can be merged into a single
|
|
291
|
+
// invalidateQueries call using a predicate, reducing N calls to 1 in the common case.
|
|
292
|
+
type Group = {
|
|
293
|
+
targets: Array<InvalidationTarget>
|
|
294
|
+
refetchType: InvalidateQueryFilters["refetchType"]
|
|
295
|
+
options: InvalidateOptions | undefined
|
|
296
|
+
}
|
|
297
|
+
const groups = new Map<string, Group>()
|
|
298
|
+
for (const target of allTargets) {
|
|
299
|
+
const key = `${target.filters?.refetchType ?? ""}|${target.options?.cancelRefetch ?? ""}|${
|
|
300
|
+
target.options?.throwOnError?.toString() ?? ""
|
|
301
|
+
}`
|
|
302
|
+
const existing = groups.get(key)
|
|
303
|
+
if (existing) {
|
|
304
|
+
existing.targets.push(target)
|
|
305
|
+
} else {
|
|
306
|
+
groups.set(key, { targets: [target], refetchType: target.filters?.refetchType, options: target.options })
|
|
307
|
+
}
|
|
168
308
|
}
|
|
309
|
+
|
|
169
310
|
return Effect
|
|
170
311
|
.andThen(
|
|
171
|
-
Effect.annotateCurrentSpan({
|
|
172
|
-
Effect.forEach(
|
|
312
|
+
Effect.annotateCurrentSpan({ clientTargets, serverKeys }),
|
|
313
|
+
Effect.forEach(
|
|
314
|
+
groups.values(),
|
|
315
|
+
({ options, refetchType, targets }) =>
|
|
316
|
+
invalidateQueriesFn(
|
|
317
|
+
{
|
|
318
|
+
...(refetchType !== undefined ? { refetchType } : {}),
|
|
319
|
+
predicate: (query) => targets.some((t) => t.filters ? matchQuery(t.filters, query) : true)
|
|
320
|
+
},
|
|
321
|
+
options
|
|
322
|
+
),
|
|
323
|
+
{ discard: true, concurrency: "inherit" }
|
|
324
|
+
)
|
|
173
325
|
)
|
|
174
|
-
.pipe(
|
|
175
|
-
|
|
326
|
+
.pipe(
|
|
327
|
+
Effect.tap(
|
|
328
|
+
// hand over control back to the event loop so that state can be updated..
|
|
329
|
+
// TODO: should we do this in general on any mutation, regardless of invalidation?
|
|
330
|
+
Effect.sleep(0)
|
|
331
|
+
),
|
|
332
|
+
Effect.withSpan("client.query.invalidation", {}, { captureStackTrace: false })
|
|
333
|
+
)
|
|
334
|
+
})
|
|
176
335
|
|
|
177
|
-
|
|
336
|
+
return invalidateCache
|
|
337
|
+
}
|
|
178
338
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
// TODO: should we do this in general on any mutation, regardless of invalidation?
|
|
188
|
-
Effect.sleep(0)
|
|
189
|
-
),
|
|
190
|
-
Effect.withSpan("client.query.invalidation", {}, { captureStackTrace: false })
|
|
191
|
-
)
|
|
192
|
-
})
|
|
339
|
+
export const invalidateQueries = (
|
|
340
|
+
queryClient: QueryClient,
|
|
341
|
+
self: { id: string; options?: ClientForOptions },
|
|
342
|
+
options?: MutationOptionsBase
|
|
343
|
+
) => {
|
|
344
|
+
const invalidateCache = buildInvalidateCache(queryClient, self, options?.queryInvalidation)
|
|
345
|
+
|
|
346
|
+
const select = options?.select
|
|
193
347
|
|
|
194
|
-
const handle = <A, E, R>(
|
|
348
|
+
const handle = <A, E, R>(eff: Effect.Effect<A, E, R>, input?: unknown) =>
|
|
349
|
+
Effect.gen(function*() {
|
|
350
|
+
const keysRef = yield* Ref.make<ReadonlyArray<InvalidationKey>>([])
|
|
351
|
+
const result = yield* eff.pipe(
|
|
352
|
+
Effect.provideService(InvalidationKeysFromServer, makeInvalidationKeysService(keysRef)),
|
|
353
|
+
Effect.onExit((exit) =>
|
|
354
|
+
Effect.gen(function*() {
|
|
355
|
+
const serverKeys = yield* Ref.get(keysRef)
|
|
356
|
+
yield* invalidateCache(input, exit, serverKeys)
|
|
357
|
+
})
|
|
358
|
+
)
|
|
359
|
+
)
|
|
360
|
+
if (select) {
|
|
361
|
+
return yield* select(result).pipe(
|
|
362
|
+
Effect.onExit((exit) =>
|
|
363
|
+
Effect.gen(function*() {
|
|
364
|
+
const serverKeys = yield* Ref.get(keysRef)
|
|
365
|
+
yield* invalidateCache(input, exit, serverKeys)
|
|
366
|
+
})
|
|
367
|
+
)
|
|
368
|
+
)
|
|
369
|
+
}
|
|
370
|
+
return result
|
|
371
|
+
})
|
|
195
372
|
|
|
196
373
|
return handle
|
|
197
374
|
}
|
|
198
375
|
|
|
376
|
+
/**
|
|
377
|
+
* A callable mutation result. When `I = void` the input argument may be omitted.
|
|
378
|
+
*/
|
|
379
|
+
export interface MutationFn<I, A, E, R, Id extends string> {
|
|
380
|
+
<B = A, E2 = never, R2 = never>(
|
|
381
|
+
input: I,
|
|
382
|
+
options?: MutationOptionsBase<A, B, E2, R2>
|
|
383
|
+
): Effect.Effect<B, E | E2, R | R2>
|
|
384
|
+
readonly id: Id
|
|
385
|
+
}
|
|
386
|
+
|
|
199
387
|
export const makeMutation = () => {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
): ((i: I) => Effect.Effect<A, E, R>) & { readonly id: Id }
|
|
209
|
-
/**
|
|
210
|
-
* Pass an Effect, e.g from a client action
|
|
211
|
-
* Executes query cache invalidation based on default rules or provided option.
|
|
212
|
-
*/
|
|
213
|
-
<E, A, R, Request extends Req, Id extends string>(
|
|
214
|
-
self: RequestHandler<A, E, R, Request, Id>,
|
|
215
|
-
options?: MutationOptionsBase
|
|
216
|
-
): Effect.Effect<A, E, R> & { readonly id: Id }
|
|
217
|
-
} = <I, E, A, R, Request extends Req, Id extends string>(
|
|
218
|
-
self: RequestHandlerWithInput<I, A, E, R, Request, Id> | RequestHandler<A, E, R, Request, Id>,
|
|
219
|
-
options?: MutationOptionsBase
|
|
220
|
-
) => {
|
|
388
|
+
/**
|
|
389
|
+
* Pass a function that returns an Effect, e.g from a client action.
|
|
390
|
+
* Executes query cache invalidation based on default rules or provided option.
|
|
391
|
+
* When `I = void` the input argument may be omitted.
|
|
392
|
+
*/
|
|
393
|
+
const useMutation = <I, E, A, R, Request extends Req, Id extends string>(
|
|
394
|
+
self: RequestHandlerWithInput<I, A, E, R, Request, Id>
|
|
395
|
+
): MutationFn<I, A, E, R, Id> => {
|
|
221
396
|
const queryClient = useQueryClient()
|
|
222
|
-
const
|
|
223
|
-
const handler = self.handler
|
|
224
|
-
const r = Effect.isEffect(handler) ? handle(handler) : (i: I) => handle(handler(i))
|
|
225
|
-
|
|
397
|
+
const r = (i: I, options?: MutationOptionsBase) => invalidateQueries(queryClient, self, options)(self.handler(i), i)
|
|
226
398
|
return Object.assign(r, { id: self.id }) as any
|
|
227
399
|
}
|
|
228
400
|
return useMutation
|
|
@@ -232,32 +404,61 @@ export const makeMutation = () => {
|
|
|
232
404
|
export const useMakeMutation = () => {
|
|
233
405
|
const queryClient = useQueryClient()
|
|
234
406
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Pass an Effect, e.g from a client action
|
|
246
|
-
* Executes query cache invalidation based on default rules or provided option.
|
|
247
|
-
*/
|
|
248
|
-
<E, A, R, Request extends Req, Id extends string>(
|
|
249
|
-
self: RequestHandler<A, E, R, Request, Id>,
|
|
250
|
-
options?: MutationOptionsBase
|
|
251
|
-
): Effect.Effect<A, E, R> & { readonly id: Id }
|
|
252
|
-
} = <I, E, A, R, Request extends Req, Id extends string>(
|
|
253
|
-
self: RequestHandlerWithInput<I, A, E, R, Request, Id> | RequestHandler<A, E, R, Request, Id>,
|
|
254
|
-
options?: MutationOptionsBase
|
|
255
|
-
) => {
|
|
256
|
-
const handle = invalidateQueries(queryClient, self, options?.queryInvalidation)
|
|
257
|
-
const handler = self.handler
|
|
258
|
-
const r = Effect.isEffect(handler) ? handle(handler) : (i: I) => handle(handler(i))
|
|
259
|
-
|
|
407
|
+
/**
|
|
408
|
+
* Pass a function that returns an Effect, e.g from a client action.
|
|
409
|
+
* Executes query cache invalidation based on default rules or provided option.
|
|
410
|
+
* When `I = void` the input argument may be omitted.
|
|
411
|
+
*/
|
|
412
|
+
const useMutation = <I, E, A, R, Request extends Req, Id extends string>(
|
|
413
|
+
self: RequestHandlerWithInput<I, A, E, R, Request, Id>
|
|
414
|
+
): MutationFn<I, A, E, R, Id> => {
|
|
415
|
+
const r = (i: I, options?: MutationOptionsBase) => invalidateQueries(queryClient, self, options)(self.handler(i), i)
|
|
260
416
|
return Object.assign(r, { id: self.id }) as any
|
|
261
417
|
}
|
|
262
418
|
return useMutation
|
|
263
419
|
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Returns a stream-based mutation factory for use with `streamFn`.
|
|
423
|
+
* The outer Effect sets up per-invocation invalidation scaffolding
|
|
424
|
+
* and returns a stream that triggers query invalidation via `Stream.ensuring` when it completes.
|
|
425
|
+
*
|
|
426
|
+
* Use with `streamFn` / `Command.streamFn(id)(mutateHandler, ...combinators)` so that
|
|
427
|
+
* the command manages its own reactive state internally.
|
|
428
|
+
*
|
|
429
|
+
* Must be called inside a Vue setup context (uses `useQueryClient` internally).
|
|
430
|
+
*/
|
|
431
|
+
export const makeStreamMutation2 = () => {
|
|
432
|
+
const queryClient = useQueryClient()
|
|
433
|
+
|
|
434
|
+
return (
|
|
435
|
+
self: {
|
|
436
|
+
id: string
|
|
437
|
+
options?: ClientForOptions
|
|
438
|
+
handler: (i: any) => Stream.Stream<any, any, any>
|
|
439
|
+
},
|
|
440
|
+
mergedInvalidation?: MutationOptionsBase["queryInvalidation"]
|
|
441
|
+
) => {
|
|
442
|
+
const invCache = buildInvalidateCache(queryClient, self, mergedInvalidation)
|
|
443
|
+
|
|
444
|
+
const makeInvocationEffect = (input: unknown, source: Stream.Stream<any, any, any>) =>
|
|
445
|
+
Effect.gen(function*() {
|
|
446
|
+
const keysRef = yield* Ref.make<ReadonlyArray<InvalidationKey>>([])
|
|
447
|
+
const invKeys = makeInvalidationKeysService(keysRef, (key) => invCache(input, Exit.succeed(undefined), [key]))
|
|
448
|
+
const lastRef = yield* Ref.make<any>(undefined)
|
|
449
|
+
return source.pipe(
|
|
450
|
+
Stream.provideService(InvalidationKeysFromServer, invKeys),
|
|
451
|
+
Stream.tap((v) => Ref.set(lastRef, v)),
|
|
452
|
+
Stream.ensuring(
|
|
453
|
+
Effect.gen(function*() {
|
|
454
|
+
const lastValue = yield* Ref.get(lastRef)
|
|
455
|
+
const serverKeys = yield* Ref.get(keysRef)
|
|
456
|
+
yield* invCache(input, Exit.succeed(lastValue), serverKeys)
|
|
457
|
+
})
|
|
458
|
+
)
|
|
459
|
+
)
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
return (i: any) => Stream.unwrap(makeInvocationEffect(i, self.handler(i)))
|
|
463
|
+
}
|
|
464
|
+
}
|