@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.
Files changed (100) hide show
  1. package/CHANGELOG.md +1224 -0
  2. package/dist/commander.d.ts +370 -0
  3. package/dist/commander.d.ts.map +1 -0
  4. package/dist/commander.js +591 -0
  5. package/dist/confirm.d.ts +19 -0
  6. package/dist/confirm.d.ts.map +1 -0
  7. package/dist/confirm.js +24 -0
  8. package/dist/errorReporter.d.ts +4 -4
  9. package/dist/errorReporter.d.ts.map +1 -1
  10. package/dist/errorReporter.js +12 -18
  11. package/dist/form.d.ts +13 -4
  12. package/dist/form.d.ts.map +1 -1
  13. package/dist/form.js +41 -12
  14. package/dist/index.d.ts +1 -1
  15. package/dist/intl.d.ts +15 -0
  16. package/dist/intl.d.ts.map +1 -0
  17. package/dist/intl.js +9 -0
  18. package/dist/lib.d.ts +6 -8
  19. package/dist/lib.d.ts.map +1 -1
  20. package/dist/lib.js +34 -7
  21. package/dist/makeClient.d.ts +148 -290
  22. package/dist/makeClient.d.ts.map +1 -1
  23. package/dist/makeClient.js +205 -361
  24. package/dist/makeContext.d.ts +1 -1
  25. package/dist/makeContext.d.ts.map +1 -1
  26. package/dist/makeIntl.d.ts +1 -1
  27. package/dist/makeIntl.d.ts.map +1 -1
  28. package/dist/makeUseCommand.d.ts +8 -0
  29. package/dist/makeUseCommand.d.ts.map +1 -0
  30. package/dist/makeUseCommand.js +13 -0
  31. package/dist/mutate.d.ts +57 -25
  32. package/dist/mutate.d.ts.map +1 -1
  33. package/dist/mutate.js +160 -33
  34. package/dist/query.d.ts +11 -15
  35. package/dist/query.d.ts.map +1 -1
  36. package/dist/query.js +19 -27
  37. package/dist/routeParams.d.ts +1 -1
  38. package/dist/runtime.d.ts +5 -2
  39. package/dist/runtime.d.ts.map +1 -1
  40. package/dist/runtime.js +27 -17
  41. package/dist/toast.d.ts +46 -0
  42. package/dist/toast.d.ts.map +1 -0
  43. package/dist/toast.js +32 -0
  44. package/dist/withToast.d.ts +26 -0
  45. package/dist/withToast.d.ts.map +1 -0
  46. package/dist/withToast.js +49 -0
  47. package/eslint.config.mjs +2 -2
  48. package/examples/streamMutation.ts +83 -0
  49. package/package.json +48 -48
  50. package/src/{experimental/commander.ts → commander.ts} +930 -255
  51. package/src/{experimental/confirm.ts → confirm.ts} +10 -14
  52. package/src/errorReporter.ts +62 -74
  53. package/src/form.ts +55 -16
  54. package/src/intl.ts +12 -0
  55. package/src/lib.ts +46 -13
  56. package/src/makeClient.ts +570 -1038
  57. package/src/{experimental/makeUseCommand.ts → makeUseCommand.ts} +3 -3
  58. package/src/mutate.ts +306 -72
  59. package/src/query.ts +39 -50
  60. package/src/runtime.ts +39 -18
  61. package/src/{experimental/toast.ts → toast.ts} +11 -25
  62. package/src/{experimental/withToast.ts → withToast.ts} +15 -6
  63. package/test/Mutation.test.ts +130 -10
  64. package/test/dist/form.test.d.ts.map +1 -1
  65. package/test/dist/lib.test.d.ts.map +1 -0
  66. package/test/dist/streamFinal.test.d.ts.map +1 -0
  67. package/test/dist/stubs.d.ts +3144 -117
  68. package/test/dist/stubs.d.ts.map +1 -1
  69. package/test/dist/stubs.js +132 -25
  70. package/test/form-validation-errors.test.ts +23 -19
  71. package/test/form.test.ts +20 -2
  72. package/test/lib.test.ts +240 -0
  73. package/test/makeClient.test.ts +241 -38
  74. package/test/streamFinal.test.ts +110 -0
  75. package/test/stubs.ts +172 -42
  76. package/tsconfig.examples.json +20 -0
  77. package/tsconfig.json +0 -1
  78. package/tsconfig.json.bak +5 -2
  79. package/tsconfig.src.json +34 -34
  80. package/tsconfig.test.json +2 -2
  81. package/vitest.config.ts +5 -5
  82. package/dist/experimental/commander.d.ts +0 -359
  83. package/dist/experimental/commander.d.ts.map +0 -1
  84. package/dist/experimental/commander.js +0 -557
  85. package/dist/experimental/confirm.d.ts +0 -19
  86. package/dist/experimental/confirm.d.ts.map +0 -1
  87. package/dist/experimental/confirm.js +0 -28
  88. package/dist/experimental/intl.d.ts +0 -16
  89. package/dist/experimental/intl.d.ts.map +0 -1
  90. package/dist/experimental/intl.js +0 -5
  91. package/dist/experimental/makeUseCommand.d.ts +0 -8
  92. package/dist/experimental/makeUseCommand.d.ts.map +0 -1
  93. package/dist/experimental/makeUseCommand.js +0 -13
  94. package/dist/experimental/toast.d.ts +0 -47
  95. package/dist/experimental/toast.d.ts.map +0 -1
  96. package/dist/experimental/toast.js +0 -41
  97. package/dist/experimental/withToast.d.ts +0 -25
  98. package/dist/experimental/withToast.d.ts.map +0 -1
  99. package/dist/experimental/withToast.js +0 -45
  100. 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.services<R>()
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 as CommanderResolved<R, RTHooks>
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, type Exit, Option } from "effect-app"
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
- * Map the handler; cache invalidation is already done in this handler.
85
- * This is useful for e.g navigating, as you know caches have already updated.
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
- * @deprecated use `Command.fn` instead of `useMutation*` with `mapHandler` option.
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
- mapHandler?: (handler: Effect.Effect<A, E, R>, input: I) => Effect.Effect<A2, E2, R2>
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
- export const invalidateQueries = (
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
- options?: MutationOptionsBase["queryInvalidation"]
200
+ queryInvalidation?: MutationOptionsBase["queryInvalidation"]
149
201
  ) => {
150
- const invalidateQueries = (
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 invalidateCache = Effect.suspend(() => {
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 (options) {
165
- const opts = options(queryKey, self.id)
166
- if (!opts.length) {
167
- return Effect.void
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({ queryKey, opts }),
172
- Effect.forEach(opts, (_) => invalidateQueries(_.filters, _.options), { concurrency: "inherit" })
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(Effect.withSpan("client.query.invalidation", {}, { captureStackTrace: false }))
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
- if (!queryKey) return Effect.void
299
+ return invalidateCache
300
+ }
178
301
 
179
- return Effect
180
- .andThen(
181
- Effect.annotateCurrentSpan({ queryKey }),
182
- invalidateQueries({ queryKey })
183
- )
184
- .pipe(
185
- Effect.tap(
186
- // hand over control back to the event loop so that state can be updated..
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>(self: Effect.Effect<A, E, R>) => Effect.ensuring(self, invalidateCache)
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
- options?: MutationOptionsBase
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
- options?: MutationOptionsBase
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) ? handle(handler) : (i: I) => handle(handler(i))
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
- options?: MutationOptionsBase
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
- options?: MutationOptionsBase
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) ? handle(handler) : (i: I) => handle(handler(i))
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
+ }