@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.
- package/CHANGELOG.md +11 -0
- package/_cjs/makeClient2.cjs +265 -0
- package/_cjs/makeClient2.cjs.map +1 -0
- package/_cjs/mutate2.cjs +112 -0
- package/_cjs/mutate2.cjs.map +1 -0
- package/_cjs/query2.cjs +127 -0
- package/_cjs/query2.cjs.map +1 -0
- package/dist/makeClient2.d.ts +134 -0
- package/dist/makeClient2.d.ts.map +1 -0
- package/dist/makeClient2.js +240 -0
- package/dist/mutate2.d.ts +45 -0
- package/dist/mutate2.d.ts.map +1 -0
- package/dist/mutate2.js +86 -0
- package/dist/query2.d.ts +24 -0
- package/dist/query2.d.ts.map +1 -0
- package/dist/query2.js +119 -0
- package/package.json +33 -3
- package/src/makeClient2.ts +524 -0
- package/src/mutate2.ts +191 -0
- package/src/query2.ts +231 -0
|
@@ -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> {}
|