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

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,242 @@
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, QueryCacheUpdater, 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
+ const fullQueryKey = (
108
+ q: { readonly queryKeyProjectionHash?: string },
109
+ queryKey: ReadonlyArray<unknown>,
110
+ input: unknown
111
+ ) => q.queryKeyProjectionHash === undefined ? [...queryKey, input] : [...queryKey, q.queryKeyProjectionHash, input]
112
+
113
+ export const makeTanstackQueryCacheUpdater = (queryClient: QueryClient): QueryCacheUpdater => ({
114
+ update: <I, A, E, R, Request extends Req, Name extends string>(
115
+ _registry: ReturnType<typeof injectRegistry>,
116
+ query: RequestHandlerWithInput<I, A, E, R, Request, Name>,
117
+ input: I,
118
+ updater: (data: NoInfer<A>) => NoInfer<A>
119
+ ) => {
120
+ const queryKey = fullQueryKey(query, makeQueryKey(query), input)
121
+ if (queryClient.getQueryData(queryKey) === undefined) {
122
+ console.warn(`Query ${query.id} has not been used yet; nothing to update`)
123
+ return
124
+ }
125
+ queryClient.setQueryData(queryKey, (data: A | undefined) => data === undefined ? data : updater(data))
126
+ }
127
+ })
128
+
129
+ export const makeTanstackQuery = <R>(
130
+ getRuntime: () => Context.Context<R>,
131
+ queryClient: QueryClient
132
+ ): MakeQuery2<R> => {
133
+ const useQuery: MakeQuery2<R> = <I, A, E, Request extends Req, Name extends string>(
134
+ q: RequestHandlerWithInput<I, A, E, R, Request, Name>
135
+ ) =>
136
+ <TData = A>(
137
+ arg: I | WatchSource<I> | undefined | WatchSource<Option.Option<I>>,
138
+ options?: LegacyTanstackOptions<A, CauseException<E>, TData>
139
+ ) => {
140
+ const runPromise = makeRunPromise(getRuntime())
141
+ const queryKey = makeQueryKey(q)
142
+ const enabled = resolveEnabled(arg, options)
143
+ const tanstackOptions = {
144
+ ...(options?.staleTime !== undefined ? { staleTime: options.staleTime } : {}),
145
+ ...(typeof options?.gcTime === "number" ? { gcTime: options.gcTime } : {}),
146
+ ...(options?.refetchOnWindowFocus !== undefined ? { refetchOnWindowFocus: options.refetchOnWindowFocus } : {}),
147
+ ...(options?.structuralSharing !== undefined ? { structuralSharing: options.structuralSharing } : {}),
148
+ ...(options?.refetchInterval !== undefined ? { refetchInterval: options.refetchInterval } : {}),
149
+ ...(options?.select !== undefined ? { select: options.select } : {})
150
+ }
151
+ const tanstack = useTanstackQuery<A, CauseException<E>, TData>({
152
+ ...tanstackOptions,
153
+ enabled,
154
+ throwOnError: false,
155
+ retry: (retryCount: number, error: unknown) => isRetryable(error) && retryCount < 5,
156
+ queryKey: computed(() => {
157
+ const input = resolveInput(arg, options?.mode)
158
+ return fullQueryKey(q, queryKey, input)
159
+ }),
160
+ queryFn: ({ signal }: { readonly signal: AbortSignal }) =>
161
+ runPromise(
162
+ q
163
+ .handler(resolveInput(arg, options?.mode)!)
164
+ .pipe(
165
+ Effect.tapCauseIf(Cause.hasDies, (cause) => reportRuntimeError(cause)),
166
+ Effect.withSpan(`query ${q.id}`, {}, { captureStackTrace: false })
167
+ ),
168
+ { signal }
169
+ )
170
+ }, queryClient)
171
+
172
+ const latestSuccess = shallowRef<TData>()
173
+ const result = computed((): AsyncResult.AsyncResult<TData, E> =>
174
+ swrToQuery({
175
+ error: tanstack.error.value,
176
+ data: tanstack.data.value === undefined ? latestSuccess.value : tanstack.data.value,
177
+ isValidating: tanstack.isFetching.value
178
+ })
179
+ )
180
+ watch(result, (value) => latestSuccess.value = Option.getOrUndefined(AsyncResult.value(value)), { immediate: true })
181
+
182
+ const registry = injectRegistry()
183
+ const latestSuccessRef = computed(() => latestSuccess.value)
184
+ const atom = computed<Atom.Atom<AsyncResult.AsyncResult<TData, E>>>(() => Atom.readable(() => result.value))
185
+
186
+ const awaitResult = (): Effect.Effect<TData, E> =>
187
+ Effect
188
+ .tryPromise({
189
+ try: async () => {
190
+ const queryResult = await tanstack.suspense()
191
+ const data = queryResult.data
192
+ if (data === undefined) {
193
+ throw new Error("TanStack query resolved without data")
194
+ }
195
+ return data
196
+ },
197
+ catch: (error) => error
198
+ })
199
+ .pipe(
200
+ Effect.catch((error) => recoverCauseException<TData, E>(error))
201
+ )
202
+
203
+ const refetch = (): Effect.Effect<TData, E> =>
204
+ Effect
205
+ .tryPromise({
206
+ try: async () => {
207
+ const queryResult = await tanstack.refetch({ throwOnError: true })
208
+ const data = queryResult.data
209
+ if (data === undefined) {
210
+ throw new Error("TanStack query refetched without data")
211
+ }
212
+ return data
213
+ },
214
+ catch: (error) => error
215
+ })
216
+ .pipe(
217
+ Effect.catch((error) => recoverCauseException<TData, E>(error))
218
+ )
219
+
220
+ const handle: QueryHandle<TData, E> = {
221
+ awaitResult,
222
+ refetch,
223
+ refresh: () => {
224
+ void tanstack.refetch()
225
+ },
226
+ registry,
227
+ atom
228
+ }
229
+
230
+ const fetch = (_options?: RefetchOptions): Effect.Effect<QueryObserverResult<TData, CauseException<E>>> =>
231
+ refetch().pipe(Effect.exit, Effect.map(AsyncResult.fromExit))
232
+
233
+ return [
234
+ result,
235
+ latestSuccessRef,
236
+ fetch,
237
+ handle
238
+ ] as const
239
+ }
240
+
241
+ return useQuery
242
+ }