@effect-app/vue 1.25.2 → 1.26.0

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.
@@ -0,0 +1,524 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { flow, pipe, tuple } from "@effect-app/core/Function"
3
+ import type { MutationResult } from "@effect-app/vue"
4
+ import { Result } from "@effect-app/vue"
5
+ import * as Sentry from "@sentry/browser"
6
+ import { type MaybeRefOrGetter, type Pausable, useIntervalFn, type UseIntervalFnOptions } from "@vueuse/core"
7
+ import type { Either } from "effect-app"
8
+ import { Array, Cause, Effect, Match, Option, Runtime, S } from "effect-app"
9
+ import { type SupportedErrors } from "effect-app/client"
10
+ import { Failure, Success } from "effect-app/Operations"
11
+ import { dropUndefinedT } from "effect-app/utils"
12
+ import { computed, type ComputedRef } from "vue"
13
+ import type { MakeIntlReturn } from "./makeIntl.js"
14
+ import type { MakeMutation2, MutationOptions } from "./mutate2.js"
15
+
16
+ /**
17
+ * Use this after handling an error yourself, still continueing on the Error track, but the error will not be reported.
18
+ */
19
+ export class SuppressErrors extends Cause.YieldableError {
20
+ readonly _tag = "SuppressErrors"
21
+ }
22
+
23
+ export type ResponseErrors = S.ParseResult.ParseError | SupportedErrors | SuppressErrors
24
+
25
+ export function pauseWhileProcessing(
26
+ iv: Pausable,
27
+ pmf: () => Promise<unknown>
28
+ ) {
29
+ return Promise
30
+ .resolve(iv.pause())
31
+ .then(() => pmf())
32
+ .finally(() => iv.resume())
33
+ }
34
+
35
+ export function useIntervalPauseWhileProcessing(
36
+ pmf: () => Promise<unknown>,
37
+ interval?: MaybeRefOrGetter<number>,
38
+ options?: Omit<UseIntervalFnOptions, "immediateCallback">
39
+ ) {
40
+ const iv = useIntervalFn(
41
+ () => pauseWhileProcessing(iv, pmf),
42
+ interval,
43
+ options ? { ...options, immediateCallback: false } : options
44
+ )
45
+ return {
46
+ isActive: iv.isActive
47
+ }
48
+ }
49
+
50
+ export interface Opts<A> extends MutationOptions {
51
+ suppressErrorToast?: boolean
52
+ suppressSuccessToast?: boolean
53
+ successToast?: (a: A) => any
54
+ }
55
+
56
+ export const withSuccess: {
57
+ <I, E extends ResponseErrors, A, X, R>(
58
+ self: {
59
+ handler: (i: I) => Effect<A, E, R>
60
+ name: string
61
+ },
62
+ onSuccess: (a: A, i: I) => Promise<X>
63
+ ): {
64
+ handler: (i: I) => Effect<X, E, R>
65
+ name: string
66
+ }
67
+ <E extends ResponseErrors, A, X, R>(
68
+ self: {
69
+ handler: Effect<A, E, R>
70
+ name: string
71
+ },
72
+ onSuccess: (_: A) => Promise<X>
73
+ ): {
74
+ handler: Effect<X, E, R>
75
+ name: string
76
+ }
77
+ } = (self: any, onSuccess: any): any => ({
78
+ ...self,
79
+ handler: typeof self.handler === "function"
80
+ ? (i: any) =>
81
+ pipe(
82
+ (
83
+ self.handler as (
84
+ i: any
85
+ ) => Effect<any, any, any>
86
+ )(i),
87
+ Effect.flatMap((_) =>
88
+ Effect.promise(() => onSuccess(_, i)).pipe(
89
+ Effect.withSpan("onSuccess")
90
+ )
91
+ )
92
+ )
93
+ : Effect.flatMap(self.handler, (_) => Effect.promise(() => onSuccess(_)).pipe(Effect.withSpan("onSuccess")))
94
+ })
95
+
96
+ export const withSuccessE: {
97
+ <I, E extends ResponseErrors, A, E2, X, R>(
98
+ self: {
99
+ handler: (i: I) => Effect<A, E, R>
100
+ name: string
101
+ },
102
+ onSuccessE: (_: A, i: I) => Effect<X, E2>
103
+ ): {
104
+ handler: (i: I) => Effect<X, E | E2, R>
105
+ name: string
106
+ }
107
+ <E extends ResponseErrors, A, E2, X, R>(
108
+ self: {
109
+ handler: Effect<A, E, R>
110
+ name: string
111
+ },
112
+ onSuccessE: (_: A) => Effect<X, E2>
113
+ ): {
114
+ handler: Effect<X, E | E2, R>
115
+ name: string
116
+ }
117
+ } = (self: any, onSuccessE: any): any => {
118
+ return {
119
+ ...self,
120
+ handler: typeof self.handler === "function"
121
+ ? (i: any) =>
122
+ pipe(
123
+ self.handler(i),
124
+ Effect.flatMap((_) => onSuccessE(_, i))
125
+ )
126
+ : Effect.flatMap(self.handler, (_) => onSuccessE(_))
127
+ }
128
+ }
129
+
130
+ export interface Res<A, E> {
131
+ readonly loading: boolean
132
+ readonly data: A | undefined
133
+ readonly error: E | undefined
134
+ }
135
+
136
+ type WithAction<A> = A & {
137
+ action: string
138
+ }
139
+
140
+ // computed() takes a getter function and returns a readonly reactive ref
141
+ // object for the returned value from the getter.
142
+ type Resp<I, E, A> = readonly [
143
+ ComputedRef<Res<A, E>>,
144
+ WithAction<(I: I) => Promise<void>>
145
+ ]
146
+
147
+ type ActResp<E, A> = readonly [
148
+ ComputedRef<Res<A, E>>,
149
+ WithAction<() => Promise<void>>
150
+ ]
151
+
152
+ export function mutationResultToVue<A, E>(
153
+ mutationResult: MutationResult<A, E>
154
+ ): Res<A, E> {
155
+ switch (mutationResult._tag) {
156
+ case "Loading": {
157
+ return { loading: true, data: undefined, error: undefined }
158
+ }
159
+ case "Success": {
160
+ return {
161
+ loading: false,
162
+ data: mutationResult.data,
163
+ error: undefined
164
+ }
165
+ }
166
+ case "Error": {
167
+ return {
168
+ loading: false,
169
+ data: undefined,
170
+ error: mutationResult.error
171
+ }
172
+ }
173
+ case "Initial": {
174
+ return { loading: false, data: undefined, error: undefined }
175
+ }
176
+ }
177
+ }
178
+
179
+ export const makeClient2 = <Locale extends string, R>(
180
+ useIntl: MakeIntlReturn<Locale>["useIntl"],
181
+ useToast: () => {
182
+ error: (message: string) => void
183
+ warning: (message: string) => void
184
+ success: (message: string) => void
185
+ },
186
+ useSafeMutation: MakeMutation2,
187
+ messages: Record<string, string | undefined> = {}
188
+ ) => {
189
+ const useHandleRequestWithToast = () => {
190
+ const toast = useToast()
191
+ const { intl } = useIntl()
192
+
193
+ return handleRequestWithToast
194
+ /**
195
+ * Pass a function that returns a Promise.
196
+ * Returns an execution function which reports errors as Toast.
197
+ */
198
+ function handleRequestWithToast<
199
+ E extends ResponseErrors,
200
+ A,
201
+ Args extends unknown[]
202
+ >(
203
+ f: (...args: Args) => Promise<Either<A, E>>,
204
+ action: string,
205
+ options: Opts<A> = { suppressErrorToast: false }
206
+ ) {
207
+ const message = messages[action] ?? action
208
+ const warnMessage = intl.value.formatMessage(
209
+ { id: "handle.with_warnings" },
210
+ { action: message }
211
+ )
212
+ const successMessage = intl.value.formatMessage(
213
+ { id: "handle.success" },
214
+ { action: message }
215
+ )
216
+ const errorMessage = intl.value.formatMessage(
217
+ { id: "handle.with_errors" },
218
+ { action: message }
219
+ )
220
+ return Object.assign(
221
+ flow(f, (p) =>
222
+ p.then(
223
+ (r) =>
224
+ r._tag === "Right"
225
+ ? S.is(Failure)(r.right)
226
+ ? Promise
227
+ .resolve(
228
+ toast.warning(
229
+ warnMessage + r.right.message
230
+ ? "\n" + r.right.message
231
+ : ""
232
+ )
233
+ )
234
+ .then((_) => {})
235
+ : Promise
236
+ .resolve(
237
+ toast.success(
238
+ successMessage
239
+ + (S.is(Success)(r.right) && r.right.message
240
+ ? "\n" + r.right.message
241
+ : "")
242
+ )
243
+ )
244
+ .then((_) => {})
245
+ : r.left._tag === "SuppressErrors"
246
+ ? Promise.resolve(void 0)
247
+ : Promise
248
+ .resolve(
249
+ !options.suppressErrorToast
250
+ && toast.error(`${errorMessage}:\n` + renderError(r.left))
251
+ )
252
+ .then((_) => {
253
+ console.warn(r.left, r.left.toString())
254
+ }),
255
+ (err) => {
256
+ if (
257
+ Cause.isInterruptedException(err)
258
+ || (Runtime.isFiberFailure(err)
259
+ && Cause.isInterruptedOnly(err[Runtime.FiberFailureCauseId]))
260
+ ) {
261
+ return
262
+ }
263
+ const extra = {
264
+ action,
265
+ message: `Unexpected Error trying to ${action}`
266
+ }
267
+ Sentry.captureException(err, {
268
+ extra
269
+ })
270
+ console.error(err, extra)
271
+
272
+ return toast.error(
273
+ intl.value.formatMessage(
274
+ { id: "handle.unexpected_error" },
275
+ {
276
+ action: message,
277
+ error: JSON.stringify(err, undefined, 2)
278
+ }
279
+ )
280
+ )
281
+ }
282
+ )),
283
+ { action }
284
+ )
285
+ }
286
+
287
+ function renderError(e: ResponseErrors): string {
288
+ return Match.value(e).pipe(
289
+ Match.tags({
290
+ // HttpErrorRequest: e =>
291
+ // intl.value.formatMessage(
292
+ // { id: "handle.request_error" },
293
+ // { error: `${e.error}` },
294
+ // ),
295
+ // HttpErrorResponse: e =>
296
+ // e.response.status >= 500 ||
297
+ // e.response.body._tag !== "Some" ||
298
+ // !e.response.body.value
299
+ // ? intl.value.formatMessage(
300
+ // { id: "handle.error_response" },
301
+ // {
302
+ // error: `${
303
+ // e.response.body._tag === "Some" && e.response.body.value
304
+ // ? parseError(e.response.body.value)
305
+ // : "Unknown"
306
+ // } (${e.response.status})`,
307
+ // },
308
+ // )
309
+ // : intl.value.formatMessage(
310
+ // { id: "handle.unexpected_error" },
311
+ // {
312
+ // error:
313
+ // JSON.stringify(e.response.body, undefined, 2) +
314
+ // "( " +
315
+ // e.response.status +
316
+ // ")",
317
+ // },
318
+ // ),
319
+ // ResponseError: e =>
320
+ // intl.value.formatMessage(
321
+ // { id: "handle.response_error" },
322
+ // { error: `${e.error}` },
323
+ // ),
324
+ ParseError: (e) => {
325
+ console.warn(e.toString())
326
+ return intl.value.formatMessage({ id: "validation.failed" })
327
+ }
328
+ }),
329
+ Match.orElse((e) =>
330
+ intl.value.formatMessage(
331
+ { id: "handle.unexpected_error" },
332
+ {
333
+ error: `${e.message ?? e._tag ?? e}`
334
+ }
335
+ )
336
+ )
337
+ )
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Pass a function that returns an Effect, e.g from a client action, give it a name, and optionally pass an onSuccess callback.
343
+ * Returns a tuple with state ref and execution function which reports errors as Toast.
344
+ */
345
+ const useAndHandleMutation: {
346
+ <I, E extends ResponseErrors, A>(
347
+ self: {
348
+ handler: (i: I) => Effect<A, E, R>
349
+ name: string
350
+ },
351
+ action: string,
352
+ options?: Opts<A>
353
+ ): Resp<I, A, E>
354
+ <E extends ResponseErrors, A>(
355
+ self: {
356
+ handler: Effect<A, E, R>
357
+ name: string
358
+ },
359
+ action: string,
360
+ options?: Opts<A>
361
+ ): ActResp<E, A>
362
+ } = (self: any, action: any, options?: Opts<any>) => {
363
+ const handleRequestWithToast = useHandleRequestWithToast()
364
+ const [a, b] = useSafeMutation(
365
+ {
366
+ handler: Effect.isEffect(self.handler)
367
+ ? (pipe(
368
+ Effect.annotateCurrentSpan({ action }),
369
+ Effect.andThen(self.handler)
370
+ ) as any)
371
+ : (...args: any[]) =>
372
+ pipe(
373
+ Effect.annotateCurrentSpan({ action }),
374
+ Effect.andThen(self.handler(...args))
375
+ ),
376
+ name: self.name
377
+ },
378
+ dropUndefinedT({
379
+ queryInvalidation: options?.queryInvalidation
380
+ })
381
+ )
382
+
383
+ return tuple(
384
+ computed(() => mutationResultToVue(a.value)),
385
+ handleRequestWithToast(b as any, action, options)
386
+ )
387
+ }
388
+
389
+ function makeUseAndHandleMutation(
390
+ onSuccess?: () => Promise<void>,
391
+ defaultOptions?: Opts<any>
392
+ ) {
393
+ return ((self: any, action: any, options: any) => {
394
+ return useAndHandleMutation(
395
+ {
396
+ handler: typeof self.handler === "function"
397
+ ? onSuccess
398
+ ? (i: any) => Effect.tap(self.handler(i), () => Effect.promise(onSuccess))
399
+ : self.handler
400
+ : onSuccess
401
+ ? (Effect.tap(self.handler, () => Effect.promise(onSuccess)) as any)
402
+ : self.handler,
403
+ name: self.name
404
+ },
405
+ action,
406
+ { ...defaultOptions, ...options }
407
+ )
408
+ }) as {
409
+ <I, E extends ResponseErrors, A>(
410
+ self: {
411
+ handler: (i: I) => Effect<A, E, R>
412
+ name: string
413
+ },
414
+ action: string,
415
+ options?: Opts<A>
416
+ ): Resp<I, A, E>
417
+ <E extends ResponseErrors, A>(
418
+ self: {
419
+ handler: Effect<A, E, R>
420
+ name: string
421
+ },
422
+ action: string,
423
+ options?: Opts<A>
424
+ ): ActResp<E, A>
425
+ }
426
+ }
427
+
428
+ const useSafeMutationWithState = <I, E, A>(self: {
429
+ handler: (i: I) => Effect<A, E, R>
430
+ name: string
431
+ }) => {
432
+ const [a, b] = useSafeMutation(self)
433
+
434
+ return tuple(
435
+ computed(() => mutationResultToVue(a.value)),
436
+ b
437
+ )
438
+ }
439
+
440
+ return {
441
+ useSafeMutationWithState,
442
+ useAndHandleMutation,
443
+ makeUseAndHandleMutation,
444
+ useHandleRequestWithToast
445
+ }
446
+ }
447
+
448
+ export const mapHandler: {
449
+ <I, E, R, A, E2, A2, R2>(
450
+ self: {
451
+ handler: (i: I) => Effect<A, E, R>
452
+ name: string
453
+ mapPath: (i: I) => string
454
+ },
455
+ map: (i: I) => (handler: Effect<A, E, R>) => Effect<A2, E2, R2>
456
+ ): {
457
+ handler: (i: I) => Effect<A2, E2, R2>
458
+ name: string
459
+ mapPath: (i: I) => string
460
+ }
461
+ <E, A, R, E2, A2, R2>(
462
+ self: {
463
+ handler: Effect<A, E, R>
464
+ name: string
465
+ mapPath: string
466
+ },
467
+ map: (handler: Effect<A, E, R>) => Effect<A2, E2, R2>
468
+ ): {
469
+ handler: Effect<A2, E2, R2>
470
+ name: string
471
+ mapPath: string
472
+ }
473
+ } = (self: any, map: any): any => ({
474
+ ...self,
475
+ handler: typeof self.handler === "function"
476
+ ? (i: any) => map(i)((self.handler as (i: any) => Effect<any, any, any>)(i))
477
+ : map(self.handler)
478
+ })
479
+
480
+ export function composeQueries<
481
+ R extends Record<string, Result.Result<any, any>>
482
+ >(
483
+ results: R,
484
+ renderPreviousOnFailure?: boolean
485
+ ): Result.Result<
486
+ {
487
+ [Property in keyof R]: R[Property] extends Result.Result<infer A, any> ? A
488
+ : never
489
+ },
490
+ {
491
+ [Property in keyof R]: R[Property] extends Result.Result<any, infer E> ? E
492
+ : never
493
+ }[keyof R]
494
+ > {
495
+ const values = renderPreviousOnFailure
496
+ ? Object.values(results).map(orPrevious)
497
+ : Object.values(results)
498
+ const error = values.find(Result.isFailure)
499
+ if (error) {
500
+ return error
501
+ }
502
+ const initial = Array.findFirst(values, (x) => x._tag === "Initial" ? Option.some(x) : Option.none())
503
+ if (initial.value !== undefined) {
504
+ return initial.value
505
+ }
506
+ const loading = Array.findFirst(values, (x) => Result.isInitial(x) && x.waiting ? Option.some(x) : Option.none())
507
+ if (loading.value !== undefined) {
508
+ return loading.value
509
+ }
510
+
511
+ const isRefreshing = values.some((x) => x.waiting)
512
+
513
+ const r = Object.entries(results).reduce((prev, [key, value]) => {
514
+ prev[key] = Result.value(value).value
515
+ return prev
516
+ }, {} as any)
517
+ return Result.success(r, isRefreshing)
518
+ }
519
+
520
+ function orPrevious<E, A>(result: Result.Result<A, E>) {
521
+ return Result.isFailure(result) && Option.isSome(result.previousValue)
522
+ ? Result.success(result.previousValue.value, result.waiting)
523
+ : result
524
+ }
package/src/mutate2.ts ADDED
@@ -0,0 +1,191 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { tuple } from "@effect-app/core/Function"
3
+ import * as Result from "@effect-rx/rx/Result"
4
+ import type { InvalidateOptions, InvalidateQueryFilters } from "@tanstack/vue-query"
5
+ import { useQueryClient } from "@tanstack/vue-query"
6
+ import { Cause, Effect, Exit, Option } from "effect-app"
7
+ import type { ComputedRef, Ref } from "vue"
8
+ import { computed, ref, shallowRef } from "vue"
9
+ import { reportRuntimeError } from "./internal.js"
10
+ import { getQueryKey } from "./mutate.js"
11
+
12
+ export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
13
+ export function make<A, E, R>(self: Effect<A, E, R>) {
14
+ const result = shallowRef(Result.initial() as Result.Result<A, E>)
15
+
16
+ const execute = Effect
17
+ .sync(() => {
18
+ result.value = Result.waiting(result.value)
19
+ })
20
+ .pipe(
21
+ Effect.andThen(self),
22
+ Effect.exit,
23
+ Effect.andThen(Result.fromExit),
24
+ Effect.flatMap((r) => Effect.sync(() => result.value = r))
25
+ )
26
+
27
+ const latestSuccess = computed(() => Option.getOrUndefined(Result.value(result.value)))
28
+
29
+ return tuple(result, latestSuccess, execute)
30
+ }
31
+
32
+ export interface MutationInitial {
33
+ readonly _tag: "Initial"
34
+ }
35
+
36
+ export interface MutationLoading {
37
+ readonly _tag: "Loading"
38
+ }
39
+
40
+ export interface MutationSuccess<A> {
41
+ readonly _tag: "Success"
42
+ readonly data: A
43
+ }
44
+
45
+ export interface MutationError<E> {
46
+ readonly _tag: "Error"
47
+ readonly error: E
48
+ }
49
+
50
+ export type MutationResult<A, E> = MutationInitial | MutationLoading | MutationSuccess<A> | MutationError<E>
51
+
52
+ export type MaybeRef<T> = Ref<T> | ComputedRef<T> | T
53
+ type MaybeRefDeep<T> = MaybeRef<
54
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
55
+ T extends Function ? T
56
+ : T extends object ? {
57
+ [Property in keyof T]: MaybeRefDeep<T[Property]>
58
+ }
59
+ : T
60
+ >
61
+
62
+ export interface MutationOptions {
63
+ queryInvalidation?: (defaultKey: string[], name: string) => {
64
+ filters?: MaybeRefDeep<InvalidateQueryFilters> | undefined
65
+ options?: MaybeRefDeep<InvalidateOptions> | undefined
66
+ }[]
67
+ }
68
+
69
+ // TODO: more efficient invalidation, including args etc
70
+ // return Effect.promise(() => queryClient.invalidateQueries({
71
+ // predicate: (_) => nses.includes(_.queryKey.filter((_) => _.startsWith("$")).join("/"))
72
+ // }))
73
+ /*
74
+ // const nses: string[] = []
75
+ // for (let i = 0; i < ns.length; i++) {
76
+ // nses.push(ns.slice(0, i + 1).join("/"))
77
+ // }
78
+ */
79
+
80
+ export const makeMutation2 = () => {
81
+ type HandlerWithInput<I, A, E, R> = {
82
+ handler: (i: I) => Effect<A, E, R>
83
+ name: string
84
+ }
85
+ type Handler<A, E, R> = { handler: Effect<A, E, R>; name: string }
86
+
87
+ /**
88
+ * Pass a function that returns an Effect, e.g from a client action, or an Effect
89
+ * Returns a tuple with state ref and execution function which reports errors as Toast.
90
+ */
91
+ const useSafeMutation: {
92
+ <I, E, A, R>(
93
+ self: HandlerWithInput<I, A, E, R>,
94
+ options?: MutationOptions
95
+ ): readonly [
96
+ Readonly<Ref<MutationResult<A, E>>>,
97
+ (i: I) => Effect<A, E, R>
98
+ ]
99
+ <E, A, R>(self: Handler<A, E, R>, options?: MutationOptions): readonly [
100
+ Readonly<Ref<MutationResult<A, E>>>,
101
+ () => Effect<A, E, R> // TODO: remove () =>
102
+ ]
103
+ } = <I, E, A, R>(
104
+ self: {
105
+ handler:
106
+ | HandlerWithInput<I, A, E, R>["handler"]
107
+ | Handler<A, E, R>["handler"]
108
+ name: string
109
+ },
110
+ options?: MutationOptions
111
+ ) => {
112
+ const queryClient = useQueryClient()
113
+ const state: Ref<MutationResult<A, E>> = ref<MutationResult<A, E>>({ _tag: "Initial" }) as any
114
+
115
+ const invalidateQueries = (
116
+ filters?: MaybeRefDeep<InvalidateQueryFilters>,
117
+ options?: MaybeRefDeep<InvalidateOptions>
118
+ ) => Effect.promise(() => queryClient.invalidateQueries(filters, options))
119
+
120
+ function handleExit(exit: Exit.Exit<A, E>) {
121
+ return Effect.sync(() => {
122
+ if (Exit.isSuccess(exit)) {
123
+ state.value = { _tag: "Success", data: exit.value }
124
+ return
125
+ }
126
+
127
+ const err = Cause.failureOption(exit.cause)
128
+ if (Option.isSome(err)) {
129
+ state.value = { _tag: "Error", error: err.value }
130
+ return
131
+ }
132
+ })
133
+ }
134
+
135
+ const invalidateCache = Effect.suspend(() => {
136
+ const queryKey = getQueryKey(self.name)
137
+
138
+ if (options?.queryInvalidation) {
139
+ const opts = options.queryInvalidation(queryKey, self.name)
140
+ if (!opts.length) {
141
+ return Effect.void
142
+ }
143
+ return Effect
144
+ .andThen(
145
+ Effect.annotateCurrentSpan({ queryKey, opts }),
146
+ Effect.forEach(opts, (_) => invalidateQueries(_.filters, _.options), { concurrency: "inherit" })
147
+ )
148
+ .pipe(Effect.withSpan("client.query.invalidation", { captureStackTrace: false }))
149
+ }
150
+
151
+ if (!queryKey) return Effect.void
152
+
153
+ return Effect
154
+ .andThen(
155
+ Effect.annotateCurrentSpan({ queryKey }),
156
+ invalidateQueries({ queryKey })
157
+ )
158
+ .pipe(Effect.withSpan("client.query.invalidation", { captureStackTrace: false }))
159
+ })
160
+
161
+ const exec = (fst?: I) => {
162
+ let effect: Effect<A, E, R>
163
+ if (Effect.isEffect(self.handler)) {
164
+ effect = self.handler as any
165
+ } else {
166
+ effect = self.handler(fst as I)
167
+ }
168
+
169
+ return Effect
170
+ .sync(() => {
171
+ state.value = { _tag: "Loading" }
172
+ })
173
+ .pipe(
174
+ Effect.zipRight(effect),
175
+ Effect.tap(invalidateCache),
176
+ Effect.tapDefect(reportRuntimeError),
177
+ Effect.onExit(handleExit),
178
+ Effect.withSpan(`mutation ${self.name}`, { captureStackTrace: false })
179
+ )
180
+ }
181
+
182
+ return tuple(
183
+ state,
184
+ exec
185
+ )
186
+ }
187
+ return useSafeMutation
188
+ }
189
+
190
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
191
+ export interface MakeMutation2 extends ReturnType<typeof makeMutation2> {}