@effect-app/vue 4.0.0-beta.272 → 4.0.0-beta.273

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,221 @@
1
+ import { injectRegistry } from "@effect/atom-vue"
2
+ import { QueryClient, useQuery as useTanstackQuery } from "@tanstack/vue-query"
3
+ import { makeQueryKey, type Req } from "effect-app/client"
4
+ import type { RequestHandlerWithInput } from "effect-app/client/clientFor"
5
+ import { CauseException, ServiceUnavailableError } from "effect-app/client/errors"
6
+ import type * as Context from "effect-app/Context"
7
+ import * as Effect from "effect-app/Effect"
8
+ import * as Option from "effect-app/Option"
9
+ import * as S from "effect-app/Schema"
10
+ import * as Cause from "effect/Cause"
11
+ import { isHttpClientError } from "effect/unstable/http/HttpClientError"
12
+ import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
13
+ import * as Atom from "effect/unstable/reactivity/Atom"
14
+ import { computed, type MaybeRefOrGetter, shallowRef, toValue, watch, type WatchSource } from "vue"
15
+ import { reportRuntimeError } from "../lib.ts"
16
+ import type { QueryInvalidator } from "../mutate.ts"
17
+ import type { CustomDefinedInitialQueryOptions, CustomDefinedPlaceholderQueryOptions, CustomUndefinedInitialQueryOptions, CustomUseQueryOptions, MakeQuery2, QueryHandle, QueryObserverResult, RefetchOptions } from "../query.ts"
18
+ import { makeRunPromise } from "../runtime.ts"
19
+
20
+ const swrToQuery = <E, A>(r: {
21
+ readonly error: CauseException<E> | null | undefined
22
+ readonly data: A | undefined
23
+ readonly isValidating: boolean
24
+ }): AsyncResult.AsyncResult<A, E> => {
25
+ if (r.error !== undefined && r.error !== null) {
26
+ return AsyncResult.failureWithPrevious(
27
+ r.error.originalCause,
28
+ {
29
+ previous: r.data === undefined ? Option.none() : Option.some(AsyncResult.success(r.data)),
30
+ waiting: r.isValidating
31
+ }
32
+ )
33
+ }
34
+ if (r.data !== undefined) {
35
+ return AsyncResult.success<A, E>(r.data, { waiting: r.isValidating })
36
+ }
37
+
38
+ return AsyncResult.initial(r.isValidating)
39
+ }
40
+
41
+ const recoverCauseException = <A, E>(error: unknown): Effect.Effect<A, E> =>
42
+ error instanceof CauseException
43
+ ? Effect.failCause(error.originalCause)
44
+ : Effect.die(error)
45
+
46
+ const isRetryable = (error: unknown) => {
47
+ if (error instanceof CauseException) {
48
+ return isHttpClientError(error.cause) || S.is(ServiceUnavailableError)(error.cause)
49
+ }
50
+ return false
51
+ }
52
+
53
+ const isInputOption = <I>(value: I | Option.Option<I> | undefined): value is Option.Option<I> => Option.isOption(value)
54
+
55
+ const resolveInput = <I>(
56
+ arg: I | WatchSource<I> | undefined | WatchSource<Option.Option<I>>,
57
+ mode: "optional" | undefined
58
+ ): I | undefined => {
59
+ if (mode === "optional") {
60
+ const option = toValue(arg)
61
+ return isInputOption(option) && Option.isSome(option) ? option.value : undefined
62
+ }
63
+ const value = toValue(arg)
64
+ return isInputOption(value) ? undefined : value
65
+ }
66
+
67
+ const resolveEnabled = <I>(
68
+ arg: I | WatchSource<I> | undefined | WatchSource<Option.Option<I>>,
69
+ options: {
70
+ readonly mode?: "optional" | undefined
71
+ readonly enabled?: MaybeRefOrGetter<boolean | undefined> | undefined
72
+ } | undefined
73
+ ) => {
74
+ if (options?.mode === "optional") {
75
+ return computed(() => {
76
+ const option = toValue(arg)
77
+ return Option.isSome(option)
78
+ })
79
+ }
80
+ return computed(() => {
81
+ const enabled = options?.enabled
82
+ if (enabled === undefined) return true
83
+ return !!toValue(enabled)
84
+ })
85
+ }
86
+
87
+ type LegacyTanstackOptions<A, E, TData> =
88
+ & (
89
+ | CustomUndefinedInitialQueryOptions<A, E, TData>
90
+ | CustomDefinedInitialQueryOptions<A, E, TData>
91
+ | CustomDefinedPlaceholderQueryOptions<A, E, TData>
92
+ | CustomUseQueryOptions<A, E, TData>
93
+ )
94
+ & { readonly mode?: "optional" | undefined }
95
+
96
+ export const makeTanstackQueryClient = () => new QueryClient()
97
+
98
+ export const makeTanstackQueryInvalidator = (queryClient: QueryClient): QueryInvalidator => ({
99
+ invalidateAndAwait: (keys) =>
100
+ Effect.forEach(
101
+ keys,
102
+ (queryKey) => Effect.promise(() => queryClient.invalidateQueries({ queryKey })),
103
+ { discard: true, concurrency: "inherit" }
104
+ )
105
+ })
106
+
107
+ export const makeTanstackQuery = <R>(
108
+ getRuntime: () => Context.Context<R>,
109
+ queryClient: QueryClient
110
+ ): MakeQuery2<R> => {
111
+ const useQuery: MakeQuery2<R> = <I, A, E, Request extends Req, Name extends string>(
112
+ q: RequestHandlerWithInput<I, A, E, R, Request, Name>
113
+ ) =>
114
+ <TData = A>(
115
+ arg: I | WatchSource<I> | undefined | WatchSource<Option.Option<I>>,
116
+ options?: LegacyTanstackOptions<A, CauseException<E>, TData>
117
+ ) => {
118
+ const runPromise = makeRunPromise(getRuntime())
119
+ const queryKey = makeQueryKey(q)
120
+ const projectionHash = q.queryKeyProjectionHash
121
+ const enabled = resolveEnabled(arg, options)
122
+ const tanstackOptions = {
123
+ ...(options?.staleTime !== undefined ? { staleTime: options.staleTime } : {}),
124
+ ...(typeof options?.gcTime === "number" ? { gcTime: options.gcTime } : {}),
125
+ ...(options?.refetchOnWindowFocus !== undefined ? { refetchOnWindowFocus: options.refetchOnWindowFocus } : {}),
126
+ ...(options?.structuralSharing !== undefined ? { structuralSharing: options.structuralSharing } : {}),
127
+ ...(options?.refetchInterval !== undefined ? { refetchInterval: options.refetchInterval } : {}),
128
+ ...(options?.select !== undefined ? { select: options.select } : {})
129
+ }
130
+ const tanstack = useTanstackQuery<A, CauseException<E>, TData>({
131
+ ...tanstackOptions,
132
+ enabled,
133
+ throwOnError: false,
134
+ retry: (retryCount: number, error: unknown) => isRetryable(error) && retryCount < 5,
135
+ queryKey: computed(() => {
136
+ const input = resolveInput(arg, options?.mode)
137
+ return projectionHash === undefined ? [...queryKey, input] : [...queryKey, projectionHash, input]
138
+ }),
139
+ queryFn: ({ signal }: { readonly signal: AbortSignal }) =>
140
+ runPromise(
141
+ q
142
+ .handler(resolveInput(arg, options?.mode)!)
143
+ .pipe(
144
+ Effect.tapCauseIf(Cause.hasDies, (cause) => reportRuntimeError(cause)),
145
+ Effect.withSpan(`query ${q.id}`, {}, { captureStackTrace: false })
146
+ ),
147
+ { signal }
148
+ )
149
+ }, queryClient)
150
+
151
+ const latestSuccess = shallowRef<TData>()
152
+ const result = computed((): AsyncResult.AsyncResult<TData, E> =>
153
+ swrToQuery({
154
+ error: tanstack.error.value,
155
+ data: tanstack.data.value === undefined ? latestSuccess.value : tanstack.data.value,
156
+ isValidating: tanstack.isFetching.value
157
+ })
158
+ )
159
+ watch(result, (value) => latestSuccess.value = Option.getOrUndefined(AsyncResult.value(value)), { immediate: true })
160
+
161
+ const registry = injectRegistry()
162
+ const latestSuccessRef = computed(() => latestSuccess.value)
163
+ const atom = computed<Atom.Atom<AsyncResult.AsyncResult<TData, E>>>(() => Atom.readable(() => result.value))
164
+
165
+ const awaitResult = (): Effect.Effect<TData, E> =>
166
+ Effect
167
+ .tryPromise({
168
+ try: async () => {
169
+ const queryResult = await tanstack.suspense()
170
+ const data = queryResult.data
171
+ if (data === undefined) {
172
+ throw new Error("TanStack query resolved without data")
173
+ }
174
+ return data
175
+ },
176
+ catch: (error) => error
177
+ })
178
+ .pipe(
179
+ Effect.catch((error) => recoverCauseException<TData, E>(error))
180
+ )
181
+
182
+ const refetch = (): Effect.Effect<TData, E> =>
183
+ Effect
184
+ .tryPromise({
185
+ try: async () => {
186
+ const queryResult = await tanstack.refetch({ throwOnError: true })
187
+ const data = queryResult.data
188
+ if (data === undefined) {
189
+ throw new Error("TanStack query refetched without data")
190
+ }
191
+ return data
192
+ },
193
+ catch: (error) => error
194
+ })
195
+ .pipe(
196
+ Effect.catch((error) => recoverCauseException<TData, E>(error))
197
+ )
198
+
199
+ const handle: QueryHandle<TData, E> = {
200
+ awaitResult,
201
+ refetch,
202
+ refresh: () => {
203
+ void tanstack.refetch()
204
+ },
205
+ registry,
206
+ atom
207
+ }
208
+
209
+ const fetch = (_options?: RefetchOptions): Effect.Effect<QueryObserverResult<TData, CauseException<E>>> =>
210
+ refetch().pipe(Effect.exit, Effect.map(AsyncResult.fromExit))
211
+
212
+ return [
213
+ result,
214
+ latestSuccessRef,
215
+ fetch,
216
+ handle
217
+ ] as const
218
+ }
219
+
220
+ return useQuery
221
+ }