@effect-app/vue 4.0.0-beta.30 → 4.0.0-beta.301

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.
Files changed (135) hide show
  1. package/CHANGELOG.md +2234 -0
  2. package/dist/atomQuery.d.ts +111 -0
  3. package/dist/atomQuery.d.ts.map +1 -0
  4. package/dist/atomQuery.js +386 -0
  5. package/dist/commander.d.ts +634 -0
  6. package/dist/commander.d.ts.map +1 -0
  7. package/dist/commander.js +1070 -0
  8. package/dist/{experimental/confirm.d.ts → confirm.d.ts} +6 -4
  9. package/dist/confirm.d.ts.map +1 -0
  10. package/dist/confirm.js +26 -0
  11. package/dist/dependencyMetadata.d.ts +10 -0
  12. package/dist/dependencyMetadata.d.ts.map +1 -0
  13. package/dist/dependencyMetadata.js +29 -0
  14. package/dist/errorReporter.d.ts +6 -4
  15. package/dist/errorReporter.d.ts.map +1 -1
  16. package/dist/errorReporter.js +14 -19
  17. package/dist/form.d.ts +14 -5
  18. package/dist/form.d.ts.map +1 -1
  19. package/dist/form.js +46 -13
  20. package/dist/internal/tanstackQuery.d.ts +9 -0
  21. package/dist/internal/tanstackQuery.d.ts.map +1 -0
  22. package/dist/internal/tanstackQuery.js +194 -0
  23. package/dist/intl.d.ts +15 -0
  24. package/dist/intl.d.ts.map +1 -0
  25. package/dist/intl.js +9 -0
  26. package/dist/lib.d.ts +7 -9
  27. package/dist/lib.d.ts.map +1 -1
  28. package/dist/lib.js +35 -10
  29. package/dist/makeClient.d.ts +236 -336
  30. package/dist/makeClient.d.ts.map +1 -1
  31. package/dist/makeClient.js +416 -408
  32. package/dist/makeContext.d.ts.map +1 -1
  33. package/dist/makeIntl.d.ts.map +1 -1
  34. package/dist/{experimental/makeUseCommand.d.ts → makeUseCommand.d.ts} +4 -3
  35. package/dist/makeUseCommand.d.ts.map +1 -0
  36. package/dist/makeUseCommand.js +13 -0
  37. package/dist/mutate.d.ts +108 -40
  38. package/dist/mutate.d.ts.map +1 -1
  39. package/dist/mutate.js +211 -58
  40. package/dist/query.d.ts +150 -46
  41. package/dist/query.d.ts.map +1 -1
  42. package/dist/query.js +370 -122
  43. package/dist/routeParams.d.ts +4 -4
  44. package/dist/routeParams.d.ts.map +1 -1
  45. package/dist/routeParams.js +4 -3
  46. package/dist/runtime.d.ts +1 -14
  47. package/dist/runtime.d.ts.map +1 -1
  48. package/dist/runtime.js +2 -26
  49. package/dist/suspense.d.ts +19 -0
  50. package/dist/suspense.d.ts.map +1 -0
  51. package/dist/suspense.js +38 -0
  52. package/dist/toast.d.ts +2 -0
  53. package/dist/toast.d.ts.map +1 -0
  54. package/dist/toast.js +2 -0
  55. package/dist/withToast.d.ts +2 -0
  56. package/dist/withToast.d.ts.map +1 -0
  57. package/dist/withToast.js +2 -0
  58. package/docs/atom-query-api-redesign.md +207 -0
  59. package/docs/query-key-invalidation.md +106 -0
  60. package/examples/streamMutation.ts +72 -0
  61. package/package.json +36 -95
  62. package/src/atomQuery.ts +540 -0
  63. package/src/commander.ts +3413 -0
  64. package/src/{experimental/confirm.ts → confirm.ts} +13 -15
  65. package/src/dependencyMetadata.ts +38 -0
  66. package/src/errorReporter.ts +65 -75
  67. package/src/form.ts +61 -18
  68. package/src/index.ts +7 -7
  69. package/src/internal/tanstackQuery.ts +289 -0
  70. package/src/intl.ts +12 -0
  71. package/src/lib.ts +49 -21
  72. package/src/makeClient.ts +967 -1159
  73. package/src/makeIntl.ts +2 -2
  74. package/src/{experimental/makeUseCommand.ts → makeUseCommand.ts} +9 -6
  75. package/src/mutate.ts +370 -152
  76. package/src/query.ts +742 -260
  77. package/src/routeParams.ts +7 -7
  78. package/src/runtime.ts +1 -31
  79. package/src/suspense.ts +43 -0
  80. package/src/toast.ts +1 -0
  81. package/src/withToast.ts +1 -0
  82. package/test/Mutation.test.ts +181 -24
  83. package/test/dependencyInvalidation.test.ts +373 -0
  84. package/test/dist/dependencyInvalidation.test.d.ts.map +1 -0
  85. package/test/dist/form.test.d.ts.map +1 -1
  86. package/test/dist/interrupt-refetch-repro.test.d.ts.map +1 -0
  87. package/test/dist/lib.test.d.ts.map +1 -0
  88. package/test/dist/query-span.test.d.ts.map +1 -0
  89. package/test/dist/queryOptions.test.d.ts.map +1 -0
  90. package/test/dist/streamFinal.test.d.ts.map +1 -0
  91. package/test/dist/streamFn.test.d.ts.map +1 -0
  92. package/test/dist/stubs.d.ts +3756 -122
  93. package/test/dist/stubs.d.ts.map +1 -1
  94. package/test/dist/stubs.js +192 -32
  95. package/test/dist/suspense-regression.test.d.ts.map +1 -0
  96. package/test/dist/suspense.test.d.ts.map +1 -0
  97. package/test/form-validation-errors.test.ts +25 -20
  98. package/test/form.test.ts +22 -3
  99. package/test/interrupt-refetch-repro.test.ts +623 -0
  100. package/test/lib.test.ts +240 -0
  101. package/test/makeClient.test.ts +437 -38
  102. package/test/query-span.test.ts +92 -0
  103. package/test/queryOptions.test.ts +88 -0
  104. package/test/streamFinal.test.ts +64 -0
  105. package/test/streamFn.test.ts +457 -0
  106. package/test/stubs.ts +228 -43
  107. package/test/suspense-regression.test.ts +168 -0
  108. package/test/suspense.test.ts +160 -0
  109. package/tsconfig.examples.json +20 -0
  110. package/tsconfig.json +3 -1
  111. package/tsconfig.src.json +34 -34
  112. package/tsconfig.test.json +2 -2
  113. package/vitest.config.ts +5 -5
  114. package/dist/experimental/commander.d.ts +0 -359
  115. package/dist/experimental/commander.d.ts.map +0 -1
  116. package/dist/experimental/commander.js +0 -557
  117. package/dist/experimental/confirm.d.ts.map +0 -1
  118. package/dist/experimental/confirm.js +0 -28
  119. package/dist/experimental/intl.d.ts +0 -16
  120. package/dist/experimental/intl.d.ts.map +0 -1
  121. package/dist/experimental/intl.js +0 -5
  122. package/dist/experimental/makeUseCommand.d.ts.map +0 -1
  123. package/dist/experimental/makeUseCommand.js +0 -13
  124. package/dist/experimental/toast.d.ts +0 -47
  125. package/dist/experimental/toast.d.ts.map +0 -1
  126. package/dist/experimental/toast.js +0 -41
  127. package/dist/experimental/withToast.d.ts +0 -25
  128. package/dist/experimental/withToast.d.ts.map +0 -1
  129. package/dist/experimental/withToast.js +0 -45
  130. package/eslint.config.mjs +0 -24
  131. package/src/experimental/commander.ts +0 -1835
  132. package/src/experimental/intl.ts +0 -9
  133. package/src/experimental/toast.ts +0 -66
  134. package/src/experimental/withToast.ts +0 -99
  135. package/tsconfig.json.bak +0 -12
package/src/query.ts CHANGED
@@ -2,33 +2,210 @@
2
2
  /* eslint-disable @typescript-eslint/no-unsafe-call */
3
3
  /* eslint-disable @typescript-eslint/no-unsafe-return */
4
4
  /* eslint-disable @typescript-eslint/no-unsafe-assignment */
5
- import { type DefaultError, type Enabled, type InitialDataFunction, type NonUndefinedGuard, type PlaceholderDataFunction, type QueryKey, type QueryObserverOptions, type QueryObserverResult, type RefetchOptions, useQuery as useTanstackQuery, useQueryClient, type UseQueryDefinedReturnType, type UseQueryReturnType } from "@tanstack/vue-query"
6
- import { Array, Cause, Effect, Exit, flow, Option, S, type ServiceMap } from "effect-app"
7
- import { type Req } from "effect-app/client"
8
- import type { RequestHandler, RequestHandlerWithInput } from "effect-app/client/clientFor"
9
- import { ServiceUnavailableError } from "effect-app/client/errors"
10
- import { type Span } from "effect/Tracer"
11
- import { isHttpClientError } from "effect/unstable/http/HttpClientError"
5
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
6
+ import { injectRegistry, useAtomValue } from "@effect/atom-vue"
7
+ import * as Array from "effect-app/Array"
8
+ import { makeQueryKey, type Req } from "effect-app/client"
9
+ import type { ClientForOptions, RequestHandlerWithInput, RequestStreamHandlerWithInput } from "effect-app/client/clientFor"
10
+ import { type CauseException } from "effect-app/client/errors"
11
+ import type * as Context from "effect-app/Context"
12
+ import * as Effect from "effect-app/Effect"
13
+ import * as Option from "effect-app/Option"
14
+ import type * as Cause from "effect/Cause"
15
+ import * as Exit from "effect/Exit"
16
+ import * as Stream from "effect/Stream"
12
17
  import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
13
- import { computed, type ComputedRef, type MaybeRefOrGetter, ref, shallowRef, watch, type WatchSource } from "vue"
14
- import { makeQueryKey, reportRuntimeError } from "./lib.js"
18
+ import * as Atom from "effect/unstable/reactivity/Atom"
19
+ import { computed, type ComputedRef, effectScope, type MaybeRefOrGetter, onBeforeUnmount, onMounted, onScopeDispose, type Ref, ref, toValue, type WatchSource } from "vue"
20
+ import { type AtomClientRuntime, type AtomQueryOptions, awaitAtomResult, buildQueryFamily, buildStreamQueryFamily, disabledQueryAtom, isStaleResult, refreshAtomWithCurrentSpan, staleTimeMsOf, withQueryOptions } from "./atomQuery.ts"
21
+ import { latestDefined } from "./suspense.ts"
15
22
 
16
- // we must use interface extends, or we get the dreaded typescript error of isn't portable blabla @tanstack/vue-query/build/modern/types.js
17
- // but because how they are dealing with some extends clause, we loose all properties except initialData
18
- // so we actually reconstruct the interfaces here from the ground up :/
23
+ // --- minimal local types (replacing the former @tanstack/vue-query type imports) ---
24
+ type DefaultError = Error
25
+ type QueryKey = ReadonlyArray<unknown>
26
+ /** Options accepted by `refetch()` — kept for source compatibility; the atom path ignores them. */
27
+ export interface RefetchOptions {
28
+ readonly cancelRefetch?: boolean
29
+ readonly throwOnError?: boolean
30
+ }
31
+ /** The 4th tuple element: private atom/registry handle for client helpers. */
32
+ export interface QueryHandle<A = unknown, E = unknown> {
33
+ readonly awaitResult: () => Effect.Effect<A, E, never>
34
+ readonly refetch: () => Effect.Effect<A, E, never>
35
+ readonly refresh: () => void
36
+ readonly registry: ReturnType<typeof injectRegistry>
37
+ readonly atom: ComputedRef<Atom.Atom<AsyncResult.AsyncResult<A, E>>>
38
+ }
39
+ export interface QueryView<A, E> extends QueryHandle<A, E> {
40
+ readonly result: ComputedRef<AsyncResult.AsyncResult<A, E>>
41
+ readonly data: ComputedRef<A | undefined>
42
+ }
43
+
44
+ const useScopedSuspenseSetup = <A>(setup: () => A) => {
45
+ const scope = effectScope()
46
+ const controller = new AbortController()
47
+ const value = scope.run(setup)
48
+ if (value === undefined) {
49
+ throw new Error("Internal Error: atom suspense setup scope did not initialize")
50
+ }
51
+
52
+ const isMounted = ref(true)
53
+ let stopped = false
54
+ const stop = () => {
55
+ if (stopped) return
56
+ stopped = true
57
+ isMounted.value = false
58
+ controller.abort()
59
+ scope.stop()
60
+ }
61
+ onBeforeUnmount(stop)
62
+ onScopeDispose(stop)
63
+
64
+ return [value, isMounted, controller.signal] as const
65
+ }
66
+
67
+ export interface QueryCacheUpdater {
68
+ readonly update: <I, A, E, R, Request extends Req, Name extends string>(
69
+ registry: ReturnType<typeof injectRegistry>,
70
+ query: RequestHandlerWithInput<I, A, E, R, Request, Name>,
71
+ input: I,
72
+ updater: (data: NoInfer<A>) => NoInfer<A>
73
+ ) => void
74
+ }
75
+
76
+ // retained generic aliases so the exported option-interface arity is unchanged for consumers
77
+ export type UseQueryReturnType<A = any, E = any> = QueryHandle<A, E>
78
+ export type UseQueryDefinedReturnType<A = any, E = any> = QueryHandle<A, E>
79
+ export type QueryObserverResult<A = any, _E = any> = AsyncResult.AsyncResult<A, any>
80
+ export type SuspenseQueryTuple<A, E> = readonly [
81
+ ComputedRef<AsyncResult.AsyncResult<A, E>>,
82
+ ComputedRef<A>,
83
+ (options?: RefetchOptions) => Effect.Effect<A, E, never>,
84
+ QueryHandle<A, E>
85
+ ]
86
+
87
+ export type SuspenseQueryView<A, E> =
88
+ & Omit<QueryView<A, E>, "data">
89
+ & {
90
+ readonly data: ComputedRef<A>
91
+ }
92
+ & SuspenseQueryTuple<A, E>
93
+
94
+ export type QueryAtomFamily<I, A, E> = (
95
+ input: I
96
+ ) => Atom.Writable<AsyncResult.AsyncResult<A, E>, AsyncResult.AsyncResult<A, E>>
97
+ export type StreamQueryAtomFamily<I, A, E> = (input: I) => Atom.Writable<Atom.PullResult<A, E>, void>
98
+
99
+ interface QueryFamilyDescriptor<I, A, E> {
100
+ readonly id: string
101
+ readonly handler: (i: I) => Effect.Effect<A, E, any>
102
+ readonly options?: ClientForOptions
103
+ readonly queryKeyProjectionHash?: string
104
+ }
105
+
106
+ interface StreamQueryFamilyDescriptor<I, A, E> {
107
+ readonly id: string
108
+ readonly handler: (i: I) => Stream.Stream<A, E, any>
109
+ readonly options?: ClientForOptions
110
+ readonly queryKeyProjectionHash?: string
111
+ }
112
+
113
+ const queryFamilyCacheKey = (
114
+ q: { readonly id: string; readonly options?: ClientForOptions; readonly queryKeyProjectionHash?: string }
115
+ ) => `${makeQueryKey(q).join("/")}:${q.queryKeyProjectionHash ?? ""}`
116
+
117
+ // One atom family per request shape, keyed by the stable query key + projection hash (not the
118
+ // handler object — `clientFor` returns a fresh proxy per call, so the object isn't shareable).
119
+ // Module-level + key-indexed => the family is process-global, so the same request+input read
120
+ // the same atom across components/pages => cross-page caching via the global registry.
121
+ const queryFamilyByKey = new Map<string, any>()
122
+ const getQueryFamily = <I, A, E>(
123
+ rt: AtomClientRuntime,
124
+ q: QueryFamilyDescriptor<I, A, E>
125
+ ): QueryAtomFamily<I, A, E> => {
126
+ const key = queryFamilyCacheKey(q)
127
+ let f = queryFamilyByKey.get(key)
128
+ if (!f) {
129
+ f = buildQueryFamily(rt, q)
130
+ queryFamilyByKey.set(key, f)
131
+ }
132
+ return f
133
+ }
134
+
135
+ const streamQueryFamilyByKey = new Map<string, any>()
136
+ const getStreamQueryFamily = <I, A, E>(
137
+ rt: AtomClientRuntime,
138
+ q: StreamQueryFamilyDescriptor<I, A, E>
139
+ ): StreamQueryAtomFamily<I, A, E> => {
140
+ const key = queryFamilyCacheKey(q)
141
+ let f = streamQueryFamilyByKey.get(key)
142
+ if (!f) {
143
+ f = buildStreamQueryFamily(rt, q)
144
+ streamQueryFamilyByKey.set(key, f)
145
+ }
146
+ return f
147
+ }
148
+
149
+ const makeAtomQueryCacheUpdater = (warnIfMissing: boolean): QueryCacheUpdater => ({
150
+ update: <I, A, E, R, Request extends Req, Name extends string>(
151
+ registry: ReturnType<typeof injectRegistry>,
152
+ query: RequestHandlerWithInput<I, A, E, R, Request, Name>,
153
+ input: I,
154
+ updater: (data: NoInfer<A>) => NoInfer<A>
155
+ ) => {
156
+ const family = queryFamilyByKey.get(queryFamilyCacheKey(query))
157
+ if (!family) {
158
+ if (warnIfMissing) {
159
+ console.warn(`Query ${query.id} has not been used yet; nothing to update`)
160
+ }
161
+ return
162
+ }
163
+ const atom: Atom.Writable<AsyncResult.AsyncResult<A, E>, AsyncResult.AsyncResult<A, E>> = family(input)
164
+ const current = registry.get(atom)
165
+ const next = AsyncResult.map(current, updater)
166
+ registry.set(atom, next)
167
+ }
168
+ })
169
+
170
+ export const atomQueryCacheUpdater = makeAtomQueryCacheUpdater(true)
171
+ export const optionalAtomQueryCacheUpdater = makeAtomQueryCacheUpdater(false)
172
+
173
+ export const combineQueryCacheUpdaters = (first: QueryCacheUpdater, second: QueryCacheUpdater): QueryCacheUpdater => ({
174
+ update: (registry, query, input, updater) => {
175
+ first.update(registry, query, input, updater)
176
+ second.update(registry, query, input, updater)
177
+ }
178
+ })
179
+
180
+ let activeQueryCacheUpdater = atomQueryCacheUpdater
181
+
182
+ export const setQueryCacheUpdater = (updater: QueryCacheUpdater) => {
183
+ activeQueryCacheUpdater = updater
184
+ }
185
+
186
+ // Atom-engine query options (formerly reconstructed from @tanstack/vue-query types).
187
+ // The generic arity is kept so the exported interface signatures are unchanged for consumers.
19
188
  export interface CustomUseQueryOptions<
20
189
  TQueryFnData = unknown,
21
190
  TError = DefaultError,
22
191
  TData = TQueryFnData,
23
192
  TQueryData = TQueryFnData,
24
193
  TQueryKey extends QueryKey = QueryKey
25
- > extends
26
- Omit<
27
- QueryObserverOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey>,
28
- "queryKey" | "queryFn" | "initialData" | "enabled" | "placeholderData"
29
- >
30
- {
31
- enabled?: MaybeRefOrGetter<boolean | undefined> | (() => Enabled<TQueryFnData, TError, TQueryData, TQueryKey>)
194
+ > {
195
+ readonly enabled?: MaybeRefOrGetter<boolean | undefined>
196
+ /** stale threshold in ms (or a Duration input) */
197
+ readonly staleTime?: number
198
+ /** garbage-collect after idle, ms (or "infinity") */
199
+ readonly gcTime?: number | "infinity"
200
+ readonly refetchOnWindowFocus?: boolean
201
+ readonly structuralSharing?: boolean
202
+ /** poll: re-fetch every N ms (tanstack refetchInterval) */
203
+ readonly refetchInterval?: number
204
+ readonly select?: (data: TQueryFnData) => TData
205
+ /** accepted for source compatibility; not used by the atom engine */
206
+ readonly retry?: boolean | number
207
+ readonly meta?: Record<string, unknown>
208
+ readonly _phantom?: [TQueryData, TQueryKey, TError]
32
209
  }
33
210
 
34
211
  // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
@@ -41,11 +218,8 @@ export interface CustomUndefinedInitialQueryOptions<
41
218
  TQueryData = TQueryFnData,
42
219
  TQueryKey extends QueryKey = QueryKey
43
220
  > extends CustomUseQueryOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey> {
44
- initialData?: undefined | InitialDataFunction<NonUndefinedGuard<TQueryFnData>> | NonUndefinedGuard<TQueryFnData>
45
- placeholderData?:
46
- | undefined
47
- | NonFunctionGuard<TQueryData>
48
- | PlaceholderDataFunction<NonFunctionGuard<TQueryData>, TError, NonFunctionGuard<TQueryData>, TQueryKey>
221
+ readonly initialData?: TQueryFnData | (() => TQueryFnData) | undefined
222
+ readonly placeholderData?: NonFunctionGuard<TQueryData> | ((prev: TQueryData | undefined) => TQueryData) | undefined
49
223
  }
50
224
  export interface CustomDefinedInitialQueryOptions<
51
225
  TQueryFnData = unknown,
@@ -54,11 +228,8 @@ export interface CustomDefinedInitialQueryOptions<
54
228
  TQueryData = TQueryFnData,
55
229
  TQueryKey extends QueryKey = QueryKey
56
230
  > extends CustomUseQueryOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey> {
57
- initialData: NonUndefinedGuard<TQueryFnData> | (() => NonUndefinedGuard<TQueryFnData>)
58
- placeholderData?:
59
- | undefined
60
- | NonFunctionGuard<TQueryData>
61
- | PlaceholderDataFunction<NonFunctionGuard<TQueryData>, TError, NonFunctionGuard<TQueryData>, TQueryKey>
231
+ readonly initialData: TQueryFnData | (() => TQueryFnData)
232
+ readonly placeholderData?: NonFunctionGuard<TQueryData> | ((prev: TQueryData | undefined) => TQueryData) | undefined
62
233
  }
63
234
 
64
235
  export interface CustomDefinedPlaceholderQueryOptions<
@@ -68,35 +239,436 @@ export interface CustomDefinedPlaceholderQueryOptions<
68
239
  TQueryData = TQueryFnData,
69
240
  TQueryKey extends QueryKey = QueryKey
70
241
  > extends CustomUseQueryOptions<TQueryFnData, TError, TData, TQueryData, TQueryKey> {
71
- initialData?: NonUndefinedGuard<TQueryFnData> | (() => NonUndefinedGuard<TQueryFnData>) | undefined
72
- placeholderData:
73
- | NonFunctionGuard<TQueryData>
74
- | PlaceholderDataFunction<NonFunctionGuard<TQueryData>, TError, NonFunctionGuard<TQueryData>, TQueryKey>
242
+ readonly initialData?: TQueryFnData | (() => TQueryFnData) | undefined
243
+ readonly placeholderData: NonFunctionGuard<TQueryData> | ((prev: TQueryData | undefined) => TQueryData)
244
+ }
245
+
246
+ export interface AtomQueryNewOptions<TQueryFnData = unknown, TData = TQueryFnData> {
247
+ readonly enabled?: MaybeRefOrGetter<boolean | undefined>
248
+ readonly staleTime?: number
249
+ readonly idleTTL?: number | "infinity"
250
+ readonly gcTime?: number | "infinity"
251
+ readonly revalidateOnFocus?: boolean
252
+ readonly refetchOnWindowFocus?: boolean
253
+ readonly structuralSharing?: boolean
254
+ readonly refreshEvery?: number
255
+ readonly refetchInterval?: number
256
+ readonly select?: (data: TQueryFnData) => TData
257
+ }
258
+
259
+ export interface AtomStreamQueryOptions {
260
+ readonly staleTime?: number
261
+ readonly idleTTL?: number | "infinity"
262
+ readonly gcTime?: number | "infinity"
263
+ readonly revalidateOnFocus?: boolean
264
+ readonly refetchOnWindowFocus?: boolean
265
+ readonly refreshEvery?: number
266
+ readonly refetchInterval?: number
267
+ }
268
+
269
+ /**
270
+ * Layer TanStack `initialData` / `placeholderData` over a raw query result as a DISPLAY fallback.
271
+ * Only while the result is still `Initial` (pending, never settled): show `initialData` as resolved
272
+ * data, or `placeholderData` as provisional (`waiting: true`). Once real data exists the seed is
273
+ * dropped. Failures are never masked — including failures without `previousSuccess` (where
274
+ * `AsyncResult.value` is empty). `placeholderData` in function form receives the last seen concrete
275
+ * value for basic keep-previous across input changes. Returns the raw ref untouched when neither
276
+ * option is set. Neither value is written to the atom cache.
277
+ */
278
+ export const withDataFallback = <A, E>(
279
+ rawResult: Ref<AsyncResult.AsyncResult<A, E>>,
280
+ options?: unknown
281
+ ): ComputedRef<AsyncResult.AsyncResult<A, E>> => {
282
+ const opts = options as
283
+ | { readonly initialData?: A | (() => A); readonly placeholderData?: A | ((prev: A | undefined) => A) }
284
+ | undefined
285
+ const initialData = opts?.initialData
286
+ const placeholderData = opts?.placeholderData
287
+ if (initialData === undefined && placeholderData === undefined) {
288
+ return rawResult as ComputedRef<AsyncResult.AsyncResult<A, E>>
289
+ }
290
+
291
+ let lastData: A | undefined
292
+ return computed(() => {
293
+ const r = rawResult.value
294
+ const concrete = AsyncResult.value(r)
295
+ if (Option.isSome(concrete)) {
296
+ lastData = concrete.value
297
+ }
298
+ // Fallback is pending-only. Failure (even without previousSuccess) and Success pass through.
299
+ if (!AsyncResult.isInitial(r)) {
300
+ return r
301
+ }
302
+ if (initialData !== undefined) {
303
+ const v = typeof initialData === "function" ? (initialData as () => A)() : initialData
304
+ return AsyncResult.success(v, { waiting: r.waiting })
305
+ }
306
+ const v = typeof placeholderData === "function"
307
+ ? (placeholderData as (prev: A | undefined) => A)(lastData)
308
+ : placeholderData
309
+ return v === undefined ? r : AsyncResult.success(v, { waiting: true })
310
+ })
311
+ }
312
+
313
+ const normalizeQueryOptions = (options?: {
314
+ readonly staleTime?: number
315
+ readonly gcTime?: number | "infinity"
316
+ readonly idleTTL?: number | "infinity"
317
+ readonly refetchOnWindowFocus?: boolean
318
+ readonly revalidateOnFocus?: boolean
319
+ readonly structuralSharing?: boolean
320
+ readonly refetchInterval?: number
321
+ readonly refreshEvery?: number
322
+ }): AtomQueryOptions => {
323
+ const out: {
324
+ staleTime?: number
325
+ gcTime?: number | "infinity"
326
+ revalidateOnFocus?: boolean
327
+ structuralSharing?: boolean
328
+ refetchInterval?: number
329
+ } = {}
330
+ if (options?.staleTime !== undefined) out.staleTime = options.staleTime
331
+ const gcTime = options?.idleTTL ?? options?.gcTime
332
+ if (gcTime !== undefined) out.gcTime = gcTime
333
+ const revalidateOnFocus = options?.revalidateOnFocus ?? options?.refetchOnWindowFocus
334
+ if (revalidateOnFocus !== undefined) out.revalidateOnFocus = revalidateOnFocus
335
+ if (options?.structuralSharing !== undefined) out.structuralSharing = options.structuralSharing
336
+ const refetchInterval = options?.refreshEvery ?? options?.refetchInterval
337
+ if (refetchInterval !== undefined) out.refetchInterval = refetchInterval
338
+ return out
339
+ }
340
+
341
+ export const useAtomQuery = <A, E>(
342
+ atom: () => Atom.Atom<AsyncResult.AsyncResult<A, E>>
343
+ ): QueryView<A, E> => {
344
+ const registry = injectRegistry()
345
+ const atomRef = computed(atom)
346
+ const atomResult = useAtomValue(() => atomRef.value)
347
+ const result = computed(() => atomResult.value)
348
+ const refresh = () => registry.refresh(atomRef.value)
349
+ const awaitResult = () => awaitAtomResult(registry, atomRef.value)
350
+ const refetch = () =>
351
+ Effect.gen(function*() {
352
+ yield* refreshAtomWithCurrentSpan(registry, atomRef.value)
353
+ return yield* awaitResult()
354
+ })
355
+ const data = computed(() => Option.getOrUndefined(AsyncResult.value(result.value)))
356
+
357
+ return {
358
+ result,
359
+ data,
360
+ awaitResult,
361
+ refetch,
362
+ refresh,
363
+ registry,
364
+ atom: atomRef
365
+ }
366
+ }
367
+
368
+ export const useAtomSuspense = <A, E>(
369
+ atom: () => Atom.Atom<AsyncResult.AsyncResult<A, E>>
370
+ ): Promise<SuspenseQueryView<A, E>> => {
371
+ const [{ view, data }, isMounted, signal] = useScopedSuspenseSetup(() => {
372
+ const view = useAtomQuery(atom)
373
+ const data = latestDefined(() => view.data.value, "atom suspense")
374
+ return { view, data }
375
+ })
376
+
377
+ const eff = Effect.gen(function*() {
378
+ const exit = yield* view.awaitResult().pipe(Effect.exit)
379
+ if (!isMounted.value) {
380
+ return yield* Effect.interrupt
381
+ }
382
+ if (Exit.isFailure(exit)) {
383
+ return yield* Exit.failCause(exit.cause)
384
+ }
385
+
386
+ const fetch = (_options?: RefetchOptions) => view.refetch()
387
+ const handle = {
388
+ awaitResult: view.awaitResult,
389
+ refetch: view.refetch,
390
+ refresh: view.refresh,
391
+ registry: view.registry,
392
+ atom: view.atom
393
+ }
394
+ return Object.assign(
395
+ [
396
+ view.result,
397
+ data,
398
+ fetch,
399
+ handle
400
+ ] as const,
401
+ {
402
+ ...view,
403
+ data
404
+ }
405
+ )
406
+ })
407
+
408
+ return Effect.runPromise(eff, { signal })
409
+ }
410
+
411
+ export type StreamQueryPullValue<A> = {
412
+ readonly done: boolean
413
+ readonly items: ReadonlyArray<A>
414
+ }
415
+
416
+ export interface StreamQueryView<A, E> {
417
+ readonly result: ComputedRef<Atom.PullResult<A, E>>
418
+ readonly items: ComputedRef<ReadonlyArray<A>>
419
+ readonly latest: ComputedRef<A | undefined>
420
+ readonly done: ComputedRef<boolean>
421
+ readonly awaitResult: () => Effect.Effect<StreamQueryPullValue<A>, E | Cause.NoSuchElementError, never>
422
+ readonly pull: () => void
423
+ readonly pullAndAwait: () => Effect.Effect<StreamQueryPullValue<A>, E | Cause.NoSuchElementError, never>
424
+ readonly refresh: () => void
425
+ readonly registry: ReturnType<typeof injectRegistry>
426
+ readonly atom: ComputedRef<Atom.Writable<Atom.PullResult<A, E>, void>>
427
+ }
428
+
429
+ export const useAtomStreamQuery = <A, E>(
430
+ atom: () => Atom.Writable<Atom.PullResult<A, E>, void>
431
+ ): StreamQueryView<A, E> => {
432
+ const registry = injectRegistry()
433
+ const atomRef = computed(atom)
434
+ const atomResult = useAtomValue(() => atomRef.value)
435
+ const result = computed(() => atomResult.value)
436
+ const pull = () => registry.set(atomRef.value, void 0)
437
+ const refresh = () => registry.refresh(atomRef.value)
438
+ const awaitResult = () => awaitAtomResult(registry, atomRef.value)
439
+ const pullAndAwait = () =>
440
+ Effect.gen(function*() {
441
+ pull()
442
+ return yield* awaitResult()
443
+ })
444
+ const items = computed(() =>
445
+ Option.getOrElse(
446
+ Option.map(AsyncResult.value(result.value), (_) => _.items),
447
+ () => []
448
+ )
449
+ )
450
+ const latest = computed(() => items.value.at(-1))
451
+ const done = computed(() =>
452
+ Option.getOrElse(
453
+ Option.map(AsyncResult.value(result.value), (_) => _.done),
454
+ () => false
455
+ )
456
+ )
457
+
458
+ return {
459
+ result,
460
+ items,
461
+ latest,
462
+ done,
463
+ awaitResult,
464
+ pull,
465
+ pullAndAwait,
466
+ refresh,
467
+ registry,
468
+ atom: atomRef
469
+ }
470
+ }
471
+
472
+ const optionValue = <I>(
473
+ arr: I | WatchSource<I> | undefined | WatchSource<Option.Option<I>>,
474
+ options?: { readonly mode?: "optional"; readonly enabled?: MaybeRefOrGetter<boolean | undefined> }
475
+ ): readonly [{ readonly value: I }, ComputedRef<boolean>] => {
476
+ if (options?.mode === "optional") {
477
+ const getOption: () => Option.Option<I> = typeof arr === "function"
478
+ ? arr as () => Option.Option<I>
479
+ : () => (arr as { value: Option.Option<I> }).value
480
+ return [
481
+ {
482
+ get value() {
483
+ return Option.getOrUndefined(getOption()) as I
484
+ }
485
+ },
486
+ computed(() => Option.isSome(getOption()))
487
+ ] as const
488
+ }
489
+ const req = !arr
490
+ ? ({ value: undefined as I })
491
+ : typeof arr === "function"
492
+ ? ({
493
+ get value() {
494
+ return (arr as any)()
495
+ }
496
+ })
497
+ : (ref(arr) as any)
498
+ const enabled = options?.enabled
499
+ return [req, computed(() => enabled === undefined ? true : !!toValue(enabled))] as const
500
+ }
501
+
502
+ const observedAtom = <A, E>(
503
+ atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>,
504
+ options?: {
505
+ readonly staleTime?: number
506
+ readonly gcTime?: number | "infinity"
507
+ readonly idleTTL?: number | "infinity"
508
+ readonly refetchOnWindowFocus?: boolean
509
+ readonly revalidateOnFocus?: boolean
510
+ readonly structuralSharing?: boolean
511
+ readonly refetchInterval?: number
512
+ readonly refreshEvery?: number
513
+ }
514
+ ): Atom.Atom<AsyncResult.AsyncResult<A, E>> => withQueryOptions(atom, normalizeQueryOptions(options))
515
+
516
+ const observedStreamAtom = <A, E>(
517
+ atom: Atom.Writable<Atom.PullResult<A, E>, void>,
518
+ options?: AtomStreamQueryOptions
519
+ ): Atom.Writable<Atom.PullResult<A, E>, void> => {
520
+ let next = atom
521
+ const refetchInterval = options?.refreshEvery ?? options?.refetchInterval
522
+ if (refetchInterval !== undefined) next = Atom.withRefresh(refetchInterval)(next)
523
+ const gcTime = options?.idleTTL ?? options?.gcTime
524
+ if (gcTime === "infinity") return Atom.keepAlive(next)
525
+ if (gcTime !== undefined) return Atom.setIdleTTL(next, gcTime)
526
+ return next
75
527
  }
76
528
 
77
- export class KnownFiberFailure<E> extends Error {
78
- readonly error: unknown
79
- constructor(public effectCause: Cause.Cause<E>) {
80
- super("Query failed with cause: " + Cause.squash(effectCause))
81
- this.error = Cause.squash(effectCause)
529
+ const queryAtomFor = <I, A, E, TData>(
530
+ rt: AtomClientRuntime,
531
+ q: QueryFamilyDescriptor<I, A, E>,
532
+ arg: I,
533
+ options?: Omit<AtomQueryNewOptions<A, TData>, "select"> | Omit<CustomUseQueryOptions<A, E, TData>, "select">
534
+ ): Atom.Atom<AsyncResult.AsyncResult<A, E>> => {
535
+ const family = getQueryFamily(rt, q)
536
+ return observedAtom(family(arg), options)
537
+ }
538
+
539
+ const makeQueryView = <I, A, E, TData>(
540
+ getAtomRt: () => AtomClientRuntime,
541
+ q: QueryFamilyDescriptor<I, A, E>,
542
+ arg: I | WatchSource<I> | undefined | WatchSource<Option.Option<I>>,
543
+ options?: (AtomQueryNewOptions<A, TData> | CustomUseQueryOptions<A, E, TData>) & {
544
+ readonly mode?: "optional"
545
+ }
546
+ ): QueryView<TData, E> => {
547
+ const atomRt = getAtomRt()
548
+ const registry = injectRegistry()
549
+ const [req, enabledRef] = optionValue<I>(arg, options)
550
+ const family = getQueryFamily(atomRt, q)
551
+ const atomRef = computed(() => enabledRef.value ? observedAtom(family(req.value), options) : disabledQueryAtom)
552
+ const rawResult = useAtomValue(() => atomRef.value) as ComputedRef<AsyncResult.AsyncResult<A, E>>
553
+ // `initialData` / `placeholderData` display fallback (old `.query()` API). Applied only while the
554
+ // result is still Initial; Failures/Success pass through unmasked. Neither is written to cache
555
+ // (the base atom stays Initial, so the mount-staleness check still triggers the real fetch). The
556
+ // fallback runs PRE-select, matching TanStack (`initialData`/`placeholderData` are TQueryData).
557
+ const fallbackResult = withDataFallback(rawResult, options)
558
+ const select = options?.select
559
+ const result = (select
560
+ ? computed(() => AsyncResult.map(fallbackResult.value, select))
561
+ : fallbackResult) as ComputedRef<AsyncResult.AsyncResult<TData, E>>
562
+ const refresh = () => registry.refresh(atomRef.value)
563
+ const awaitResult = () =>
564
+ select
565
+ ? awaitAtomResult(registry, atomRef.value).pipe(Effect.map(select))
566
+ : awaitAtomResult(registry, atomRef.value)
567
+ const refetch = () =>
568
+ Effect.gen(function*() {
569
+ yield* refreshAtomWithCurrentSpan(registry, atomRef.value)
570
+ return yield* awaitResult()
571
+ })
572
+ const staleMs = staleTimeMsOf(normalizeQueryOptions(options))
573
+ onMounted(() => {
574
+ if (!enabledRef.value) return
575
+ if (isStaleResult(registry.get(atomRef.value), staleMs)) refresh()
576
+ })
577
+ const data = computed(() => Option.getOrUndefined(AsyncResult.value(result.value)))
578
+
579
+ return {
580
+ result,
581
+ data,
582
+ awaitResult,
583
+ refetch,
584
+ refresh,
585
+ registry,
586
+ atom: atomRef
82
587
  }
83
588
  }
84
589
 
85
- export const makeQuery = <R>(getRuntime: () => ServiceMap.ServiceMap<R>) => {
590
+ export const makeQueryFamily = <R>(_getRuntime: () => Context.Context<R>, getAtomRt: () => AtomClientRuntime) => {
591
+ const useQueryFamily: {
592
+ <I, E, A, Request extends Req, Name extends string>(
593
+ q: RequestHandlerWithInput<I, A, E, R, Request, Name>
594
+ ): QueryAtomFamily<I, A, E>
595
+ } = <I, E, A, Request extends Req, Name extends string>(
596
+ q: RequestHandlerWithInput<I, A, E, R, Request, Name>
597
+ ) => getQueryFamily(getAtomRt(), q)
598
+
599
+ return useQueryFamily
600
+ }
601
+
602
+ export const makeQueryAtom = <R>(_getRuntime: () => Context.Context<R>, getAtomRt: () => AtomClientRuntime) => {
603
+ const useQueryAtom: {
604
+ <I, E, A, Request extends Req, Name extends string>(
605
+ q: RequestHandlerWithInput<I, A, E, R, Request, Name>
606
+ ): {
607
+ <TData = A>(
608
+ arg: I,
609
+ options?: Omit<AtomQueryNewOptions<A, TData>, "select">
610
+ ): Atom.Atom<AsyncResult.AsyncResult<A, E>>
611
+ }
612
+ } = <I, E, A, Request extends Req, Name extends string>(
613
+ q: RequestHandlerWithInput<I, A, E, R, Request, Name>
614
+ ) =>
615
+ <TData = A>(
616
+ arg: I,
617
+ options?: Omit<AtomQueryNewOptions<A, TData>, "select">
618
+ ) => queryAtomFor(getAtomRt(), q, arg, options)
619
+
620
+ return useQueryAtom
621
+ }
622
+
623
+ export const makeQueryNew = <R>(_getRuntime: () => Context.Context<R>, getAtomRt: () => AtomClientRuntime) => {
624
+ const useQueryNew: {
625
+ <I, E, A, Request extends Req, Name extends string>(
626
+ q: RequestHandlerWithInput<I, A, E, R, Request, Name>
627
+ ): {
628
+ <TData = A>(
629
+ arg: WatchSource<Option.Option<I>>,
630
+ options: Omit<AtomQueryNewOptions<A, TData>, "enabled"> & { mode: "optional" }
631
+ ): QueryView<TData, E>
632
+
633
+ <TData = A>(
634
+ arg: I | WatchSource<I> | undefined,
635
+ options?: AtomQueryNewOptions<A, TData>
636
+ ): QueryView<TData, E>
637
+ }
638
+ } = <I, E, A, Request extends Req, Name extends string>(
639
+ q: RequestHandlerWithInput<I, A, E, R, Request, Name>
640
+ ) =>
641
+ <TData = A>(
642
+ arg: I | WatchSource<I> | undefined | WatchSource<Option.Option<I>>,
643
+ options?: AtomQueryNewOptions<A, TData> & { readonly mode?: "optional" }
644
+ ) => makeQueryView<I, A, E, TData>(getAtomRt, q, arg, options)
645
+
646
+ return useQueryNew
647
+ }
648
+
649
+ export const makeQuery = <R>(_getRuntime: () => Context.Context<R>, getAtomRt: () => AtomClientRuntime) => {
86
650
  const useQuery_: {
87
651
  <I, A, E, Request extends Req, Name extends string>(
88
- q:
89
- | RequestHandlerWithInput<I, A, E, R, Request, Name>
90
- | RequestHandler<A, E, R, Request, Name>
652
+ q: RequestHandlerWithInput<I, A, E, R, Request, Name>
91
653
  ): {
654
+ <TData = A>(
655
+ arg: WatchSource<Option.Option<I>>,
656
+ options: Omit<CustomUndefinedInitialQueryOptions<A, E, TData>, "enabled"> & { mode: "optional" }
657
+ ): readonly [
658
+ ComputedRef<AsyncResult.AsyncResult<TData, E>>,
659
+ ComputedRef<TData | undefined>,
660
+ (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, CauseException<E>>>,
661
+ UseQueryDefinedReturnType<TData, CauseException<E>>
662
+ ]
663
+
92
664
  <TData = A>(
93
665
  arg: I | WatchSource<I> | undefined,
94
666
  options?: CustomUndefinedInitialQueryOptions<A, E, TData>
95
667
  ): readonly [
96
668
  ComputedRef<AsyncResult.AsyncResult<TData, E>>,
97
669
  ComputedRef<TData | undefined>,
98
- (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, KnownFiberFailure<E>>, never, never>,
99
- UseQueryDefinedReturnType<TData, KnownFiberFailure<E>>
670
+ (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, CauseException<E>>>,
671
+ UseQueryDefinedReturnType<TData, CauseException<E>>
100
672
  ]
101
673
 
102
674
  <TData = A>(
@@ -105,8 +677,8 @@ export const makeQuery = <R>(getRuntime: () => ServiceMap.ServiceMap<R>) => {
105
677
  ): readonly [
106
678
  ComputedRef<AsyncResult.AsyncResult<TData, E>>,
107
679
  ComputedRef<TData>,
108
- (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, KnownFiberFailure<E>>, never, never>,
109
- UseQueryDefinedReturnType<TData, KnownFiberFailure<E>>
680
+ (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, CauseException<E>>>,
681
+ UseQueryDefinedReturnType<TData, CauseException<E>>
110
682
  ]
111
683
 
112
684
  <TData = A>(
@@ -115,253 +687,178 @@ export const makeQuery = <R>(getRuntime: () => ServiceMap.ServiceMap<R>) => {
115
687
  ): readonly [
116
688
  ComputedRef<AsyncResult.AsyncResult<TData, E>>,
117
689
  ComputedRef<TData>,
118
- (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, KnownFiberFailure<E>>, never, never>,
119
- UseQueryDefinedReturnType<TData, KnownFiberFailure<E>>
690
+ (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, CauseException<E>>>,
691
+ UseQueryDefinedReturnType<TData, CauseException<E>>
120
692
  ]
121
693
  }
122
694
  } = <I, A, E, Request extends Req, Name extends string>(
123
- q:
124
- | RequestHandlerWithInput<I, A, E, R, Request, Name>
125
- | RequestHandler<A, E, R, Request, Name>
695
+ q: RequestHandlerWithInput<I, A, E, R, Request, Name>
126
696
  ) =>
127
697
  <TData = A>(
128
- arg: I | WatchSource<I> | undefined,
129
- // todo QueryKey type would be [string, ...string[]], but with I it would be [string, ...string[], I]
698
+ arg: I | WatchSource<I> | undefined | WatchSource<Option.Option<I>>,
130
699
  options?: any
131
- // TODO
132
700
  ) => {
133
- // we wrap into KnownFiberFailure because we want to keep the full cause of the failure.
134
- const runPromise = flow(Effect.runPromiseExitWith(getRuntime()), (_) =>
135
- _.then(
136
- Exit.match({
137
- onFailure: (cause) => Promise.reject(new KnownFiberFailure(cause)),
138
- onSuccess: (value) => Promise.resolve(value)
139
- })
140
- ))
141
- const arr = arg
142
- const req: { value: I } = !arg
143
- ? undefined
144
- : typeof arr === "function"
145
- ? ({
146
- get value() {
147
- return (arr as any)()
148
- }
149
- } as any)
150
- : ref(arg)
151
- const queryKey = makeQueryKey(q)
152
- const handler = q.handler
153
-
154
- const defaultOptions = {
155
- // we do not want to throw errors, because we turn the success and error responses into a Result type
156
- // why don't we turn the error/success response into a Result type before returning to tanstack query? because we want to leverage tanstack query's retry and caching mechanism, which relies on throwing errors to trigger retries, and we don't want to interfere with that by catching the errors too early.
157
- // but if we allow tanstack query to throw, it will trigger the error boundary in Vue - via a "watcher callback" error - which we currently report and log, which is not what we want.
158
- // TODO: we might want to rethink the strategy of how to handle errors that happen after the initial load.
159
- // For suspense, the initial load is captured by the suspense boundary.
160
- // For subsequent loads (or non suspense use) we currently are required to use the QueryResult component to conditionally render error/loading/etc.
161
- throwOnError: false
162
- }
163
-
164
- const r = useTanstackQuery<A, KnownFiberFailure<E>, TData>(
165
- Effect.isEffect(handler)
166
- ? {
167
- ...defaultOptions,
168
- ...options,
169
- retry: (retryCount, error) => {
170
- if (error instanceof KnownFiberFailure) {
171
- if (!isHttpClientError(error.error) && !S.is(ServiceUnavailableError)(error.error)) {
172
- return false
173
- }
174
- }
175
-
176
- return retryCount < 5
177
- },
178
- queryKey,
179
- queryFn: ({ meta, signal }) =>
180
- runPromise(
181
- handler
182
- .pipe(
183
- Effect.tapCauseIf(Cause.hasDies, (cause) => reportRuntimeError(cause)),
184
- Effect.withSpan(`query ${q.id}`, {}, { captureStackTrace: false }),
185
- meta?.["span"] ? Effect.withParentSpan(meta["span"] as Span) : (_) => _
186
- ),
187
- { signal }
188
- )
189
- }
190
- : {
191
- ...defaultOptions,
192
- ...options,
193
- retry: (retryCount, error) => {
194
- if (error instanceof KnownFiberFailure) {
195
- if (!isHttpClientError(error.error) && !S.is(ServiceUnavailableError)(error.error)) {
196
- return false
197
- }
198
- }
199
-
200
- return retryCount < 5
201
- },
202
- queryKey: [...queryKey, req],
203
- queryFn: ({ meta, signal }) =>
204
- runPromise(
205
- handler(req.value)
206
- .pipe(
207
- Effect.tapCauseIf(Cause.hasDies, (cause) => reportRuntimeError(cause)),
208
- Effect.withSpan(`query ${q.id}`, {}, { captureStackTrace: false }),
209
- meta?.["span"] ? Effect.withParentSpan(meta["span"] as Span) : (_) => _
210
- ),
211
- { signal }
212
- )
213
- }
214
- )
701
+ const view = makeQueryView<I, A, E, TData>(getAtomRt, q, arg, options)
215
702
 
216
- const latestSuccess = shallowRef<TData>()
217
- const result = computed((): AsyncResult.AsyncResult<TData, E> =>
218
- swrToQuery({
219
- error: r.error.value ?? undefined,
220
- data: r.data.value === undefined ? latestSuccess.value : r.data.value, // we fall back to existing data, as tanstack query might loose it when the key changes
221
- isValidating: r.isFetching.value
222
- })
223
- )
224
- // not using `computed` here as we have a circular dependency
225
- watch(result, (value) => latestSuccess.value = Option.getOrUndefined(AsyncResult.value(value)), { immediate: true })
703
+ // 4th element is internal-only; the public `.suspense()` Promise boundary lives in makeClient.
704
+ const handle = {
705
+ awaitResult: view.awaitResult,
706
+ refetch: view.refetch,
707
+ refresh: view.refresh,
708
+ registry: view.registry,
709
+ atom: view.atom
710
+ }
226
711
 
227
712
  return [
228
- result,
229
- computed(() => latestSuccess.value),
230
- // one thing to keep in mind is that span will be disconnected as Context does not pass from outside.
231
- // TODO: consider how we should handle the Result here which is `QueryObserverResult<A, E>`
232
- // and always ends up in the success channel, even when error..
233
- (options?: RefetchOptions) =>
234
- Effect.currentSpan.pipe(
235
- Effect.orElseSucceed(() => null),
236
- Effect.flatMap((span) => Effect.promise(() => r.refetch({ ...options, updateMeta: { span } })))
237
- ),
238
- r
713
+ view.result,
714
+ view.data,
715
+ (_options?: RefetchOptions) => view.refetch(),
716
+ handle
239
717
  ] as any
240
718
  }
241
719
 
242
- function swrToQuery<E, A>(r: {
243
- error: KnownFiberFailure<E> | undefined
244
- data: A | undefined
245
- isValidating: boolean
246
- }): AsyncResult.AsyncResult<A, E> {
247
- if (r.error !== undefined) {
248
- return AsyncResult.failureWithPrevious(
249
- r.error.effectCause,
250
- {
251
- previous: r.data === undefined ? Option.none() : Option.some(AsyncResult.success(r.data)),
252
- waiting: r.isValidating
253
- }
254
- )
255
- }
256
- if (r.data !== undefined) {
257
- return AsyncResult.success<A, E>(r.data, { waiting: r.isValidating })
258
- }
259
-
260
- return AsyncResult.initial(r.isValidating)
261
- }
262
-
263
720
  const useQuery: {
264
721
  /**
265
722
  * Effect results are passed to the caller, including errors.
723
+ * When `I = void` the input argument may be omitted.
266
724
  * @deprecated use client helpers instead (.query())
267
725
  */
268
- <E, A, Request extends Req, Name extends string>(
269
- self: RequestHandler<A, E, R, Request, Name>
726
+ <I, E, A, Request extends Req, Name extends string>(
727
+ self: RequestHandlerWithInput<I, A, E, R, Request, Name>
270
728
  ): {
271
- // required options, with initialData
272
- /**
273
- * Effect results are passed to the caller, including errors.
274
- */
275
729
  <TData = A>(
276
- options: CustomDefinedInitialQueryOptions<A, KnownFiberFailure<E>, TData>
730
+ arg: WatchSource<Option.Option<I>>,
731
+ options: Omit<CustomUndefinedInitialQueryOptions<A, CauseException<E>, TData>, "enabled"> & { mode: "optional" }
277
732
  ): readonly [
278
733
  ComputedRef<AsyncResult.AsyncResult<TData, E>>,
279
- ComputedRef<TData>,
280
- (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, KnownFiberFailure<E>>>,
281
- UseQueryReturnType<any, any>
282
- ]
283
- <TData = A>(
284
- options: CustomDefinedPlaceholderQueryOptions<A, E, TData>
285
- ): readonly [
286
- ComputedRef<AsyncResult.AsyncResult<TData, E>>,
287
- ComputedRef<TData>,
288
- (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, KnownFiberFailure<E>>, never, never>,
289
- UseQueryDefinedReturnType<TData, KnownFiberFailure<E>>
290
- ]
291
- // optional options, optional A
292
- /**
293
- * Effect results are passed to the caller, including errors.
294
- */
295
- <TData = A>(options?: CustomUndefinedInitialQueryOptions<A, KnownFiberFailure<E>, TData>): readonly [
296
- ComputedRef<AsyncResult.AsyncResult<A, E>>,
297
- ComputedRef<A | undefined>,
298
- (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, KnownFiberFailure<E>>>,
734
+ ComputedRef<TData | undefined>,
735
+ (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, CauseException<E>>>,
299
736
  UseQueryReturnType<any, any>
300
737
  ]
301
- }
302
- /**
303
- * Effect results are passed to the caller, including errors.
304
- * @deprecated use client helpers instead (.query())
305
- */
306
- <Arg, E, A, Request extends Req, Name extends string>(
307
- self: RequestHandlerWithInput<Arg, A, E, R, Request, Name>
308
- ): {
309
- // required options, with initialData
310
- /**
311
- * Effect results are passed to the caller, including errors.
312
- */
313
738
  <TData = A>(
314
- arg: Arg | WatchSource<Arg>,
315
- options: CustomDefinedInitialQueryOptions<A, KnownFiberFailure<E>, TData>
739
+ arg: I | WatchSource<I>,
740
+ options: CustomDefinedInitialQueryOptions<A, CauseException<E>, TData>
316
741
  ): readonly [
317
742
  ComputedRef<AsyncResult.AsyncResult<TData, E>>,
318
743
  ComputedRef<TData>,
319
- (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, KnownFiberFailure<E>>>,
744
+ (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, CauseException<E>>>,
320
745
  UseQueryReturnType<any, any>
321
746
  ]
322
- // required options, with placeholderData
323
- /**
324
- * Effect results are passed to the caller, including errors.
325
- */
326
747
  <TData = A>(
327
- arg: Arg | WatchSource<Arg>,
328
- options: CustomDefinedPlaceholderQueryOptions<A, KnownFiberFailure<E>, TData>
748
+ arg: I | WatchSource<I>,
749
+ options: CustomDefinedPlaceholderQueryOptions<A, CauseException<E>, TData>
329
750
  ): readonly [
330
751
  ComputedRef<AsyncResult.AsyncResult<TData, E>>,
331
752
  ComputedRef<TData>,
332
- (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, KnownFiberFailure<E>>>,
753
+ (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, CauseException<E>>>,
333
754
  UseQueryReturnType<any, any>
334
755
  ]
335
- // optional options, optional A
336
- /**
337
- * Effect results are passed to the caller, including errors.
338
- */
339
756
  <TData = A>(
340
- arg: Arg | WatchSource<Arg>,
341
- options?: CustomUndefinedInitialQueryOptions<A, KnownFiberFailure<E>, TData>
757
+ arg: I | WatchSource<I>,
758
+ options?: CustomUndefinedInitialQueryOptions<A, CauseException<E>, TData>
342
759
  ): readonly [
343
760
  ComputedRef<AsyncResult.AsyncResult<TData, E>>,
344
761
  ComputedRef<TData | undefined>,
345
- (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, KnownFiberFailure<E>>>,
762
+ (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, CauseException<E>>>,
346
763
  UseQueryReturnType<any, any>
347
764
  ]
348
765
  }
349
- } = (
766
+ } = ((
350
767
  self: any
351
768
  ) => {
352
769
  const q = useQuery_(self)
353
-
354
- return (argOrOptions?: any, options?: any) =>
355
- Effect.isEffect(self.handler)
356
- ? q(undefined, argOrOptions)
357
- : q(argOrOptions, options)
358
- }
770
+ return (arg?: any, options?: any) => q(arg, options)
771
+ }) as any
359
772
  return useQuery
360
773
  }
361
774
 
362
775
  // eslint-disable-next-line @typescript-eslint/no-empty-object-type
363
776
  export interface MakeQuery2<R> extends ReturnType<typeof makeQuery<R>> {}
364
777
 
778
+ const streamQueryAtomFor = <I, A, E>(
779
+ rt: AtomClientRuntime,
780
+ q: StreamQueryFamilyDescriptor<I, A, E>,
781
+ arg: I,
782
+ options?: AtomStreamQueryOptions
783
+ ): Atom.Writable<Atom.PullResult<A, E>, void> => {
784
+ const family = getStreamQueryFamily(rt, q)
785
+ return observedStreamAtom(family(arg), options)
786
+ }
787
+
788
+ export const makeStreamQueryFamily = <R>(_getRuntime: () => Context.Context<R>, getAtomRt: () => AtomClientRuntime) => {
789
+ const useStreamQueryFamily: {
790
+ <I, E, A, Request extends Req, Name extends string>(
791
+ q: RequestStreamHandlerWithInput<I, A, E, R, Request, Name>
792
+ ): StreamQueryAtomFamily<I, A, E>
793
+ } = <I, E, A, Request extends Req, Name extends string>(
794
+ q: RequestStreamHandlerWithInput<I, A, E, R, Request, Name>
795
+ ) => getStreamQueryFamily(getAtomRt(), q)
796
+
797
+ return useStreamQueryFamily
798
+ }
799
+
800
+ export const makeStreamQueryAtom = <R>(_getRuntime: () => Context.Context<R>, getAtomRt: () => AtomClientRuntime) => {
801
+ const useStreamQueryAtom: {
802
+ <I, E, A, Request extends Req, Name extends string>(
803
+ q: RequestStreamHandlerWithInput<I, A, E, R, Request, Name>
804
+ ): (arg: I, options?: AtomStreamQueryOptions) => Atom.Writable<Atom.PullResult<A, E>, void>
805
+ } = <I, E, A, Request extends Req, Name extends string>(
806
+ q: RequestStreamHandlerWithInput<I, A, E, R, Request, Name>
807
+ ) =>
808
+ (arg: I, options?: AtomStreamQueryOptions) => streamQueryAtomFor(getAtomRt(), q, arg, options)
809
+
810
+ return useStreamQueryAtom
811
+ }
812
+
813
+ export const makeStreamQueryNew = <R>(_getRuntime: () => Context.Context<R>, getAtomRt: () => AtomClientRuntime) => {
814
+ const useStreamQueryNew: {
815
+ <I, E, A, Request extends Req, Name extends string>(
816
+ q: RequestStreamHandlerWithInput<I, A, E, R, Request, Name>
817
+ ): (arg: MaybeRefOrGetter<I>, options?: AtomStreamQueryOptions) => StreamQueryView<A, E>
818
+ } = <I, E, A, Request extends Req, Name extends string>(
819
+ q: RequestStreamHandlerWithInput<I, A, E, R, Request, Name>
820
+ ) =>
821
+ (arg: MaybeRefOrGetter<I>, options?: AtomStreamQueryOptions) =>
822
+ useAtomStreamQuery(() => streamQueryAtomFor(getAtomRt(), q, toValue(arg), options))
823
+
824
+ return useStreamQueryNew
825
+ }
826
+
827
+ type StreamQueryResult<A, E> = readonly [
828
+ ComputedRef<AsyncResult.AsyncResult<A[], E>>,
829
+ ComputedRef<A[] | undefined>,
830
+ (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<A[], CauseException<E>>>,
831
+ UseQueryReturnType<any, any>
832
+ ]
833
+
834
+ export const makeStreamQuery = <R>(
835
+ getRuntime: () => Context.Context<R>,
836
+ getAtomRt: () => AtomClientRuntime
837
+ ) => {
838
+ const query = makeQuery(getRuntime, getAtomRt)
839
+ // A stream query is an ordinary atom query over an effect that collects the whole stream
840
+ // into an array (`Stream.runCollect`). It reuses all the atom machinery (family cache, swr,
841
+ // invalidation, structural sharing). Note: unlike the old tanstack `streamedQuery`, the result
842
+ // appears once the stream completes, not incrementally (stream queries are not used today).
843
+ const streamQuery_: {
844
+ <I, E, A, Request extends Req, Name extends string>(
845
+ q: RequestStreamHandlerWithInput<I, A, E, R, Request, Name>
846
+ ): (arg: I | WatchSource<I>) => StreamQueryResult<A, E>
847
+ } = (q: any) => {
848
+ const hook = query({
849
+ id: q.id,
850
+ options: q.options,
851
+ handler: (i: any) => Stream.runCollect(q.handler(i)).pipe(Effect.map((chunk) => [...chunk]))
852
+ } as any)
853
+ return (arg?: any) => hook(arg) as any
854
+ }
855
+
856
+ return streamQuery_
857
+ }
858
+
859
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
860
+ export interface MakeStreamQuery2<R> extends ReturnType<typeof makeStreamQuery<R>> {}
861
+
365
862
  function orPrevious<E, A>(result: AsyncResult.AsyncResult<A, E>) {
366
863
  return AsyncResult.isFailure(result) && Option.isSome(result.previousSuccess)
367
864
  ? AsyncResult.success(result.previousSuccess.value, { waiting: result.waiting })
@@ -409,29 +906,14 @@ export function composeQueries<
409
906
  }
410
907
 
411
908
  export const useUpdateQuery = () => {
412
- const queryClient = useQueryClient()
909
+ const registry = injectRegistry()
413
910
 
414
911
  const f: {
415
- <A>(
416
- query: RequestHandler<A, any, any, any, any>,
417
- updater: (data: NoInfer<A>) => NoInfer<A>
418
- ): void
419
- <I, A>(
420
- query: RequestHandlerWithInput<I, A, any, any, any, any>,
912
+ <I, A, E, R, Request extends Req, Name extends string>(
913
+ query: RequestHandlerWithInput<I, A, E, R, Request, Name>,
421
914
  input: I,
422
915
  updater: (data: NoInfer<A>) => NoInfer<A>
423
916
  ): void
424
- } = (query: any, updateOrInput: any, updaterMaybe?: any) => {
425
- const updater = updaterMaybe !== undefined ? updaterMaybe : updateOrInput
426
- const key = updaterMaybe !== undefined
427
- ? [...makeQueryKey(query), updateOrInput]
428
- : makeQueryKey(query)
429
- const data = queryClient.getQueryData(key)
430
- if (data) {
431
- queryClient.setQueryData(key, updater)
432
- } else {
433
- console.warn(`Query data for key ${key} not found`, key)
434
- }
435
- }
917
+ } = (query, input, updater) => activeQueryCacheUpdater.update(registry, query, input, updater)
436
918
  return f
437
919
  }