@effect-app/vue 4.0.0-beta.28 → 4.0.0-beta.281

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 (125) hide show
  1. package/CHANGELOG.md +2073 -0
  2. package/dist/atomQuery.d.ts +105 -0
  3. package/dist/atomQuery.d.ts.map +1 -0
  4. package/dist/atomQuery.js +283 -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/errorReporter.d.ts +6 -4
  12. package/dist/errorReporter.d.ts.map +1 -1
  13. package/dist/errorReporter.js +14 -19
  14. package/dist/form.d.ts +14 -5
  15. package/dist/form.d.ts.map +1 -1
  16. package/dist/form.js +46 -13
  17. package/dist/internal/tanstackQuery.d.ts +9 -0
  18. package/dist/internal/tanstackQuery.d.ts.map +1 -0
  19. package/dist/internal/tanstackQuery.js +163 -0
  20. package/dist/intl.d.ts +15 -0
  21. package/dist/intl.d.ts.map +1 -0
  22. package/dist/intl.js +9 -0
  23. package/dist/lib.d.ts +7 -9
  24. package/dist/lib.d.ts.map +1 -1
  25. package/dist/lib.js +35 -10
  26. package/dist/makeClient.d.ts +212 -336
  27. package/dist/makeClient.d.ts.map +1 -1
  28. package/dist/makeClient.js +399 -411
  29. package/dist/makeContext.d.ts.map +1 -1
  30. package/dist/makeIntl.d.ts.map +1 -1
  31. package/dist/{experimental/makeUseCommand.d.ts → makeUseCommand.d.ts} +4 -3
  32. package/dist/makeUseCommand.d.ts.map +1 -0
  33. package/dist/makeUseCommand.js +13 -0
  34. package/dist/mutate.d.ts +106 -40
  35. package/dist/mutate.d.ts.map +1 -1
  36. package/dist/mutate.js +187 -58
  37. package/dist/query.d.ts +139 -45
  38. package/dist/query.d.ts.map +1 -1
  39. package/dist/query.js +330 -122
  40. package/dist/routeParams.d.ts +4 -4
  41. package/dist/routeParams.d.ts.map +1 -1
  42. package/dist/routeParams.js +4 -3
  43. package/dist/runtime.d.ts +1 -14
  44. package/dist/runtime.d.ts.map +1 -1
  45. package/dist/runtime.js +2 -26
  46. package/dist/suspense.d.ts +5 -0
  47. package/dist/suspense.d.ts.map +1 -0
  48. package/dist/suspense.js +12 -0
  49. package/dist/toast.d.ts +2 -0
  50. package/dist/toast.d.ts.map +1 -0
  51. package/dist/toast.js +2 -0
  52. package/dist/withToast.d.ts +2 -0
  53. package/dist/withToast.d.ts.map +1 -0
  54. package/dist/withToast.js +2 -0
  55. package/docs/atom-query-api-redesign.md +207 -0
  56. package/examples/streamMutation.ts +72 -0
  57. package/package.json +30 -94
  58. package/src/atomQuery.ts +381 -0
  59. package/src/commander.ts +3413 -0
  60. package/src/{experimental/confirm.ts → confirm.ts} +13 -15
  61. package/src/errorReporter.ts +65 -75
  62. package/src/form.ts +61 -18
  63. package/src/index.ts +7 -7
  64. package/src/internal/tanstackQuery.ts +250 -0
  65. package/src/intl.ts +12 -0
  66. package/src/lib.ts +49 -21
  67. package/src/makeClient.ts +941 -1159
  68. package/src/makeIntl.ts +2 -2
  69. package/src/{experimental/makeUseCommand.ts → makeUseCommand.ts} +9 -6
  70. package/src/mutate.ts +343 -151
  71. package/src/query.ts +688 -260
  72. package/src/routeParams.ts +7 -7
  73. package/src/runtime.ts +1 -31
  74. package/src/suspense.ts +17 -0
  75. package/src/toast.ts +1 -0
  76. package/src/withToast.ts +1 -0
  77. package/test/Mutation.test.ts +181 -24
  78. package/test/dist/form.test.d.ts.map +1 -1
  79. package/test/dist/lib.test.d.ts.map +1 -0
  80. package/test/dist/patchableQueryAtom.test.d.ts.map +1 -0
  81. package/test/dist/queryOptions.test.d.ts.map +1 -0
  82. package/test/dist/retrySchedule.test.d.ts.map +1 -0
  83. package/test/dist/streamFinal.test.d.ts.map +1 -0
  84. package/test/dist/streamFn.test.d.ts.map +1 -0
  85. package/test/dist/stubs.d.ts +3697 -122
  86. package/test/dist/stubs.d.ts.map +1 -1
  87. package/test/dist/stubs.js +192 -32
  88. package/test/dist/suspense-regression.test.d.ts.map +1 -0
  89. package/test/dist/suspense.test.d.ts.map +1 -0
  90. package/test/form-validation-errors.test.ts +25 -20
  91. package/test/form.test.ts +22 -3
  92. package/test/lib.test.ts +240 -0
  93. package/test/makeClient.test.ts +437 -38
  94. package/test/streamFinal.test.ts +64 -0
  95. package/test/streamFn.test.ts +457 -0
  96. package/test/stubs.ts +228 -43
  97. package/test/suspense-regression.test.ts +115 -0
  98. package/test/suspense.test.ts +160 -0
  99. package/tsconfig.examples.json +20 -0
  100. package/tsconfig.json +3 -1
  101. package/tsconfig.src.json +34 -34
  102. package/tsconfig.test.json +2 -2
  103. package/vitest.config.ts +5 -5
  104. package/dist/experimental/commander.d.ts +0 -359
  105. package/dist/experimental/commander.d.ts.map +0 -1
  106. package/dist/experimental/commander.js +0 -557
  107. package/dist/experimental/confirm.d.ts.map +0 -1
  108. package/dist/experimental/confirm.js +0 -28
  109. package/dist/experimental/intl.d.ts +0 -16
  110. package/dist/experimental/intl.d.ts.map +0 -1
  111. package/dist/experimental/intl.js +0 -5
  112. package/dist/experimental/makeUseCommand.d.ts.map +0 -1
  113. package/dist/experimental/makeUseCommand.js +0 -13
  114. package/dist/experimental/toast.d.ts +0 -47
  115. package/dist/experimental/toast.d.ts.map +0 -1
  116. package/dist/experimental/toast.js +0 -41
  117. package/dist/experimental/withToast.d.ts +0 -25
  118. package/dist/experimental/withToast.d.ts.map +0 -1
  119. package/dist/experimental/withToast.js +0 -45
  120. package/eslint.config.mjs +0 -24
  121. package/src/experimental/commander.ts +0 -1835
  122. package/src/experimental/intl.ts +0 -9
  123. package/src/experimental/toast.ts +0 -66
  124. package/src/experimental/withToast.ts +0 -99
  125. package/tsconfig.json.bak +0 -12
@@ -0,0 +1,381 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ /**
3
+ * Shared atom core for the query/mutation engine (used by query.ts + mutate.ts).
4
+ *
5
+ * Replaces the @tanstack/vue-query engine with Effect `Atom`, keeping the public
6
+ * `.query()/.suspense()/.mutate()` contract unchanged:
7
+ * - cache identity = the atom reference (one per [handler, input], via Atom.family)
8
+ * - invalidation key = reactivity keys (= the app's existing namespace query keys)
9
+ * - SWR + focus = Atom.swr (+ windowFocusSignal)
10
+ * - gcTime = Atom.setIdleTTL / Atom.keepAlive
11
+ * - retry = Effect.retry inside the atom effect
12
+ *
13
+ * Built over the app's RPC client through the existing `RequestHandlerWithInput`
14
+ * abstraction (mirrors AtomRpc's recipe; see docs/atom-query-plan.md).
15
+ */
16
+ import { defaultRegistry } from "@effect/atom-vue"
17
+ import { makeQueryKey } from "effect-app/client"
18
+ import type { ClientForOptions } from "effect-app/client/clientFor"
19
+ import { ServiceUnavailableError } from "effect-app/client/errors"
20
+ import * as Effect from "effect-app/Effect"
21
+ import * as Option from "effect-app/Option"
22
+ import * as S from "effect-app/Schema"
23
+ import * as Cause from "effect/Cause"
24
+ import * as Duration from "effect/Duration"
25
+ import * as Equal from "effect/Equal"
26
+ import * as Hash from "effect/Hash"
27
+ import type * as Layer from "effect/Layer"
28
+ import * as Stream from "effect/Stream"
29
+ import { isHttpClientError } from "effect/unstable/http/HttpClientError"
30
+ import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
31
+ import * as Atom from "effect/unstable/reactivity/Atom"
32
+ import * as AtomRegistry from "effect/unstable/reactivity/AtomRegistry"
33
+ import * as Reactivity from "effect/unstable/reactivity/Reactivity"
34
+ import { reportRuntimeError } from "./lib.ts"
35
+
36
+ /** All non-empty prefixes of a key, longest last. `[a,b,c]` -> `[[a],[a,b],[a,b,c]]`. */
37
+ const prefixesOf = (key: ReadonlyArray<unknown>): ReadonlyArray<ReadonlyArray<unknown>> =>
38
+ key.map((_, i) => key.slice(0, i + 1))
39
+
40
+ const uniqueKeys = (keys: ReadonlyArray<ReadonlyArray<unknown>>): ReadonlyArray<ReadonlyArray<unknown>> => {
41
+ const out: Array<ReadonlyArray<unknown>> = []
42
+ const seen = new Set<number>()
43
+ for (const key of keys) {
44
+ const hash = Hash.hash(key)
45
+ if (seen.has(hash)) continue
46
+ seen.add(hash)
47
+ out.push(key)
48
+ }
49
+ return out
50
+ }
51
+
52
+ // --- awaitable invalidation -------------------------------------------------------------------
53
+ // keyHash -> live query atoms registered under that key. A query atom is tracked while it is alive
54
+ // in the registry (mounted OR cached within idle-ttl) and removed on GC, so invalidation reaches
55
+ // cached-but-unmounted queries too (e.g. a list you navigated away from).
56
+ const keyAtoms = new Map<number, Set<Atom.Atom<AsyncResult.AsyncResult<any, any>>>>()
57
+
58
+ const trackByKeys =
59
+ (keys: ReadonlyArray<ReadonlyArray<unknown>>) =>
60
+ <A, E>(atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>): Atom.Atom<AsyncResult.AsyncResult<A, E>> =>
61
+ Atom.transform(atom, (get) => {
62
+ for (const key of keys) {
63
+ const h = Hash.hash(key)
64
+ let set = keyAtoms.get(h)
65
+ if (!set) keyAtoms.set(h, set = new Set())
66
+ set.add(atom)
67
+ get.addFinalizer(() => {
68
+ set.delete(atom)
69
+ if (set.size === 0) keyAtoms.delete(h)
70
+ })
71
+ }
72
+ return get(atom)
73
+ }, { initialValueTarget: atom })
74
+
75
+ const trackWritableByKeys =
76
+ (keys: ReadonlyArray<ReadonlyArray<unknown>>) =>
77
+ <A, E, W>(atom: Atom.Writable<AsyncResult.AsyncResult<A, E>, W>): Atom.Writable<AsyncResult.AsyncResult<A, E>, W> => {
78
+ const tracked = trackByKeys(keys)(atom)
79
+ return Atom.writable(
80
+ (get) => get(tracked),
81
+ (ctx, value) => ctx.set(atom, value),
82
+ (refresh) => refresh(tracked)
83
+ )
84
+ }
85
+
86
+ const atomsForKeys = (keys: ReadonlyArray<unknown>): ReadonlyArray<Atom.Atom<AsyncResult.AsyncResult<any, any>>> => {
87
+ const atoms = new Set<Atom.Atom<AsyncResult.AsyncResult<any, any>>>()
88
+ for (const key of keys) {
89
+ const set = keyAtoms.get(Hash.hash(key))
90
+ if (set) { for (const a of set) atoms.add(a) }
91
+ }
92
+ return [...atoms]
93
+ }
94
+
95
+ /**
96
+ * Invalidate the given keys and AWAIT the result. The invalidation (refetch trigger) goes through
97
+ * the built-in `Reactivity` service — the same one query atoms register against via
98
+ * `factory.withReactivity`, shared via the runtime memoMap. The await uses our own `keyAtoms`
99
+ * tracking + `awaitAtomResult`, since `Reactivity.invalidate` returns void and can't be awaited.
100
+ *
101
+ * Resolves once the affected queries have settled, so a mutation can `yield*` this and know the
102
+ * affected queries are fresh. (The await reads via the module-global default registry — the one the
103
+ * vue composables resolve via `injectRegistry`'s fallback.)
104
+ */
105
+ export const invalidateAndAwait = (keys: ReadonlyArray<unknown>): Effect.Effect<void, never, Reactivity.Reactivity> =>
106
+ Effect.gen(function*() {
107
+ yield* Reactivity.invalidate(keys) // invalidates everything but only refreshes what's mounted
108
+ const atoms = atomsForKeys(keys)
109
+ // for (const a of atoms) defaultRegistry.refresh(a) // refreshes everything even when not mounted
110
+ if (atoms.length === 0) return
111
+ yield* Effect.forEach(atoms, (a) => awaitAtomResult(defaultRegistry, a).pipe(Effect.exit))
112
+ })
113
+
114
+ const isPlainObject = (o: unknown): o is Record<string, unknown> => {
115
+ if (typeof o !== "object" || o === null) return false
116
+ const proto = Object.getPrototypeOf(o)
117
+ return proto === Object.prototype || proto === null
118
+ }
119
+
120
+ /**
121
+ * Structural sharing (tanstack `replaceEqualDeep`): walk `next` against `prev` and reuse `prev`'s
122
+ * reference for any unchanged array/object sub-tree, so unchanged data keeps referential identity
123
+ * (Vue skips re-rendering it). Leaves — including decoded Schema CLASS INSTANCES — are compared with
124
+ * Effect `Equal.equals` (structural), which reuses equal instances that tanstack's `===` could not.
125
+ */
126
+ export const replaceEqualDeep = (prev: any, next: any): any => {
127
+ if (prev === next) return prev
128
+ const bothArrays = Array.isArray(prev) && Array.isArray(next)
129
+ if (bothArrays || (isPlainObject(prev) && isPlainObject(next))) {
130
+ const a: Record<PropertyKey, any> = prev
131
+ const b: Record<PropertyKey, any> = next
132
+ const copy: Record<PropertyKey, any> = bothArrays ? [] : {}
133
+ const nextKeys: Array<PropertyKey> = bothArrays ? (b as Array<any>).map((_, i) => i) : Object.keys(b)
134
+ const nextSize = nextKeys.length
135
+ const prevSize = bothArrays ? (a as Array<any>).length : Object.keys(a).length
136
+ let equalItems = 0
137
+ for (let i = 0; i < nextSize; i++) {
138
+ const key = nextKeys[i]!
139
+ copy[key] = replaceEqualDeep(a[key], b[key])
140
+ if (copy[key] === a[key] && a[key] !== undefined) equalItems++
141
+ }
142
+ return prevSize === nextSize && equalItems === prevSize ? prev : copy
143
+ }
144
+ return Equal.equals(prev, next) ? prev : next
145
+ }
146
+
147
+ /** Atom combinator: share each new `Success` value structurally against the previous one. */
148
+ const structuralShare = <A, E>(
149
+ self: Atom.Atom<AsyncResult.AsyncResult<A, E>>
150
+ ): Atom.Atom<AsyncResult.AsyncResult<A, E>> =>
151
+ Atom.transform(self, (get) => {
152
+ const next = get(self)
153
+ if (next._tag !== "Success") return next
154
+ const prev = Option.flatMap(get.self<AsyncResult.AsyncResult<A, E>>(), AsyncResult.value)
155
+ if (Option.isNone(prev)) return next
156
+ const shared = replaceEqualDeep(prev.value, next.value)
157
+ return shared === next.value
158
+ ? next
159
+ : AsyncResult.success(shared, { waiting: next.waiting, timestamp: next.timestamp })
160
+ }, { initialValueTarget: self })
161
+
162
+ export interface AtomClientRuntime {
163
+ readonly runtime: Atom.AtomRuntime<any, never>
164
+ readonly factory: Atom.RuntimeFactory
165
+ }
166
+
167
+ /**
168
+ * Build one AtomRuntime (and its factory) from an already-built app context.
169
+ * Shares the ManagedRuntime's `memoMap` so layers are not built twice, and so
170
+ * query-registration and mutation-invalidation resolve the SAME `Reactivity`.
171
+ */
172
+ export const makeAtomClientRuntime = (
173
+ getContext: () => Layer.Layer<any, never, never>,
174
+ memoMap: Layer.MemoMap
175
+ ): AtomClientRuntime => {
176
+ const factory = Atom.context({ memoMap })
177
+ const runtime = factory((_get) => getContext())
178
+ return { runtime, factory }
179
+ }
180
+
181
+ const isRetryable = (e: unknown): boolean =>
182
+ isHttpClientError(e)
183
+ || S.is(ServiceUnavailableError)(e)
184
+ || (typeof e === "object" && e !== null && (e as any)._tag === "RpcClientError")
185
+
186
+ export interface AtomQueryOptions {
187
+ /** background-refresh threshold (TanStack staleTime; default 5s) */
188
+ readonly staleTime?: Duration.Input
189
+ /** dispose-when-idle (TanStack gcTime; default 5min). "infinity" => keepAlive */
190
+ readonly gcTime?: Duration.Input | "infinity"
191
+ /**
192
+ * Revalidate a stale query on window focus AND on network reconnect (default on, matching
193
+ * tanstack refetchOnWindowFocus + refetchOnReconnect).
194
+ */
195
+ readonly revalidateOnFocus?: boolean
196
+ /**
197
+ * Reuse references of unchanged sub-trees across refetches (default on, matching tanstack
198
+ * structuralSharing). Uses Effect `Equal` so decoded Schema instances share too — more effective
199
+ * than tanstack's `===`, but a deep compare per refetch (O(rows·fields)). Set `false` for very
200
+ * large or mostly-changing result sets where the compare costs more than the saved re-renders.
201
+ */
202
+ readonly structuralSharing?: boolean
203
+ /** poll: re-fetch every N ms (tanstack refetchInterval). */
204
+ readonly refetchInterval?: number
205
+ }
206
+
207
+ const defaults = { staleTime: Duration.seconds(5), gcTime: Duration.minutes(5) }
208
+
209
+ export interface AtomQueryMetadata {
210
+ readonly staleTimeMs: number
211
+ }
212
+
213
+ const atomQueryMetadata = new WeakMap<Atom.Atom<AsyncResult.AsyncResult<any, any>>, AtomQueryMetadata>()
214
+
215
+ const setAtomQueryMetadata = <A, E>(
216
+ atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>,
217
+ opts: AtomQueryOptions = {}
218
+ ) => {
219
+ const staleTimeMs = staleTimeMsOf(opts)
220
+ const previous = atomQueryMetadata.get(atom)
221
+ atomQueryMetadata.set(atom, {
222
+ staleTimeMs: previous === undefined ? staleTimeMs : Math.min(previous.staleTimeMs, staleTimeMs)
223
+ })
224
+ return atom
225
+ }
226
+
227
+ export const getAtomQueryMetadata = <A, E>(
228
+ atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>
229
+ ): AtomQueryMetadata | undefined => atomQueryMetadata.get(atom)
230
+
231
+ /** Exported so the vue hook can do refetch-on-mount-per-observer with the same rule as swr. */
232
+ export const isStaleResult = (r: AsyncResult.AsyncResult<any, any>, staleTimeMs: number): boolean => {
233
+ if (r.waiting) return false
234
+ const ts = r._tag === "Success"
235
+ ? r.timestamp
236
+ : r._tag === "Failure"
237
+ ? Option.getOrUndefined(Option.map(r.previousSuccess, (s) => s.timestamp))
238
+ : undefined
239
+ if (ts === undefined) return r._tag !== "Initial"
240
+ return Date.now() - ts >= staleTimeMs
241
+ }
242
+
243
+ export const staleTimeMsOf = (opts: AtomQueryOptions): number =>
244
+ Duration.toMillis(Duration.fromInputUnsafe(opts.staleTime ?? defaults.staleTime))
245
+
246
+ export const withQueryOptions = <A, E>(
247
+ self: Atom.Atom<AsyncResult.AsyncResult<A, E>>,
248
+ opts: AtomQueryOptions = {}
249
+ ): Atom.Atom<AsyncResult.AsyncResult<A, E>> => {
250
+ setAtomQueryMetadata(self, opts)
251
+ const staleTime: Duration.Input = opts.staleTime ?? defaults.staleTime
252
+ let atom = self
253
+ const revalidateOnFocus = opts.revalidateOnFocus ?? true
254
+ atom = Atom.swr({
255
+ staleTime,
256
+ revalidateOnFocus,
257
+ focusSignal: revalidateOnFocus ? focusOrReconnectSignal : undefined
258
+ })(atom)
259
+ if (opts.refetchInterval) atom = Atom.withRefresh(Duration.millis(opts.refetchInterval))(atom)
260
+ if (opts.structuralSharing ?? true) atom = structuralShare(atom)
261
+ return atom
262
+ }
263
+
264
+ /** Constant atom for disabled / `mode:"optional"`-None queries: stays Initial, never fetches. */
265
+ export const disabledQueryAtom: Atom.Atom<AsyncResult.AsyncResult<any, any>> = Atom.readable(() =>
266
+ AsyncResult.initial(false)
267
+ )
268
+
269
+ /**
270
+ * Bumps when the browser regains connectivity (the `online` event) — the tanstack
271
+ * `refetchOnReconnect` trigger. One shared listener (module-level). SSR-guarded.
272
+ */
273
+ const onlineSignal: Atom.Atom<number> = Atom.readable((get) => {
274
+ let count = 0
275
+ if (typeof window === "undefined") return count
276
+ const update = () => {
277
+ if (navigator.onLine) get.setSelf(++count)
278
+ }
279
+ window.addEventListener("online", update)
280
+ get.addFinalizer(() => window.removeEventListener("online", update))
281
+ return count
282
+ })
283
+
284
+ /**
285
+ * Focus OR reconnect, as a single signal for `swr` — both should stale-revalidate a query.
286
+ * swr takes one `focusSignal`, so we fold window-focus + reconnect into one derived atom;
287
+ * a bump from either triggers swr's stale check.
288
+ */
289
+ const focusOrReconnectSignal: Atom.Atom<number> = Atom.make((get) => get(Atom.windowFocusSignal) + get(onlineSignal))
290
+
291
+ /**
292
+ * Build the per-input atom family for a request handler — the query CACHE IDENTITY.
293
+ *
294
+ * This is the TanStack `queryKey = [handler, input]` equivalent and the piece that makes
295
+ * caching cross-component: `Atom.family` memoizes one atom per structurally-distinct input
296
+ * (v4 hashes the input via Hash/Equal), so every component querying the same handler+input
297
+ * reads the SAME atom instance => one fetch, one shared result in the global registry,
298
+ * ref-counted and GC'd on idle ttl. (The registry + ttl give lifetime; reactivity keys give
299
+ * invalidation; the family gives identity/sharing — all three are needed.)
300
+ *
301
+ * The family is created once per handler (see query.ts's per-handler cache), so it is shared
302
+ * process-wide via the registry.
303
+ *
304
+ * Invalidation is hierarchical: each atom registers under EVERY prefix of its full key
305
+ * `[...makeQueryKey(self), input]`. Since reactivity matches keys by exact hash, registering
306
+ * all prefixes means `invalidate(P)` refreshes every atom whose key starts with `P` — e.g.
307
+ * `["$X"]` refreshes all inputs, `["$X","$List",input]` only that input. (`makeQueryKey`'s
308
+ * collapsed form `getQueryKey` — what mutations invalidate by default — is one of the prefixes.)
309
+ */
310
+ export const buildQueryFamily = <I, A, E>(
311
+ rt: AtomClientRuntime,
312
+ self: {
313
+ readonly id: string
314
+ readonly handler: (i: I) => Effect.Effect<A, E, any>
315
+ readonly options?: ClientForOptions
316
+ readonly queryKeyProjectionHash?: string
317
+ }
318
+ ) => {
319
+ const baseKey = makeQueryKey(self) // hierarchical, input-independent
320
+
321
+ return Atom.family((input: I) => {
322
+ let atom: Atom.Atom<AsyncResult.AsyncResult<A, E>> = rt.runtime.atom(
323
+ self
324
+ .handler(input)
325
+ .pipe(
326
+ Effect.retry({ times: 5, while: isRetryable }),
327
+ Effect.tapCauseIf(Cause.hasDies, (cause) => reportRuntimeError(cause)),
328
+ Effect.withSpan(`query ${self.id}`, {}, { captureStackTrace: false })
329
+ )
330
+ )
331
+ // Register under every prefix of the full key => hierarchical (prefix) invalidation. Two roles:
332
+ // - withReactivity: `Reactivity.invalidate(key)` refreshes this atom (the actual refetch).
333
+ // - trackByKeys: records the atom in `keyAtoms` so the mutation can AWAIT its settle.
334
+ const fullKey = [...baseKey, input]
335
+ const projectedFullKey = self.queryKeyProjectionHash === undefined
336
+ ? fullKey
337
+ : [...baseKey, self.queryKeyProjectionHash, input]
338
+ const reactivityKeys = uniqueKeys([...prefixesOf(fullKey), ...prefixesOf(projectedFullKey)])
339
+ atom = rt.factory.withReactivity(reactivityKeys)(atom)
340
+ atom = trackByKeys(reactivityKeys)(atom)
341
+ // gcTime LAST so the whole chain (incl. the registration + tracking) stays alive through the
342
+ // idle window, letting invalidation reach a cached-but-unmounted query.
343
+ atom = Atom.setIdleTTL(atom, defaults.gcTime)
344
+ return setAtomQueryMetadata(Atom.withLabel(`query:${self.id}`)(atom))
345
+ })
346
+ }
347
+
348
+ export const buildStreamQueryFamily = <I, A, E>(
349
+ rt: AtomClientRuntime,
350
+ self: {
351
+ readonly id: string
352
+ readonly handler: (i: I) => Stream.Stream<A, E, any>
353
+ readonly options?: ClientForOptions
354
+ readonly queryKeyProjectionHash?: string
355
+ }
356
+ ) => {
357
+ const baseKey = makeQueryKey(self)
358
+
359
+ return Atom.family((input: I) => {
360
+ let atom = rt.runtime.pull(
361
+ self.handler(input).pipe(
362
+ Stream.tapCause((cause) => Cause.hasDies(cause) ? reportRuntimeError(cause) : Effect.void)
363
+ )
364
+ )
365
+ const fullKey = [...baseKey, input]
366
+ const projectedFullKey = self.queryKeyProjectionHash === undefined
367
+ ? fullKey
368
+ : [...baseKey, self.queryKeyProjectionHash, input]
369
+ const reactivityKeys = uniqueKeys([...prefixesOf(fullKey), ...prefixesOf(projectedFullKey)])
370
+ atom = rt.factory.withReactivity(reactivityKeys)(atom)
371
+ atom = trackWritableByKeys(reactivityKeys)(atom)
372
+ atom = Atom.setIdleTTL(atom, defaults.gcTime)
373
+ return setAtomQueryMetadata(Atom.withLabel(`stream-query:${self.id}`)(atom))
374
+ })
375
+ }
376
+
377
+ /** Await the first resolved (non-Waiting) result of an atom. Failing query results fail the Effect. */
378
+ export const awaitAtomResult = <A, E>(
379
+ registry: AtomRegistry.AtomRegistry,
380
+ atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>
381
+ ) => AtomRegistry.getResult(registry, atom, { suspendOnWaiting: true })