@effect-app/vue 4.0.0-beta.271 → 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.
- package/CHANGELOG.md +16 -0
- package/dist/atomQuery.d.ts +90 -0
- package/dist/atomQuery.d.ts.map +1 -0
- package/dist/atomQuery.js +275 -0
- package/dist/internal/tanstackQuery.d.ts +8 -0
- package/dist/internal/tanstackQuery.d.ts.map +1 -0
- package/dist/internal/tanstackQuery.js +145 -0
- package/dist/makeClient.d.ts +75 -13
- package/dist/makeClient.d.ts.map +1 -1
- package/dist/makeClient.js +203 -78
- package/dist/mutate.d.ts +20 -12
- package/dist/mutate.d.ts.map +1 -1
- package/dist/mutate.js +65 -64
- package/dist/query.d.ts +114 -12
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +275 -179
- package/docs/atom-query-api-redesign.md +191 -0
- package/package.json +2 -2
- package/src/atomQuery.ts +361 -0
- package/src/internal/tanstackQuery.ts +221 -0
- package/src/makeClient.ts +382 -91
- package/src/mutate.ts +101 -110
- package/src/query.ts +564 -243
- package/test/dist/stubs.d.ts +169 -2
- package/test/dist/stubs.d.ts.map +1 -1
- package/test/dist/stubs.js +11 -6
- package/test/makeClient.test.ts +110 -0
- package/test/stubs.ts +10 -5
- package/tsconfig.json.bak +72 -14
|
@@ -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
|
+
}
|