@effect-app/vue 0.130.0 → 0.131.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 +20 -0
- package/_cjs/_global.cjs +1 -2
- package/_cjs/_global.cjs.map +1 -1
- package/_cjs/form.cjs +12 -14
- package/_cjs/form.cjs.map +1 -1
- package/_cjs/hooks.cjs +27 -190
- package/_cjs/hooks.cjs.map +1 -1
- package/_cjs/internal.cjs +6 -5
- package/_cjs/internal.cjs.map +1 -1
- package/_cjs/mutate.cjs +105 -0
- package/_cjs/mutate.cjs.map +1 -0
- package/_cjs/query.cjs +53 -28
- package/_cjs/query.cjs.map +1 -1
- package/_cjs/routeParams.cjs +9 -12
- package/_cjs/routeParams.cjs.map +1 -1
- package/_cjs/runtime.cjs +19 -22
- package/_cjs/runtime.cjs.map +1 -1
- package/_src/_global.ts +1 -10
- package/_src/form.ts +9 -12
- package/_src/hooks.ts +2 -323
- package/_src/internal.ts +9 -4
- package/_src/mutate.ts +163 -0
- package/_src/query.ts +103 -32
- package/_src/routeParams.ts +6 -3
- package/_src/runtime.ts +18 -13
- package/_src/swrv.bak +196 -0
- package/dist/_global.d.ts.map +1 -1
- package/dist/_global.js +2 -10
- package/dist/form.d.ts.map +1 -1
- package/dist/form.js +15 -17
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +3 -195
- package/dist/internal.d.ts +5 -4
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +5 -3
- package/dist/mutate.d.ts +43 -0
- package/dist/mutate.d.ts.map +1 -0
- package/dist/mutate.js +101 -0
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +62 -33
- package/dist/routeParams.d.ts.map +1 -1
- package/dist/routeParams.js +14 -15
- package/dist/runtime.d.ts +24 -23
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +27 -24
- package/package.json +13 -4
- package/tsconfig.json +1 -0
- package/tsconfig.json.bak +1 -0
- package/dist/_global.d.ts +0 -9
- package/dist/form.d.ts +0 -53
- package/dist/hooks.d.ts +0 -80
- package/dist/index.d.ts +0 -4
- package/dist/query.d.ts +0 -12
- package/dist/routeParams.d.ts +0 -14
package/_src/hooks.ts
CHANGED
|
@@ -1,326 +1,5 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import { Done, Initial, Loading } from "effect-app/client"
|
|
3
|
-
import type { ApiConfig, FetchResponse } from "effect-app/client"
|
|
4
|
-
import type * as HttpClient from "@effect/platform/Http/Client"
|
|
5
|
-
import { InterruptedException } from "effect/Cause"
|
|
6
|
-
import * as Either from "effect/Either"
|
|
7
|
-
import { FiberFailureCauseId, isFiberFailure } from "effect/Runtime"
|
|
8
|
-
import * as swrv from "swrv"
|
|
9
|
-
import type { fetcherFn, IKey, IResponse } from "swrv/dist/types.js"
|
|
10
|
-
import type { ComputedRef, Ref } from "vue"
|
|
11
|
-
import { computed, ref, shallowRef } from "vue"
|
|
12
|
-
import { run } from "./internal.js"
|
|
13
2
|
|
|
14
3
|
export { isFailed, isInitializing, isSuccess } from "effect-app/client"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
<Data, Error>(key: IKey): IResponse<Data, Error>
|
|
18
|
-
<Data, Error>(
|
|
19
|
-
key: IKey,
|
|
20
|
-
fn?: fetcherFn<Data>,
|
|
21
|
-
config?: swrv.IConfig<Data, fetcherFn<Data>>
|
|
22
|
-
): IResponse<Data, Error>
|
|
23
|
-
}
|
|
24
|
-
type MutateType = {
|
|
25
|
-
<Data>(
|
|
26
|
-
key: string,
|
|
27
|
-
res: Data | Promise<Data>,
|
|
28
|
-
cache?: swrv.SWRVCache<Omit<IResponse<any, any>, "mutate">>,
|
|
29
|
-
ttl?: number
|
|
30
|
-
): Promise<{
|
|
31
|
-
data: any
|
|
32
|
-
error: any
|
|
33
|
-
isValidating: any
|
|
34
|
-
}>
|
|
35
|
-
}
|
|
36
|
-
// madness - workaround different import behavior on server and client
|
|
37
|
-
const useSWRV = (swrv.default.default ? swrv.default.default : swrv.default) as unknown as useSWRVType
|
|
38
|
-
export const mutate = (swrv.default.mutate ? swrv.default.mutate : swrv.mutate) as unknown as MutateType
|
|
39
|
-
|
|
40
|
-
function swrToQuery<E, A>(
|
|
41
|
-
r: { error: E | undefined; data: A | undefined; isValidating: boolean }
|
|
42
|
-
): QueryResult<E, A> {
|
|
43
|
-
if (r.error) {
|
|
44
|
-
return r.isValidating
|
|
45
|
-
? Refreshing.fail<E, A>(r.error, r.data)
|
|
46
|
-
: Done.fail<E, A>(r.error, r.data)
|
|
47
|
-
}
|
|
48
|
-
if (r.data !== undefined) {
|
|
49
|
-
return r.isValidating
|
|
50
|
-
? Refreshing.succeed<A, E>(r.data)
|
|
51
|
-
: Done.succeed<A, E>(r.data)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return r.isValidating ? new Loading() : new Initial()
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function useMutate<E, A>(
|
|
58
|
-
self: { handler: Effect<FetchResponse<A>, E, ApiConfig | HttpClient.Client.Default>; mapPath: string }
|
|
59
|
-
) {
|
|
60
|
-
const fn = () =>
|
|
61
|
-
run.value(self.handler).then((_) => _.body).catch((_) => {
|
|
62
|
-
if (!isFiberFailure(_)) throw _
|
|
63
|
-
const cause = _[FiberFailureCauseId]
|
|
64
|
-
throw cause.squash
|
|
65
|
-
})
|
|
66
|
-
return () => mutate(self.mapPath, fn)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function useMutateWithArg<Arg, E, A>(
|
|
70
|
-
self: {
|
|
71
|
-
handler: (arg: Arg) => Effect<FetchResponse<A>, E, ApiConfig | HttpClient.Client.Default>
|
|
72
|
-
mapPath: (arg: Arg) => string
|
|
73
|
-
}
|
|
74
|
-
) {
|
|
75
|
-
const fn = (arg: Arg) =>
|
|
76
|
-
run.value(self.handler(arg)).then((_) => _.body).catch((_) => {
|
|
77
|
-
if (!isFiberFailure(_)) throw _
|
|
78
|
-
const cause = _[FiberFailureCauseId]
|
|
79
|
-
throw cause.squash
|
|
80
|
-
})
|
|
81
|
-
return (arg: Arg) => mutate(self.mapPath(arg), fn(arg))
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
|
|
85
|
-
|
|
86
|
-
export function useSafeQuery<E, A>(
|
|
87
|
-
self: {
|
|
88
|
-
handler: Effect<FetchResponse<A>, E, ApiConfig | HttpClient.Client.Default>
|
|
89
|
-
mapPath: string
|
|
90
|
-
},
|
|
91
|
-
config?: swrv.IConfig<A, fetcherFn<A>> | undefined
|
|
92
|
-
): readonly [ComputedRef<QueryResult<E, A>>, ComputedRef<A | undefined>, () => Promise<void>, IResponse<A, E>]
|
|
93
|
-
export function useSafeQuery<Arg, E, A>(
|
|
94
|
-
self: {
|
|
95
|
-
handler: (arg: Arg) => Effect<FetchResponse<A>, E, ApiConfig | HttpClient.Client.Default>
|
|
96
|
-
mapPath: (arg: Arg) => string
|
|
97
|
-
},
|
|
98
|
-
arg: Arg | WatchSource<Arg>,
|
|
99
|
-
config?: swrv.IConfig<A, fetcherFn<A>> | undefined
|
|
100
|
-
): readonly [ComputedRef<QueryResult<E, A>>, ComputedRef<A | undefined>, () => Promise<void>, IResponse<A, E>]
|
|
101
|
-
export function useSafeQuery(
|
|
102
|
-
self: any, // {
|
|
103
|
-
// handler: (arg: Arg) => Effect<FetchResponse<A>, E, ApiConfig | HttpClient.Client.Default>
|
|
104
|
-
// mapPath: (arg: Arg) => string
|
|
105
|
-
// } | {
|
|
106
|
-
// handler: Effect<FetchResponse<A>, E, ApiConfig | HttpClient.Client.Default>
|
|
107
|
-
// mapPath: string
|
|
108
|
-
// },
|
|
109
|
-
arg?: any, // Arg | WatchSource<Arg> | swrv.IConfig<A, fetcherFn<A>> | undefined,
|
|
110
|
-
config?: any // swrv.IConfig<A, fetcherFn<A>> | undefined
|
|
111
|
-
) {
|
|
112
|
-
return Effect.isEffect(self.handler)
|
|
113
|
-
? useSafeQuery_(self.mapPath, () => self.handler, arg)
|
|
114
|
-
: useSafeQueryWithArg_(self.handler, self.mapPath, arg, config)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export function useSafeQueryWithArg_<Arg, E, A>(
|
|
118
|
-
self: (arg: Arg) => Effect<FetchResponse<A>, E, ApiConfig | HttpClient.Client.Default>,
|
|
119
|
-
mapPath: (arg: Arg) => string,
|
|
120
|
-
arg: Arg | WatchSource<Arg>,
|
|
121
|
-
config?: swrv.IConfig<A, fetcherFn<A>> | undefined
|
|
122
|
-
) {
|
|
123
|
-
const arr = arg
|
|
124
|
-
const r: { value: Arg } = typeof arr === "function"
|
|
125
|
-
? {
|
|
126
|
-
get value() {
|
|
127
|
-
return (arr as any)()
|
|
128
|
-
}
|
|
129
|
-
} as any
|
|
130
|
-
: ref(arg)
|
|
131
|
-
return useSafeQuery_(computed(() => mapPath(r.value)), () => self(r.value), config)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export function useSafeQuery_<E, A>(
|
|
135
|
-
key: string | WatchSource<string>,
|
|
136
|
-
self: () => Effect<FetchResponse<A>, E, ApiConfig | HttpClient.Client.Default>,
|
|
137
|
-
config?: swrv.IConfig<A, fetcherFn<A>> | undefined
|
|
138
|
-
) {
|
|
139
|
-
// const [result, latestSuccess, execute] = make(self)
|
|
140
|
-
|
|
141
|
-
// TODO: support with interruption
|
|
142
|
-
// const sem = Semaphore.unsafeMake(1)
|
|
143
|
-
// const lock = sem.withPermits(1)
|
|
144
|
-
// let fib: Fiber.FiberContext<E, FetchResponse<A>> | undefined = undefined
|
|
145
|
-
// const execute = self
|
|
146
|
-
// const runNew = execute.fork()
|
|
147
|
-
// .tap(newFiber =>
|
|
148
|
-
// Effect.sync(() => {
|
|
149
|
-
// fib = newFiber
|
|
150
|
-
// })
|
|
151
|
-
// )
|
|
152
|
-
|
|
153
|
-
// const ex = lock(
|
|
154
|
-
// Effect.suspend(() => {
|
|
155
|
-
// return fib
|
|
156
|
-
// ? Fiber.interrupt(fib).zipRight(runNew)
|
|
157
|
-
// : runNew
|
|
158
|
-
// })
|
|
159
|
-
// ).flatMap(Fiber.await)
|
|
160
|
-
// function execWithInterruption() {
|
|
161
|
-
// return ex.provide(Layers)
|
|
162
|
-
// .runPromise()
|
|
163
|
-
// .catch(err => {
|
|
164
|
-
// if (!Cause.isInterruptedException(err)) throw err
|
|
165
|
-
// return undefined
|
|
166
|
-
// })
|
|
167
|
-
// }
|
|
168
|
-
|
|
169
|
-
// const swr = useSWRV<A, E>(key, () => execWithInterruption().then(_ => _?.body as any)) // Effect.runPromise(self.provide(Layers))
|
|
170
|
-
const swr = useSWRV<A, E>(key, () =>
|
|
171
|
-
run
|
|
172
|
-
.value(self())
|
|
173
|
-
.then((_) => _.body)
|
|
174
|
-
.catch((_) => {
|
|
175
|
-
if (!isFiberFailure(_)) throw _
|
|
176
|
-
const cause = _[FiberFailureCauseId]
|
|
177
|
-
throw cause.squash
|
|
178
|
-
}), config)
|
|
179
|
-
const result = computed(() =>
|
|
180
|
-
swrToQuery({ data: swr.data.value, error: swr.error.value, isValidating: swr.isValidating.value })
|
|
181
|
-
) // ref<QueryResult<E, A>>()
|
|
182
|
-
const latestSuccess = computed(() => {
|
|
183
|
-
const value = result.value
|
|
184
|
-
return value.isSuccess()
|
|
185
|
-
? value.current.isRight()
|
|
186
|
-
? value.current.right
|
|
187
|
-
: value.previous.isSome()
|
|
188
|
-
? value.previous.value
|
|
189
|
-
: undefined
|
|
190
|
-
: undefined
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
return tuple(result, latestSuccess, () => swr.mutate(), swr)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
export function make<R, E, A>(self: Effect<FetchResponse<A>, E, R>) {
|
|
197
|
-
const result = shallowRef(new Initial() as QueryResult<E, A>)
|
|
198
|
-
|
|
199
|
-
const execute = Effect
|
|
200
|
-
.sync(() => {
|
|
201
|
-
result.value = result.value.isInitializing()
|
|
202
|
-
? new Loading()
|
|
203
|
-
: new Refreshing(result.value)
|
|
204
|
-
})
|
|
205
|
-
.zipRight(self.map((_) => _.body).asQueryResult)
|
|
206
|
-
.flatMap((r) => Effect.sync(() => result.value = r))
|
|
207
|
-
|
|
208
|
-
const latestSuccess = computed(() => {
|
|
209
|
-
const value = result.value
|
|
210
|
-
return value.hasValue()
|
|
211
|
-
? value.current.isRight()
|
|
212
|
-
? value.current.right
|
|
213
|
-
: value.previous.isSome()
|
|
214
|
-
? value.previous.value
|
|
215
|
-
: undefined
|
|
216
|
-
: undefined
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
return tuple(result, latestSuccess, execute)
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
export interface MutationInitial {
|
|
223
|
-
readonly _tag: "Initial"
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
export interface MutationLoading {
|
|
227
|
-
readonly _tag: "Loading"
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
export interface MutationSuccess<A> {
|
|
231
|
-
readonly _tag: "Success"
|
|
232
|
-
readonly data: A
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
export interface MutationError<E> {
|
|
236
|
-
readonly _tag: "Error"
|
|
237
|
-
readonly error: E
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
export type MutationResult<E, A> = MutationInitial | MutationLoading | MutationSuccess<A> | MutationError<E>
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Pass a function that returns an Effect, e.g from a client action, or an Effect
|
|
244
|
-
* Returns a tuple with state ref and execution function which reports errors as Toast.
|
|
245
|
-
*/
|
|
246
|
-
export const useMutation: {
|
|
247
|
-
<I, E, A>(self: { handler: (i: I) => Effect<A, E, ApiConfig | HttpClient.Client.Default> }): readonly [
|
|
248
|
-
Readonly<Ref<MutationResult<E, A>>>,
|
|
249
|
-
(
|
|
250
|
-
i: I,
|
|
251
|
-
abortSignal?: AbortSignal
|
|
252
|
-
) => Promise<Either.Either<E, A>>
|
|
253
|
-
]
|
|
254
|
-
<E, A>(self: { handler: Effect<A, E, ApiConfig | HttpClient.Client.Default> }): readonly [
|
|
255
|
-
Readonly<Ref<MutationResult<E, A>>>,
|
|
256
|
-
(
|
|
257
|
-
abortSignal?: AbortSignal
|
|
258
|
-
) => Promise<Either.Either<E, A>>
|
|
259
|
-
]
|
|
260
|
-
} = <I, E, A>(
|
|
261
|
-
self: {
|
|
262
|
-
handler:
|
|
263
|
-
| ((i: I) => Effect<A, E, ApiConfig | HttpClient.Client.Default>)
|
|
264
|
-
| Effect<A, E, ApiConfig | HttpClient.Client.Default>
|
|
265
|
-
}
|
|
266
|
-
) => {
|
|
267
|
-
const state: Ref<MutationResult<E, A>> = ref<MutationResult<E, A>>({ _tag: "Initial" }) as any
|
|
268
|
-
|
|
269
|
-
function handleExit(exit: Exit<A, E>): Effect<Either.Either<E, A>, never, never> {
|
|
270
|
-
return Effect.sync(() => {
|
|
271
|
-
if (exit.isSuccess()) {
|
|
272
|
-
state.value = { _tag: "Success", data: exit.value }
|
|
273
|
-
return Either.right(exit.value)
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const err = exit.cause.failureOption
|
|
277
|
-
if (err.isSome()) {
|
|
278
|
-
state.value = { _tag: "Error", error: err.value }
|
|
279
|
-
return Either.left(err.value)
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const died = exit.cause.dieOption
|
|
283
|
-
if (died.value) {
|
|
284
|
-
throw died.value
|
|
285
|
-
}
|
|
286
|
-
const interrupted = exit.cause.interruptOption
|
|
287
|
-
if (interrupted.value) {
|
|
288
|
-
throw new InterruptedException()
|
|
289
|
-
}
|
|
290
|
-
throw new Error("Invalid state")
|
|
291
|
-
})
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const exec = (fst?: I | AbortSignal, snd?: AbortSignal) => {
|
|
295
|
-
let effect: Effect<A, E, ApiConfig | HttpClient.Client.Default>
|
|
296
|
-
let abortSignal: AbortSignal | undefined
|
|
297
|
-
if (Effect.isEffect(self.handler)) {
|
|
298
|
-
effect = self.handler as any
|
|
299
|
-
abortSignal = fst as AbortSignal | undefined
|
|
300
|
-
} else {
|
|
301
|
-
effect = self.handler(fst as I)
|
|
302
|
-
abortSignal = snd
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
return run.value(
|
|
306
|
-
Effect
|
|
307
|
-
.sync(() => {
|
|
308
|
-
state.value = { _tag: "Loading" }
|
|
309
|
-
})
|
|
310
|
-
.zipRight(effect)
|
|
311
|
-
.exit
|
|
312
|
-
.flatMap(handleExit)
|
|
313
|
-
.fork
|
|
314
|
-
.flatMap((f) => {
|
|
315
|
-
const cancel = () => run.value(f.interrupt)
|
|
316
|
-
abortSignal?.addEventListener("abort", () => void cancel().catch(console.error))
|
|
317
|
-
return f.join
|
|
318
|
-
})
|
|
319
|
-
)
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return tuple(
|
|
323
|
-
state,
|
|
324
|
-
exec
|
|
325
|
-
)
|
|
326
|
-
}
|
|
4
|
+
export * from "./mutate.js"
|
|
5
|
+
export * from "./query.js"
|
package/_src/internal.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
|
-
import type { ApiConfig } from "effect-app/client"
|
|
2
1
|
import type * as HttpClient from "@effect/platform/Http/Client"
|
|
3
|
-
import type {
|
|
2
|
+
import type { Effect } from "effect-app"
|
|
3
|
+
import { pipe, Runtime } from "effect-app"
|
|
4
|
+
import type { ApiConfig } from "effect-app/client"
|
|
4
5
|
|
|
5
6
|
export const run = {
|
|
6
7
|
value<E, A>(_: Effect<A, E, ApiConfig | HttpClient.Client.Default>): Promise<A> {
|
|
7
8
|
throw new Error("Runtime not initialized, please run `initRuntime` first")
|
|
8
9
|
}
|
|
9
10
|
}
|
|
10
|
-
export function initRuntime<A>(rt: Runtime<A | ApiConfig | HttpClient.Client.Default>) {
|
|
11
|
+
export function initRuntime<A>(rt: Runtime.Runtime<A | ApiConfig | HttpClient.Client.Default>) {
|
|
12
|
+
const runPromise = Runtime.runPromise(rt)
|
|
11
13
|
run.value = function<E, A>(self: Effect<A, E, ApiConfig | HttpClient.Client.Default>): Promise<A> {
|
|
12
|
-
return
|
|
14
|
+
return runPromise(self)
|
|
13
15
|
}
|
|
14
16
|
}
|
|
17
|
+
|
|
18
|
+
export const makeQueryKey = (name: string) =>
|
|
19
|
+
pipe(name.split("/"), (split) => split.map((_) => "$" + _)).join("/").split(".")
|
package/_src/mutate.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { tuple } from "@effect-app/core/Function"
|
|
3
|
+
import type * as HttpClient from "@effect/platform/Http/Client"
|
|
4
|
+
import { useQueryClient } from "@tanstack/vue-query"
|
|
5
|
+
import { Cause, Effect, Exit, Fiber, Option } from "effect-app"
|
|
6
|
+
import { hasValue, Initial, isInitializing, Loading, queryResult, Refreshing } from "effect-app/client"
|
|
7
|
+
import type { ApiConfig, FetchResponse, QueryResult } from "effect-app/client"
|
|
8
|
+
import { InterruptedException } from "effect/Cause"
|
|
9
|
+
import * as Either from "effect/Either"
|
|
10
|
+
import type { ComputedRef, Ref } from "vue"
|
|
11
|
+
import { computed, ref, shallowRef } from "vue"
|
|
12
|
+
import { makeQueryKey, run } from "./internal.js"
|
|
13
|
+
|
|
14
|
+
export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
|
|
15
|
+
|
|
16
|
+
export function make<R, E, A>(self: Effect<FetchResponse<A>, E, R>) {
|
|
17
|
+
const result = shallowRef(new Initial() as QueryResult<E, A>)
|
|
18
|
+
|
|
19
|
+
const execute = Effect
|
|
20
|
+
.sync(() => {
|
|
21
|
+
result.value = isInitializing(result.value)
|
|
22
|
+
? new Loading()
|
|
23
|
+
: new Refreshing(result.value)
|
|
24
|
+
})
|
|
25
|
+
.andThen(queryResult(self.map((_) => _.body)))
|
|
26
|
+
.flatMap((r) => Effect.sync(() => result.value = r))
|
|
27
|
+
|
|
28
|
+
const latestSuccess = computed(() => {
|
|
29
|
+
const value = result.value
|
|
30
|
+
return hasValue(value)
|
|
31
|
+
? Either.isRight(value.current)
|
|
32
|
+
? value.current.right
|
|
33
|
+
: Option.isSome(value.previous)
|
|
34
|
+
? value.previous.value
|
|
35
|
+
: undefined
|
|
36
|
+
: undefined
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
return tuple(result, latestSuccess, execute)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface MutationInitial {
|
|
43
|
+
readonly _tag: "Initial"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface MutationLoading {
|
|
47
|
+
readonly _tag: "Loading"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface MutationSuccess<A> {
|
|
51
|
+
readonly _tag: "Success"
|
|
52
|
+
readonly data: A
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface MutationError<E> {
|
|
56
|
+
readonly _tag: "Error"
|
|
57
|
+
readonly error: E
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type MutationResult<E, A> = MutationInitial | MutationLoading | MutationSuccess<A> | MutationError<E>
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Pass a function that returns an Effect, e.g from a client action, or an Effect
|
|
64
|
+
* Returns a tuple with state ref and execution function which reports errors as Toast.
|
|
65
|
+
*/
|
|
66
|
+
export const useSafeMutation: {
|
|
67
|
+
<I, E, A>(self: { handler: (i: I) => Effect<A, E, ApiConfig | HttpClient.Client.Default>; name: string }): readonly [
|
|
68
|
+
Readonly<Ref<MutationResult<E, A>>>,
|
|
69
|
+
(
|
|
70
|
+
i: I,
|
|
71
|
+
abortSignal?: AbortSignal
|
|
72
|
+
) => Promise<Either.Either<E, A>>
|
|
73
|
+
]
|
|
74
|
+
<E, A>(self: { handler: Effect<A, E, ApiConfig | HttpClient.Client.Default>; name: string }): readonly [
|
|
75
|
+
Readonly<Ref<MutationResult<E, A>>>,
|
|
76
|
+
(
|
|
77
|
+
abortSignal?: AbortSignal
|
|
78
|
+
) => Promise<Either.Either<E, A>>
|
|
79
|
+
]
|
|
80
|
+
} = <I, E, A>(
|
|
81
|
+
self: {
|
|
82
|
+
handler:
|
|
83
|
+
| ((i: I) => Effect<A, E, ApiConfig | HttpClient.Client.Default>)
|
|
84
|
+
| Effect<A, E, ApiConfig | HttpClient.Client.Default>
|
|
85
|
+
name: string
|
|
86
|
+
}
|
|
87
|
+
) => {
|
|
88
|
+
const queryClient = useQueryClient()
|
|
89
|
+
const state: Ref<MutationResult<E, A>> = ref<MutationResult<E, A>>({ _tag: "Initial" }) as any
|
|
90
|
+
|
|
91
|
+
function handleExit(exit: Exit.Exit<A, E>): Effect<Either.Either<E, A>, never, never> {
|
|
92
|
+
return Effect.sync(() => {
|
|
93
|
+
if (Exit.isSuccess(exit)) {
|
|
94
|
+
state.value = { _tag: "Success", data: exit.value }
|
|
95
|
+
return Either.right(exit.value)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const err = Cause.failureOption(exit.cause)
|
|
99
|
+
if (Option.isSome(err)) {
|
|
100
|
+
state.value = { _tag: "Error", error: err.value }
|
|
101
|
+
return Either.left(err.value)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const died = Cause.dieOption(exit.cause)
|
|
105
|
+
if (Option.isSome(died)) {
|
|
106
|
+
throw died.value
|
|
107
|
+
}
|
|
108
|
+
const interrupted = Cause.interruptOption(exit.cause)
|
|
109
|
+
if (Option.isSome(interrupted)) {
|
|
110
|
+
throw new InterruptedException()
|
|
111
|
+
}
|
|
112
|
+
throw new Error("Invalid state")
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const exec = (fst?: I | AbortSignal, snd?: AbortSignal) => {
|
|
117
|
+
let effect: Effect<A, E, ApiConfig | HttpClient.Client.Default>
|
|
118
|
+
let abortSignal: AbortSignal | undefined
|
|
119
|
+
if (Effect.isEffect(self.handler)) {
|
|
120
|
+
effect = self.handler as any
|
|
121
|
+
abortSignal = fst as AbortSignal | undefined
|
|
122
|
+
} else {
|
|
123
|
+
effect = self.handler(fst as I)
|
|
124
|
+
abortSignal = snd
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return run.value(
|
|
128
|
+
Effect
|
|
129
|
+
.sync(() => {
|
|
130
|
+
state.value = { _tag: "Loading" }
|
|
131
|
+
})
|
|
132
|
+
.andThen(effect)
|
|
133
|
+
.tap(() =>
|
|
134
|
+
Effect.suspend(() => {
|
|
135
|
+
const key = makeQueryKey(self.name)
|
|
136
|
+
const ns = key.filter((_) => _.startsWith("$"))
|
|
137
|
+
const nses: string[] = []
|
|
138
|
+
for (let i = 0; i < ns.length; i++) {
|
|
139
|
+
nses.push(ns.slice(0, i + 1).join("/"))
|
|
140
|
+
}
|
|
141
|
+
return Effect.promise(() => queryClient.invalidateQueries({ queryKey: [ns[0]] }))
|
|
142
|
+
// TODO: more efficient invalidation, including args etc
|
|
143
|
+
// return Effect.promise(() => queryClient.invalidateQueries({
|
|
144
|
+
// predicate: (_) => nses.includes(_.queryKey.filter((_) => _.startsWith("$")).join("/"))
|
|
145
|
+
// }))
|
|
146
|
+
})
|
|
147
|
+
)
|
|
148
|
+
.pipe(Effect.exit)
|
|
149
|
+
.flatMap(handleExit)
|
|
150
|
+
.pipe(Effect.fork)
|
|
151
|
+
.flatMap((f) => {
|
|
152
|
+
const cancel = () => run.value(Fiber.interrupt(f))
|
|
153
|
+
abortSignal?.addEventListener("abort", () => void cancel().catch(console.error))
|
|
154
|
+
return Fiber.join(f)
|
|
155
|
+
})
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return tuple(
|
|
160
|
+
state,
|
|
161
|
+
exec
|
|
162
|
+
)
|
|
163
|
+
}
|