@effect-app/vue 4.0.0-beta.18 → 4.0.0-beta.180
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 +1224 -0
- package/dist/commander.d.ts +370 -0
- package/dist/commander.d.ts.map +1 -0
- package/dist/commander.js +591 -0
- package/dist/confirm.d.ts +19 -0
- package/dist/confirm.d.ts.map +1 -0
- package/dist/confirm.js +24 -0
- package/dist/errorReporter.d.ts +4 -4
- package/dist/errorReporter.d.ts.map +1 -1
- package/dist/errorReporter.js +12 -18
- package/dist/form.d.ts +13 -4
- package/dist/form.d.ts.map +1 -1
- package/dist/form.js +41 -12
- 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 +6 -8
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +34 -7
- package/dist/makeClient.d.ts +148 -290
- package/dist/makeClient.d.ts.map +1 -1
- package/dist/makeClient.js +205 -361
- 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 +8 -0
- package/dist/makeUseCommand.d.ts.map +1 -0
- package/dist/makeUseCommand.js +13 -0
- package/dist/mutate.d.ts +57 -25
- package/dist/mutate.d.ts.map +1 -1
- package/dist/mutate.js +160 -33
- package/dist/query.d.ts +11 -15
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +19 -27
- package/dist/routeParams.d.ts +1 -1
- package/dist/runtime.d.ts +5 -2
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +27 -17
- package/dist/toast.d.ts +46 -0
- package/dist/toast.d.ts.map +1 -0
- package/dist/toast.js +32 -0
- package/dist/withToast.d.ts +26 -0
- package/dist/withToast.d.ts.map +1 -0
- package/dist/withToast.js +49 -0
- package/eslint.config.mjs +2 -2
- package/examples/streamMutation.ts +83 -0
- package/package.json +48 -48
- package/src/{experimental/commander.ts → commander.ts} +930 -255
- package/src/{experimental/confirm.ts → confirm.ts} +10 -14
- package/src/errorReporter.ts +62 -74
- package/src/form.ts +55 -16
- package/src/intl.ts +12 -0
- package/src/lib.ts +46 -13
- package/src/makeClient.ts +570 -1038
- package/src/{experimental/makeUseCommand.ts → makeUseCommand.ts} +3 -3
- package/src/mutate.ts +306 -72
- package/src/query.ts +39 -50
- package/src/runtime.ts +39 -18
- package/src/{experimental/toast.ts → toast.ts} +11 -25
- package/src/{experimental/withToast.ts → withToast.ts} +15 -6
- package/test/Mutation.test.ts +130 -10
- 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/stubs.d.ts +3144 -117
- package/test/dist/stubs.d.ts.map +1 -1
- package/test/dist/stubs.js +132 -25
- package/test/form-validation-errors.test.ts +23 -19
- package/test/form.test.ts +20 -2
- package/test/lib.test.ts +240 -0
- package/test/makeClient.test.ts +241 -38
- package/test/streamFinal.test.ts +110 -0
- package/test/stubs.ts +172 -42
- package/tsconfig.examples.json +20 -0
- package/tsconfig.json +0 -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/src/experimental/intl.ts +0 -9
|
@@ -12,15 +12,15 @@ export interface CommanderResolved<RT, RTHooks>
|
|
|
12
12
|
export const makeUseCommand = Effect.fnUntraced(
|
|
13
13
|
function*<R = never, RTHooks = never>(rtHooks: Layer.Layer<RTHooks, never, R>) {
|
|
14
14
|
const cmndr = yield* Commander
|
|
15
|
-
const runtime = yield* Effect.
|
|
15
|
+
const runtime = yield* Effect.context<R>()
|
|
16
16
|
|
|
17
17
|
const comm = cmndr(runtime, rtHooks)
|
|
18
18
|
|
|
19
|
-
const command = {
|
|
19
|
+
const command: CommanderResolved<R, RTHooks> = {
|
|
20
20
|
...comm,
|
|
21
21
|
...CommanderStatic
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
return command
|
|
24
|
+
return command
|
|
25
25
|
}
|
|
26
26
|
)
|
package/src/mutate.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
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 Cause, Effect,
|
|
4
|
-
import { type Req } from "effect-app/client"
|
|
4
|
+
import { type Cause, Effect, Exit, Option } from "effect-app"
|
|
5
|
+
import { type InvalidationKey, InvalidationKeysFromServer, makeInvalidationKeysService, makeQueryKey, type Req } from "effect-app/client"
|
|
5
6
|
import type { ClientForOptions, RequestHandler, RequestHandlerWithInput } from "effect-app/client/clientFor"
|
|
6
7
|
import { tuple } from "effect-app/Function"
|
|
8
|
+
import * as Ref from "effect/Ref"
|
|
9
|
+
import * as Stream from "effect/Stream"
|
|
7
10
|
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
|
|
8
11
|
import { computed, type ComputedRef, shallowRef } from "vue"
|
|
9
|
-
import { makeQueryKey } from "./lib.js"
|
|
10
12
|
|
|
11
13
|
export const getQueryKey = (h: { id: string; options?: ClientForOptions }) => {
|
|
12
14
|
const key = makeQueryKey(h)
|
|
@@ -67,39 +69,33 @@ export function make<A, E, R>(self: Effect.Effect<A, E, R>) {
|
|
|
67
69
|
return tuple(result, latestSuccess, execute)
|
|
68
70
|
}
|
|
69
71
|
|
|
70
|
-
export interface MutationOptionsBase {
|
|
72
|
+
export interface MutationOptionsBase<A = unknown, B = A, E2 = never, R2 = never> {
|
|
71
73
|
/**
|
|
72
74
|
* By default we invalidate one level of the query key, e.g $project/$configuration.get, we invalidate $project.
|
|
73
75
|
* This can be overridden by providing a function that returns an array of filters and options.
|
|
74
76
|
*/
|
|
75
|
-
queryInvalidation?: (defaultKey: string[], name: string) => {
|
|
77
|
+
queryInvalidation?: (defaultKey: string[], name: string, input?: unknown, output?: Exit.Exit<unknown, unknown>) => {
|
|
76
78
|
filters?: InvalidateQueryFilters | undefined
|
|
77
79
|
options?: InvalidateOptions | undefined
|
|
78
80
|
}[]
|
|
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 {
|
|
83
81
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
82
|
+
* Run an additional Effect after the mutation succeeds. Its output becomes the
|
|
83
|
+
* final result returned to the caller. Query cache is invalidated once on
|
|
84
|
+
* mutation exit and again after this Effect completes. Useful for long-running
|
|
85
|
+
* operations (e.g. polling a background job) where you want the caller to
|
|
86
|
+
* receive the downstream result and the cache to refresh once it is ready.
|
|
86
87
|
*
|
|
87
|
-
* @
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* useMutation(startExportCommand, {
|
|
91
|
+
* select: (result) => pollUntilDone(result.jobId)
|
|
92
|
+
* // caller receives the pollUntilDone output, not the original result
|
|
93
|
+
* })
|
|
94
|
+
* ```
|
|
88
95
|
*/
|
|
89
|
-
|
|
96
|
+
select?: (result: A) => Effect.Effect<B, E2, R2>
|
|
90
97
|
}
|
|
91
98
|
|
|
92
|
-
// TODO: more efficient invalidation, including args etc
|
|
93
|
-
// return Effect.promise(() => queryClient.invalidateQueries({
|
|
94
|
-
// predicate: (_) => nses.includes(_.queryKey.filter((_) => _.startsWith("$")).join("/"))
|
|
95
|
-
// }))
|
|
96
|
-
/*
|
|
97
|
-
// const nses: string[] = []`
|
|
98
|
-
// for (let i = 0; i < ns.length; i++) {
|
|
99
|
-
// nses.push(ns.slice(0, i + 1).join("/"))
|
|
100
|
-
// }
|
|
101
|
-
*/
|
|
102
|
-
|
|
103
99
|
export const asResult: {
|
|
104
100
|
<A, E, R>(
|
|
105
101
|
handler: Effect.Effect<A, E, R>
|
|
@@ -142,12 +138,73 @@ export const asResult: {
|
|
|
142
138
|
return tuple(computed(() => state.value), act) as any
|
|
143
139
|
}
|
|
144
140
|
|
|
145
|
-
|
|
141
|
+
/**
|
|
142
|
+
* Like `asResult`, but for streams. The ref is updated with each emitted value
|
|
143
|
+
* (keeping `waiting: true`) and is finalised (with `waiting: false`) once the
|
|
144
|
+
* stream terminates successfully. Errors are surfaced as `AsyncResult.failure`.
|
|
145
|
+
*/
|
|
146
|
+
export const asStreamResult: {
|
|
147
|
+
<A, E, R>(
|
|
148
|
+
handler: Stream.Stream<A, E, R>
|
|
149
|
+
): readonly [ComputedRef<AsyncResult.AsyncResult<A, E>>, Effect.Effect<void, never, R>]
|
|
150
|
+
<Args extends readonly any[], A, E, R>(
|
|
151
|
+
handler: (...args: Args) => Stream.Stream<A, E, R>
|
|
152
|
+
): readonly [ComputedRef<AsyncResult.AsyncResult<A, E>>, (...args: Args) => Effect.Effect<void, never, R>]
|
|
153
|
+
} = <Args extends readonly any[], A, E, R>(
|
|
154
|
+
handler: Stream.Stream<A, E, R> | ((...args: Args) => Stream.Stream<A, E, R>)
|
|
155
|
+
) => {
|
|
156
|
+
const state = shallowRef<AsyncResult.AsyncResult<A, E>>(AsyncResult.initial())
|
|
157
|
+
|
|
158
|
+
const runStream = (stream: Stream.Stream<A, E, R>): Effect.Effect<void, never, R> =>
|
|
159
|
+
Effect
|
|
160
|
+
.sync(() => {
|
|
161
|
+
state.value = AsyncResult.initial(true)
|
|
162
|
+
})
|
|
163
|
+
.pipe(
|
|
164
|
+
Effect.andThen(
|
|
165
|
+
stream.pipe(
|
|
166
|
+
Stream.runForEach((value) =>
|
|
167
|
+
Effect.sync(() => {
|
|
168
|
+
state.value = AsyncResult.success(value, { waiting: true })
|
|
169
|
+
})
|
|
170
|
+
),
|
|
171
|
+
Effect.exit,
|
|
172
|
+
Effect.flatMap((exit) =>
|
|
173
|
+
Effect.sync(() => {
|
|
174
|
+
if (exit._tag === "Success") {
|
|
175
|
+
const current = state.value
|
|
176
|
+
if (AsyncResult.isSuccess(current)) {
|
|
177
|
+
state.value = AsyncResult.success(current.value, { waiting: false })
|
|
178
|
+
} else {
|
|
179
|
+
state.value = AsyncResult.initial(false)
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
state.value = AsyncResult.failure(exit.cause)
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
const act = Stream.isStream(handler)
|
|
191
|
+
? runStream(handler)
|
|
192
|
+
: (...args: Args) => runStream(handler(...args))
|
|
193
|
+
|
|
194
|
+
return tuple(computed(() => state.value), act) as any
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const buildInvalidateCache = (
|
|
146
198
|
queryClient: QueryClient,
|
|
147
199
|
self: { id: string; options?: ClientForOptions },
|
|
148
|
-
|
|
200
|
+
queryInvalidation?: MutationOptionsBase["queryInvalidation"]
|
|
149
201
|
) => {
|
|
150
|
-
|
|
202
|
+
type InvalidationTarget = {
|
|
203
|
+
readonly filters: InvalidateQueryFilters | undefined
|
|
204
|
+
readonly options: InvalidateOptions | undefined
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const invalidateQueriesFn = (
|
|
151
208
|
filters?: InvalidateQueryFilters,
|
|
152
209
|
options?: InvalidateOptions
|
|
153
210
|
) =>
|
|
@@ -158,44 +215,142 @@ export const invalidateQueries = (
|
|
|
158
215
|
)
|
|
159
216
|
)
|
|
160
217
|
|
|
161
|
-
const
|
|
218
|
+
const getClientInvalidationTargets = (
|
|
219
|
+
input: unknown,
|
|
220
|
+
output: Exit.Exit<unknown, unknown>
|
|
221
|
+
): ReadonlyArray<InvalidationTarget> => {
|
|
162
222
|
const queryKey = getQueryKey(self)
|
|
163
223
|
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
224
|
+
if (queryInvalidation) {
|
|
225
|
+
return queryInvalidation(queryKey, self.id, input, output).map((_) => ({
|
|
226
|
+
filters: _.filters,
|
|
227
|
+
options: _.options
|
|
228
|
+
}))
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!queryKey) {
|
|
232
|
+
return []
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return [{ filters: { queryKey }, options: undefined }]
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const invalidateCache = (
|
|
239
|
+
input: unknown,
|
|
240
|
+
output: Exit.Exit<unknown, unknown>,
|
|
241
|
+
serverKeys: ReadonlyArray<InvalidationKey>
|
|
242
|
+
) =>
|
|
243
|
+
Effect.suspend(() => {
|
|
244
|
+
const clientTargets = getClientInvalidationTargets(input, output)
|
|
245
|
+
const serverTargets: ReadonlyArray<InvalidationTarget> = serverKeys.map((queryKey) => ({
|
|
246
|
+
filters: { queryKey },
|
|
247
|
+
options: undefined
|
|
248
|
+
}))
|
|
249
|
+
const allTargets: ReadonlyArray<InvalidationTarget> = [...clientTargets, ...serverTargets]
|
|
250
|
+
|
|
251
|
+
if (!allTargets.length) return Effect.void
|
|
252
|
+
|
|
253
|
+
// Group targets by refetchType + options so each group can be merged into a single
|
|
254
|
+
// invalidateQueries call using a predicate, reducing N calls to 1 in the common case.
|
|
255
|
+
type Group = {
|
|
256
|
+
targets: Array<InvalidationTarget>
|
|
257
|
+
refetchType: InvalidateQueryFilters["refetchType"]
|
|
258
|
+
options: InvalidateOptions | undefined
|
|
259
|
+
}
|
|
260
|
+
const groups = new Map<string, Group>()
|
|
261
|
+
for (const target of allTargets) {
|
|
262
|
+
const key = `${target.filters?.refetchType ?? ""}|${target.options?.cancelRefetch ?? ""}|${
|
|
263
|
+
target.options?.throwOnError?.toString() ?? ""
|
|
264
|
+
}`
|
|
265
|
+
const existing = groups.get(key)
|
|
266
|
+
if (existing) {
|
|
267
|
+
existing.targets.push(target)
|
|
268
|
+
} else {
|
|
269
|
+
groups.set(key, { targets: [target], refetchType: target.filters?.refetchType, options: target.options })
|
|
270
|
+
}
|
|
168
271
|
}
|
|
272
|
+
|
|
169
273
|
return Effect
|
|
170
274
|
.andThen(
|
|
171
|
-
Effect.annotateCurrentSpan({
|
|
172
|
-
Effect.forEach(
|
|
275
|
+
Effect.annotateCurrentSpan({ clientTargets, serverKeys }),
|
|
276
|
+
Effect.forEach(
|
|
277
|
+
groups.values(),
|
|
278
|
+
({ options, refetchType, targets }) =>
|
|
279
|
+
invalidateQueriesFn(
|
|
280
|
+
{
|
|
281
|
+
...(refetchType !== undefined ? { refetchType } : {}),
|
|
282
|
+
predicate: (query) => targets.some((t) => t.filters ? matchQuery(t.filters, query) : true)
|
|
283
|
+
},
|
|
284
|
+
options
|
|
285
|
+
),
|
|
286
|
+
{ discard: true, concurrency: "inherit" }
|
|
287
|
+
)
|
|
173
288
|
)
|
|
174
|
-
.pipe(
|
|
175
|
-
|
|
289
|
+
.pipe(
|
|
290
|
+
Effect.tap(
|
|
291
|
+
// hand over control back to the event loop so that state can be updated..
|
|
292
|
+
// TODO: should we do this in general on any mutation, regardless of invalidation?
|
|
293
|
+
Effect.sleep(0)
|
|
294
|
+
),
|
|
295
|
+
Effect.withSpan("client.query.invalidation", {}, { captureStackTrace: false })
|
|
296
|
+
)
|
|
297
|
+
})
|
|
176
298
|
|
|
177
|
-
|
|
299
|
+
return invalidateCache
|
|
300
|
+
}
|
|
178
301
|
|
|
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
|
-
})
|
|
302
|
+
export const invalidateQueries = (
|
|
303
|
+
queryClient: QueryClient,
|
|
304
|
+
self: { id: string; options?: ClientForOptions },
|
|
305
|
+
options?: MutationOptionsBase
|
|
306
|
+
) => {
|
|
307
|
+
const invalidateCache = buildInvalidateCache(queryClient, self, options?.queryInvalidation)
|
|
308
|
+
|
|
309
|
+
const select = options?.select
|
|
193
310
|
|
|
194
|
-
const handle = <A, E, R>(
|
|
311
|
+
const handle = <A, E, R>(eff: Effect.Effect<A, E, R>, input?: unknown) =>
|
|
312
|
+
Effect.gen(function*() {
|
|
313
|
+
const keysRef = yield* Ref.make<ReadonlyArray<InvalidationKey>>([])
|
|
314
|
+
const result = yield* eff.pipe(
|
|
315
|
+
Effect.provideService(InvalidationKeysFromServer, makeInvalidationKeysService(keysRef)),
|
|
316
|
+
Effect.onExit((exit) =>
|
|
317
|
+
Effect.gen(function*() {
|
|
318
|
+
const serverKeys = yield* Ref.get(keysRef)
|
|
319
|
+
yield* invalidateCache(input, exit, serverKeys)
|
|
320
|
+
})
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
if (select) {
|
|
324
|
+
return yield* select(result).pipe(
|
|
325
|
+
Effect.onExit((exit) =>
|
|
326
|
+
Effect.gen(function*() {
|
|
327
|
+
const serverKeys = yield* Ref.get(keysRef)
|
|
328
|
+
yield* invalidateCache(input, exit, serverKeys)
|
|
329
|
+
})
|
|
330
|
+
)
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
return result
|
|
334
|
+
})
|
|
195
335
|
|
|
196
336
|
return handle
|
|
197
337
|
}
|
|
198
338
|
|
|
339
|
+
export interface MutationFnWithInput<I, A, E, R, Id extends string> {
|
|
340
|
+
<B = A, E2 = never, R2 = never>(
|
|
341
|
+
input: I,
|
|
342
|
+
options?: MutationOptionsBase<A, B, E2, R2>
|
|
343
|
+
): Effect.Effect<B, E | E2, R | R2>
|
|
344
|
+
readonly id: Id
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export interface MutationFn<A, E, R, Id extends string> {
|
|
348
|
+
<B = A, E2 = never, R2 = never>(
|
|
349
|
+
options?: MutationOptionsBase<A, B, E2, R2>
|
|
350
|
+
): Effect.Effect<B, E | E2, R | R2>
|
|
351
|
+
readonly id: Id
|
|
352
|
+
}
|
|
353
|
+
|
|
199
354
|
export const makeMutation = () => {
|
|
200
355
|
const useMutation: {
|
|
201
356
|
/**
|
|
@@ -203,25 +358,23 @@ export const makeMutation = () => {
|
|
|
203
358
|
* Executes query cache invalidation based on default rules or provided option.
|
|
204
359
|
*/
|
|
205
360
|
<I, E, A, R, Request extends Req, Id extends string>(
|
|
206
|
-
self: RequestHandlerWithInput<I, A, E, R, Request, Id
|
|
207
|
-
|
|
208
|
-
): ((i: I) => Effect.Effect<A, E, R>) & { readonly id: Id }
|
|
361
|
+
self: RequestHandlerWithInput<I, A, E, R, Request, Id>
|
|
362
|
+
): MutationFnWithInput<I, A, E, R, Id>
|
|
209
363
|
/**
|
|
210
364
|
* Pass an Effect, e.g from a client action
|
|
211
365
|
* Executes query cache invalidation based on default rules or provided option.
|
|
212
366
|
*/
|
|
213
367
|
<E, A, R, Request extends Req, Id extends string>(
|
|
214
|
-
self: RequestHandler<A, E, R, Request, Id
|
|
215
|
-
|
|
216
|
-
): Effect.Effect<A, E, R> & { readonly id: Id }
|
|
368
|
+
self: RequestHandler<A, E, R, Request, Id>
|
|
369
|
+
): MutationFn<A, E, R, Id>
|
|
217
370
|
} = <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
|
|
371
|
+
self: RequestHandlerWithInput<I, A, E, R, Request, Id> | RequestHandler<A, E, R, Request, Id>
|
|
220
372
|
) => {
|
|
221
373
|
const queryClient = useQueryClient()
|
|
222
|
-
const handle = invalidateQueries(queryClient, self, options?.queryInvalidation)
|
|
223
374
|
const handler = self.handler
|
|
224
|
-
const r = Effect.isEffect(handler)
|
|
375
|
+
const r = Effect.isEffect(handler)
|
|
376
|
+
? (options?: MutationOptionsBase) => invalidateQueries(queryClient, self, options)(handler)
|
|
377
|
+
: (i: I, options?: MutationOptionsBase) => invalidateQueries(queryClient, self, options)(handler(i), i)
|
|
225
378
|
|
|
226
379
|
return Object.assign(r, { id: self.id }) as any
|
|
227
380
|
}
|
|
@@ -238,26 +391,107 @@ export const useMakeMutation = () => {
|
|
|
238
391
|
* Executes query cache invalidation based on default rules or provided option.
|
|
239
392
|
*/
|
|
240
393
|
<I, E, A, R, Request extends Req, Id extends string>(
|
|
241
|
-
self: RequestHandlerWithInput<I, A, E, R, Request, Id
|
|
242
|
-
|
|
243
|
-
): ((i: I) => Effect.Effect<A, E, R>) & { readonly id: Id }
|
|
394
|
+
self: RequestHandlerWithInput<I, A, E, R, Request, Id>
|
|
395
|
+
): MutationFnWithInput<I, A, E, R, Id>
|
|
244
396
|
/**
|
|
245
397
|
* Pass an Effect, e.g from a client action
|
|
246
398
|
* Executes query cache invalidation based on default rules or provided option.
|
|
247
399
|
*/
|
|
248
400
|
<E, A, R, Request extends Req, Id extends string>(
|
|
249
|
-
self: RequestHandler<A, E, R, Request, Id
|
|
250
|
-
|
|
251
|
-
): Effect.Effect<A, E, R> & { readonly id: Id }
|
|
401
|
+
self: RequestHandler<A, E, R, Request, Id>
|
|
402
|
+
): MutationFn<A, E, R, Id>
|
|
252
403
|
} = <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
|
|
404
|
+
self: RequestHandlerWithInput<I, A, E, R, Request, Id> | RequestHandler<A, E, R, Request, Id>
|
|
255
405
|
) => {
|
|
256
|
-
const handle = invalidateQueries(queryClient, self, options?.queryInvalidation)
|
|
257
406
|
const handler = self.handler
|
|
258
|
-
const r = Effect.isEffect(handler)
|
|
407
|
+
const r = Effect.isEffect(handler)
|
|
408
|
+
? (options?: MutationOptionsBase) => invalidateQueries(queryClient, self, options)(handler)
|
|
409
|
+
: (i: I, options?: MutationOptionsBase) => invalidateQueries(queryClient, self, options)(handler(i), i)
|
|
259
410
|
|
|
260
411
|
return Object.assign(r, { id: self.id }) as any
|
|
261
412
|
}
|
|
262
413
|
return useMutation
|
|
263
414
|
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Like `makeMutation`, but for stream-type request handlers.
|
|
418
|
+
* Returns a `[ref, execute]` tuple where `ref` is a reactive `AsyncResult` updated per
|
|
419
|
+
* stream element. Queries are invalidated once when the stream finishes, regardless of
|
|
420
|
+
* success or failure.
|
|
421
|
+
*
|
|
422
|
+
* When the request declares a `final` schema, `execute` resolves with the last emitted value
|
|
423
|
+
* typed as `Final`; otherwise it resolves with `void`.
|
|
424
|
+
*
|
|
425
|
+
* Must be called inside a Vue setup context (uses `useQueryClient` internally).
|
|
426
|
+
*/
|
|
427
|
+
export const makeStreamMutation = () => {
|
|
428
|
+
const queryClient = useQueryClient()
|
|
429
|
+
|
|
430
|
+
return (
|
|
431
|
+
self: {
|
|
432
|
+
id: string
|
|
433
|
+
options?: ClientForOptions
|
|
434
|
+
handler: Stream.Stream<any, any, any> | ((i: any) => Stream.Stream<any, any, any>)
|
|
435
|
+
},
|
|
436
|
+
mergedInvalidation?: MutationOptionsBase["queryInvalidation"]
|
|
437
|
+
) => {
|
|
438
|
+
const state = shallowRef<AsyncResult.AsyncResult<any, any>>(AsyncResult.initial())
|
|
439
|
+
|
|
440
|
+
const runStream = (stream: Stream.Stream<any, any, any>, input?: unknown): Effect.Effect<any, never, any> => {
|
|
441
|
+
const invCache = buildInvalidateCache(queryClient, self, mergedInvalidation)
|
|
442
|
+
const keysRef = Ref.makeUnsafe<ReadonlyArray<InvalidationKey>>([])
|
|
443
|
+
// V3: pass onAdded so each mid-stream metadata chunk triggers query
|
|
444
|
+
// invalidation immediately rather than waiting for stream completion.
|
|
445
|
+
const invKeys = makeInvalidationKeysService(keysRef, (key) => invCache(input, Exit.succeed(undefined), [key]))
|
|
446
|
+
return Effect
|
|
447
|
+
.sync(() => {
|
|
448
|
+
state.value = AsyncResult.initial(true)
|
|
449
|
+
})
|
|
450
|
+
.pipe(
|
|
451
|
+
Effect.andThen(
|
|
452
|
+
stream.pipe(
|
|
453
|
+
Stream.provideService(InvalidationKeysFromServer, invKeys),
|
|
454
|
+
Stream.runForEach((value) =>
|
|
455
|
+
Effect.sync(() => {
|
|
456
|
+
state.value = AsyncResult.success(value, { waiting: true })
|
|
457
|
+
})
|
|
458
|
+
),
|
|
459
|
+
Effect.exit,
|
|
460
|
+
Effect.tap((exit) =>
|
|
461
|
+
Effect.sync(() => {
|
|
462
|
+
if (exit._tag === "Success") {
|
|
463
|
+
const current = state.value
|
|
464
|
+
if (AsyncResult.isSuccess(current)) {
|
|
465
|
+
state.value = AsyncResult.success(current.value, { waiting: false })
|
|
466
|
+
} else {
|
|
467
|
+
state.value = AsyncResult.initial(false)
|
|
468
|
+
}
|
|
469
|
+
} else {
|
|
470
|
+
state.value = AsyncResult.failure(exit.cause)
|
|
471
|
+
}
|
|
472
|
+
})
|
|
473
|
+
),
|
|
474
|
+
Effect.flatMap((exit) => {
|
|
475
|
+
const current = state.value
|
|
476
|
+
const lastValue = AsyncResult.isSuccess(current) ? current.value : undefined
|
|
477
|
+
const invExit = exit._tag === "Success" ? Exit.succeed(lastValue) : exit
|
|
478
|
+
const serverKeys = Ref.getUnsafe(keysRef)
|
|
479
|
+
// Note: when the stream fails, `lastValue` is undefined. The failure is
|
|
480
|
+
// communicated via the reactive `state` ref (AsyncResult.failure). The
|
|
481
|
+
// execute effect always resolves successfully; callers should inspect the
|
|
482
|
+
// ref to distinguish success from failure.
|
|
483
|
+
return invCache(input, invExit, serverKeys).pipe(Effect.as(lastValue))
|
|
484
|
+
})
|
|
485
|
+
)
|
|
486
|
+
)
|
|
487
|
+
)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const handler = self.handler
|
|
491
|
+
const act = Stream.isStream(handler)
|
|
492
|
+
? runStream(handler)
|
|
493
|
+
: (i: any) => runStream((handler as (i: any) => Stream.Stream<any, any, any>)(i), i)
|
|
494
|
+
|
|
495
|
+
return tuple(computed(() => state.value), act)
|
|
496
|
+
}
|
|
497
|
+
}
|