@effect-app/vue 4.0.0-beta.99 → 4.0.0
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 +2701 -0
- package/dist/atomQuery.d.ts +120 -0
- package/dist/atomQuery.d.ts.map +1 -0
- package/dist/atomQuery.js +434 -0
- package/dist/commander.d.ts +318 -43
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +593 -81
- package/dist/confirm.d.ts +5 -3
- package/dist/confirm.d.ts.map +1 -1
- package/dist/confirm.js +12 -14
- package/dist/dependencyMetadata.d.ts +18 -0
- package/dist/dependencyMetadata.d.ts.map +1 -0
- package/dist/dependencyMetadata.js +67 -0
- package/dist/errorReporter.d.ts +6 -4
- package/dist/errorReporter.d.ts.map +1 -1
- package/dist/errorReporter.js +14 -19
- package/dist/form.d.ts +5 -5
- package/dist/form.d.ts.map +1 -1
- package/dist/form.js +16 -10
- package/dist/internal/tanstackQuery.d.ts +9 -0
- package/dist/internal/tanstackQuery.d.ts.map +1 -0
- package/dist/internal/tanstackQuery.js +204 -0
- package/dist/intl.d.ts +4 -4
- package/dist/intl.d.ts.map +1 -1
- package/dist/intl.js +2 -2
- package/dist/lib.d.ts +8 -10
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +37 -12
- package/dist/liveQueryInvalidation.d.ts +16 -0
- package/dist/liveQueryInvalidation.d.ts.map +1 -0
- package/dist/liveQueryInvalidation.js +115 -0
- package/dist/makeClient.d.ts +232 -109
- package/dist/makeClient.d.ts.map +1 -1
- package/dist/makeClient.js +416 -84
- package/dist/makeContext.d.ts.map +1 -1
- package/dist/makeIntl.d.ts.map +1 -1
- package/dist/makeUseCommand.d.ts +3 -2
- package/dist/makeUseCommand.d.ts.map +1 -1
- package/dist/makeUseCommand.js +2 -2
- package/dist/mutate.d.ts +110 -41
- package/dist/mutate.d.ts.map +1 -1
- package/dist/mutate.js +236 -59
- package/dist/query.d.ts +161 -44
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +382 -113
- package/dist/queryLifetime.d.ts +25 -0
- package/dist/queryLifetime.d.ts.map +1 -0
- package/dist/queryLifetime.js +54 -0
- package/dist/routeParams.d.ts +4 -4
- package/dist/routeParams.d.ts.map +1 -1
- package/dist/routeParams.js +4 -3
- package/dist/runtime.d.ts +1 -17
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +2 -38
- package/dist/suspense.d.ts +19 -0
- package/dist/suspense.d.ts.map +1 -0
- package/dist/suspense.js +38 -0
- package/dist/toast.d.ts +1 -45
- package/dist/toast.d.ts.map +1 -1
- package/dist/toast.js +2 -32
- package/dist/withToast.d.ts +1 -24
- package/dist/withToast.d.ts.map +1 -1
- package/dist/withToast.js +2 -45
- package/docs/atom-query-api-redesign.md +207 -0
- package/docs/mutation-command-atoms.md +40 -0
- package/docs/query-key-invalidation.md +107 -0
- package/examples/streamMutation.ts +72 -0
- package/package.json +35 -94
- package/src/atomQuery.ts +600 -0
- package/src/commander.ts +1064 -116
- package/src/confirm.ts +12 -14
- package/src/dependencyMetadata.ts +84 -0
- package/src/errorReporter.ts +65 -75
- package/src/form.ts +22 -15
- package/src/index.ts +7 -7
- package/src/internal/tanstackQuery.ts +308 -0
- package/src/intl.ts +2 -2
- package/src/lib.ts +51 -23
- package/src/liveQueryInvalidation.ts +138 -0
- package/src/makeClient.ts +972 -263
- package/src/makeIntl.ts +2 -2
- package/src/makeUseCommand.ts +8 -5
- package/src/mutate.ts +407 -154
- package/src/query.ts +758 -242
- package/src/queryLifetime.ts +74 -0
- package/src/routeParams.ts +7 -7
- package/src/runtime.ts +1 -54
- package/src/suspense.ts +43 -0
- package/src/toast.ts +1 -52
- package/src/withToast.ts +1 -99
- package/test/Mutation.test.ts +104 -17
- package/test/dependencyInvalidation.test.ts +444 -0
- package/test/dist/dependencyInvalidation.test.d.ts.map +1 -0
- package/test/dist/form.test.d.ts.map +1 -1
- package/test/dist/inactiveInvalidation.test.d.ts.map +1 -0
- package/test/dist/interrupt-refetch-repro.test.d.ts.map +1 -0
- package/test/dist/lib.test.d.ts.map +1 -0
- package/test/dist/liveQueryInvalidation.test.d.ts.map +1 -0
- package/test/dist/query-span.test.d.ts.map +1 -0
- package/test/dist/queryOptions.test.d.ts.map +1 -0
- package/test/dist/streamFinal.test.d.ts.map +1 -0
- package/test/dist/streamFn.test.d.ts.map +1 -0
- package/test/dist/stubs.d.ts +3681 -256
- package/test/dist/stubs.d.ts.map +1 -1
- package/test/dist/stubs.js +166 -25
- package/test/dist/suspense-regression.test.d.ts.map +1 -0
- package/test/dist/suspense.test.d.ts.map +1 -0
- package/test/form-validation-errors.test.ts +2 -1
- package/test/form.test.ts +2 -1
- package/test/inactiveInvalidation.test.ts +172 -0
- package/test/interrupt-refetch-repro.test.ts +625 -0
- package/test/lib.test.ts +240 -0
- package/test/liveQueryInvalidation.test.ts +108 -0
- package/test/makeClient.test.ts +414 -32
- package/test/query-span.test.ts +92 -0
- package/test/queryOptions.test.ts +88 -0
- package/test/streamFinal.test.ts +64 -0
- package/test/streamFn.test.ts +457 -0
- package/test/stubs.ts +194 -36
- package/test/suspense-regression.test.ts +168 -0
- package/test/suspense.test.ts +160 -0
- package/tsconfig.examples.json +20 -0
- package/tsconfig.json +9 -1
- package/tsconfig.src.json +34 -34
- package/tsconfig.test.json +2 -2
- package/vitest.config.ts +5 -5
- package/eslint.config.mjs +0 -24
- package/tsconfig.json.bak +0 -12
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { injectRegistry } from "@effect/atom-vue"
|
|
2
|
+
import { QueryClient, useQuery as useTanstackQuery } from "@tanstack/vue-query"
|
|
3
|
+
import { DataDependencies, 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/http/HttpClientError"
|
|
12
|
+
import * as AsyncResult from "effect/reactivity/AsyncResult"
|
|
13
|
+
import * as Atom from "effect/reactivity/Atom"
|
|
14
|
+
import * as Ref from "effect/Ref"
|
|
15
|
+
import type * as Tracer from "effect/Tracer"
|
|
16
|
+
import { computed, type MaybeRefOrGetter, shallowRef, toValue, watch, type WatchSource } from "vue"
|
|
17
|
+
import { replaceEqualDeep } from "../atomQuery.ts"
|
|
18
|
+
import { clearQueryReadDependencies, registerQueryInvalidationMode, setQueryReadDependencies } from "../dependencyMetadata.ts"
|
|
19
|
+
import { reportRuntimeError } from "../lib.ts"
|
|
20
|
+
import type { QueryInvalidator } from "../mutate.ts"
|
|
21
|
+
import type { CustomDefinedInitialQueryOptions, CustomDefinedPlaceholderQueryOptions, CustomUndefinedInitialQueryOptions, CustomUseQueryOptions, MakeQuery2, QueryCacheUpdater, QueryHandle, QueryObserverResult, RefetchOptions } from "../query.ts"
|
|
22
|
+
import { makeRunPromise } from "../runtime.ts"
|
|
23
|
+
|
|
24
|
+
const swrToQuery = <E, A>(r: {
|
|
25
|
+
readonly error: CauseException<E> | null | undefined
|
|
26
|
+
readonly data: A | undefined
|
|
27
|
+
readonly isValidating: boolean
|
|
28
|
+
}): AsyncResult.AsyncResult<A, E> => {
|
|
29
|
+
if (r.error !== undefined && r.error !== null) {
|
|
30
|
+
return AsyncResult.failureWithPrevious(
|
|
31
|
+
r.error.originalCause,
|
|
32
|
+
{
|
|
33
|
+
previous: r.data === undefined ? Option.none() : Option.some(AsyncResult.success(r.data)),
|
|
34
|
+
waiting: r.isValidating
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
if (r.data !== undefined) {
|
|
39
|
+
return AsyncResult.success<A, E>(r.data, { waiting: r.isValidating })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return AsyncResult.initial(r.isValidating)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const recoverCauseException = <A, E>(error: unknown): Effect.Effect<A, E> =>
|
|
46
|
+
error instanceof CauseException
|
|
47
|
+
? Effect.failCause(error.originalCause)
|
|
48
|
+
: Effect.die(error)
|
|
49
|
+
|
|
50
|
+
const isRetryable = (error: unknown) => {
|
|
51
|
+
if (error instanceof CauseException) {
|
|
52
|
+
return isHttpClientError(error.cause) || S.is(ServiceUnavailableError)(error.cause)
|
|
53
|
+
}
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const isInputOption = <I>(value: I | Option.Option<I> | undefined): value is Option.Option<I> => Option.isOption(value)
|
|
58
|
+
|
|
59
|
+
const resolveInput = <I>(
|
|
60
|
+
arg: I | WatchSource<I> | undefined | WatchSource<Option.Option<I>>,
|
|
61
|
+
mode: "optional" | undefined
|
|
62
|
+
): I | undefined => {
|
|
63
|
+
if (mode === "optional") {
|
|
64
|
+
const option = toValue(arg)
|
|
65
|
+
return isInputOption(option) && Option.isSome(option) ? option.value : undefined
|
|
66
|
+
}
|
|
67
|
+
const value = toValue(arg)
|
|
68
|
+
return isInputOption(value) ? undefined : value
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const resolveEnabled = <I>(
|
|
72
|
+
arg: I | WatchSource<I> | undefined | WatchSource<Option.Option<I>>,
|
|
73
|
+
options: {
|
|
74
|
+
readonly mode?: "optional" | undefined
|
|
75
|
+
readonly enabled?: MaybeRefOrGetter<boolean | undefined> | undefined
|
|
76
|
+
} | undefined
|
|
77
|
+
) => {
|
|
78
|
+
if (options?.mode === "optional") {
|
|
79
|
+
return computed(() => {
|
|
80
|
+
const option = toValue(arg)
|
|
81
|
+
return Option.isSome(option)
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
return computed(() => {
|
|
85
|
+
const enabled = options?.enabled
|
|
86
|
+
if (enabled === undefined) return true
|
|
87
|
+
return !!toValue(enabled)
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
type LegacyTanstackOptions<A, E, TData> =
|
|
92
|
+
& (
|
|
93
|
+
| CustomUndefinedInitialQueryOptions<A, E, TData>
|
|
94
|
+
| CustomDefinedInitialQueryOptions<A, E, TData>
|
|
95
|
+
| CustomDefinedPlaceholderQueryOptions<A, E, TData>
|
|
96
|
+
| CustomUseQueryOptions<A, E, TData>
|
|
97
|
+
)
|
|
98
|
+
& { readonly mode?: "optional" | undefined }
|
|
99
|
+
|
|
100
|
+
export const makeTanstackQueryClient = () => new QueryClient()
|
|
101
|
+
|
|
102
|
+
export const makeTanstackQueryInvalidator = (queryClient: QueryClient): QueryInvalidator => ({
|
|
103
|
+
invalidateAndAwait: (keys) =>
|
|
104
|
+
Effect
|
|
105
|
+
.gen(function*() {
|
|
106
|
+
const span = yield* Effect.currentParentSpan.pipe(Effect.orElseSucceed(() => undefined))
|
|
107
|
+
yield* Effect.forEach(
|
|
108
|
+
keys,
|
|
109
|
+
(queryKey) => {
|
|
110
|
+
const options = { updateMeta: { span } }
|
|
111
|
+
return Effect.promise(() => queryClient.invalidateQueries({ queryKey }, options))
|
|
112
|
+
},
|
|
113
|
+
{ discard: true, concurrency: "unbounded" }
|
|
114
|
+
)
|
|
115
|
+
})
|
|
116
|
+
.pipe(Effect.orDie),
|
|
117
|
+
invalidateSoft: (keys) =>
|
|
118
|
+
Effect
|
|
119
|
+
.gen(function*() {
|
|
120
|
+
const span = yield* Effect.currentParentSpan.pipe(Effect.orElseSucceed(() => undefined))
|
|
121
|
+
yield* Effect.forEach(
|
|
122
|
+
keys,
|
|
123
|
+
(queryKey) => Effect.promise(() => queryClient.invalidateQueries({ queryKey }, { updateMeta: { span } })),
|
|
124
|
+
{ discard: true, concurrency: "unbounded" }
|
|
125
|
+
)
|
|
126
|
+
})
|
|
127
|
+
.pipe(Effect.orDie)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
const fullQueryKey = (
|
|
131
|
+
q: { readonly queryKeyProjectionHash?: string },
|
|
132
|
+
queryKey: ReadonlyArray<unknown>,
|
|
133
|
+
input: unknown
|
|
134
|
+
) => q.queryKeyProjectionHash === undefined ? [...queryKey, input] : [...queryKey, q.queryKeyProjectionHash, input]
|
|
135
|
+
|
|
136
|
+
export const makeTanstackQueryCacheUpdater = (queryClient: QueryClient): QueryCacheUpdater => ({
|
|
137
|
+
update: <I, A, E, R, Request extends Req, Name extends string>(
|
|
138
|
+
_registry: ReturnType<typeof injectRegistry>,
|
|
139
|
+
query: RequestHandlerWithInput<I, A, E, R, Request, Name>,
|
|
140
|
+
input: I,
|
|
141
|
+
updater: (data: NoInfer<A>) => NoInfer<A>
|
|
142
|
+
) => {
|
|
143
|
+
const queryKey = fullQueryKey(query, makeQueryKey(query), input)
|
|
144
|
+
if (queryClient.getQueryData(queryKey) === undefined) {
|
|
145
|
+
console.warn(`Query ${query.id} has not been used yet; nothing to update`)
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
queryClient.setQueryData(queryKey, (data: A | undefined) => data === undefined ? data : updater(data))
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
export const makeTanstackQuery = <R>(
|
|
153
|
+
getRuntime: () => Context.Context<R>,
|
|
154
|
+
queryClient: QueryClient
|
|
155
|
+
): MakeQuery2<R> => {
|
|
156
|
+
// Drop a query's recorded read-dependencies when tanstack evicts it from the cache, so the
|
|
157
|
+
// registry mirrors the live queries (mirrors the atom engine's `trackReadDependencies` finalizer).
|
|
158
|
+
queryClient.getQueryCache().subscribe((event) => {
|
|
159
|
+
if (event.type === "removed") clearQueryReadDependencies(event.query.queryKey)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
const useQuery: MakeQuery2<R> = <I, A, E, Request extends Req, Name extends string>(
|
|
163
|
+
q: RequestHandlerWithInput<I, A, E, R, Request, Name>
|
|
164
|
+
) =>
|
|
165
|
+
<TData = A>(
|
|
166
|
+
arg: I | WatchSource<I> | undefined | WatchSource<Option.Option<I>>,
|
|
167
|
+
options?: LegacyTanstackOptions<A, CauseException<E>, TData>
|
|
168
|
+
) => {
|
|
169
|
+
const runPromise = makeRunPromise(getRuntime())
|
|
170
|
+
const queryKey = makeQueryKey(q)
|
|
171
|
+
const enabled = resolveEnabled(arg, options)
|
|
172
|
+
const structuralSharing = options?.structuralSharing === false ? false : replaceEqualDeep
|
|
173
|
+
const tanstackOptions = {
|
|
174
|
+
...(options?.staleTime !== undefined ? { staleTime: options.staleTime } : {}),
|
|
175
|
+
...(typeof options?.gcTime === "number" ? { gcTime: options.gcTime } : {}),
|
|
176
|
+
...(options?.refetchOnWindowFocus !== undefined ? { refetchOnWindowFocus: options.refetchOnWindowFocus } : {}),
|
|
177
|
+
structuralSharing,
|
|
178
|
+
...(options?.refetchInterval !== undefined ? { refetchInterval: options.refetchInterval } : {}),
|
|
179
|
+
...(options?.select !== undefined ? { select: options.select } : {})
|
|
180
|
+
}
|
|
181
|
+
const resolvedQueryKey = computed(() => {
|
|
182
|
+
const input = resolveInput(arg, options?.mode)
|
|
183
|
+
return fullQueryKey(q, queryKey, input)
|
|
184
|
+
})
|
|
185
|
+
watch(
|
|
186
|
+
resolvedQueryKey,
|
|
187
|
+
(key, _, onCleanup) => onCleanup(registerQueryInvalidationMode(key, options?.invalidation ?? "await")),
|
|
188
|
+
{ immediate: true }
|
|
189
|
+
)
|
|
190
|
+
const tanstack = useTanstackQuery<A, CauseException<E>, TData>({
|
|
191
|
+
...tanstackOptions,
|
|
192
|
+
enabled,
|
|
193
|
+
throwOnError: false,
|
|
194
|
+
retry: (retryCount: number, error: unknown) => isRetryable(error) && retryCount < 5,
|
|
195
|
+
queryKey: resolvedQueryKey,
|
|
196
|
+
queryFn: (
|
|
197
|
+
{ meta, signal }: {
|
|
198
|
+
readonly meta?: { readonly span?: Tracer.AnySpan | undefined } | undefined
|
|
199
|
+
readonly signal: AbortSignal
|
|
200
|
+
}
|
|
201
|
+
) =>
|
|
202
|
+
runPromise(
|
|
203
|
+
Effect.gen(function*() {
|
|
204
|
+
const input = resolveInput(arg, options?.mode)!
|
|
205
|
+
// Record the repository/server read-dependencies seen while fetching, keyed by the
|
|
206
|
+
// tanstack queryKey, so a later mutation whose writes intersect them derives this query
|
|
207
|
+
// as an invalidation target (the tanstack invalidator invalidates by this same key).
|
|
208
|
+
const readsRef = yield* Ref.make(DataDependencies.empty())
|
|
209
|
+
const writesRef = yield* Ref.make(DataDependencies.empty())
|
|
210
|
+
const recorder = DataDependencies.makeDataDependencyRecorder(readsRef, writesRef)
|
|
211
|
+
const result = yield* q
|
|
212
|
+
.handler(input)
|
|
213
|
+
.pipe(
|
|
214
|
+
Effect.provideService(DataDependencies.DataDependencyRecorder, recorder),
|
|
215
|
+
Effect.tapCauseIf(Cause.hasDies, (cause) => reportRuntimeError(cause)),
|
|
216
|
+
Effect.withSpan(`query ${q.id}`, {}, { captureStackTrace: false }),
|
|
217
|
+
meta?.span === undefined
|
|
218
|
+
? (effect) => effect
|
|
219
|
+
: Effect.withParentSpan(meta.span, { captureStackTrace: false })
|
|
220
|
+
)
|
|
221
|
+
setQueryReadDependencies(fullQueryKey(q, queryKey, input), yield* Ref.get(readsRef))
|
|
222
|
+
return result
|
|
223
|
+
}),
|
|
224
|
+
{ signal }
|
|
225
|
+
)
|
|
226
|
+
}, queryClient)
|
|
227
|
+
|
|
228
|
+
const latestSuccess = shallowRef<TData>()
|
|
229
|
+
const result = computed((): AsyncResult.AsyncResult<TData, E> =>
|
|
230
|
+
swrToQuery({
|
|
231
|
+
error: tanstack.error.value,
|
|
232
|
+
data: tanstack.data.value === undefined ? latestSuccess.value : tanstack.data.value,
|
|
233
|
+
isValidating: tanstack.isFetching.value
|
|
234
|
+
})
|
|
235
|
+
)
|
|
236
|
+
watch(result, (value) => latestSuccess.value = Option.getOrUndefined(AsyncResult.value(value)), { immediate: true })
|
|
237
|
+
const seedLatestSuccess = (data: TData) => {
|
|
238
|
+
if (latestSuccess.value === undefined) {
|
|
239
|
+
latestSuccess.value = data
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const registry = injectRegistry()
|
|
244
|
+
const latestSuccessRef = computed(() => latestSuccess.value)
|
|
245
|
+
const atom = computed<Atom.Atom<AsyncResult.AsyncResult<TData, E>>>(() => Atom.readable(() => result.value))
|
|
246
|
+
|
|
247
|
+
const awaitResult = (): Effect.Effect<TData, E> =>
|
|
248
|
+
Effect
|
|
249
|
+
.tryPromise({
|
|
250
|
+
try: async () => {
|
|
251
|
+
const queryResult = await tanstack.suspense()
|
|
252
|
+
const data = queryResult.data
|
|
253
|
+
if (data === undefined) {
|
|
254
|
+
throw new Error("TanStack query resolved without data")
|
|
255
|
+
}
|
|
256
|
+
seedLatestSuccess(data)
|
|
257
|
+
return data
|
|
258
|
+
},
|
|
259
|
+
catch: (error) => error
|
|
260
|
+
})
|
|
261
|
+
.pipe(
|
|
262
|
+
Effect.catch((error) => recoverCauseException<TData, E>(error))
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
const refetch = (): Effect.Effect<TData, E> =>
|
|
266
|
+
Effect.gen(function*() {
|
|
267
|
+
const span = yield* Effect.currentParentSpan.pipe(Effect.orElseSucceed(() => undefined))
|
|
268
|
+
return yield* Effect
|
|
269
|
+
.tryPromise({
|
|
270
|
+
try: async () => {
|
|
271
|
+
const options = { throwOnError: true, updateMeta: { span } }
|
|
272
|
+
const queryResult = await tanstack.refetch(options)
|
|
273
|
+
const data = queryResult.data
|
|
274
|
+
if (data === undefined) {
|
|
275
|
+
throw new Error("TanStack query refetched without data")
|
|
276
|
+
}
|
|
277
|
+
return data
|
|
278
|
+
},
|
|
279
|
+
catch: (error) => error
|
|
280
|
+
})
|
|
281
|
+
.pipe(
|
|
282
|
+
Effect.catch((error) => recoverCauseException<TData, E>(error))
|
|
283
|
+
)
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
const handle: QueryHandle<TData, E> = {
|
|
287
|
+
awaitResult,
|
|
288
|
+
refetch,
|
|
289
|
+
refresh: () => {
|
|
290
|
+
void tanstack.refetch()
|
|
291
|
+
},
|
|
292
|
+
registry,
|
|
293
|
+
atom
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const fetch = (_options?: RefetchOptions): Effect.Effect<QueryObserverResult<TData, CauseException<E>>> =>
|
|
297
|
+
refetch().pipe(Effect.exit, Effect.map(AsyncResult.fromExit))
|
|
298
|
+
|
|
299
|
+
return [
|
|
300
|
+
result,
|
|
301
|
+
latestSuccessRef,
|
|
302
|
+
fetch,
|
|
303
|
+
handle
|
|
304
|
+
] as const
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return useQuery
|
|
308
|
+
}
|
package/src/intl.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as Context from "effect-app/Context"
|
|
2
2
|
import { accessCn, accessFn } from "effect-app/Context"
|
|
3
|
-
import { type MakeIntlReturn } from "./makeIntl.
|
|
3
|
+
import { type MakeIntlReturn } from "./makeIntl.ts"
|
|
4
4
|
|
|
5
5
|
type I18nShape = ReturnType<MakeIntlReturn<string>["useIntl"]>
|
|
6
6
|
|
package/src/lib.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { type Pausable, useIntervalFn, type UseIntervalFnOptions } from "@vueuse/core"
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import type
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
2
|
+
import type { Req } from "effect-app/client"
|
|
3
|
+
import type { RequestHandlerWithInput } from "effect-app/client/clientFor"
|
|
4
|
+
import type * as Effect from "effect-app/Effect"
|
|
5
|
+
import * as Cause from "effect/Cause"
|
|
6
|
+
import { isHttpClientError } from "effect/http/HttpClientError"
|
|
7
|
+
import { isProxy, isReactive, isRef, type MaybeRefOrGetter, toRaw } from "vue"
|
|
8
|
+
import { reportError } from "./errorReporter.ts"
|
|
8
9
|
|
|
9
|
-
export * as AsyncResult from "effect/
|
|
10
|
+
export * as AsyncResult from "effect/reactivity/AsyncResult"
|
|
10
11
|
|
|
11
12
|
const reportRuntimeError_ = reportError("Runtime")
|
|
12
13
|
|
|
@@ -31,15 +32,7 @@ const determineLevel = (cause: Cause.Cause<unknown>) => {
|
|
|
31
32
|
export const reportRuntimeError = (cause: Cause.Cause<unknown>, extras?: Record<string, unknown>) =>
|
|
32
33
|
reportRuntimeError_(cause, extras, determineLevel(cause))
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
// -> "$Project", "$Configuration", "Index"
|
|
36
|
-
export const makeQueryKey = ({ id, options }: { id: string; options?: ClientForOptions }) =>
|
|
37
|
-
pipe(
|
|
38
|
-
id.split("/"),
|
|
39
|
-
(split) => split.filter((_) => !options || !options?.skipQueryKey?.includes(_)).map((_) => "$" + _)
|
|
40
|
-
)
|
|
41
|
-
.join(".")
|
|
42
|
-
.split(".")
|
|
35
|
+
export { makeQueryKey } from "effect-app/client"
|
|
43
36
|
|
|
44
37
|
export function pauseWhileProcessing(
|
|
45
38
|
iv: Pausable,
|
|
@@ -75,13 +68,48 @@ export const mapHandler: {
|
|
|
75
68
|
self: RequestHandlerWithInput<I, A, E, R, Request, Name>,
|
|
76
69
|
map: (handler: (i: I) => Effect.Effect<A, E, R>) => (i: I) => Effect.Effect<A2, E2, R2>
|
|
77
70
|
): RequestHandlerWithInput<I, A2, E2, R2, Request, Name>
|
|
78
|
-
<E, A, R, E2, A2, R2, Request extends Req, Name extends string>(
|
|
79
|
-
self: RequestHandler<A, E, R, Request, Name>,
|
|
80
|
-
map: (handler: Effect.Effect<A, E, R>) => Effect.Effect<A2, E2, R2>
|
|
81
|
-
): RequestHandler<A2, E2, R2, Request, Name>
|
|
82
71
|
} = (self: any, map: any): any => ({
|
|
83
72
|
...self,
|
|
84
|
-
handler:
|
|
85
|
-
? (i: any) => map(self.handler as (i: any) => Effect.Effect<any, any, any>)(i)
|
|
86
|
-
: map(self.handler)
|
|
73
|
+
handler: (i: any) => map(self.handler as (i: any) => Effect.Effect<any, any, any>)(i)
|
|
87
74
|
})
|
|
75
|
+
|
|
76
|
+
export function deepToRaw<T>(sourceObj: T): T {
|
|
77
|
+
const objectIterator = (input: any): any => {
|
|
78
|
+
if (isRef(input)) {
|
|
79
|
+
return objectIterator(input.value)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const rawInput = isReactive(input) || isProxy(input)
|
|
83
|
+
? toRaw(input)
|
|
84
|
+
: input
|
|
85
|
+
|
|
86
|
+
if (Array.isArray(rawInput)) {
|
|
87
|
+
return rawInput.map((item) => objectIterator(item))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (rawInput instanceof Map) {
|
|
91
|
+
return new Map(
|
|
92
|
+
Array.from(rawInput.entries(), ([key, value]) => [objectIterator(key), objectIterator(value)])
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (rawInput instanceof Set) {
|
|
97
|
+
return new Set(Array.from(rawInput.values(), (value) => objectIterator(value)))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (rawInput instanceof Date) {
|
|
101
|
+
return new Date(rawInput)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (rawInput && typeof rawInput === "object") {
|
|
105
|
+
return Object.keys(rawInput).reduce((acc, key) => {
|
|
106
|
+
acc[key] = objectIterator(rawInput[key])
|
|
107
|
+
return acc
|
|
108
|
+
}, {} as Record<string, unknown>)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return rawInput
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return objectIterator(sourceObj)
|
|
115
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { DataDependencies } from "effect-app/client"
|
|
2
|
+
import * as Hash from "effect/Hash"
|
|
3
|
+
|
|
4
|
+
export interface LiveQueryOptions {
|
|
5
|
+
/** Fixed maximum delay from the first matching write. Zero reacts immediately. */
|
|
6
|
+
readonly maxDelayMs?: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface LiveQueryInvalidationSource {
|
|
10
|
+
/** Resolve only after events received after this point can no longer be missed. */
|
|
11
|
+
readonly ready: () => Promise<void>
|
|
12
|
+
readonly subscribe: (
|
|
13
|
+
onWrites: (writes: DataDependencies.DataDependencies) => void,
|
|
14
|
+
onReset: () => void
|
|
15
|
+
) => () => void
|
|
16
|
+
readonly invalidate: (keys: ReadonlyArray<ReadonlyArray<unknown>>) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface LiveQueryEntry {
|
|
20
|
+
readonly key: ReadonlyArray<unknown>
|
|
21
|
+
readonly reads: () => DataDependencies.DataDependencies
|
|
22
|
+
observers: number
|
|
23
|
+
readonly delays: Map<number, number>
|
|
24
|
+
timer: ReturnType<typeof setTimeout> | undefined
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const entries = new Map<number, LiveQueryEntry>()
|
|
28
|
+
let source: LiveQueryInvalidationSource | undefined
|
|
29
|
+
let unsubscribe: (() => void) | undefined
|
|
30
|
+
let discovering = 0
|
|
31
|
+
let bufferedWrites = DataDependencies.empty()
|
|
32
|
+
|
|
33
|
+
const invalidateEntry = (entry: LiveQueryEntry) => {
|
|
34
|
+
entry.timer = undefined
|
|
35
|
+
source?.invalidate([entry.key])
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const schedule = (entry: LiveQueryEntry) => {
|
|
39
|
+
if (entry.timer !== undefined) return
|
|
40
|
+
const maxDelayMs = Math.min(...entry.delays.keys())
|
|
41
|
+
if (maxDelayMs <= 0) return invalidateEntry(entry)
|
|
42
|
+
entry.timer = setTimeout(() => invalidateEntry(entry), maxDelayMs)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const dispatch = (writes: DataDependencies.DataDependencies) => {
|
|
46
|
+
if (!DataDependencies.isNonEmpty(writes)) return
|
|
47
|
+
for (const entry of entries.values()) {
|
|
48
|
+
if (DataDependencies.intersects(entry.reads(), writes)) schedule(entry)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const receive = (writes: DataDependencies.DataDependencies) => {
|
|
53
|
+
if (discovering > 0) {
|
|
54
|
+
bufferedWrites = DataDependencies.merge(bufferedWrites, writes)
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
dispatch(writes)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const reset = () => {
|
|
61
|
+
bufferedWrites = DataDependencies.empty()
|
|
62
|
+
for (const entry of entries.values()) {
|
|
63
|
+
if (DataDependencies.isNonEmpty(entry.reads())) schedule(entry)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const ensureSubscribed = () => {
|
|
68
|
+
if (unsubscribe === undefined && source !== undefined && entries.size > 0) {
|
|
69
|
+
unsubscribe = source.subscribe(receive, reset)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const configureLiveQueryInvalidation = (next: LiveQueryInvalidationSource) => {
|
|
74
|
+
unsubscribe?.()
|
|
75
|
+
unsubscribe = undefined
|
|
76
|
+
source = next
|
|
77
|
+
ensureSubscribed()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const registerLiveQuery = (
|
|
81
|
+
key: ReadonlyArray<unknown>,
|
|
82
|
+
reads: () => DataDependencies.DataDependencies,
|
|
83
|
+
options: LiveQueryOptions
|
|
84
|
+
) => {
|
|
85
|
+
const hash = Hash.hash(key)
|
|
86
|
+
const delay = options.maxDelayMs ?? 0
|
|
87
|
+
const current = entries.get(hash)
|
|
88
|
+
if (current !== undefined) {
|
|
89
|
+
current.observers++
|
|
90
|
+
current.delays.set(delay, (current.delays.get(delay) ?? 0) + 1)
|
|
91
|
+
} else {
|
|
92
|
+
entries.set(hash, {
|
|
93
|
+
key,
|
|
94
|
+
reads,
|
|
95
|
+
observers: 1,
|
|
96
|
+
delays: new Map([[delay, 1]]),
|
|
97
|
+
timer: undefined
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
ensureSubscribed()
|
|
101
|
+
|
|
102
|
+
return () => {
|
|
103
|
+
const entry = entries.get(hash)
|
|
104
|
+
if (entry === undefined) return
|
|
105
|
+
const delayObservers = entry.delays.get(delay) ?? 0
|
|
106
|
+
if (delayObservers <= 1) entry.delays.delete(delay)
|
|
107
|
+
else entry.delays.set(delay, delayObservers - 1)
|
|
108
|
+
if (--entry.observers > 0) return
|
|
109
|
+
if (entry.timer !== undefined) clearTimeout(entry.timer)
|
|
110
|
+
entries.delete(hash)
|
|
111
|
+
if (entries.size === 0) {
|
|
112
|
+
unsubscribe?.()
|
|
113
|
+
unsubscribe = undefined
|
|
114
|
+
bufferedWrites = DataDependencies.empty()
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const beginLiveQueryFetch = async (key: ReadonlyArray<unknown>) => {
|
|
120
|
+
if (!entries.has(Hash.hash(key)) || source === undefined) return false
|
|
121
|
+
discovering++
|
|
122
|
+
try {
|
|
123
|
+
await source.ready()
|
|
124
|
+
return true
|
|
125
|
+
} catch (error) {
|
|
126
|
+
discovering--
|
|
127
|
+
throw error
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export const endLiveQueryFetch = (wasLive: boolean) => {
|
|
132
|
+
if (!wasLive) return
|
|
133
|
+
discovering = Math.max(0, discovering - 1)
|
|
134
|
+
if (discovering > 0 || !DataDependencies.isNonEmpty(bufferedWrites)) return
|
|
135
|
+
const writes = bufferedWrites
|
|
136
|
+
bufferedWrites = DataDependencies.empty()
|
|
137
|
+
dispatch(writes)
|
|
138
|
+
}
|