@effect-app/vue 4.0.0-beta.24 → 4.0.0-beta.241

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