@effect-app/vue 4.0.0-beta.33 → 4.0.0-beta.331

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