@effect-app/vue 4.0.0-beta.22 → 4.0.0-beta.220

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 (103) hide show
  1. package/CHANGELOG.md +1605 -0
  2. package/dist/commander.d.ts +628 -0
  3. package/dist/commander.d.ts.map +1 -0
  4. package/dist/commander.js +1060 -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 +14 -5
  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 -9
  19. package/dist/lib.d.ts.map +1 -1
  20. package/dist/lib.js +35 -10
  21. package/dist/makeClient.d.ts +152 -339
  22. package/dist/makeClient.d.ts.map +1 -1
  23. package/dist/makeClient.js +221 -376
  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 +52 -34
  32. package/dist/mutate.d.ts.map +1 -1
  33. package/dist/mutate.js +137 -46
  34. package/dist/query.d.ts +19 -39
  35. package/dist/query.d.ts.map +1 -1
  36. package/dist/query.js +128 -72
  37. package/dist/routeParams.d.ts +1 -1
  38. package/dist/runtime.d.ts +7 -4
  39. package/dist/runtime.d.ts.map +1 -1
  40. package/dist/runtime.js +27 -17
  41. package/dist/toast.d.ts +50 -0
  42. package/dist/toast.d.ts.map +1 -0
  43. package/dist/toast.js +32 -0
  44. package/dist/withToast.d.ts +27 -0
  45. package/dist/withToast.d.ts.map +1 -0
  46. package/dist/withToast.js +59 -0
  47. package/examples/streamMutation.ts +70 -0
  48. package/package.json +48 -50
  49. package/src/commander.ts +3393 -0
  50. package/src/{experimental/confirm.ts → confirm.ts} +10 -14
  51. package/src/errorReporter.ts +62 -74
  52. package/src/form.ts +56 -17
  53. package/src/intl.ts +12 -0
  54. package/src/lib.ts +47 -20
  55. package/src/makeClient.ts +568 -1134
  56. package/src/{experimental/makeUseCommand.ts → makeUseCommand.ts} +6 -4
  57. package/src/mutate.ts +265 -127
  58. package/src/query.ts +197 -183
  59. package/src/runtime.ts +41 -20
  60. package/src/{experimental/toast.ts → toast.ts} +13 -27
  61. package/src/{experimental/withToast.ts → withToast.ts} +40 -12
  62. package/test/Mutation.test.ts +176 -23
  63. package/test/dist/form.test.d.ts.map +1 -1
  64. package/test/dist/lib.test.d.ts.map +1 -0
  65. package/test/dist/streamFinal.test.d.ts.map +1 -0
  66. package/test/dist/streamFn.test.d.ts.map +1 -0
  67. package/test/dist/stubs.d.ts +3529 -122
  68. package/test/dist/stubs.d.ts.map +1 -1
  69. package/test/dist/stubs.js +182 -31
  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 +292 -38
  74. package/test/streamFinal.test.ts +63 -0
  75. package/test/streamFn.test.ts +455 -0
  76. package/test/stubs.ts +218 -42
  77. package/tsconfig.examples.json +20 -0
  78. package/tsconfig.json +0 -1
  79. package/tsconfig.json.bak +5 -2
  80. package/tsconfig.src.json +34 -34
  81. package/tsconfig.test.json +2 -2
  82. package/vitest.config.ts +5 -5
  83. package/dist/experimental/commander.d.ts +0 -359
  84. package/dist/experimental/commander.d.ts.map +0 -1
  85. package/dist/experimental/commander.js +0 -557
  86. package/dist/experimental/confirm.d.ts +0 -19
  87. package/dist/experimental/confirm.d.ts.map +0 -1
  88. package/dist/experimental/confirm.js +0 -28
  89. package/dist/experimental/intl.d.ts +0 -16
  90. package/dist/experimental/intl.d.ts.map +0 -1
  91. package/dist/experimental/intl.js +0 -5
  92. package/dist/experimental/makeUseCommand.d.ts +0 -8
  93. package/dist/experimental/makeUseCommand.d.ts.map +0 -1
  94. package/dist/experimental/makeUseCommand.js +0 -13
  95. package/dist/experimental/toast.d.ts +0 -47
  96. package/dist/experimental/toast.d.ts.map +0 -1
  97. package/dist/experimental/toast.js +0 -41
  98. package/dist/experimental/withToast.d.ts +0 -25
  99. package/dist/experimental/withToast.d.ts.map +0 -1
  100. package/dist/experimental/withToast.js +0 -45
  101. package/eslint.config.mjs +0 -24
  102. package/src/experimental/commander.ts +0 -1835
  103. package/src/experimental/intl.ts +0 -9
@@ -5,22 +5,24 @@ type X<X> = X
5
5
 
6
6
  // helps retain JSDoc
7
7
  export interface CommanderResolved<RT, RTHooks>
8
- extends X<typeof CommanderStatic>, Pick<CommanderImpl<RT, RTHooks>, "fn" | "wrap" | "alt" | "alt2">
8
+ extends
9
+ X<typeof CommanderStatic>,
10
+ Pick<CommanderImpl<RT, RTHooks>, "fn" | "wrap" | "streamWrap" | "streamFn" | "alt" | "alt2">
9
11
  {
10
12
  }
11
13
 
12
14
  export const makeUseCommand = Effect.fnUntraced(
13
15
  function*<R = never, RTHooks = never>(rtHooks: Layer.Layer<RTHooks, never, R>) {
14
16
  const cmndr = yield* Commander
15
- const runtime = yield* Effect.services<R>()
17
+ const runtime = yield* Effect.context<R>()
16
18
 
17
19
  const comm = cmndr(runtime, rtHooks)
18
20
 
19
- const command = {
21
+ const command: CommanderResolved<R, RTHooks> = {
20
22
  ...comm,
21
23
  ...CommanderStatic
22
24
  }
23
25
 
24
- return command as CommanderResolved<R, RTHooks>
26
+ return command
25
27
  }
26
28
  )
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"
5
- import type { ClientForOptions, RequestHandler, RequestHandlerWithInput } from "effect-app/client/clientFor"
4
+ import { type Cause, Effect, Exit, Option } from "effect-app"
5
+ import { type InvalidationKey, InvalidationKeysFromServer, makeInvalidationKeysService, makeQueryKey, type Req } from "effect-app/client"
6
+ import type { ClientForOptions, 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,87 +69,116 @@ 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
- 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
- ) => {
99
+ export const asResult = <Args extends readonly any[], A, E, R>(
100
+ handler: (...args: Args) => Effect.Effect<A, E, R>
101
+ ): readonly [
102
+ ComputedRef<AsyncResult.AsyncResult<A, E>>,
103
+ (...args: Args) => Effect.Effect<Exit.Exit<A, E>, never, R>
104
+ ] => {
113
105
  const state = shallowRef<AsyncResult.AsyncResult<A, E>>(AsyncResult.initial())
114
106
 
115
- const act = Effect.isEffect(handler)
116
- ? Effect
107
+ const act = (...args: Args) =>
108
+ Effect
117
109
  .sync(() => {
118
110
  state.value = AsyncResult.initial(true)
119
111
  })
120
112
  .pipe(
121
113
  Effect.andThen(Effect.suspend(() =>
122
- handler.pipe(
114
+ handler(...args).pipe(
123
115
  Effect.exit,
124
116
  Effect.tap((exit) => Effect.sync(() => (state.value = AsyncResult.fromExit(exit))))
125
117
  )
126
118
  ))
127
119
  )
128
- : (...args: Args) =>
129
- Effect
130
- .sync(() => {
131
- state.value = AsyncResult.initial(true)
132
- })
133
- .pipe(
134
- Effect.andThen(Effect.suspend(() =>
135
- handler(...args).pipe(
136
- Effect.exit,
137
- Effect.tap((exit) => Effect.sync(() => (state.value = AsyncResult.fromExit(exit))))
120
+
121
+ return tuple(computed(() => state.value), act) as any
122
+ }
123
+
124
+ /**
125
+ * Like `asResult`, but for streams. The ref is updated with each emitted value
126
+ * (keeping `waiting: true`) and is finalised (with `waiting: false`) once the
127
+ * stream terminates successfully. Errors are surfaced as `AsyncResult.failure`.
128
+ */
129
+ export const asStreamResult = <Args extends readonly any[], A, E, R>(
130
+ handler: (...args: Args) => Stream.Stream<A, E, R>
131
+ ): readonly [ComputedRef<AsyncResult.AsyncResult<A, E>>, (...args: Args) => Effect.Effect<void, never, R>] => {
132
+ const state = shallowRef<AsyncResult.AsyncResult<A, E>>(AsyncResult.initial())
133
+
134
+ const runStream = (stream: Stream.Stream<A, E, R>): Effect.Effect<void, never, R> =>
135
+ Effect
136
+ .sync(() => {
137
+ state.value = AsyncResult.initial(true)
138
+ })
139
+ .pipe(
140
+ Effect.andThen(
141
+ stream.pipe(
142
+ Stream.runForEach((value) =>
143
+ Effect.sync(() => {
144
+ state.value = AsyncResult.success(value, { waiting: true })
145
+ })
146
+ ),
147
+ Effect.exit,
148
+ Effect.flatMap((exit) =>
149
+ Effect.sync(() => {
150
+ if (exit._tag === "Success") {
151
+ const current = state.value
152
+ if (AsyncResult.isSuccess(current)) {
153
+ state.value = AsyncResult.success(current.value, { waiting: false })
154
+ } else {
155
+ state.value = AsyncResult.initial(false)
156
+ }
157
+ } else {
158
+ state.value = AsyncResult.failure(exit.cause)
159
+ }
160
+ })
138
161
  )
139
- ))
162
+ )
140
163
  )
164
+ )
165
+
166
+ const act = (...args: Args) => runStream(handler(...args))
141
167
 
142
168
  return tuple(computed(() => state.value), act) as any
143
169
  }
144
170
 
145
- export const invalidateQueries = (
171
+ const buildInvalidateCache = (
146
172
  queryClient: QueryClient,
147
173
  self: { id: string; options?: ClientForOptions },
148
- options?: MutationOptionsBase["queryInvalidation"]
174
+ queryInvalidation?: MutationOptionsBase["queryInvalidation"]
149
175
  ) => {
150
- const invalidateQueries = (
176
+ type InvalidationTarget = {
177
+ readonly filters: InvalidateQueryFilters | undefined
178
+ readonly options: InvalidateOptions | undefined
179
+ }
180
+
181
+ const invalidateQueriesFn = (
151
182
  filters?: InvalidateQueryFilters,
152
183
  options?: InvalidateOptions
153
184
  ) =>
@@ -158,71 +189,149 @@ export const invalidateQueries = (
158
189
  )
159
190
  )
160
191
 
161
- const invalidateCache = Effect.suspend(() => {
192
+ const getClientInvalidationTargets = (
193
+ input: unknown,
194
+ output: Exit.Exit<unknown, unknown>
195
+ ): ReadonlyArray<InvalidationTarget> => {
162
196
  const queryKey = getQueryKey(self)
163
197
 
164
- if (options) {
165
- const opts = options(queryKey, self.id)
166
- if (!opts.length) {
167
- return Effect.void
198
+ if (queryInvalidation) {
199
+ return queryInvalidation(queryKey, self.id, input, output).map((_) => ({
200
+ filters: _.filters,
201
+ options: _.options
202
+ }))
203
+ }
204
+
205
+ if (!queryKey) {
206
+ return []
207
+ }
208
+
209
+ return [{ filters: { queryKey }, options: undefined }]
210
+ }
211
+
212
+ const invalidateCache = (
213
+ input: unknown,
214
+ output: Exit.Exit<unknown, unknown>,
215
+ serverKeys: ReadonlyArray<InvalidationKey>
216
+ ) =>
217
+ Effect.suspend(() => {
218
+ const clientTargets = getClientInvalidationTargets(input, output)
219
+ const serverTargets: ReadonlyArray<InvalidationTarget> = serverKeys.map((queryKey) => ({
220
+ filters: { queryKey },
221
+ options: undefined
222
+ }))
223
+ const allTargets: ReadonlyArray<InvalidationTarget> = [...clientTargets, ...serverTargets]
224
+
225
+ if (!allTargets.length) return Effect.void
226
+
227
+ // Group targets by refetchType + options so each group can be merged into a single
228
+ // invalidateQueries call using a predicate, reducing N calls to 1 in the common case.
229
+ type Group = {
230
+ targets: Array<InvalidationTarget>
231
+ refetchType: InvalidateQueryFilters["refetchType"]
232
+ options: InvalidateOptions | undefined
233
+ }
234
+ const groups = new Map<string, Group>()
235
+ for (const target of allTargets) {
236
+ const key = `${target.filters?.refetchType ?? ""}|${target.options?.cancelRefetch ?? ""}|${
237
+ target.options?.throwOnError?.toString() ?? ""
238
+ }`
239
+ const existing = groups.get(key)
240
+ if (existing) {
241
+ existing.targets.push(target)
242
+ } else {
243
+ groups.set(key, { targets: [target], refetchType: target.filters?.refetchType, options: target.options })
244
+ }
168
245
  }
246
+
169
247
  return Effect
170
248
  .andThen(
171
- Effect.annotateCurrentSpan({ queryKey, opts }),
172
- Effect.forEach(opts, (_) => invalidateQueries(_.filters, _.options), { concurrency: "inherit" })
249
+ Effect.annotateCurrentSpan({ clientTargets, serverKeys }),
250
+ Effect.forEach(
251
+ groups.values(),
252
+ ({ options, refetchType, targets }) =>
253
+ invalidateQueriesFn(
254
+ {
255
+ ...(refetchType !== undefined ? { refetchType } : {}),
256
+ predicate: (query) => targets.some((t) => t.filters ? matchQuery(t.filters, query) : true)
257
+ },
258
+ options
259
+ ),
260
+ { discard: true, concurrency: "inherit" }
261
+ )
173
262
  )
174
- .pipe(Effect.withSpan("client.query.invalidation", {}, { captureStackTrace: false }))
175
- }
263
+ .pipe(
264
+ Effect.tap(
265
+ // hand over control back to the event loop so that state can be updated..
266
+ // TODO: should we do this in general on any mutation, regardless of invalidation?
267
+ Effect.sleep(0)
268
+ ),
269
+ Effect.withSpan("client.query.invalidation", {}, { captureStackTrace: false })
270
+ )
271
+ })
176
272
 
177
- if (!queryKey) return Effect.void
273
+ return invalidateCache
274
+ }
178
275
 
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
- })
276
+ export const invalidateQueries = (
277
+ queryClient: QueryClient,
278
+ self: { id: string; options?: ClientForOptions },
279
+ options?: MutationOptionsBase
280
+ ) => {
281
+ const invalidateCache = buildInvalidateCache(queryClient, self, options?.queryInvalidation)
282
+
283
+ const select = options?.select
193
284
 
194
- const handle = <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.ensuring(self, invalidateCache)
285
+ const handle = <A, E, R>(eff: Effect.Effect<A, E, R>, input?: unknown) =>
286
+ Effect.gen(function*() {
287
+ const keysRef = yield* Ref.make<ReadonlyArray<InvalidationKey>>([])
288
+ const result = yield* eff.pipe(
289
+ Effect.provideService(InvalidationKeysFromServer, makeInvalidationKeysService(keysRef)),
290
+ Effect.onExit((exit) =>
291
+ Effect.gen(function*() {
292
+ const serverKeys = yield* Ref.get(keysRef)
293
+ yield* invalidateCache(input, exit, serverKeys)
294
+ })
295
+ )
296
+ )
297
+ if (select) {
298
+ return yield* select(result).pipe(
299
+ Effect.onExit((exit) =>
300
+ Effect.gen(function*() {
301
+ const serverKeys = yield* Ref.get(keysRef)
302
+ yield* invalidateCache(input, exit, serverKeys)
303
+ })
304
+ )
305
+ )
306
+ }
307
+ return result
308
+ })
195
309
 
196
310
  return handle
197
311
  }
198
312
 
313
+ /**
314
+ * A callable mutation result. When `I = void` the input argument may be omitted.
315
+ */
316
+ export interface MutationFn<I, A, E, R, Id extends string> {
317
+ <B = A, E2 = never, R2 = never>(
318
+ input: I,
319
+ options?: MutationOptionsBase<A, B, E2, R2>
320
+ ): Effect.Effect<B, E | E2, R | R2>
321
+ readonly id: Id
322
+ }
323
+
199
324
  export const makeMutation = () => {
200
- const useMutation: {
201
- /**
202
- * Pass a function that returns an Effect, e.g from a client action
203
- * Executes query cache invalidation based on default rules or provided option.
204
- */
205
- <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 }
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
- ) => {
325
+ /**
326
+ * Pass a function that returns an Effect, e.g from a client action.
327
+ * Executes query cache invalidation based on default rules or provided option.
328
+ * When `I = void` the input argument may be omitted.
329
+ */
330
+ const useMutation = <I, E, A, R, Request extends Req, Id extends string>(
331
+ self: RequestHandlerWithInput<I, A, E, R, Request, Id>
332
+ ): MutationFn<I, A, E, R, Id> => {
221
333
  const queryClient = useQueryClient()
222
- const handle = invalidateQueries(queryClient, self, options?.queryInvalidation)
223
- const handler = self.handler
224
- const r = Effect.isEffect(handler) ? handle(handler) : (i: I) => handle(handler(i))
225
-
334
+ const r = (i: I, options?: MutationOptionsBase) => invalidateQueries(queryClient, self, options)(self.handler(i), i)
226
335
  return Object.assign(r, { id: self.id }) as any
227
336
  }
228
337
  return useMutation
@@ -232,32 +341,61 @@ export const makeMutation = () => {
232
341
  export const useMakeMutation = () => {
233
342
  const queryClient = useQueryClient()
234
343
 
235
- const useMutation: {
236
- /**
237
- * Pass a function that returns an Effect, e.g from a client action
238
- * Executes query cache invalidation based on default rules or provided option.
239
- */
240
- <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 }
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
-
344
+ /**
345
+ * Pass a function that returns an Effect, e.g from a client action.
346
+ * Executes query cache invalidation based on default rules or provided option.
347
+ * When `I = void` the input argument may be omitted.
348
+ */
349
+ const useMutation = <I, E, A, R, Request extends Req, Id extends string>(
350
+ self: RequestHandlerWithInput<I, A, E, R, Request, Id>
351
+ ): MutationFn<I, A, E, R, Id> => {
352
+ const r = (i: I, options?: MutationOptionsBase) => invalidateQueries(queryClient, self, options)(self.handler(i), i)
260
353
  return Object.assign(r, { id: self.id }) as any
261
354
  }
262
355
  return useMutation
263
356
  }
357
+
358
+ /**
359
+ * Returns a stream-based mutation factory for use with `streamFn`.
360
+ * The outer Effect sets up per-invocation invalidation scaffolding
361
+ * and returns a stream that triggers query invalidation via `Stream.ensuring` when it completes.
362
+ *
363
+ * Use with `streamFn` / `Command.streamFn(id)(mutateHandler, ...combinators)` so that
364
+ * the command manages its own reactive state internally.
365
+ *
366
+ * Must be called inside a Vue setup context (uses `useQueryClient` internally).
367
+ */
368
+ export const makeStreamMutation2 = () => {
369
+ const queryClient = useQueryClient()
370
+
371
+ return (
372
+ self: {
373
+ id: string
374
+ options?: ClientForOptions
375
+ handler: (i: any) => Stream.Stream<any, any, any>
376
+ },
377
+ mergedInvalidation?: MutationOptionsBase["queryInvalidation"]
378
+ ) => {
379
+ const invCache = buildInvalidateCache(queryClient, self, mergedInvalidation)
380
+
381
+ const makeInvocationEffect = (input: unknown, source: Stream.Stream<any, any, any>) =>
382
+ Effect.gen(function*() {
383
+ const keysRef = yield* Ref.make<ReadonlyArray<InvalidationKey>>([])
384
+ const invKeys = makeInvalidationKeysService(keysRef, (key) => invCache(input, Exit.succeed(undefined), [key]))
385
+ const lastRef = yield* Ref.make<any>(undefined)
386
+ return source.pipe(
387
+ Stream.provideService(InvalidationKeysFromServer, invKeys),
388
+ Stream.tap((v) => Ref.set(lastRef, v)),
389
+ Stream.ensuring(
390
+ Effect.gen(function*() {
391
+ const lastValue = yield* Ref.get(lastRef)
392
+ const serverKeys = yield* Ref.get(keysRef)
393
+ yield* invCache(input, Exit.succeed(lastValue), serverKeys)
394
+ })
395
+ )
396
+ )
397
+ })
398
+
399
+ return (i: any) => Stream.unwrap(makeInvocationEffect(i, self.handler(i)))
400
+ }
401
+ }