@effect-app/vue 4.0.0-beta.99 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/CHANGELOG.md +2701 -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 +318 -43
  6. package/dist/commander.d.ts.map +1 -1
  7. package/dist/commander.js +593 -81
  8. package/dist/confirm.d.ts +5 -3
  9. package/dist/confirm.d.ts.map +1 -1
  10. package/dist/confirm.js +12 -14
  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 +16 -10
  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 +4 -4
  24. package/dist/intl.d.ts.map +1 -1
  25. package/dist/intl.js +2 -2
  26. package/dist/lib.d.ts +8 -10
  27. package/dist/lib.d.ts.map +1 -1
  28. package/dist/lib.js +37 -12
  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 +232 -109
  33. package/dist/makeClient.d.ts.map +1 -1
  34. package/dist/makeClient.js +416 -84
  35. package/dist/makeContext.d.ts.map +1 -1
  36. package/dist/makeIntl.d.ts.map +1 -1
  37. package/dist/makeUseCommand.d.ts +3 -2
  38. package/dist/makeUseCommand.d.ts.map +1 -1
  39. package/dist/makeUseCommand.js +2 -2
  40. package/dist/mutate.d.ts +110 -41
  41. package/dist/mutate.d.ts.map +1 -1
  42. package/dist/mutate.js +236 -59
  43. package/dist/query.d.ts +161 -44
  44. package/dist/query.d.ts.map +1 -1
  45. package/dist/query.js +382 -113
  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 -17
  53. package/dist/runtime.d.ts.map +1 -1
  54. package/dist/runtime.js +2 -38
  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 +1 -45
  59. package/dist/toast.d.ts.map +1 -1
  60. package/dist/toast.js +2 -32
  61. package/dist/withToast.d.ts +1 -24
  62. package/dist/withToast.d.ts.map +1 -1
  63. package/dist/withToast.js +2 -45
  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 +35 -94
  69. package/src/atomQuery.ts +600 -0
  70. package/src/commander.ts +1064 -116
  71. package/src/confirm.ts +12 -14
  72. package/src/dependencyMetadata.ts +84 -0
  73. package/src/errorReporter.ts +65 -75
  74. package/src/form.ts +22 -15
  75. package/src/index.ts +7 -7
  76. package/src/internal/tanstackQuery.ts +308 -0
  77. package/src/intl.ts +2 -2
  78. package/src/lib.ts +51 -23
  79. package/src/liveQueryInvalidation.ts +138 -0
  80. package/src/makeClient.ts +972 -263
  81. package/src/makeIntl.ts +2 -2
  82. package/src/makeUseCommand.ts +8 -5
  83. package/src/mutate.ts +407 -154
  84. package/src/query.ts +758 -242
  85. package/src/queryLifetime.ts +74 -0
  86. package/src/routeParams.ts +7 -7
  87. package/src/runtime.ts +1 -54
  88. package/src/suspense.ts +43 -0
  89. package/src/toast.ts +1 -52
  90. package/src/withToast.ts +1 -99
  91. package/test/Mutation.test.ts +104 -17
  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 +3681 -256
  104. package/test/dist/stubs.d.ts.map +1 -1
  105. package/test/dist/stubs.js +166 -25
  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 +2 -1
  109. package/test/form.test.ts +2 -1
  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 +414 -32
  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 +194 -36
  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 -1
  124. package/tsconfig.src.json +34 -34
  125. package/tsconfig.test.json +2 -2
  126. package/vitest.config.ts +5 -5
  127. package/eslint.config.mjs +0 -24
  128. package/tsconfig.json.bak +0 -12
package/src/makeClient.ts CHANGED
@@ -1,157 +1,351 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { type InvalidateOptions, type InvalidateQueryFilters, isCancelledError, type QueryObserverResult, type RefetchOptions, type UseQueryReturnType } from "@tanstack/vue-query"
3
2
  import { camelCase } from "change-case"
4
- import { type Context, Effect, Exit, type Layer, type ManagedRuntime, Struct } from "effect-app"
5
3
  import { type ApiClientFactory, type Req } from "effect-app/client"
6
- import type { ExtractModuleName, RequestHandler, RequestHandlers, RequestHandlerWithInput, RequestsAny } from "effect-app/client/clientFor"
7
- import { extendM } from "effect-app/utils"
4
+ import type { ExtractModuleName, HandlerInput, RequestHandlers, RequestHandlerWithInput, RequestsAny, RequestStreamHandlerWithInput } from "effect-app/client/clientFor"
5
+ import type { CauseException } from "effect-app/client/errors"
6
+ import type { InvalidationCallback } from "effect-app/client/makeClient"
7
+ import * as Context from "effect-app/Context"
8
+ import * as Effect from "effect-app/Effect"
9
+ import * as Layer from "effect-app/Layer"
10
+ import * as S from "effect-app/Schema"
11
+ import * as Cause from "effect/Cause"
12
+ import * as Data from "effect/Data"
13
+ import * as Exit from "effect/Exit"
8
14
  import { type Fiber } from "effect/Fiber"
9
- import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
10
- import { type ComputedRef, onBeforeUnmount, ref, type WatchSource } from "vue"
11
- import { type Commander, CommanderStatic } from "./commander.js"
12
- import { type I18n } from "./intl.js"
13
- import { type CommanderResolved, makeUseCommand } from "./makeUseCommand.js"
14
- import { makeMutation, type MutationOptionsBase, useMakeMutation } from "./mutate.js"
15
- import { type CustomUndefinedInitialQueryOptions, makeQuery } from "./query.js"
16
- import { makeRunPromise } from "./runtime.js"
17
- import { type Toast } from "./toast.js"
18
-
19
- const mapHandler = <A, E, R, I = void, A2 = A, E2 = E, R2 = R>(
20
- handler: Effect.Effect<A, E, R> | ((i: I) => Effect.Effect<A, E, R>),
21
- map: (self: Effect.Effect<A, E, R>, i: I) => Effect.Effect<A2, E2, R2>
22
- ) => Effect.isEffect(handler) ? map(handler, undefined as any) : (i: I) => map(handler(i), i)
23
-
24
- export interface RequestExtensions<RT, Id extends string, I, A, E, R> {
15
+ import * as Hash from "effect/Hash"
16
+ import type * as ManagedRuntime from "effect/ManagedRuntime"
17
+ import type * as AsyncResult from "effect/reactivity/AsyncResult"
18
+ import * as Reactivity from "effect/reactivity/Reactivity"
19
+ import type * as Stream from "effect/Stream"
20
+ import * as Struct from "effect/Struct"
21
+ import { type ComputedRef, effectScope, onBeforeUnmount, onScopeDispose, ref, type WatchSource } from "vue"
22
+ import { type AtomClientRuntime, invalidateAndAwait, invalidateSoft, makeAtomClientRuntime } from "./atomQuery.ts"
23
+ import { type Commander, CommanderStatic, type Progress } from "./commander.ts"
24
+ import { makeTanstackQuery, makeTanstackQueryCacheUpdater, makeTanstackQueryClient, makeTanstackQueryInvalidator } from "./internal/tanstackQuery.ts"
25
+ import { type I18n } from "./intl.ts"
26
+ import { type CommanderResolved, makeUseCommand } from "./makeUseCommand.ts"
27
+ import { atomQueryInvalidator, combineQueryInvalidators, type InvalidationEntry, makeMutation, makeStreamMutation2, type MutationOptionsBase, type QueryInvalidator, useMakeMutation } from "./mutate.ts"
28
+ import { atomQueryCacheUpdater, type AtomQueryNewOptions, combineQueryCacheUpdaters, type CustomUndefinedInitialQueryOptions, makeQuery, makeQueryAtom, makeQueryFamily, makeQueryNew, makeStreamQuery, makeStreamQueryAtom, makeStreamQueryFamily, makeStreamQueryNew, optionalAtomQueryCacheUpdater, type QueryObserverResult, type RefetchOptions, setQueryCacheUpdater, type StreamQueryAtomFamily, type SuspenseQueryView, type UseQueryReturnType } from "./query.ts"
29
+ import { makeRunPromise } from "./runtime.ts"
30
+ import { latestDefined } from "./suspense.ts"
31
+ import { type Toast } from "./toast.ts"
32
+
33
+ export type { Progress }
34
+
35
+ const useScopedSuspenseSetup = <A>(setup: () => A) => {
36
+ const scope = effectScope()
37
+ const controller = new AbortController()
38
+ const value = scope.run(setup)
39
+ if (value === undefined) {
40
+ throw new Error("Internal Error: suspense setup scope did not initialize")
41
+ }
42
+
43
+ const isMounted = ref(true)
44
+ let stopped = false
45
+ const stop = () => {
46
+ if (stopped) return
47
+ stopped = true
48
+ isMounted.value = false
49
+ controller.abort()
50
+ scope.stop()
51
+ }
52
+ onBeforeUnmount(stop)
53
+ onScopeDispose(stop)
54
+
55
+ return [value, isMounted, controller.signal] as const
56
+ }
57
+
58
+ /**
59
+ * A suspense query settled with an interrupt-only cause while its component was still MOUNTED.
60
+ *
61
+ * That combination classifies the interrupt's origin: a navigation/unmount cancel interrupts the
62
+ * suspense *fiber* (via the scope's AbortSignal) or is hidden by effect-core before it can settle —
63
+ * it never yields a still-mounted `Failure` exit. A `Failure(interrupt-only)` observed while mounted
64
+ * therefore came from the QUERY side — e.g. the server returned an RPC interrupt `Exit` (over a 200),
65
+ * or the fetch was superseded — and silencing it leaves the user staring at a blank page.
66
+ *
67
+ * Error boundaries can rely on this split: keep silencing raw interrupt-only causes (those are
68
+ * genuinely "user navigated away"), and render this error as "the request was interrupted — reload?".
69
+ */
70
+ export class SuspenseInterruptedError extends Data.TaggedError("SuspenseInterruptedError")<{
71
+ readonly originalCause: Cause.Cause<unknown>
72
+ readonly source: "suspense" | "suspenseNew"
73
+ }> {
74
+ override get message() {
75
+ return "The request was interrupted while the page was still active"
76
+ }
77
+ }
78
+
79
+ /** Structural check (by `_tag`) so consumers compiled against older published types can detect it. */
80
+ export const isSuspenseInterruptedError = (e: unknown): e is SuspenseInterruptedError =>
81
+ typeof e === "object" && e !== null && "_tag" in e && e._tag === "SuspenseInterruptedError"
82
+
83
+ // TODO: optimize - work from encoded shape directly
84
+ const projectHandler = <
85
+ I,
86
+ A,
87
+ E,
88
+ R,
89
+ SuccessSchema extends S.Top & { readonly "EncodingServices": R },
90
+ ProjSchema extends S.Top & { readonly "DecodingServices": R }
91
+ >(
92
+ handler: (i: I) => Effect.Effect<A, E, R>,
93
+ successSchema: SuccessSchema,
94
+ projectionSchema: ProjSchema
95
+ ) => {
96
+ const encode = S.encodeUnknownEffect(successSchema)
97
+ const decode = S.decodeUnknownEffect(projectionSchema)
98
+ return (i: I) => handler(i).pipe(Effect.flatMap(encode), Effect.flatMap(decode))
99
+ }
100
+
101
+ const projectionSchemaHash = (schema: S.Top) => String(Hash.hash(schema.ast))
102
+
103
+ export interface CommandRequestExtensions<RT, Id extends string, I, A, E, R> {
25
104
  /** Defines a Command based on this call, taking the `id` of the call as the `id` of the Command.
26
105
  * The Request function will be taken as the first member of the Command, the Command required input will be the Request input.
27
106
  * see Command.wrap for details */
28
- wrap: Commander.CommanderWrap<RT, Id, Id, undefined, I, A, E, R>
107
+ wrap: <I18nKey extends string = Id, State extends Commander.IntlRecord | undefined = undefined>(
108
+ options?: Commander.FnOptions<Id, I18nKey, State>
109
+ ) => Commander.CommanderWrap<RT, Id, I18nKey, State, I, A, E, R>
29
110
  /** Defines a Command based on this call, taking the `id` of the call as the `id` of the Command.
30
111
  * see Command.fn for details */
31
- fn: Commander.CommanderFn<RT, Id, Id, undefined>
112
+ fn: <I18nKey extends string = Id, State extends Commander.IntlRecord | undefined = undefined>(
113
+ options?: Commander.FnOptions<Id, I18nKey, State>
114
+ ) => Commander.CommanderFn<RT, Id, I18nKey, State>
32
115
  }
33
116
 
34
117
  /** my other doc */
35
- export interface RequestExtWithInput<
118
+ export interface RequestExt<
36
119
  RT,
37
120
  Id extends string,
38
121
  I,
39
122
  A,
40
123
  E,
41
124
  R
42
- > extends Commander.CommandContextLocal<Id, Id>, RequestExtensions<RT, Id, I, A, E, R> {
125
+ > extends
126
+ Commander.CommandContextLocal<Id, Id>,
127
+ Commander.CommanderWrap<RT, Id, Id, undefined, I, A, E, R>,
128
+ CommandRequestExtensions<RT, Id, I, A, E, R>
129
+ {
43
130
  /**
44
- * Request the endpoint with input
131
+ * Send the request to the endpoint and return the raw Effect response.
132
+ * This does not perform query cache invalidation.
45
133
  */
46
- (i: I): Effect.Effect<A, E, R>
134
+ request: (i: I) => Effect.Effect<A, E, R>
47
135
  }
48
136
 
49
- /**
50
- * Request the endpoint
51
- */
52
- export interface RequestExt<
53
- RT,
54
- Id extends string,
55
- A,
56
- E,
57
- R
58
- > extends
59
- Commander.CommandContextLocal<Id, Id>,
60
- Commander.CommanderWrap<RT, Id, Id, undefined, void, A, E, R>,
61
- RequestExtensions<RT, Id, void, A, E, R>,
62
- Effect.Effect<A, E, R>
63
- {
137
+ export type CommandRequestWithExtensions<RT, Req> = Req extends
138
+ RequestHandlerWithInput<infer I, infer A, infer E, infer R, infer _Request, infer Id> ? RequestExt<RT, Id, I, A, E, R>
139
+ : never
140
+
141
+ export interface QueryExtensions<I, A, E, R> {
142
+ /**
143
+ * Send the request to the endpoint and return the raw Effect response.
144
+ * This does not set up query state tracking.
145
+ */
146
+ request: (i: I) => Effect.Effect<A, E, R>
64
147
  }
65
148
 
66
- export type RequestWithExtensions<RT, Req> = Req extends
67
- RequestHandlerWithInput<infer I, infer A, infer E, infer R, infer _Request, infer Id>
68
- ? RequestExtWithInput<RT, Id, I, A, E, R>
69
- : Req extends RequestHandler<infer A, infer E, infer R, infer _Request, infer Id> ? RequestExt<RT, Id, A, E, R>
149
+ export type QueryRequestWithExtensions<Req> = Req extends
150
+ RequestHandlerWithInput<infer I, infer A, infer E, infer R, infer _Request, infer _Id> ? QueryExtensions<I, A, E, R>
151
+ : never
152
+
153
+ type QueryHandler<Req> = Req extends
154
+ RequestHandlerWithInput<infer I, infer A, infer E, infer R, infer Request, infer Id>
155
+ ? Request["type"] extends "query" ? RequestHandlerWithInput<I, A, E, R, Request, Id> : never
156
+ : never
157
+
158
+ type CommandHandler<Req> = Req extends
159
+ RequestHandlerWithInput<infer I, infer A, infer E, infer R, infer Request, infer Id>
160
+ ? Request["type"] extends "command" ? RequestHandlerWithInput<I, A, E, R, Request, Id> : never
161
+ : never
162
+
163
+ type QueryStreamHandler<Req> = Req extends
164
+ RequestStreamHandlerWithInput<infer I, infer A, infer E, infer R, infer Request, infer Id, infer Final>
165
+ ? [Request["stream"], Request["type"]] extends [true, "query"]
166
+ ? RequestStreamHandlerWithInput<I, A, E, R, Request, Id, Final>
167
+ : never
168
+ : never
169
+
170
+ type CommandStreamHandler<Req> = Req extends
171
+ RequestStreamHandlerWithInput<infer I, infer A, infer E, infer R, infer Request, infer Id, infer Final>
172
+ ? [Request["stream"], Request["type"]] extends [true, "command"]
173
+ ? RequestStreamHandlerWithInput<I, A, E, R, Request, Id, Final>
174
+ : never
70
175
  : never
71
176
 
72
177
  export interface MutationExtensions<RT, Id extends string, I, A, E, R> {
73
178
  /** Defines a Command based on this mutation, taking the `id` of the mutation as the `id` of the Command.
74
179
  * The Mutation function will be taken as the first member of the Command, the Command required input will be the Mutation input.
75
180
  * see Command.wrap for details */
76
- wrap: Commander.CommanderWrap<RT, Id, Id, undefined, I, A, E, R>
77
- /** Defines a Command based on this mutation, taking the `id` of the mutation as the `id` of the Command.
181
+ wrap: <I18nKey extends string = Id, State extends Commander.IntlRecord | undefined = undefined>(
182
+ options?: Commander.FnOptions<Id, I18nKey, State>
183
+ ) => Commander.CommanderWrap<RT, Id, I18nKey, State, I, A, E, R>
184
+ /** Defines a Command based on this call, taking the `id` of the mutation as the `id` of the Command.
78
185
  * see Command.fn for details */
79
- fn: Commander.CommanderFn<RT, Id, Id, undefined>
80
- }
81
-
82
- /** my other doc */
83
- export interface MutationExtWithInput<
84
- RT,
85
- Id extends string,
86
- I,
87
- A,
88
- E,
89
- R
90
- > extends Commander.CommandContextLocal<Id, Id>, MutationExtensions<RT, Id, I, A, E, R> {
91
- /**
92
- * Call the endpoint with input
93
- * Invalidate queries based on namespace of this mutation.
94
- * Do not use for queries.
95
- */
96
- (i: I): Effect.Effect<A, E, R>
186
+ fn: <I18nKey extends string = Id, State extends Commander.IntlRecord | undefined = undefined>(
187
+ options?: Commander.FnOptions<Id, I18nKey, State>
188
+ ) => Commander.CommanderFn<RT, Id, I18nKey, State>
97
189
  }
98
190
 
99
191
  /**
100
- * Call the endpoint
101
- * Invalidate queries based on namespace of this mutation.
102
- * Do not use for queries.
192
+ * Send the request to the endpoint and return the raw Effect response.
193
+ * Also invalidates query caches using the request namespace by default.
194
+ * Namespace invalidation targets parent namespace keys
195
+ * (for example `$project/$configuration.get` invalidates `$project`).
196
+ * Override invalidation in client options via `queryInvalidation`.
197
+ *
198
+ * Pass `options` to attach a `select` Effect that runs after the mutation
199
+ * succeeds (its output is returned to the caller) and/or override the default
200
+ * `queryInvalidation`.
201
+ *
202
+ * When `I = void` the input argument may be omitted.
103
203
  */
104
204
  export interface MutationExt<
105
205
  RT,
106
206
  Id extends string,
207
+ I,
107
208
  A,
108
209
  E,
109
- R
110
- > extends
111
- Commander.CommandContextLocal<Id, Id>,
112
- Commander.CommanderWrap<RT, Id, Id, undefined, void, A, E, R>,
113
- MutationExtensions<RT, Id, void, A, E, R>,
114
- Effect.Effect<A, E, R>
115
- {
210
+ R,
211
+ EA = unknown
212
+ > extends MutationExtensions<RT, Id, I, A, E, R> {
213
+ <B = A, E2 = never, R2 = never>(
214
+ input: I,
215
+ options?: MutationOptionsBase<A, B, E2, R2>
216
+ ): Effect.Effect<B, E | E2, R | R2>
217
+
218
+ project: <ProjSchema extends S.Top>(
219
+ schema: EA extends ProjSchema["Encoded"] ? ProjSchema : never
220
+ ) => MutationExt<
221
+ RT,
222
+ Id,
223
+ I,
224
+ ProjSchema["Type"],
225
+ E | S.SchemaError,
226
+ R | ProjSchema["DecodingServices"],
227
+ ProjSchema["Encoded"]
228
+ >
116
229
  }
117
230
 
118
231
  export type MutationWithExtensions<RT, Req> = Req extends
119
- RequestHandlerWithInput<infer I, infer A, infer E, infer R, infer _Request, infer Id>
120
- ? MutationExtWithInput<RT, Id, I, A, E, R>
121
- : Req extends RequestHandler<infer A, infer E, infer R, infer _Request, infer Id> ? MutationExt<RT, Id, A, E, R>
232
+ RequestHandlerWithInput<infer I, infer A, infer E, infer R, infer Request, infer Id>
233
+ ? MutationExt<RT, Id, I, A, E, R, Request["success"]["Encoded"]>
234
+ : never
235
+
236
+ /**
237
+ * The `streamFn` builder for a stream-type request handler, using the stream-specific overloads.
238
+ */
239
+ export type StreamFnStreamExtension<RT, Req> = Req extends
240
+ RequestStreamHandlerWithInput<infer _I, infer _A, infer _E, infer _R, infer _Request, infer Id, infer _Final>
241
+ ? <I18nKey extends string = Id, State extends Commander.IntlRecord | undefined = undefined>(
242
+ options?: Commander.FnOptions<Id, I18nKey, State>
243
+ ) => Commander.StreamGen<RT, Id, I18nKey, State> & Commander.NonGenStream<RT, Id, I18nKey, State>
244
+ : never
245
+
246
+ /**
247
+ * `mutate` factory — wraps per-invocation invalidation scaffolding
248
+ * into the stream itself (via `Stream.unwrap`) for use with `streamFn` combinators.
249
+ */
250
+ export type StreamMutation2WithExtensions<RT, Req> = Req extends
251
+ RequestStreamHandlerWithInput<infer I, infer A, infer E, infer R, infer _Request, infer Id, infer _Final> ?
252
+ & ((input: I) => Stream.Stream<A, E, R>)
253
+ & {
254
+ readonly id: Id
255
+ readonly wrap: <I18nKey extends string = Id, State extends Commander.IntlRecord | undefined = undefined>(
256
+ options?: Commander.FnOptions<Id, I18nKey, State>
257
+ ) => Commander.StreamerWrap<RT, Id, I18nKey, State, I, A, E, R>
258
+ }
122
259
  : never
123
260
 
124
261
  // we don't really care about the RT, as we are in charge of ensuring runtime safety anyway
125
262
  // eslint-disable-next-line unused-imports/no-unused-vars
126
263
  declare const useQuery_: QueryImpl<any>["useQuery"]
127
264
  // eslint-disable-next-line unused-imports/no-unused-vars
265
+ declare const useQueryNew_: QueryImpl<any>["useQueryNew"]
266
+ // eslint-disable-next-line unused-imports/no-unused-vars
267
+ declare const useQueryAtom_: QueryImpl<any>["useQueryAtom"]
268
+ // eslint-disable-next-line unused-imports/no-unused-vars
269
+ declare const useQueryFamily_: QueryImpl<any>["useQueryFamily"]
270
+ // eslint-disable-next-line unused-imports/no-unused-vars
271
+ declare const useSuspenseQueryNew_: QueryImpl<any>["useSuspenseQueryNew"]
272
+ // eslint-disable-next-line unused-imports/no-unused-vars
128
273
  declare const useSuspenseQuery_: QueryImpl<any>["useSuspenseQuery"]
274
+ // eslint-disable-next-line unused-imports/no-unused-vars
275
+ declare const useStreamQuery_: QueryImpl<any>["useStreamQuery"]
276
+ // eslint-disable-next-line unused-imports/no-unused-vars
277
+ declare const useStreamQueryFamily_: QueryImpl<any>["useStreamQueryFamily"]
278
+ // eslint-disable-next-line unused-imports/no-unused-vars
279
+ declare const useStreamQueryAtom_: QueryImpl<any>["useStreamQueryAtom"]
280
+ // eslint-disable-next-line unused-imports/no-unused-vars
281
+ declare const useStreamQueryNew_: QueryImpl<any>["useStreamQueryNew"]
282
+
283
+ export interface ProjectResult<RT, I, B, E, R, Request extends Req, Id extends string> {
284
+ request: (i: I) => Effect.Effect<B, E, R>
285
+ family: Exclude<R, RT> extends never ? ReturnType<typeof useQueryFamily_<I, E, B, Request, Id>>
286
+ : MissingDependencies<RT, R> & {}
287
+ query: Exclude<R, RT> extends never ? ReturnType<typeof useQuery_<I, E, B, Request, Id>>
288
+ : MissingDependencies<RT, R> & {}
289
+ queryNew: Exclude<R, RT> extends never ? ReturnType<typeof useQueryNew_<I, E, B, Request, Id>>
290
+ : MissingDependencies<RT, R> & {}
291
+ suspense: Exclude<R, RT> extends never ? ReturnType<typeof useSuspenseQuery_<I, E, B, Request, Id>>
292
+ : MissingDependencies<RT, R> & {}
293
+ suspenseNew: Exclude<R, RT> extends never ? ReturnType<typeof useSuspenseQueryNew_<I, E, B, Request, Id>>
294
+ : MissingDependencies<RT, R> & {}
295
+ atom: Exclude<R, RT> extends never ? ReturnType<typeof useQueryAtom_<I, E, B, Request, Id>>
296
+ : MissingDependencies<RT, R> & {}
297
+ }
129
298
 
130
- export interface QueriesWithInput<Request extends Req, Id extends string, I, A, E> {
299
+ export type QueryProjection<RT, HandlerReq> = HandlerReq extends
300
+ RequestHandlerWithInput<infer I, infer _A, infer E, infer R, infer Request, infer Id>
301
+ ? Request["type"] extends "query" ? {
302
+ project: <ProjSchema extends S.Top>(
303
+ schema: Request["success"]["Encoded"] extends ProjSchema["Encoded"] ? ProjSchema : never
304
+ ) => ProjectResult<
305
+ RT,
306
+ I,
307
+ ProjSchema["Type"],
308
+ E | S.SchemaError,
309
+ R | ProjSchema["DecodingServices"],
310
+ Request,
311
+ Id
312
+ >
313
+ }
314
+ : {}
315
+ : {}
316
+
317
+ export interface QueryResultExtensions<Request extends Req, Id extends string, I, A, E> {
131
318
  /**
132
- * Effect results are passed to the caller, including errors.
319
+ * Read helper for query requests.
320
+ * Runs as a tracked Vue Query and returns reactive state.
321
+ * Queries read state and should not be used to mutate it.
322
+ * When `I = void` the input argument may be omitted.
133
323
  */
134
324
  query: ReturnType<typeof useQuery_<I, E, A, Request, Id>>
325
+ /**
326
+ * Atom-native query helper with object return shape.
327
+ * Additive migration surface; `.query()` remains the compatibility tuple API.
328
+ */
329
+ queryNew: ReturnType<typeof useQueryNew_<I, E, A, Request, Id>>
135
330
  // TODO or suspense as Option?
136
331
  /**
137
- * The difference with useQuery is that this function will return a Promise you can await in the Setup,
138
- * which ensures that either there always is a latest value, or an error occurs on load.
139
- * So that Suspense and error boundaries can be used.
332
+ * Like `.query`, but returns a Promise for setup-time awaiting.
333
+ * Use this when integrating with Vue Suspense / error boundaries.
140
334
  */
141
335
  suspense: ReturnType<typeof useSuspenseQuery_<I, E, A, Request, Id>>
142
- }
143
- export interface QueriesWithoutInput<Request extends Req, Id extends string, A, E> {
144
336
  /**
145
- * Effect results are passed to the caller, including errors.
337
+ * Promise-based setup helper with object return shape.
338
+ * Additive migration surface; `.suspense()` remains the compatibility tuple API.
146
339
  */
147
- query: ReturnType<typeof useQuery_<E, A, Request, Id>>
148
- // TODO or suspense as Option?
340
+ suspenseNew: ReturnType<typeof useSuspenseQueryNew_<I, E, A, Request, Id>>
149
341
  /**
150
- * The difference with useQuery is that this function will return a Promise you can await in the Setup,
151
- * which ensures that either there always is a latest value, or an error occurs on load.
152
- * So that Suspense and error boundaries can be used.
342
+ * Raw query atom for composition outside Vue refs.
343
+ */
344
+ atom: ReturnType<typeof useQueryAtom_<I, E, A, Request, Id>>
345
+ /**
346
+ * Raw query atom family for composing query graphs before choosing a Vue observer.
153
347
  */
154
- suspense: ReturnType<typeof useSuspenseQuery_<E, A, Request, Id>>
348
+ family: ReturnType<typeof useQueryFamily_<I, E, A, Request, Id>>
155
349
  }
156
350
 
157
351
  export type MissingDependencies<RT, R> = {
@@ -161,24 +355,59 @@ export type MissingDependencies<RT, R> = {
161
355
 
162
356
  export type Queries<RT, Req> = Req extends
163
357
  RequestHandlerWithInput<infer I, infer A, infer E, infer R, infer Request, infer Id>
164
- ? Exclude<R, RT> extends never ? QueriesWithInput<Request, Id, I, A, E>
358
+ ? Request["type"] extends "query" ? Exclude<R, RT> extends never ? QueryResultExtensions<Request, Id, I, A, E>
359
+ : {
360
+ atom: MissingDependencies<RT, R> & {}
361
+ family: MissingDependencies<RT, R> & {}
362
+ query: MissingDependencies<RT, R> & {}
363
+ queryNew: MissingDependencies<RT, R> & {}
364
+ suspense: MissingDependencies<RT, R> & {}
365
+ suspenseNew: MissingDependencies<RT, R> & {}
366
+ }
367
+ : never
368
+ : never
369
+
370
+ export interface StreamQueryExtensions<Request extends Req, Id extends string, I, A, E> {
371
+ atom: ReturnType<typeof useStreamQueryAtom_<I, E, A, Request, Id>>
372
+ family: StreamQueryAtomFamily<I, A, E>
373
+ /**
374
+ * Stream helper for query-stream requests.
375
+ * Legacy compatibility helper. Collects the whole stream into an array before
376
+ * publishing data.
377
+ * When `I = void` the input argument may be omitted.
378
+ */
379
+ query: ReturnType<typeof useStreamQuery_<I, E, A, Request, Id>>
380
+ /**
381
+ * Atom-native stream query helper. Exposes incremental pull state and a `pull`
382
+ * command instead of collecting the whole stream first.
383
+ */
384
+ queryNew: ReturnType<typeof useStreamQueryNew_<I, E, A, Request, Id>>
385
+ }
386
+ export type StreamQueries<RT, HandlerReq> = HandlerReq extends
387
+ RequestStreamHandlerWithInput<infer I, infer A, infer E, infer R, infer Request, infer Id, infer _Final>
388
+ ? Exclude<R, RT> extends never ? StreamQueryExtensions<Request, Id, I, A, E>
165
389
  : {
390
+ atom: MissingDependencies<RT, R> & {}
391
+ family: MissingDependencies<RT, R> & {}
166
392
  query: MissingDependencies<RT, R> & {}
167
- suspense: MissingDependencies<RT, R> & {}
393
+ queryNew: MissingDependencies<RT, R> & {}
168
394
  }
169
- : Req extends RequestHandler<infer A, infer E, infer R, infer Request, infer Id>
170
- ? Exclude<R, RT> extends never ? QueriesWithoutInput<Request, Id, A, E>
171
- : { query: MissingDependencies<RT, R> & {}; suspense: MissingDependencies<RT, R> & {} }
172
395
  : never
173
396
 
174
- const _useMutation = makeMutation()
397
+ const _useMutation = makeMutation(atomQueryInvalidator)
398
+
399
+ const wrapWithSpan = (self: { id: string }, mut: any) => {
400
+ const span = (eff: Effect.Effect<any, any, any>) =>
401
+ Effect.withSpan(`mutation ${self.id}`, {}, { captureStackTrace: false })(eff)
402
+ return (input: any, options?: MutationOptionsBase) => span(mut(input, options))
403
+ }
175
404
 
176
405
  /**
177
406
  * Pass an Effect or a function that returns an Effect, e.g from a client action
178
407
  * Executes query cache invalidation based on default rules or provided option.
179
408
  * adds a span with the mutation id
180
409
  */
181
- export const useMutation: typeof _useMutation = <
410
+ export const useMutation: typeof _useMutation = (<
182
411
  I,
183
412
  E,
184
413
  A,
@@ -186,25 +415,21 @@ export const useMutation: typeof _useMutation = <
186
415
  Request extends Req,
187
416
  Name extends string
188
417
  >(
189
- self: RequestHandlerWithInput<I, A, E, R, Request, Name> | RequestHandler<A, E, R, Request, Name>,
190
- options?: MutationOptionsBase
418
+ self: RequestHandlerWithInput<I, A, E, R, Request, Name>
191
419
  ) =>
192
420
  Object.assign(
193
- mapHandler(
194
- _useMutation(self as any, options),
195
- Effect.withSpan(`mutation ${self.id}`, {}, { captureStackTrace: false })
196
- ) as any,
421
+ wrapWithSpan(self, _useMutation(self as any)),
197
422
  { id: self.id }
198
- )
423
+ )) as any
199
424
 
200
425
  /**
201
426
  * Pass an Effect or a function that returns an Effect, e.g from a client action
202
427
  * Executes query cache invalidation based on default rules or provided option.
203
428
  * adds a span with the mutation id
204
429
  */
205
- export const useMutationInt = (): typeof _useMutation => {
206
- const _useMutation = useMakeMutation()
207
- return <
430
+ export const useMutationInt = (queryInvalidator: QueryInvalidator): typeof _useMutation => {
431
+ const _useMutation = useMakeMutation(queryInvalidator)
432
+ return (<
208
433
  I,
209
434
  E,
210
435
  A,
@@ -212,23 +437,41 @@ export const useMutationInt = (): typeof _useMutation => {
212
437
  Request extends Req,
213
438
  Name extends string
214
439
  >(
215
- self: RequestHandlerWithInput<I, A, E, R, Request, Name> | RequestHandler<A, E, R, Request, Name>,
216
- options?: MutationOptionsBase
440
+ self: RequestHandlerWithInput<I, A, E, R, Request, Name>
217
441
  ) =>
218
442
  Object.assign(
219
- mapHandler(
220
- _useMutation(self as any, options),
221
- Effect.withSpan(`mutation ${self.id}`, {}, { captureStackTrace: false })
222
- ) as any,
443
+ wrapWithSpan(self, _useMutation(self as any)),
223
444
  { id: self.id }
224
- )
445
+ )) as any
225
446
  }
226
447
 
227
448
  export type ClientFrom<M extends RequestsAny> = RequestHandlers<never, never, M, ExtractModuleName<M>>
228
449
 
450
+ export interface MakeClientOptions {
451
+ /**
452
+ * Selects the engine behind legacy `.query()` / `.suspense()` helpers.
453
+ * Atom-native `.atom()` / `.family()` / `.queryNew()` / `.suspenseNew()` always use Atom.
454
+ */
455
+ readonly legacyQueryEngine?: "atom" | "tanstack"
456
+ }
457
+
229
458
  export class QueryImpl<R> {
230
- constructor(readonly getRuntime: () => Context.Context<R>) {
231
- this.useQuery = makeQuery(this.getRuntime)
459
+ readonly getRuntime: () => Context.Context<R>
460
+
461
+ constructor(
462
+ getRuntime: () => Context.Context<R>,
463
+ getAtomRt: () => AtomClientRuntime,
464
+ legacyUseQuery?: ReturnType<typeof makeQuery<R>>
465
+ ) {
466
+ this.getRuntime = getRuntime
467
+ this.useQuery = legacyUseQuery ?? makeQuery(this.getRuntime, getAtomRt)
468
+ this.useQueryNew = makeQueryNew(this.getRuntime, getAtomRt)
469
+ this.useQueryAtom = makeQueryAtom(this.getRuntime, getAtomRt)
470
+ this.useQueryFamily = makeQueryFamily(this.getRuntime, getAtomRt)
471
+ this.useStreamQuery = makeStreamQuery(this.getRuntime, getAtomRt)
472
+ this.useStreamQueryFamily = makeStreamQueryFamily(this.getRuntime, getAtomRt)
473
+ this.useStreamQueryAtom = makeStreamQueryAtom(this.getRuntime, getAtomRt)
474
+ this.useStreamQueryNew = makeStreamQueryNew(this.getRuntime, getAtomRt)
232
475
  }
233
476
  /**
234
477
  * Effect results are passed to the caller, including errors.
@@ -236,6 +479,24 @@ export class QueryImpl<R> {
236
479
  */
237
480
  readonly useQuery: ReturnType<typeof makeQuery<R>>
238
481
 
482
+ readonly useQueryNew: ReturnType<typeof makeQueryNew<R>>
483
+
484
+ readonly useQueryAtom: ReturnType<typeof makeQueryAtom<R>>
485
+
486
+ readonly useQueryFamily: ReturnType<typeof makeQueryFamily<R>>
487
+
488
+ /**
489
+ * Stream results are accumulated as an array of chunks and returned as reactive state.
490
+ * @deprecated use client helpers instead (.query())
491
+ */
492
+ readonly useStreamQuery: ReturnType<typeof makeStreamQuery<R>>
493
+
494
+ readonly useStreamQueryFamily: ReturnType<typeof makeStreamQueryFamily<R>>
495
+
496
+ readonly useStreamQueryAtom: ReturnType<typeof makeStreamQueryAtom<R>>
497
+
498
+ readonly useStreamQueryNew: ReturnType<typeof makeStreamQueryNew<R>>
499
+
239
500
  /**
240
501
  * The difference with useQuery is that this function will return a Promise you can await in the Setup,
241
502
  * which ensures that either there always is a latest value, or an error occurs on load.
@@ -247,22 +508,21 @@ export class QueryImpl<R> {
247
508
  * The difference with useQuery is that this function will return a Promise you can await in the Setup,
248
509
  * which ensures that either there always is a latest value, or an error occurs on load.
249
510
  * So that Suspense and error boundaries can be used.
250
- * @deprecated use client helpers instead (.suspense())
511
+ * When `I = void` the input argument may be omitted.
251
512
  */
252
513
  <
514
+ I,
253
515
  E,
254
516
  A,
255
517
  Request extends Req,
256
518
  Name extends string
257
519
  >(
258
- self: RequestHandler<A, E, R, Request, Name>
520
+ self: RequestHandlerWithInput<I, A, E, R, Request, Name>
259
521
  ): {
260
- /**
261
- * The difference with useQuery is that this function will return a Promise you can await in the Setup,
262
- * which ensures that either there always is a latest value, or an error occurs on load.
263
- * So that Suspense and error boundaries can be used.
264
- */
265
- <TData = A>(options?: CustomUndefinedInitialQueryOptions<A, E, TData>): Promise<
522
+ <TData = A>(
523
+ arg: I | WatchSource<I>,
524
+ options?: CustomUndefinedInitialQueryOptions<A, CauseException<E>, TData>
525
+ ): Promise<
266
526
  readonly [
267
527
  ComputedRef<AsyncResult.AsyncResult<TData, E>>,
268
528
  ComputedRef<TData>,
@@ -273,85 +533,115 @@ export class QueryImpl<R> {
273
533
  ]
274
534
  >
275
535
  }
276
- /**
277
- * The difference with useQuery is that this function will return a Promise you can await in the Setup,
278
- * which ensures that either there always is a latest value, or an error occurs on load.
279
- * So that Suspense and error boundaries can be used.
280
- */
536
+ } = <I, E, A, Request extends Req, Name extends string>(
537
+ self: RequestHandlerWithInput<I, A, E, R, Request, Name>
538
+ ) => {
539
+ const runPromise = makeRunPromise(this.getRuntime())
540
+ const q = this.useQuery(self)
541
+ return <TData = A>(
542
+ argOrOptions: I | WatchSource<I>,
543
+ options?: CustomUndefinedInitialQueryOptions<A, CauseException<E>, TData>
544
+ ) => {
545
+ const [
546
+ [resultRef, latestDefinedRef, fetch, uqrt],
547
+ isMounted,
548
+ signal
549
+ ] = useScopedSuspenseSetup(() => {
550
+ const [resultRef, latestRef, fetch, uqrt] = q<TData>(argOrOptions, options)
551
+ const latestDefinedRef = latestDefined(() => latestRef.value, "suspense")
552
+ return [resultRef, latestDefinedRef, fetch, uqrt] as const
553
+ })
554
+
555
+ // @effect-diagnostics effect/missingEffectError:off
556
+ const eff = Effect.gen(function*() {
557
+ const exit = yield* uqrt.awaitResult().pipe(Effect.exit)
558
+ if (!isMounted.value) {
559
+ return yield* Effect.interrupt
560
+ }
561
+ if (Exit.isFailure(exit)) {
562
+ // Still mounted, so this interrupt came from the query side (e.g. a server RPC interrupt
563
+ // Exit), not from navigation/unmount — surface it as a renderable typed error instead of a
564
+ // raw interrupt the error boundary would silence into a blank page.
565
+ if (Cause.hasInterruptsOnly(exit.cause)) {
566
+ return yield* new SuspenseInterruptedError({ originalCause: exit.cause, source: "suspense" })
567
+ }
568
+ return yield* Exit.failCause(exit.cause)
569
+ }
570
+
571
+ return [resultRef, latestDefinedRef, fetch, uqrt] as const
572
+ })
573
+
574
+ return runPromise(eff, { signal })
575
+ }
576
+ }
577
+
578
+ readonly useSuspenseQueryNew: {
281
579
  <
282
- Arg,
580
+ I,
283
581
  E,
284
582
  A,
285
583
  Request extends Req,
286
584
  Name extends string
287
585
  >(
288
- self: RequestHandlerWithInput<Arg, A, E, R, Request, Name>
586
+ self: RequestHandlerWithInput<I, A, E, R, Request, Name>
289
587
  ): {
290
- /**
291
- * The difference with useQuery is that this function will return a Promise you can await in the Setup,
292
- * which ensures that either there always is a latest value, or an error occurs on load.
293
- * So that Suspense and error boundaries can be used.
294
- */
295
- <TData = A>(arg: Arg | WatchSource<Arg>, options?: CustomUndefinedInitialQueryOptions<A, E, TData>): Promise<
296
- readonly [
297
- ComputedRef<AsyncResult.AsyncResult<TData, E>>,
298
- ComputedRef<TData>,
299
- (
300
- options?: RefetchOptions
301
- ) => Effect.Effect<QueryObserverResult<TData, E>>,
302
- UseQueryReturnType<any, any>
303
- ]
304
- >
588
+ <TData = A>(
589
+ arg: I | WatchSource<I>,
590
+ options?: AtomQueryNewOptions<A, TData>
591
+ ): Promise<SuspenseQueryView<TData, E>>
305
592
  }
306
- } = <Arg, E, A, Request extends Req, Name extends string>(
307
- self: RequestHandlerWithInput<Arg, A, E, R, Request, Name> | RequestHandler<A, E, R, Request, Name>
593
+ } = <I, E, A, Request extends Req, Name extends string>(
594
+ self: RequestHandlerWithInput<I, A, E, R, Request, Name>
308
595
  ) => {
309
596
  const runPromise = makeRunPromise(this.getRuntime())
310
- const q = this.useQuery(self as any) as any
311
- return (argOrOptions?: any, options?: any) => {
312
- const [resultRef, latestRef, fetch, uqrt] = q(argOrOptions, { ...options, suspense: true } // experimental_prefetchInRender: true }
313
- )
314
-
315
- const isMounted = ref(true)
316
- onBeforeUnmount(() => {
317
- isMounted.value = false
597
+ const q = this.useQueryNew(self)
598
+ return <TData = A>(
599
+ argOrOptions: I | WatchSource<I>,
600
+ options?: AtomQueryNewOptions<A, TData>
601
+ ) => {
602
+ const [{ view, data }, isMounted, signal] = useScopedSuspenseSetup(() => {
603
+ const view = q<TData>(argOrOptions, options)
604
+ const data = latestDefined(() => view.data.value, "suspenseNew")
605
+ return { view, data }
318
606
  })
319
607
 
320
608
  // @effect-diagnostics effect/missingEffectError:off
321
609
  const eff = Effect.gen(function*() {
322
- // we want to throw on error so that we can catch cancelled error and skip handling it
323
- // what's the difference with just calling `fetch` ?
324
- // we will receive a CancelledError which we will have to ignore in our ErrorBoundary, otherwise the user ends up on an error page even if the user e.g cancelled a navigation
325
- const r = yield* Effect.tryPromise(() => uqrt.suspense()).pipe(
326
- Effect.catchTag("UnknownError", (err) =>
327
- isCancelledError(err.cause)
328
- ? Effect.interrupt
329
- : Effect.die(err.cause))
330
- )
610
+ const exit = yield* view.awaitResult().pipe(Effect.exit)
331
611
  if (!isMounted.value) {
332
612
  return yield* Effect.interrupt
333
613
  }
334
- const result = resultRef.value
335
- if (AsyncResult.isInitial(result)) {
336
- console.error("Internal Error: Promise should be resolved already", {
337
- self,
338
- argOrOptions,
339
- options,
340
- r,
341
- resultRef
342
- })
343
- return yield* Effect.die(
344
- "Internal Error: Promise should be resolved already"
345
- )
346
- }
347
- if (AsyncResult.isFailure(result)) {
348
- return yield* Exit.failCause(result.cause)
614
+ if (Exit.isFailure(exit)) {
615
+ // Still mounted → query-side interrupt (see SuspenseInterruptedError) — render, don't silence.
616
+ if (Cause.hasInterruptsOnly(exit.cause)) {
617
+ return yield* new SuspenseInterruptedError({ originalCause: exit.cause, source: "suspenseNew" })
618
+ }
619
+ return yield* Exit.failCause(exit.cause)
349
620
  }
350
621
 
351
- return [resultRef, latestRef, fetch, uqrt] as const
622
+ const fetch = (_options?: RefetchOptions) => view.refetch()
623
+ const handle = {
624
+ awaitResult: view.awaitResult,
625
+ refetch: view.refetch,
626
+ refresh: view.refresh,
627
+ registry: view.registry,
628
+ atom: view.atom
629
+ }
630
+ return Object.assign(
631
+ [
632
+ view.result,
633
+ data,
634
+ fetch,
635
+ handle
636
+ ] as const,
637
+ {
638
+ ...view,
639
+ data
640
+ }
641
+ )
352
642
  })
353
643
 
354
- return runPromise(eff)
644
+ return runPromise(eff, { signal })
355
645
  }
356
646
  }
357
647
  }
@@ -361,11 +651,89 @@ const managedRuntimeRt = <A, E>(mrt: ManagedRuntime.ManagedRuntime<A, E>) => mrt
361
651
 
362
652
  type Base = I18n | Toast
363
653
  type Mix = ApiClientFactory | Commander | Base
654
+
655
+ type InvalidationResources = Record<string, Record<string, unknown>>
656
+ type UnionToIntersection<U> = (U extends unknown ? (arg: U) => void : never) extends ((arg: infer I) => void) ? I
657
+ : never
658
+
659
+ type CommandInvalidationResources<Req> = Req extends {
660
+ readonly type: "command"
661
+ readonly "~invalidationResources"?: infer Resources
662
+ } ? NonNullable<Resources> extends InvalidationResources ? NonNullable<Resources> : never
663
+ : Req extends {
664
+ readonly type: "command"
665
+ readonly config?: infer Config
666
+ } ? Config extends {
667
+ readonly invalidationResources?: infer LegacyResources
668
+ } ? NonNullable<LegacyResources> extends InvalidationResources ? NonNullable<LegacyResources> : never
669
+ : Config extends {
670
+ readonly invalidatesQueries?: InvalidationCallback<infer LegacyResources, any, any, any>
671
+ } ? NonNullable<LegacyResources> extends InvalidationResources ? NonNullable<LegacyResources> : never
672
+ : never
673
+ : never
674
+
675
+ type InvalidationResourcesForUnion<M extends RequestsAny> = {
676
+ [K in keyof M]: CommandInvalidationResources<M[K]>
677
+ }[keyof M]
678
+
679
+ type InvalidationResourcesFor<M extends RequestsAny> = [InvalidationResourcesForUnion<M>] extends [never] ? never
680
+ : UnionToIntersection<InvalidationResourcesForUnion<M>> extends infer R ? R extends InvalidationResources ? R
681
+ : never
682
+ : never
683
+
684
+ type QueryInvalidationFactory<M extends RequestsAny> = (client: ClientFrom<M>) => QueryInvalidation<M>
685
+
686
+ type StrictResourcesArg<Shape, Actual extends Shape = Shape> =
687
+ & Actual
688
+ & Record<Exclude<keyof Actual, keyof Shape>, never>
689
+
690
+ type ClientForArgs<
691
+ M extends RequestsAny,
692
+ Resources extends InvalidationResourcesFor<M> = InvalidationResourcesFor<M>
693
+ > = [InvalidationResourcesFor<M>] extends [never] ? [
694
+ queryInvalidation?: QueryInvalidationFactory<M>,
695
+ invalidationResources?: StrictResourcesArg<
696
+ InvalidationResourcesFor<M>,
697
+ Resources
698
+ >
699
+ ]
700
+ : [
701
+ queryInvalidation: QueryInvalidationFactory<M> | undefined,
702
+ invalidationResources: StrictResourcesArg<
703
+ InvalidationResourcesFor<M>,
704
+ Resources
705
+ >
706
+ ]
707
+
708
+ const makeResolvedAtomQueryInvalidator = <R>(getContext: () => Context.Context<R>): QueryInvalidator => {
709
+ let reactivity: Context.Service.Shape<typeof Reactivity.Reactivity> | undefined
710
+ const getReactivity = () => {
711
+ if (reactivity !== undefined) return reactivity
712
+ const service = Context.getOrUndefined(getContext(), Reactivity.Reactivity)
713
+ if (service === undefined) {
714
+ throw new Error("Reactivity service is missing from the client runtime")
715
+ }
716
+ return reactivity = service
717
+ }
718
+
719
+ return {
720
+ invalidateAndAwait: (keys) =>
721
+ invalidateAndAwait(keys).pipe(
722
+ Effect.provideService(Reactivity.Reactivity, getReactivity())
723
+ ),
724
+ invalidateSoft: (keys) =>
725
+ invalidateSoft(keys).pipe(
726
+ Effect.provideService(Reactivity.Reactivity, getReactivity())
727
+ )
728
+ }
729
+ }
730
+
364
731
  export const makeClient = <RT_, RTHooks>(
365
732
  // global, but only accessible after startup has completed
366
733
  getBaseMrt: () => ManagedRuntime.ManagedRuntime<RT_ | Mix, never>,
367
734
  clientFor_: ReturnType<typeof ApiClientFactory["makeFor"]>,
368
- rtHooks: Layer.Layer<RTHooks, never, Mix>
735
+ rtHooks: Layer.Layer<RTHooks, never, Mix>,
736
+ options?: MakeClientOptions
369
737
  ) => {
370
738
  type RT = RT_ | Mix
371
739
  const getBaseRt = () => managedRuntimeRt(getBaseMrt())
@@ -373,39 +741,241 @@ export const makeClient = <RT_, RTHooks>(
373
741
  let cmd: Effect.Success<typeof makeCommand>
374
742
  const useCommand = () => cmd ??= getBaseMrt().runSync(makeCommand)
375
743
 
744
+ // one AtomRuntime for the query engine, built lazily from the live app context;
745
+ // shares the ManagedRuntime memoMap so layers + Reactivity are the same instances.
746
+ let atomRt: AtomClientRuntime | undefined
747
+ const getAtomRt =
748
+ () => (atomRt ??= makeAtomClientRuntime(() => Layer.succeedContext(getBaseRt()), getBaseMrt().memoMap))
749
+
750
+ const legacyQueryEngine = options?.legacyQueryEngine ?? "tanstack"
751
+ let tanstackQueryClient: ReturnType<typeof makeTanstackQueryClient> | undefined
752
+ const getTanstackQueryClient = () => tanstackQueryClient ??= makeTanstackQueryClient()
753
+ const atomInvalidator = makeResolvedAtomQueryInvalidator(getBaseRt)
754
+ const queryInvalidator = legacyQueryEngine === "tanstack"
755
+ ? combineQueryInvalidators(atomInvalidator, makeTanstackQueryInvalidator(getTanstackQueryClient()))
756
+ : atomInvalidator
757
+ setQueryCacheUpdater(
758
+ legacyQueryEngine === "tanstack"
759
+ ? combineQueryCacheUpdaters(
760
+ makeTanstackQueryCacheUpdater(getTanstackQueryClient()),
761
+ optionalAtomQueryCacheUpdater
762
+ )
763
+ : atomQueryCacheUpdater
764
+ )
765
+
376
766
  let m: ReturnType<typeof useMutationInt>
377
- const useMutation = () => m ??= useMutationInt()
767
+ const useMutation = () => m ??= useMutationInt(queryInvalidator)
768
+
769
+ let sm2: ReturnType<typeof makeStreamMutation2>
770
+ const useStreamMutation2 = () => sm2 ??= makeStreamMutation2(queryInvalidator)
378
771
 
379
- const query = new QueryImpl(getBaseRt)
772
+ const legacyUseQuery = legacyQueryEngine === "tanstack"
773
+ ? makeTanstackQuery(getBaseRt, getTanstackQueryClient())
774
+ : undefined
775
+ const query = new QueryImpl(getBaseRt, getAtomRt, legacyUseQuery)
380
776
  const useQuery = query.useQuery
777
+ const useQueryNew = query.useQueryNew
778
+ const useQueryAtom = query.useQueryAtom
779
+ const useQueryFamily = query.useQueryFamily
381
780
  const useSuspenseQuery = query.useSuspenseQuery
781
+ const useSuspenseQueryNew = query.useSuspenseQueryNew
782
+ const useStreamQuery = query.useStreamQuery
783
+ const useStreamQueryFamily = query.useStreamQueryFamily
784
+ const useStreamQueryAtom = query.useStreamQueryAtom
785
+ const useStreamQueryNew = query.useStreamQueryNew
786
+
787
+ const isQueryHandler = <H extends { readonly Request: Req }>(handler: H): handler is QueryHandler<H> =>
788
+ handler.Request.type === "query" && !handler.Request.stream
789
+
790
+ const isStreamQueryHandler = <H extends { readonly Request: Req }>(handler: H): handler is QueryStreamHandler<H> =>
791
+ handler.Request.type === "query" && handler.Request.stream
792
+
793
+ const queryHelpersFor = <I, A, E, Request extends Req, Name extends string>(
794
+ handler: RequestHandlerWithInput<I, A, E, RT, Request, Name>
795
+ ) => ({
796
+ family: useQueryFamily(handler),
797
+ atom: useQueryAtom(handler),
798
+ query: useQuery(handler),
799
+ queryNew: useQueryNew(handler),
800
+ suspense: useSuspenseQuery(handler),
801
+ suspenseNew: useSuspenseQueryNew(handler)
802
+ })
803
+
804
+ const streamQueryHelpersFor = <I, A, E, Request extends Req, Name extends string>(
805
+ handler: RequestStreamHandlerWithInput<I, A, E, RT, Request, Name>
806
+ ) => ({
807
+ family: useStreamQueryFamily(handler),
808
+ atom: useStreamQueryAtom(handler),
809
+ query: useStreamQuery(handler),
810
+ queryNew: useStreamQueryNew(handler)
811
+ })
812
+
813
+ const projectQueryFor = <
814
+ I,
815
+ A,
816
+ E,
817
+ Request extends Req & { readonly success: S.Top & { readonly "EncodingServices": RT } },
818
+ Name extends string
819
+ >(
820
+ handler: RequestHandlerWithInput<I, A, E, RT, Request, Name>
821
+ ) =>
822
+ <ProjSchema extends S.Top & { readonly "DecodingServices": RT }>(projectionSchema: ProjSchema) => {
823
+ const successSchema = handler.Request.success
824
+ const projectionHash = projectionSchemaHash(projectionSchema)
825
+ const projected = projectHandler(handler.handler, successSchema, projectionSchema)
826
+ const projectedHandler = {
827
+ handler: projected,
828
+ id: handler.id,
829
+ Request: handler.Request,
830
+ queryKeyProjectionHash: projectionHash
831
+ }
832
+ if (handler.options) {
833
+ return {
834
+ request: projected,
835
+ ...queryHelpersFor({ ...projectedHandler, options: handler.options })
836
+ }
837
+ }
838
+ return {
839
+ request: projected,
840
+ ...queryHelpersFor(projectedHandler)
841
+ }
842
+ }
843
+
844
+ const mergeInvalidation = (
845
+ a?: MutationOptionsBase["queryInvalidation"],
846
+ b?: MutationOptionsBase["queryInvalidation"]
847
+ ): MutationOptionsBase["queryInvalidation"] | undefined => {
848
+ if (!a && !b) {
849
+ return undefined
850
+ }
851
+ return (defaultKey, name, input, output) => [
852
+ ...(a?.(defaultKey, name, input, output) ?? []),
853
+ ...(b?.(defaultKey, name, input, output) ?? [])
854
+ ]
855
+ }
856
+
857
+ const withDefaultInvalidation = (
858
+ mut: any,
859
+ defaultInvalidation?: MutationOptionsBase["queryInvalidation"]
860
+ ) => {
861
+ if (!defaultInvalidation) return mut
862
+ const apply = (callerOpts?: MutationOptionsBase) => ({
863
+ ...callerOpts,
864
+ queryInvalidation: callerOpts?.queryInvalidation
865
+ ? mergeInvalidation(defaultInvalidation, callerOpts.queryInvalidation)
866
+ : defaultInvalidation
867
+ })
868
+ return (input: any, callerOpts?: MutationOptionsBase) => mut(input, apply(callerOpts))
869
+ }
870
+
871
+ const makeQueryResources = <Resources extends InvalidationResources>(resources: Resources | undefined) => {
872
+ if (!resources) {
873
+ return {} as Record<string, Record<string, unknown>>
874
+ }
875
+ return resources as Record<string, Record<string, unknown>>
876
+ }
382
877
 
383
878
  const mapQuery = <M extends RequestsAny>(
384
879
  client: ClientFrom<M>
385
880
  ) => {
386
881
  const queries = Struct.keys(client).reduce(
387
882
  (acc, key) => {
388
- ;(acc as any)[camelCase(key) + "Query"] = Object.assign(useQuery(client[key] as any), {
389
- id: client[key].id
390
- })
391
- ;(acc as any)[camelCase(key) + "SuspenseQuery"] = Object.assign(useSuspenseQuery(client[key] as any), {
392
- id: client[key].id
393
- })
883
+ const handler = client[key]
884
+ if (isQueryHandler(handler)) {
885
+ Object.assign(acc, {
886
+ [camelCase(key) + "QueryFamily"]: Object.assign(useQueryFamily(handler), {
887
+ id: client[key].id
888
+ })
889
+ })
890
+ ;(acc as any)[camelCase(key) + "Query"] = Object.assign(useQuery(handler), {
891
+ id: client[key].id
892
+ })
893
+ ;(acc as any)[camelCase(key) + "QueryNew"] = Object.assign(useQueryNew(handler), {
894
+ id: client[key].id
895
+ })
896
+ ;(acc as any)[camelCase(key) + "SuspenseQuery"] = Object.assign(useSuspenseQuery(handler), {
897
+ id: client[key].id
898
+ })
899
+ ;(acc as any)[camelCase(key) + "SuspenseQueryNew"] = Object.assign(
900
+ useSuspenseQueryNew(handler),
901
+ {
902
+ id: client[key].id
903
+ }
904
+ )
905
+ } else if (isStreamQueryHandler(handler)) {
906
+ const streamHelpers = streamQueryHelpersFor(handler)
907
+ Object.assign(acc, {
908
+ [camelCase(key) + "QueryFamily"]: Object.assign(streamHelpers.family, {
909
+ id: client[key].id
910
+ }),
911
+ [camelCase(key) + "Query"]: Object.assign(streamHelpers.query, {
912
+ id: client[key].id
913
+ }),
914
+ [camelCase(key) + "QueryNew"]: Object.assign(streamHelpers.queryNew, {
915
+ id: client[key].id
916
+ })
917
+ })
918
+ }
394
919
  return acc
395
920
  },
396
921
  {} as
922
+ & {
923
+ [
924
+ Key in keyof typeof client as QueryHandler<typeof client[Key]> extends never ? never
925
+ : `${ToCamel<string & Key>}QueryFamily`
926
+ ]: Queries<RT, QueryHandler<typeof client[Key]>>["family"]
927
+ }
397
928
  & {
398
929
  // apparently can't get JSDoc in here..
399
- [Key in keyof typeof client as `${ToCamel<string & Key>}Query`]: Queries<RT, typeof client[Key]>["query"]
930
+ [
931
+ Key in keyof typeof client as QueryHandler<typeof client[Key]> extends never ? never
932
+ : `${ToCamel<string & Key>}Query`
933
+ ]: Queries<RT, QueryHandler<typeof client[Key]>>["query"]
934
+ }
935
+ & {
936
+ [
937
+ Key in keyof typeof client as QueryHandler<typeof client[Key]> extends never ? never
938
+ : `${ToCamel<string & Key>}QueryNew`
939
+ ]: Queries<RT, QueryHandler<typeof client[Key]>>["queryNew"]
400
940
  }
401
941
  // todo: or suspense as an Option?
402
942
  & {
403
943
  // apparently can't get JSDoc in here..
404
- [Key in keyof typeof client as `${ToCamel<string & Key>}SuspenseQuery`]: Queries<
944
+ [
945
+ Key in keyof typeof client as QueryHandler<typeof client[Key]> extends never ? never
946
+ : `${ToCamel<string & Key>}SuspenseQuery`
947
+ ]: Queries<
405
948
  RT,
406
- typeof client[Key]
949
+ QueryHandler<typeof client[Key]>
407
950
  >["suspense"]
408
951
  }
952
+ & {
953
+ [
954
+ Key in keyof typeof client as QueryHandler<typeof client[Key]> extends never ? never
955
+ : `${ToCamel<string & Key>}SuspenseQueryNew`
956
+ ]: Queries<
957
+ RT,
958
+ QueryHandler<typeof client[Key]>
959
+ >["suspenseNew"]
960
+ }
961
+ & {
962
+ [
963
+ Key in keyof typeof client as QueryStreamHandler<typeof client[Key]> extends never ? never
964
+ : `${ToCamel<string & Key>}QueryFamily`
965
+ ]: StreamQueries<RT, QueryStreamHandler<typeof client[Key]>>["family"]
966
+ }
967
+ & {
968
+ [
969
+ Key in keyof typeof client as QueryStreamHandler<typeof client[Key]> extends never ? never
970
+ : `${ToCamel<string & Key>}Query`
971
+ ]: StreamQueries<RT, QueryStreamHandler<typeof client[Key]>>["query"]
972
+ }
973
+ & {
974
+ [
975
+ Key in keyof typeof client as QueryStreamHandler<typeof client[Key]> extends never ? never
976
+ : `${ToCamel<string & Key>}QueryNew`
977
+ ]: StreamQueries<RT, QueryStreamHandler<typeof client[Key]>>["queryNew"]
978
+ }
409
979
  )
410
980
  return queries
411
981
  }
@@ -416,20 +986,27 @@ export const makeClient = <RT_, RTHooks>(
416
986
  const Command = useCommand()
417
987
  const mutations = Struct.keys(client).reduce(
418
988
  (acc, key) => {
989
+ if (!(client[key].Request.type === "command" && !client[key].Request.stream)) {
990
+ return acc
991
+ }
419
992
  const mut = client[key].handler
420
- const fn = Command.fn(client[key].id)
421
- const wrap = Command.wrap({ mutate: Effect.isEffect(mut) ? () => mut : mut, id: client[key].id })
993
+ const request = mut
994
+ const fn = (options?: any) => Command.fn(client[key].id, options)
995
+ const wrap = (options?: any) => Command.wrap({ mutate: request, id: client[key].id }, options)
422
996
  ;(acc as any)[camelCase(key) + "Request"] = Object.assign(
423
997
  mut,
424
- fn, // to get the i18n key etc.
425
- { wrap, fn }
998
+ Command.fn(client[key].id), // to get the i18n key etc.
999
+ { wrap, fn, request }
426
1000
  )
427
1001
  return acc
428
1002
  },
429
1003
  {} as {
430
- [Key in keyof typeof client as `${ToCamel<string & Key>}Request`]: RequestWithExtensions<
1004
+ [
1005
+ Key in keyof typeof client as CommandHandler<typeof client[Key]> extends never ? never
1006
+ : `${ToCamel<string & Key>}Request`
1007
+ ]: CommandRequestWithExtensions<
431
1008
  RT | RTHooks,
432
- typeof client[Key]
1009
+ CommandHandler<typeof client[Key]>
433
1010
  >
434
1011
  }
435
1012
  )
@@ -437,26 +1014,56 @@ export const makeClient = <RT_, RTHooks>(
437
1014
  }
438
1015
 
439
1016
  const mapMutation = <M extends RequestsAny>(
440
- client: ClientFrom<M>
1017
+ client: ClientFrom<M>,
1018
+ queryInvalidation?: (client: ClientFrom<M>) => QueryInvalidation<M>,
1019
+ invalidationResources?: InvalidationResourcesFor<M>
441
1020
  ) => {
442
1021
  const Command = useCommand()
443
1022
  const mutation = useMutation()
1023
+ const invalidation = queryInvalidation?.(client)
1024
+ const queryResources = makeQueryResources(invalidationResources)
444
1025
  const mutations = Struct.keys(client).reduce(
445
1026
  (acc, key) => {
446
- const mut: any = mutation(client[key] as any)
447
- const fn = Command.fn(client[key].id)
448
- const wrap = Command.wrap({ mutate: Effect.isEffect(mut) ? () => mut : mut, id: client[key].id })
449
- ;(acc as any)[camelCase(key) + "Mutation"] = Object.assign(
450
- mut,
451
- fn, // to get the i18n key etc.
452
- { wrap, fn }
453
- )
1027
+ if (!(client[key].Request.type === "command" && !client[key].Request.stream)) {
1028
+ return acc
1029
+ }
1030
+ const fromRequestConfig = client[key].Request.config?.["invalidatesQueries"] as
1031
+ | InvalidationCallback<InvalidationResourcesFor<M>>
1032
+ | undefined
1033
+ const fromRequest = fromRequestConfig
1034
+ ? ((defaultKey: string[], _name: string, input?: unknown, output?: unknown) =>
1035
+ fromRequestConfig(
1036
+ defaultKey,
1037
+ queryResources as never,
1038
+ input,
1039
+ output as never
1040
+ ) as InvalidationEntry[])
1041
+ : undefined
1042
+ const mergedInvalidation = mergeInvalidation(fromRequest, invalidation?.[key])
1043
+ const makeProjectedMutation = (handler: any): any => {
1044
+ const mut: any = withDefaultInvalidation(mutation(handler), mergedInvalidation)
1045
+ return Object.assign(mut, {
1046
+ wrap: (options?: any) => Command.wrap({ mutate: mut, id: client[key].id }, options),
1047
+ fn: (options?: any) => Command.fn(client[key].id, options),
1048
+ project: (projectionSchema: any) => {
1049
+ const projected = {
1050
+ ...handler,
1051
+ handler: projectHandler(handler.handler, client[key].Request.success, projectionSchema)
1052
+ }
1053
+ return makeProjectedMutation(projected)
1054
+ }
1055
+ })
1056
+ }
1057
+ ;(acc as any)[camelCase(key) + "Mutation"] = makeProjectedMutation(client[key] as any)
454
1058
  return acc
455
1059
  },
456
1060
  {} as {
457
- [Key in keyof typeof client as `${ToCamel<string & Key>}Mutation`]: MutationWithExtensions<
1061
+ [
1062
+ Key in keyof typeof client as CommandHandler<typeof client[Key]> extends never ? never
1063
+ : `${ToCamel<string & Key>}Mutation`
1064
+ ]: MutationWithExtensions<
458
1065
  RT | RTHooks,
459
- typeof client[Key]
1066
+ CommandHandler<typeof client[Key]>
460
1067
  >
461
1068
  }
462
1069
  )
@@ -466,7 +1073,8 @@ export const makeClient = <RT_, RTHooks>(
466
1073
  // make available .query, .suspense and .mutate for each operation
467
1074
  // and a .helpers with all mutations and queries
468
1075
  const mapClient = <M extends RequestsAny>(
469
- queryInvalidation?: (client: ClientFrom<M>) => QueryInvalidation<M>
1076
+ queryInvalidation?: (client: ClientFrom<M>) => QueryInvalidation<M>,
1077
+ invalidationResources?: InvalidationResourcesFor<M>
470
1078
  ) =>
471
1079
  (
472
1080
  client: ClientFrom<M>
@@ -474,74 +1082,167 @@ export const makeClient = <RT_, RTHooks>(
474
1082
  const Command = useCommand()
475
1083
  const mutation = useMutation()
476
1084
  const invalidation = queryInvalidation?.(client)
1085
+ const queryResources = makeQueryResources(invalidationResources)
477
1086
  const extended = Struct.keys(client).reduce(
478
1087
  (acc, key) => {
1088
+ const handler = client[key]
1089
+ const requestType = handler.Request.type
1090
+ const isStream = handler.Request.stream
479
1091
  const fn = Command.fn(client[key].id)
480
- const mutate = extendM(
481
- mutation(
482
- client[key] as any,
483
- invalidation?.[key] ? { queryInvalidation: invalidation[key] } : undefined
484
- ),
485
- (mutate) =>
486
- Object.assign(
487
- mutate,
488
- fn, // to get the i18n key etc.
489
- {
490
- wrap: Command.wrap({ mutate: Effect.isEffect(mutate) ? () => mutate : mutate, id: client[key].id }),
491
- fn
492
- }
493
- )
494
- )
495
-
496
- const h_ = client[key].handler
497
- const h = Effect.isEffect(h_)
498
- ? () => h_
499
- : (...args: [any]) => h_(...args)
1092
+ const h_ = handler.handler
1093
+ const request = h_
500
1094
  ;(acc as any)[key] = Object.assign(
501
- h,
502
- client[key],
503
- fn, // to get the i18n key etc.
504
- {
505
- mutate,
506
- query: useQuery(client[key] as any),
507
- suspense: useSuspenseQuery(client[key] as any),
508
- wrap: Command.wrap({ mutate: h, id: client[key].id }),
509
- fn
510
- }
1095
+ isQueryHandler(handler)
1096
+ ? {
1097
+ ...handler,
1098
+ request,
1099
+ ...queryHelpersFor(handler),
1100
+ project: projectQueryFor(handler)
1101
+ }
1102
+ : isStreamQueryHandler(handler)
1103
+ ? {
1104
+ ...client[key],
1105
+ request,
1106
+ ...streamQueryHelpersFor(handler)
1107
+ }
1108
+ : requestType === "command" && isStream
1109
+ ? (() => {
1110
+ const fromRequestConfig = client[key].Request.config?.["invalidatesQueries"] as
1111
+ | InvalidationCallback<InvalidationResourcesFor<M>>
1112
+ | undefined
1113
+ const fromRequest = fromRequestConfig
1114
+ ? ((defaultKey: string[], _name: string, input?: unknown, output?: unknown) =>
1115
+ fromRequestConfig(
1116
+ defaultKey,
1117
+ queryResources as never,
1118
+ input,
1119
+ output as never
1120
+ ) as InvalidationEntry[])
1121
+ : undefined
1122
+ const mergedInvalidation = mergeInvalidation(fromRequest, invalidation?.[key])
1123
+ const streamCmd = useCommand()
1124
+ return {
1125
+ ...client[key],
1126
+ request,
1127
+ query: useStreamQuery(client[key] as any),
1128
+ fn: (options?: any) => streamCmd.streamFn(client[key].id as any, options),
1129
+ mutate: (() => {
1130
+ const sm2Act = useStreamMutation2()(client[key] as any, mergedInvalidation)
1131
+ const sm2Handler = (input: any, _ctx: any) => (sm2Act as (i: any) => any)(input)
1132
+ return Object.assign(sm2Act, {
1133
+ id: client[key].id,
1134
+ wrap: (options?: any) => streamCmd.streamWrap(sm2Handler, client[key].id as any, options)
1135
+ })
1136
+ })()
1137
+ }
1138
+ })()
1139
+ : {
1140
+ mutate: ((handler: any) => {
1141
+ const fromRequestConfig = client[key].Request.config?.["invalidatesQueries"] as
1142
+ | InvalidationCallback<InvalidationResourcesFor<M>>
1143
+ | undefined
1144
+ const fromRequest = fromRequestConfig
1145
+ ? ((defaultKey: string[], _name: string, input?: unknown, output?: unknown) =>
1146
+ fromRequestConfig(
1147
+ defaultKey,
1148
+ queryResources as never,
1149
+ input,
1150
+ output as never
1151
+ ) as InvalidationEntry[])
1152
+ : undefined
1153
+ const mergedInvalidation = mergeInvalidation(fromRequest, invalidation?.[key])
1154
+ const makeProjectedMutation = (h: any): any => {
1155
+ const mutate = withDefaultInvalidation(mutation(h), mergedInvalidation)
1156
+ return Object.assign(
1157
+ mutate,
1158
+ {
1159
+ wrap: (options?: any) =>
1160
+ Command.wrap({
1161
+ mutate,
1162
+ id: client[key].id
1163
+ }, options),
1164
+ fn: (options?: any) => Command.fn(client[key].id, options),
1165
+ project: (projectionSchema: any) => {
1166
+ const projected = {
1167
+ ...h,
1168
+ handler: projectHandler(h.handler, client[key].Request.success, projectionSchema)
1169
+ }
1170
+ return makeProjectedMutation(projected)
1171
+ }
1172
+ }
1173
+ )
1174
+ }
1175
+ return makeProjectedMutation(handler)
1176
+ })(client[key] as any),
1177
+ ...client[key],
1178
+ ...fn, // to get the i18n key etc.
1179
+ request,
1180
+ fn: (options?: any) => Command.fn(client[key].id, options),
1181
+ wrap: (options?: any) => Command.wrap({ mutate: h_, id: client[key].id }, options)
1182
+ }
511
1183
  )
512
1184
  return acc
513
1185
  },
514
1186
  {} as {
515
1187
  [Key in keyof typeof client]:
516
1188
  & typeof client[Key]
517
- & RequestWithExtensions<RT | RTHooks, typeof client[Key]>
1189
+ & (QueryHandler<typeof client[Key]> extends never ? {}
1190
+ :
1191
+ & QueryRequestWithExtensions<QueryHandler<typeof client[Key]>>
1192
+ & Queries<RT, QueryHandler<typeof client[Key]>>
1193
+ & QueryProjection<RT, QueryHandler<typeof client[Key]>>)
1194
+ & (QueryStreamHandler<typeof client[Key]> extends never ? {}
1195
+ : StreamQueries<RT, QueryStreamHandler<typeof client[Key]>>)
1196
+ & (CommandHandler<typeof client[Key]> extends never ? {}
1197
+ : CommandRequestWithExtensions<RT | RTHooks, CommandHandler<typeof client[Key]>>)
1198
+ & (CommandHandler<typeof client[Key]> extends never ? {}
1199
+ : { mutate: MutationWithExtensions<RT | RTHooks, CommandHandler<typeof client[Key]>> })
1200
+ & (CommandStreamHandler<typeof client[Key]> extends never ? {}
1201
+ : {
1202
+ fn: StreamFnStreamExtension<RT | RTHooks, CommandStreamHandler<typeof client[Key]>>
1203
+ mutate: StreamMutation2WithExtensions<RT | RTHooks, CommandStreamHandler<typeof client[Key]>>
1204
+ })
518
1205
  & {
519
- mutate: MutationWithExtensions<RT | RTHooks, typeof client[Key]>
520
- Input: typeof client[Key] extends RequestHandlerWithInput<infer I, any, any, any, any, any> ? I : never
1206
+ Input: typeof client[Key] extends RequestStreamHandlerWithInput<infer I, any, any, any, any, any, any> ? I
1207
+ : typeof client[Key] extends RequestHandlerWithInput<infer I, any, any, any, any, any> ? I
1208
+ : never
521
1209
  }
522
- & Queries<RT, typeof client[Key]>
523
1210
  }
524
1211
  )
525
- return Object.assign(extended, { helpers: { ...mapRequest(client), ...mapMutation(client), ...mapQuery(client) } })
1212
+ return Object.assign(extended, {
1213
+ helpers: {
1214
+ ...mapRequest(client),
1215
+ ...mapMutation(client, queryInvalidation, invalidationResources),
1216
+ ...mapQuery(client)
1217
+ }
1218
+ })
526
1219
  }
527
1220
 
528
1221
  // TODO: Clean up this delay initialisation messs
529
1222
  // TODO; invalidateQueries should perhaps be configured in the Request impl themselves?
530
1223
  const clientFor__ = <M extends RequestsAny>(
531
1224
  m: M,
532
- queryInvalidation?: (client: ClientFrom<M>) => QueryInvalidation<M>
533
- ) => getBaseMrt().runSync(clientFor_(m).pipe(Effect.map(mapClient(queryInvalidation))))
1225
+ queryInvalidation?: QueryInvalidationFactory<M>,
1226
+ invalidationResources?: InvalidationResourcesFor<M>
1227
+ ) => getBaseMrt().runSync(clientFor_(m).pipe(Effect.map(mapClient(queryInvalidation, invalidationResources))))
534
1228
 
535
1229
  // delay client creation until first access
536
1230
  // the idea is that we don't need the useNuxtApp().$runtime (only available at later initialisation stage)
537
1231
  // until we are at a place where it is available..
538
- const clientFor = <M extends RequestsAny>(
1232
+ const clientFor = <
1233
+ M extends RequestsAny,
1234
+ Resources extends InvalidationResourcesFor<M> = InvalidationResourcesFor<M>
1235
+ >(
539
1236
  m: M,
540
- queryInvalidation?: (client: ClientFrom<M>) => QueryInvalidation<M>
1237
+ ...args: ClientForArgs<M, Resources>
541
1238
  ) => {
1239
+ const [queryInvalidation, invalidationResources] = args as [
1240
+ QueryInvalidationFactory<M> | undefined,
1241
+ InvalidationResourcesFor<M> | undefined
1242
+ ]
542
1243
  type Client = ReturnType<typeof clientFor__<M>>
543
1244
  let client: Client | undefined = undefined
544
- const getOrMakeClient = () => (client ??= clientFor__(m, queryInvalidation))
1245
+ const getOrMakeClient = () => (client ??= clientFor__(m, queryInvalidation, invalidationResources))
545
1246
 
546
1247
  // initialize on first use..
547
1248
  const proxy = Struct.keys(m).concat(["helpers"]).reduce((acc, key) => {
@@ -564,6 +1265,7 @@ export const makeClient = <RT_, RTHooks>(
564
1265
  // delay initialisation until first use...
565
1266
  fn: (...args: [any]) => useCommand().fn(...args),
566
1267
  wrap: (...args: [any]) => useCommand().wrap(...args),
1268
+ streamFn: (...args: [any]) => useCommand().streamFn(...args),
567
1269
  alt: (...args: [any]) => useCommand().alt(...args),
568
1270
  alt2: (...args: [any]) => useCommand().alt2(...args)
569
1271
  } as ReturnType<typeof useCommand>,
@@ -573,15 +1275,18 @@ export const makeClient = <RT_, RTHooks>(
573
1275
  return {
574
1276
  Command,
575
1277
  useCommand,
576
- clientFor
1278
+ clientFor,
1279
+ tanstackQueryClient: getTanstackQueryClient()
577
1280
  }
578
1281
  }
579
1282
 
580
1283
  export type QueryInvalidation<M> = {
581
- [K in keyof M]?: (defaultKey: string[], name: string) => {
582
- filters?: InvalidateQueryFilters | undefined
583
- options?: InvalidateOptions | undefined
584
- }[]
1284
+ [K in keyof M]?: (
1285
+ defaultKey: string[],
1286
+ name: string,
1287
+ input?: unknown,
1288
+ output?: Exit.Exit<unknown, unknown>
1289
+ ) => InvalidationEntry[]
585
1290
  }
586
1291
 
587
1292
  export type ToCamel<S extends string | number | symbol> = S extends string
@@ -589,17 +1294,21 @@ export type ToCamel<S extends string | number | symbol> = S extends string
589
1294
  : Uncapitalize<S>
590
1295
  : never
591
1296
 
592
- export interface CommandBase<I = void, A = void> {
1297
+ export interface CommandBase<I = void, A = void, RA = unknown, RE = unknown> {
593
1298
  handle: (input: I) => A
594
1299
  waiting: boolean
595
1300
  blocked: boolean
596
1301
  allowed: boolean
597
1302
  action: string
598
1303
  label: string
1304
+ /** formatted progress info for current `running` state, when `progress` was supplied */
1305
+ progress?: Progress | undefined
1306
+ /** reactive result state, available on stream-backed commands */
1307
+ result?: AsyncResult.AsyncResult<RA, RE>
599
1308
  }
600
1309
 
601
- export interface EffectCommand<I = void, A = unknown, E = unknown> extends CommandBase<I, Fiber<A, E>> {}
1310
+ export interface EffectCommand<I = void, A = unknown, E = unknown> extends CommandBase<I, Fiber<A, E>, A, E> {}
602
1311
 
603
- export interface CommandFromRequest<I extends abstract new(...args: any) => any, A = unknown, E = unknown>
604
- extends EffectCommand<ConstructorParameters<I>[0], A, E>
1312
+ export interface CommandFromRequest<I extends { readonly make: (...args: any[]) => any }, A = unknown, E = unknown>
1313
+ extends EffectCommand<HandlerInput<I>, A, E>
605
1314
  {}