@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/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>> {}