@effect-app/vue 1.25.2 → 1.26.1
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 +19 -0
- package/_cjs/makeClient2.cjs +270 -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 +101 -0
- package/dist/makeClient2.d.ts.map +1 -0
- package/dist/makeClient2.js +223 -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 +445 -0
- package/src/mutate2.ts +191 -0
- package/src/query2.ts +231 -0
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> {}
|
package/src/query2.ts
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
|
3
|
+
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
|
4
|
+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
5
|
+
import { isHttpRequestError, isHttpResponseError } from "@effect-app/core/http/http-client"
|
|
6
|
+
import * as Result from "@effect-rx/rx/Result"
|
|
7
|
+
import type {
|
|
8
|
+
QueryKey,
|
|
9
|
+
QueryObserverOptions,
|
|
10
|
+
QueryObserverResult,
|
|
11
|
+
RefetchOptions,
|
|
12
|
+
UseQueryReturnType
|
|
13
|
+
} from "@tanstack/vue-query"
|
|
14
|
+
import { useQuery } from "@tanstack/vue-query"
|
|
15
|
+
import { Cause, Effect, Option, Runtime, S } from "effect-app"
|
|
16
|
+
import { ServiceUnavailableError } from "effect-app/client"
|
|
17
|
+
import { computed, ref } from "vue"
|
|
18
|
+
import type { ComputedRef, Ref, WatchSource } from "vue"
|
|
19
|
+
import { makeQueryKey, reportRuntimeError } from "./internal.js"
|
|
20
|
+
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
22
|
+
export interface QueryObserverOptionsCustom<
|
|
23
|
+
TQueryFnData = unknown,
|
|
24
|
+
TError = Error,
|
|
25
|
+
TData = TQueryFnData,
|
|
26
|
+
TQueryData = TQueryFnData,
|
|
27
|
+
TQueryKey extends QueryKey = QueryKey,
|
|
28
|
+
TPageParam = never
|
|
29
|
+
> extends
|
|
30
|
+
Omit<QueryObserverOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey, TPageParam>, "queryKey" | "queryFn">
|
|
31
|
+
{}
|
|
32
|
+
|
|
33
|
+
export interface KnownFiberFailure<E> extends Runtime.FiberFailure {
|
|
34
|
+
readonly [Runtime.FiberFailureCauseId]: Cause.Cause<E>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const makeQuery2 = <R>(runtime: Ref<Runtime.Runtime<R>>) => {
|
|
38
|
+
// TODO: options
|
|
39
|
+
// declare function useQuery<TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey>(options: UndefinedInitialQueryOptions<TQueryFnData, TError, TData, TQueryKey>, queryClient?: QueryClient): UseQueryReturnType<TData, TError>;
|
|
40
|
+
// declare function useQuery<TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey>(options: DefinedInitialQueryOptions<TQueryFnData, TError, TData, TQueryKey>, queryClient?: QueryClient): UseQueryDefinedReturnType<TData, TError>;
|
|
41
|
+
// declare function useQuery<TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey>(options: UseQueryOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey>, queryClient?: QueryClient): UseQueryReturnType<TData, TError>;
|
|
42
|
+
const useSafeQuery_ = <I, A, E>(
|
|
43
|
+
q:
|
|
44
|
+
| {
|
|
45
|
+
readonly handler: (
|
|
46
|
+
req: I
|
|
47
|
+
) => Effect<
|
|
48
|
+
A,
|
|
49
|
+
E,
|
|
50
|
+
R
|
|
51
|
+
>
|
|
52
|
+
mapPath: (req: I) => string
|
|
53
|
+
name: string
|
|
54
|
+
}
|
|
55
|
+
| {
|
|
56
|
+
readonly handler: Effect<
|
|
57
|
+
A,
|
|
58
|
+
E,
|
|
59
|
+
R
|
|
60
|
+
>
|
|
61
|
+
mapPath: string
|
|
62
|
+
name: string
|
|
63
|
+
},
|
|
64
|
+
arg?: I | WatchSource<I>,
|
|
65
|
+
options: QueryObserverOptionsCustom<unknown, KnownFiberFailure<E>, A> = {} // TODO
|
|
66
|
+
) => {
|
|
67
|
+
const runPromise = Runtime.runPromise(runtime.value)
|
|
68
|
+
const arr = arg
|
|
69
|
+
const req: { value: I } = !arg
|
|
70
|
+
? undefined
|
|
71
|
+
: typeof arr === "function"
|
|
72
|
+
? ({
|
|
73
|
+
get value() {
|
|
74
|
+
return (arr as any)()
|
|
75
|
+
}
|
|
76
|
+
} as any)
|
|
77
|
+
: ref(arg)
|
|
78
|
+
const queryKey = makeQueryKey(q.name)
|
|
79
|
+
const handler = q.handler
|
|
80
|
+
const r = useQuery<unknown, KnownFiberFailure<E>, A>(
|
|
81
|
+
Effect.isEffect(handler)
|
|
82
|
+
? {
|
|
83
|
+
...options,
|
|
84
|
+
retry: (retryCount, error) => {
|
|
85
|
+
if (Runtime.isFiberFailure(error)) {
|
|
86
|
+
const cause = error[Runtime.FiberFailureCauseId]
|
|
87
|
+
const sq = Cause.squash(cause)
|
|
88
|
+
if (!isHttpRequestError(sq) && !isHttpResponseError(sq) && !S.is(ServiceUnavailableError)(sq)) {
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return retryCount < 5
|
|
94
|
+
},
|
|
95
|
+
queryKey,
|
|
96
|
+
queryFn: ({ signal }) =>
|
|
97
|
+
runPromise(
|
|
98
|
+
handler
|
|
99
|
+
.pipe(
|
|
100
|
+
Effect.tapDefect(reportRuntimeError),
|
|
101
|
+
Effect.withSpan(`query ${q.name}`, { captureStackTrace: false })
|
|
102
|
+
),
|
|
103
|
+
{ signal }
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
: {
|
|
107
|
+
...options,
|
|
108
|
+
retry: (retryCount, error) => {
|
|
109
|
+
if (Runtime.isFiberFailure(error)) {
|
|
110
|
+
const cause = error[Runtime.FiberFailureCauseId]
|
|
111
|
+
const sq = Cause.squash(cause)
|
|
112
|
+
if (!isHttpRequestError(sq) && !isHttpResponseError(sq) && !S.is(ServiceUnavailableError)(sq)) {
|
|
113
|
+
return false
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return retryCount < 5
|
|
118
|
+
},
|
|
119
|
+
queryKey: [...queryKey, req],
|
|
120
|
+
queryFn: ({ signal }) =>
|
|
121
|
+
runPromise(
|
|
122
|
+
handler(req.value)
|
|
123
|
+
.pipe(
|
|
124
|
+
Effect.tapDefect(reportRuntimeError),
|
|
125
|
+
Effect.withSpan(`query ${q.name}`, { captureStackTrace: false })
|
|
126
|
+
),
|
|
127
|
+
{ signal }
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
const result = computed(() =>
|
|
133
|
+
swrToQuery({
|
|
134
|
+
error: r.error.value ?? undefined,
|
|
135
|
+
data: r.data.value,
|
|
136
|
+
isValidating: r.isFetching.value
|
|
137
|
+
})
|
|
138
|
+
)
|
|
139
|
+
const latestSuccess = computed(() => Option.getOrUndefined(Result.value(result.value)))
|
|
140
|
+
return [
|
|
141
|
+
result,
|
|
142
|
+
latestSuccess,
|
|
143
|
+
// one thing to keep in mind is that span will be disconnected as Context does not pass from outside.
|
|
144
|
+
(options?: RefetchOptions) => Effect.promise(() => r.refetch(options)),
|
|
145
|
+
r
|
|
146
|
+
] as const
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function swrToQuery<E, A>(r: {
|
|
150
|
+
error: KnownFiberFailure<E> | undefined
|
|
151
|
+
data: A | undefined
|
|
152
|
+
isValidating: boolean
|
|
153
|
+
}): Result.Result<A, E> {
|
|
154
|
+
if (r.error) {
|
|
155
|
+
return Result.failureWithPrevious(
|
|
156
|
+
r.error[Runtime.FiberFailureCauseId],
|
|
157
|
+
r.data === undefined ? Option.none() : Option.some(Result.success(r.data)),
|
|
158
|
+
r.isValidating
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
if (r.data !== undefined) {
|
|
162
|
+
return Result.success<A, E>(r.data, r.isValidating)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return Result.initial(r.isValidating)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function useSafeQuery<E, A>(
|
|
169
|
+
self: {
|
|
170
|
+
handler: Effect<A, E, R>
|
|
171
|
+
mapPath: string
|
|
172
|
+
name: string
|
|
173
|
+
},
|
|
174
|
+
options?: QueryObserverOptionsCustom // TODO
|
|
175
|
+
): readonly [
|
|
176
|
+
ComputedRef<Result.Result<A, E>>,
|
|
177
|
+
ComputedRef<A | undefined>,
|
|
178
|
+
(options?: RefetchOptions) => Effect<QueryObserverResult<A, KnownFiberFailure<E>>>,
|
|
179
|
+
UseQueryReturnType<any, any>
|
|
180
|
+
]
|
|
181
|
+
function useSafeQuery<Arg, E, A>(
|
|
182
|
+
self: {
|
|
183
|
+
handler: (arg: Arg) => Effect<A, E, R>
|
|
184
|
+
mapPath: (arg: Arg) => string
|
|
185
|
+
name: string
|
|
186
|
+
},
|
|
187
|
+
arg: Arg | WatchSource<Arg>,
|
|
188
|
+
options?: QueryObserverOptionsCustom // TODO
|
|
189
|
+
): readonly [
|
|
190
|
+
ComputedRef<Result.Result<A, E>>,
|
|
191
|
+
ComputedRef<A | undefined>,
|
|
192
|
+
(options?: RefetchOptions) => Effect<QueryObserverResult<A, KnownFiberFailure<E>>>,
|
|
193
|
+
UseQueryReturnType<any, any>
|
|
194
|
+
]
|
|
195
|
+
function useSafeQuery(
|
|
196
|
+
self: any,
|
|
197
|
+
/*
|
|
198
|
+
q:
|
|
199
|
+
| {
|
|
200
|
+
handler: (
|
|
201
|
+
req: I
|
|
202
|
+
) => Effect<
|
|
203
|
+
A,
|
|
204
|
+
E,
|
|
205
|
+
R
|
|
206
|
+
>
|
|
207
|
+
mapPath: (req: I) => string
|
|
208
|
+
name: string
|
|
209
|
+
}
|
|
210
|
+
| {
|
|
211
|
+
handler: Effect<
|
|
212
|
+
A,
|
|
213
|
+
E,
|
|
214
|
+
R
|
|
215
|
+
>
|
|
216
|
+
mapPath: string
|
|
217
|
+
name: string
|
|
218
|
+
},
|
|
219
|
+
*/
|
|
220
|
+
argOrOptions?: any,
|
|
221
|
+
options?: any
|
|
222
|
+
) {
|
|
223
|
+
return Effect.isEffect(self.handler)
|
|
224
|
+
? useSafeQuery_(self, undefined, argOrOptions)
|
|
225
|
+
: useSafeQuery_(self, argOrOptions, options)
|
|
226
|
+
}
|
|
227
|
+
return useSafeQuery
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
231
|
+
export interface MakeQuery2<R> extends ReturnType<typeof makeQuery2<R>> {}
|