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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/CHANGELOG.md +2463 -0
  2. package/dist/atomQuery.d.ts +120 -0
  3. package/dist/atomQuery.d.ts.map +1 -0
  4. package/dist/atomQuery.js +434 -0
  5. package/dist/commander.d.ts +634 -0
  6. package/dist/commander.d.ts.map +1 -0
  7. package/dist/commander.js +1068 -0
  8. package/dist/{experimental/confirm.d.ts → confirm.d.ts} +6 -4
  9. package/dist/confirm.d.ts.map +1 -0
  10. package/dist/confirm.js +26 -0
  11. package/dist/dependencyMetadata.d.ts +18 -0
  12. package/dist/dependencyMetadata.d.ts.map +1 -0
  13. package/dist/dependencyMetadata.js +67 -0
  14. package/dist/errorReporter.d.ts +6 -4
  15. package/dist/errorReporter.d.ts.map +1 -1
  16. package/dist/errorReporter.js +14 -19
  17. package/dist/form.d.ts +5 -5
  18. package/dist/form.d.ts.map +1 -1
  19. package/dist/form.js +42 -16
  20. package/dist/internal/tanstackQuery.d.ts +9 -0
  21. package/dist/internal/tanstackQuery.d.ts.map +1 -0
  22. package/dist/internal/tanstackQuery.js +204 -0
  23. package/dist/intl.d.ts +15 -0
  24. package/dist/intl.d.ts.map +1 -0
  25. package/dist/intl.js +9 -0
  26. package/dist/lib.d.ts +7 -9
  27. package/dist/lib.d.ts.map +1 -1
  28. package/dist/lib.js +35 -10
  29. package/dist/liveQueryInvalidation.d.ts +16 -0
  30. package/dist/liveQueryInvalidation.d.ts.map +1 -0
  31. package/dist/liveQueryInvalidation.js +115 -0
  32. package/dist/makeClient.d.ts +236 -336
  33. package/dist/makeClient.d.ts.map +1 -1
  34. package/dist/makeClient.js +417 -408
  35. package/dist/makeContext.d.ts.map +1 -1
  36. package/dist/makeIntl.d.ts.map +1 -1
  37. package/dist/{experimental/makeUseCommand.d.ts → makeUseCommand.d.ts} +4 -3
  38. package/dist/makeUseCommand.d.ts.map +1 -0
  39. package/dist/makeUseCommand.js +13 -0
  40. package/dist/mutate.d.ts +109 -40
  41. package/dist/mutate.d.ts.map +1 -1
  42. package/dist/mutate.js +235 -58
  43. package/dist/query.d.ts +158 -45
  44. package/dist/query.d.ts.map +1 -1
  45. package/dist/query.js +380 -122
  46. package/dist/queryLifetime.d.ts +25 -0
  47. package/dist/queryLifetime.d.ts.map +1 -0
  48. package/dist/queryLifetime.js +54 -0
  49. package/dist/routeParams.d.ts +4 -4
  50. package/dist/routeParams.d.ts.map +1 -1
  51. package/dist/routeParams.js +4 -3
  52. package/dist/runtime.d.ts +1 -14
  53. package/dist/runtime.d.ts.map +1 -1
  54. package/dist/runtime.js +2 -26
  55. package/dist/suspense.d.ts +19 -0
  56. package/dist/suspense.d.ts.map +1 -0
  57. package/dist/suspense.js +38 -0
  58. package/dist/toast.d.ts +2 -0
  59. package/dist/toast.d.ts.map +1 -0
  60. package/dist/toast.js +2 -0
  61. package/dist/withToast.d.ts +2 -0
  62. package/dist/withToast.d.ts.map +1 -0
  63. package/dist/withToast.js +2 -0
  64. package/docs/atom-query-api-redesign.md +207 -0
  65. package/docs/mutation-command-atoms.md +40 -0
  66. package/docs/query-key-invalidation.md +107 -0
  67. package/examples/streamMutation.ts +72 -0
  68. package/package.json +36 -95
  69. package/src/atomQuery.ts +600 -0
  70. package/src/commander.ts +3408 -0
  71. package/src/{experimental/confirm.ts → confirm.ts} +13 -15
  72. package/src/dependencyMetadata.ts +84 -0
  73. package/src/errorReporter.ts +65 -75
  74. package/src/form.ts +57 -21
  75. package/src/index.ts +7 -7
  76. package/src/internal/tanstackQuery.ts +308 -0
  77. package/src/intl.ts +12 -0
  78. package/src/lib.ts +49 -21
  79. package/src/liveQueryInvalidation.ts +138 -0
  80. package/src/makeClient.ts +971 -1159
  81. package/src/makeIntl.ts +2 -2
  82. package/src/{experimental/makeUseCommand.ts → makeUseCommand.ts} +9 -6
  83. package/src/mutate.ts +406 -153
  84. package/src/query.ts +763 -260
  85. package/src/queryLifetime.ts +74 -0
  86. package/src/routeParams.ts +7 -7
  87. package/src/runtime.ts +1 -31
  88. package/src/suspense.ts +43 -0
  89. package/src/toast.ts +1 -0
  90. package/src/withToast.ts +1 -0
  91. package/test/Mutation.test.ts +181 -24
  92. package/test/dependencyInvalidation.test.ts +444 -0
  93. package/test/dist/dependencyInvalidation.test.d.ts.map +1 -0
  94. package/test/dist/form.test.d.ts.map +1 -1
  95. package/test/dist/inactiveInvalidation.test.d.ts.map +1 -0
  96. package/test/dist/interrupt-refetch-repro.test.d.ts.map +1 -0
  97. package/test/dist/lib.test.d.ts.map +1 -0
  98. package/test/dist/liveQueryInvalidation.test.d.ts.map +1 -0
  99. package/test/dist/query-span.test.d.ts.map +1 -0
  100. package/test/dist/queryOptions.test.d.ts.map +1 -0
  101. package/test/dist/streamFinal.test.d.ts.map +1 -0
  102. package/test/dist/streamFn.test.d.ts.map +1 -0
  103. package/test/dist/stubs.d.ts +3751 -120
  104. package/test/dist/stubs.d.ts.map +1 -1
  105. package/test/dist/stubs.js +190 -30
  106. package/test/dist/suspense-regression.test.d.ts.map +1 -0
  107. package/test/dist/suspense.test.d.ts.map +1 -0
  108. package/test/form-validation-errors.test.ts +24 -19
  109. package/test/form.test.ts +25 -6
  110. package/test/inactiveInvalidation.test.ts +172 -0
  111. package/test/interrupt-refetch-repro.test.ts +625 -0
  112. package/test/lib.test.ts +240 -0
  113. package/test/liveQueryInvalidation.test.ts +108 -0
  114. package/test/makeClient.test.ts +437 -38
  115. package/test/query-span.test.ts +92 -0
  116. package/test/queryOptions.test.ts +88 -0
  117. package/test/streamFinal.test.ts +64 -0
  118. package/test/streamFn.test.ts +457 -0
  119. package/test/stubs.ts +227 -42
  120. package/test/suspense-regression.test.ts +168 -0
  121. package/test/suspense.test.ts +160 -0
  122. package/tsconfig.examples.json +20 -0
  123. package/tsconfig.json +9 -2
  124. package/tsconfig.src.json +34 -34
  125. package/tsconfig.test.json +2 -2
  126. package/vitest.config.ts +5 -5
  127. package/dist/experimental/commander.d.ts +0 -359
  128. package/dist/experimental/commander.d.ts.map +0 -1
  129. package/dist/experimental/commander.js +0 -557
  130. package/dist/experimental/confirm.d.ts.map +0 -1
  131. package/dist/experimental/confirm.js +0 -28
  132. package/dist/experimental/intl.d.ts +0 -16
  133. package/dist/experimental/intl.d.ts.map +0 -1
  134. package/dist/experimental/intl.js +0 -5
  135. package/dist/experimental/makeUseCommand.d.ts.map +0 -1
  136. package/dist/experimental/makeUseCommand.js +0 -13
  137. package/dist/experimental/toast.d.ts +0 -47
  138. package/dist/experimental/toast.d.ts.map +0 -1
  139. package/dist/experimental/toast.js +0 -41
  140. package/dist/experimental/withToast.d.ts +0 -25
  141. package/dist/experimental/withToast.d.ts.map +0 -1
  142. package/dist/experimental/withToast.js +0 -45
  143. package/eslint.config.mjs +0 -24
  144. package/src/experimental/commander.ts +0 -1835
  145. package/src/experimental/intl.ts +0 -9
  146. package/src/experimental/toast.ts +0 -66
  147. package/src/experimental/withToast.ts +0 -99
  148. package/tsconfig.json.bak +0 -12
@@ -1,95 +1,494 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { type Effect } from "effect-app"
3
- import { Something, useClient, useExperimental } from "./stubs.js"
4
-
5
- it.skip("works2", () => {
6
- const { legacy } = useClient()
7
- const n = legacy.useQuery({
8
- Request: null as any,
9
- handler: null as any as (a: string) => Effect.Effect<number>,
10
- id: "id"
2
+ import { expect, expectTypeOf, it } from "@effect/vitest"
3
+ import { configureInvalidation, makeQueryKey } from "effect-app/client"
4
+ import type { HandlerInput } from "effect-app/client/clientFor"
5
+ import * as Effect from "effect-app/Effect"
6
+ import * as S from "effect-app/Schema"
7
+ import * as Exit from "effect/Exit"
8
+ import * as Fiber from "effect/Fiber"
9
+ import { TestClock } from "effect/testing"
10
+ import * as Atom from "effect/unstable/reactivity/Atom"
11
+ import type { CommandFromRequest } from "../src/makeClient.js"
12
+ import { combineQueryInvalidators, invalidateQueries } from "../src/mutate.js"
13
+ import { useAtomQuery, useAtomSuspense } from "../src/query.js"
14
+ import { Something, SomethingElse, SomethingElseReq, SomethingReq, useClient, useExperimental } from "./stubs.js"
15
+
16
+ const somethingInvalidationResources = {
17
+ Something: {
18
+ GetSomething2: Something.GetSomething2,
19
+ GetSomething2WithDependencies: Something.GetSomething2WithDependencies,
20
+ GetSomething3: Something.GetSomething3,
21
+ GetSomething4: Something.GetSomething4
22
+ }
23
+ }
24
+
25
+ it("TaggedRequestFor .moduleName and request .id / .moduleName", () => {
26
+ expectTypeOf(SomethingReq.moduleName).toEqualTypeOf<"Something">()
27
+ expectTypeOf(SomethingElseReq.moduleName).toEqualTypeOf<"SomethingElse">()
28
+
29
+ expectTypeOf(Something.GetSomething2.moduleName).toEqualTypeOf<"Something">()
30
+ expectTypeOf(Something.GetSomething2.id).toEqualTypeOf<"Something.GetSomething2">()
31
+ expectTypeOf(Something.GetSomething2.type).toEqualTypeOf<"query">()
32
+ expectTypeOf(Something.DoSomething.type).toEqualTypeOf<"command">()
33
+
34
+ expectTypeOf(SomethingElse.GetSomething2.moduleName).toEqualTypeOf<"SomethingElse">()
35
+ expectTypeOf(SomethingElse.GetSomething2.id).toEqualTypeOf<"SomethingElse.GetSomething2">()
36
+
37
+ const invalidates = configureInvalidation<{
38
+ Something: typeof Something
39
+ SomethingElse: typeof SomethingElse
40
+ }>()((queryKey, { Something, SomethingElse }) => [
41
+ { filters: { queryKey } },
42
+ { filters: { queryKey: makeQueryKey(Something.GetSomething2) } },
43
+ { filters: { queryKey: makeQueryKey(SomethingElse.GetSomething2) } }
44
+ ])
45
+
46
+ expectTypeOf(invalidates.invalidatesQueries).toBeFunction()
47
+ configureInvalidation<{ Something: typeof Something }>()((_queryKey) => [
48
+ // @ts-expect-error predicate filters are not supported by the atom query engine
49
+ { filters: { predicate: () => true } }
50
+ ])
51
+ configureInvalidation<{ Something: typeof Something }>()((_queryKey, { Something }) => {
52
+ // @ts-expect-error commands are intentionally excluded from configured resources
53
+ void Something.DoSomething
54
+ return []
11
55
  })
12
56
 
13
- const [, z] = n("a")
57
+ const { clientFor } = useClient()
58
+ const client = clientFor(
59
+ Something,
60
+ undefined,
61
+ somethingInvalidationResources
62
+ )
63
+
64
+ // only queries, no commands, and no commands who require resources; shouldn't require invalidation resources args!
65
+ clientFor({ GetSomething: Something.GetSomething2 })
66
+
67
+ // @ts-expect-error invalidation resources should be required when any command configures them
68
+ clientFor(Something)
69
+
70
+ // @ts-expect-error invalidation resources for this module reject extra top-level resources
71
+ clientFor(Something, undefined, { ...somethingInvalidationResources, SomethingElse })
72
+
73
+ const doSomethingInvalidation = client.DoSomething.Request.config["invalidatesQueries"]
74
+ if (doSomethingInvalidation) {
75
+ const entries = doSomethingInvalidation(
76
+ ["$Something"],
77
+ somethingInvalidationResources,
78
+ { id: "abc" },
79
+ Exit.succeed(123)
80
+ )
81
+ expect(Array.isArray(entries)).toBe(true)
82
+ }
83
+
84
+ const SomethingCommand = SomethingReq.Command
85
+
86
+ class TypeInferenceWithSuccess extends SomethingCommand<TypeInferenceWithSuccess>()("TypeInferenceWithSuccess", {
87
+ id: S.String
88
+ }, {
89
+ success: S.FiniteFromString
90
+ }, (_queryKey, _resources, input, result) => {
91
+ expectTypeOf(input).toEqualTypeOf<{ readonly id: string }>()
92
+ expectTypeOf(result).toEqualTypeOf<Exit.Exit<number, never>>()
93
+ return []
94
+ }) {}
95
+ void TypeInferenceWithSuccess
96
+
97
+ class TypeInferenceWithoutSuccess extends SomethingCommand<TypeInferenceWithoutSuccess>()(
98
+ "TypeInferenceWithoutSuccess",
99
+ {
100
+ id: S.String
101
+ },
102
+ {},
103
+ (_queryKey, _resources, input, result) => {
104
+ expectTypeOf(input).toEqualTypeOf<{ readonly id: string }>()
105
+ expectTypeOf(result).toEqualTypeOf<Exit.Exit<void, never>>()
106
+ return []
107
+ }
108
+ ) {}
109
+ void TypeInferenceWithoutSuccess
110
+
111
+ type MixedResources = {
112
+ Something: typeof Something
113
+ Misc: {
114
+ value: number
115
+ GetSomething2: typeof Something.GetSomething2
116
+ }
117
+ }
118
+
119
+ class TypeInferenceResourceFiltering extends SomethingCommand<
120
+ TypeInferenceResourceFiltering,
121
+ MixedResources
122
+ >()("TypeInferenceResourceFiltering", {
123
+ id: S.String
124
+ }, {
125
+ success: S.FiniteFromString
126
+ }, (_queryKey, resources, _input, _result) => {
127
+ expectTypeOf(resources.Something.GetSomething2).toEqualTypeOf<typeof Something.GetSomething2>()
128
+ expectTypeOf(resources.Misc.GetSomething2).toEqualTypeOf<typeof Something.GetSomething2>()
129
+
130
+ // @ts-expect-error commands must be filtered from invalidation resources
131
+ void resources.Something.DoSomething
132
+ // @ts-expect-error non-query values must be filtered from invalidation resources
133
+ void resources.Misc.value
134
+
135
+ return []
136
+ }) {}
137
+ void TypeInferenceResourceFiltering
138
+
139
+ type WithSuccessInvalidation = NonNullable<typeof TypeInferenceWithSuccess.config.invalidatesQueries> // @ts-expect-error input should be required when command payload is non-empty
140
+ ;((_queryKey, _resources) => []) satisfies WithSuccessInvalidation
141
+ })
142
+
143
+ it("clientFor handler shape — props variants", () => {
144
+ const { clientFor } = useClient()
145
+ const client = clientFor(
146
+ Something,
147
+ undefined,
148
+ somethingInvalidationResources
149
+ )
150
+ expect(client).toBeDefined()
151
+
152
+ // no-props (no fields): handler is (i: void) => Effect — callable without arg
153
+ expectTypeOf(client.DoNoProps.handler).toBeFunction()
154
+ client.DoNoProps.handler()
155
+
156
+ // no-props: request mirrors handler — (i: void) => Effect, callable without arg
157
+ expectTypeOf(client.DoNoProps.request).toBeFunction()
158
+ client.DoNoProps.request()
159
+ // optional-only: any fields → function handler. Input matches `make`, which for
160
+ // fully-optional payload is omittable.
161
+ expectTypeOf(client.DoOptionalOnly.handler).toBeFunction()
162
+ // arg may be omitted entirely
163
+ client.DoOptionalOnly.handler()
164
+ // or supplied with all-optional payload
165
+ client.DoOptionalOnly.handler({})
166
+ client.DoOptionalOnly.handler({ name: "x" })
167
+
168
+ // required-only: function, `id` required
169
+ expectTypeOf(client.DoRequiredOnly.handler).toBeFunction()
170
+ client.DoRequiredOnly.handler({ id: "x" })
171
+ // @ts-expect-error id is required
172
+ client.DoRequiredOnly.handler({})
173
+ // @ts-expect-error arg cannot be omitted
174
+ client.DoRequiredOnly.handler()
175
+
176
+ // mixed: id required, name optional
177
+ expectTypeOf(client.DoMixed.handler).toBeFunction()
178
+ client.DoMixed.handler({ id: "x" })
179
+ client.DoMixed.handler({ id: "x", name: "y" })
180
+ // @ts-expect-error id required
181
+ client.DoMixed.handler({ name: "y" })
182
+ })
183
+
184
+ it.effect("mutation invalidation awaits the injected query invalidator", () =>
185
+ Effect.gen(function*() {
186
+ const recorded: Array<readonly [string, ReadonlyArray<unknown>]> = []
187
+ const keys = [["$Something"], ["$Something", "GetSomething2"]]
188
+ const queryInvalidator = combineQueryInvalidators(
189
+ { invalidateAndAwait: (keys) => Effect.sync(() => keys.forEach((key) => recorded.push(["atom", key]))) },
190
+ { invalidateAndAwait: (keys) => Effect.sync(() => keys.forEach((key) => recorded.push(["tanstack", key]))) }
191
+ )
192
+ const mutate = invalidateQueries(
193
+ { id: "Something.DoSomething" },
194
+ { queryInvalidation: () => keys },
195
+ queryInvalidator
196
+ )
197
+
198
+ const fiber = yield* Effect.forkChild(mutate(Effect.succeed(123), { id: "abc" }))
199
+ yield* TestClock.adjust("1 millis")
200
+ const result = yield* Fiber.join(fiber)
201
+
202
+ expect(result).toBe(123)
203
+ expect(recorded).toEqual([
204
+ ["atom", keys[0]],
205
+ ["atom", keys[1]],
206
+ ["tanstack", keys[0]],
207
+ ["tanstack", keys[1]]
208
+ ])
209
+ }))
210
+
211
+ it("client[Key].Input — extracted input type per props variant", () => {
212
+ const { clientFor } = useClient()
213
+ const client = clientFor(
214
+ Something,
215
+ undefined,
216
+ somethingInvalidationResources
217
+ )
218
+
219
+ // Input mirrors HandlerInput<typeof Request> — the same type the handler accepts.
220
+ expectTypeOf(client.GetSomething2.Input).toEqualTypeOf<HandlerInput<typeof Something.GetSomething2>>()
221
+ expectTypeOf(client.GetSomething3.Input).toEqualTypeOf<HandlerInput<typeof Something.GetSomething3>>()
222
+ // GetSomething4: no fields → void
223
+ expectTypeOf(client.GetSomething4.Input).toBeVoid()
224
+ expectTypeOf(client.GetSomething4.Input).toEqualTypeOf<HandlerInput<typeof Something.GetSomething4>>()
225
+
226
+ // commands — props variants
227
+ expectTypeOf(client.DoNoProps.Input).toBeVoid()
228
+ expectTypeOf(client.DoNoProps.Input).toEqualTypeOf<HandlerInput<typeof Something.DoNoProps>>()
229
+ expectTypeOf(client.DoOptionalOnly.Input).toEqualTypeOf<HandlerInput<typeof Something.DoOptionalOnly>>()
230
+ expectTypeOf(client.DoRequiredOnly.Input).toEqualTypeOf<HandlerInput<typeof Something.DoRequiredOnly>>()
231
+ expectTypeOf(client.DoMixed.Input).toEqualTypeOf<HandlerInput<typeof Something.DoMixed>>()
232
+ expectTypeOf(client.DoSomething.Input).toEqualTypeOf<HandlerInput<typeof Something.DoSomething>>()
233
+
234
+ // Sanity: a non-trivial handler Input matches the handler's parameter type
235
+ type HandlerArg = Parameters<typeof client.DoMixed.handler>[0]
236
+ expectTypeOf<typeof client.DoMixed.Input>().toEqualTypeOf<HandlerArg>()
237
+
238
+ // Stream handlers — Input now extracts via RequestStreamHandlerWithInput fallback.
239
+ expectTypeOf(client.StreamQuery.Input).toEqualTypeOf<{ readonly id: string; readonly _tag?: "StreamQuery" }>()
240
+ expectTypeOf(client.StreamWithoutFinal.Input).toEqualTypeOf<
241
+ { readonly id: string; readonly _tag?: "StreamWithoutFinal" }
242
+ >()
243
+ expectTypeOf(client.StreamWithFinal.Input).toEqualTypeOf<{ readonly id: string; readonly _tag?: "StreamWithFinal" }>()
244
+
245
+ const assertStreamQueryTypes = () => {
246
+ const streamAtom = client.StreamQuery.atom({ id: "abc" })
247
+ expectTypeOf(streamAtom).toEqualTypeOf<Atom.Writable<Atom.PullResult<number, never>, void>>()
248
+ const streamView = client.StreamQuery.queryNew({ id: "abc" })
249
+ expectTypeOf(streamView.items.value).toEqualTypeOf<ReadonlyArray<number>>()
250
+ expectTypeOf(streamView.latest.value).toEqualTypeOf<number | undefined>()
251
+ expectTypeOf(streamView.done.value).toBeBoolean()
252
+ expectTypeOf(streamView.pull).toBeFunction()
253
+ expectTypeOf(streamView.pullAndAwait).toBeFunction()
254
+ }
255
+ void assertStreamQueryTypes
256
+ })
257
+
258
+ it("CommandFromRequest input shape — props variants", () => {
259
+ type NoPropsArg = Parameters<CommandFromRequest<typeof Something.DoNoProps>["handle"]>[0]
260
+
261
+ // no-props (no fields) → void input; void parameter is implicitly optional, so handle() works
262
+ expectTypeOf<NoPropsArg>().toBeVoid()
263
+
264
+ // type-only assignability checks for the remaining variants
265
+ if (false as boolean) {
266
+ const noProps = null as unknown as CommandFromRequest<typeof Something.DoNoProps>
267
+ const optOnly = null as unknown as CommandFromRequest<typeof Something.DoOptionalOnly>
268
+ const reqOnly = null as unknown as CommandFromRequest<typeof Something.DoRequiredOnly>
269
+ const mixed = null as unknown as CommandFromRequest<typeof Something.DoMixed>
270
+
271
+ // no-props → void param, calling without args is fine
272
+ noProps.handle()
273
+
274
+ // optional-only → matches `make` (fully optional, arg omittable)
275
+ optOnly.handle()
276
+ optOnly.handle({})
277
+ optOnly.handle({ name: "x" })
278
+
279
+ // required-only → id required
280
+ reqOnly.handle({ id: "x" })
281
+ // @ts-expect-error id required
282
+ reqOnly.handle({})
283
+
284
+ // mixed → id required, name optional
285
+ mixed.handle({ id: "x" })
286
+ mixed.handle({ id: "x", name: "y" })
287
+ // @ts-expect-error id required
288
+ mixed.handle({ name: "y" })
289
+ }
290
+ })
291
+
292
+ it.skip("query type tests", () => {
293
+ const { clientFor } = useClient()
294
+ const client = clientFor(
295
+ Something,
296
+ () => ({
297
+ GetSomething2WithDependencies: (queryKey) => [
298
+ { filters: { queryKey } },
299
+ {
300
+ filters: {
301
+ queryKey: makeQueryKey(
302
+ SomethingElse
303
+ .GetSomething2
304
+ )
305
+ }
306
+ }
307
+ ]
308
+ }),
309
+ somethingInvalidationResources
310
+ )
311
+
312
+ const q = client.GetSomething2.query
313
+
314
+ const [, z] = q({ id: "a" })
14
315
  const valz = z.value
15
316
  expectTypeOf(valz).toEqualTypeOf<number | undefined>()
16
317
 
17
- const [, a] = n("a", { placeholderData: () => 123 })
318
+ const [, a] = q({ id: "a" }, { placeholderData: () => 123 })
18
319
  const val1 = a.value
19
320
  expectTypeOf(val1).toEqualTypeOf<number>()
20
321
 
21
- const [, bbbb] = n("a", { select: (data) => data.toString() })
322
+ const [, bbbb] = q({ id: "a" }, { select: (data) => data.toString() })
22
323
  const val = bbbb.value
23
324
  expectTypeOf(val).toEqualTypeOf<string | undefined>()
24
325
 
25
- const [, ccc] = n("a", { placeholderData: () => 123, select: (data) => data.toString() })
326
+ const [, ccc] = q({ id: "a" }, { placeholderData: () => 123, select: (data) => data.toString() })
26
327
  const val2 = ccc.value
27
328
  expectTypeOf(val2).toEqualTypeOf<string>()
28
329
 
29
- const [, ddd] = n("a", { initialData: 123, select: (data) => data.toString() })
330
+ const [, ddd] = q({ id: "a" }, { initialData: 123, select: (data) => data.toString() })
30
331
  const val3 = ddd.value
31
332
  expectTypeOf(val3).toEqualTypeOf<string>()
32
333
 
33
- const [, eee] = n("a", { initialData: 123, placeholderData: () => 123, select: (data) => data.toString() })
334
+ const [, eee] = q({ id: "a" }, { initialData: 123, placeholderData: () => 123, select: (data) => data.toString() })
34
335
  const val4 = eee.value
35
336
  expectTypeOf(val4).toEqualTypeOf<string>()
36
337
  })
37
338
 
339
+ it("additive atom query api type tests", () => {
340
+ const { clientFor } = useClient()
341
+ const client = clientFor(
342
+ Something,
343
+ undefined,
344
+ somethingInvalidationResources
345
+ )
346
+
347
+ if (false as boolean) {
348
+ const family = client.GetSomething2.family
349
+ const familyAtom = family({ id: "a" })
350
+ expectTypeOf<Atom.Success<typeof familyAtom>>().toEqualTypeOf<number>()
351
+
352
+ const mappedFamilyAtom = Atom.mapResult(familyAtom, (data) => data.toString())
353
+ expectTypeOf<Atom.Success<typeof mappedFamilyAtom>>().toEqualTypeOf<string>()
354
+ expectTypeOf(useAtomQuery(() => mappedFamilyAtom).data.value).toEqualTypeOf<string | undefined>()
355
+ useAtomSuspense(() => mappedFamilyAtom).then((view) => {
356
+ expectTypeOf(view[1].value).toEqualTypeOf<string>()
357
+ return view
358
+ })
359
+
360
+ const atom = client.GetSomething2.atom({ id: "a" })
361
+ expectTypeOf<Atom.Success<typeof atom>>().toEqualTypeOf<number>()
362
+ // @ts-expect-error raw atom exposure does not accept Vue select projections
363
+ client.GetSomething2.atom({ id: "a" }, { select: (data) => data.toString() })
364
+
365
+ const query = client.GetSomething2.queryNew({ id: "a" })
366
+ expectTypeOf(query.data.value).toEqualTypeOf<number | undefined>()
367
+
368
+ const selected = client.GetSomething2.queryNew({ id: "a" }, { select: (data) => data.toString() })
369
+ expectTypeOf(selected.data.value).toEqualTypeOf<string | undefined>()
370
+
371
+ client.GetSomething2.suspenseNew({ id: "a" }, { select: (data) => data.toString() }).then((view) => {
372
+ expectTypeOf(view[1].value).toEqualTypeOf<string>()
373
+ return view
374
+ })
375
+
376
+ const projected = client.GetSomething2.project(S.String)
377
+ const projectedFamilyAtom = projected.family({ id: "a" })
378
+ expectTypeOf<Atom.Success<typeof projectedFamilyAtom>>().toEqualTypeOf<string>()
379
+ const projectedAtom = projected.atom({ id: "a" })
380
+ expectTypeOf<Atom.Success<typeof projectedAtom>>().toEqualTypeOf<string>()
381
+ expectTypeOf(projected.queryNew({ id: "a" }).data.value).toEqualTypeOf<string | undefined>()
382
+ projected.suspenseNew({ id: "a" }).then((view) => {
383
+ expectTypeOf(view[1].value).toEqualTypeOf<string>()
384
+ return view
385
+ })
386
+
387
+ const helperFamilyAtom = client.helpers.getSomething2QueryFamily({ id: "a" })
388
+ expectTypeOf<Atom.Success<typeof helperFamilyAtom>>().toEqualTypeOf<number>()
389
+ expectTypeOf(client.helpers.getSomething2QueryNew({ id: "a" }).data.value).toEqualTypeOf<number | undefined>()
390
+ client.helpers.getSomething2SuspenseQueryNew({ id: "a" }).then((view) => {
391
+ expectTypeOf(view[1].value).toEqualTypeOf<number>()
392
+ return view
393
+ })
394
+ }
395
+
396
+ expect(true).toBe(true)
397
+ })
398
+
38
399
  it.skip("works", () => {
39
- const { clientFor, legacy } = useClient()
40
- const client = clientFor(Something)
400
+ const { clientFor } = useClient()
401
+ const client = clientFor(Something, undefined, somethingInvalidationResources)
41
402
  const Command = useExperimental()
42
403
 
43
404
  // just for jsdoc / type testing.
44
- const a0 = client.GetSomething2(null as any)
45
- const a00 = client.GetSomething2.mutate(null as any)
405
+ const a0 = client.GetSomething2.request(null as any)
406
+ const a00 = client.DoSomething.mutate(null as any)
46
407
  const a = client.GetSomething2.suspense(null as any)
47
408
  const b = client.GetSomething2.query(null as any)
48
409
 
49
- const c0 = legacy.useSafeMutation(null as any)
50
- const c = legacy.useQuery(null as any)
51
- const d = legacy.useSuspenseQuery(null as any)
410
+ const de = client.GetSomething3.handler(null as any)
411
+ const de2 = client.GetSomething3.handler({ id: null })
412
+
413
+ const de3 = client.GetSomething4.handler()
414
+ void client.GetSomething4.handler
52
415
 
416
+ // @ts-expect-error query requests no longer expose command helpers
53
417
  const e = client.GetSomething2.wrap(null as any)
418
+ // @ts-expect-error query requests no longer expose command helpers
54
419
  const f = client.GetSomething2.fn(null as any)
55
420
 
56
- // @ts-expect-error dependencies required that are not provided
57
- const e0 = client.GetSomething2WithDependencies.wrap().handle // not available as we require dependencies not provided by the runtime
58
- // @ts-expect-error dependencies required that are not provided
59
- const e000 = Command.wrap(client.GetSomething2WithDependencies)().handle // not available as we require dependencies not provided by the runtime
60
- const e00 = client.GetSomething2WithDependencies.wrap((_) => _ as Effect.Effect<number, never, never>).handle(
61
- null as any
62
- )
63
- const e0000 =
64
- Command.wrap(client.GetSomething2WithDependencies)((_) => _ as Effect.Effect<number, never, never>).handle
421
+ // @ts-expect-error query requests no longer expose command helpers
422
+ const e0 = client.GetSomething2WithDependencies.wrap
423
+ // @ts-expect-error query request does not match Command.wrap mutation signature
424
+ const e000 = Command.wrap(client.GetSomething2WithDependencies)
425
+ const e00 = client.GetSomething2WithDependencies.request(null as any)
65
426
  // @ts-expect-error dependencies required that are not provided
66
427
  const e1 = client.GetSomething2WithDependencies.suspense(null as any)
67
428
  // @ts-expect-error dependencies required that are not provided
68
429
  const e2 = client.GetSomething2WithDependencies.query(null as any)
430
+ // @ts-expect-error query requests no longer expose command helpers
69
431
  const f0 = client.GetSomething2WithDependencies.fn(null as any)
70
432
 
71
- const g = client.GetSomething2.mutate.wrap(null as any)
72
- const h = client.GetSomething2.mutate.fn(null as any)
433
+ const g0 = client.DoSomething.wrap(null as any)
434
+ const g = client.DoSomething.mutate.wrap(null as any)
435
+ const g1 = client.DoSomething.mutate.project(S.String)
436
+ const g2 = g1(null as any)
437
+ const g3 = g1.wrap(null as any)
438
+ const g4 = client.helpers.doSomethingMutation.project(S.String)
439
+ const g5 = g4(null as any)
440
+ const g6 = g4.wrap(null as any)
441
+ const h = client.DoSomething.mutate.fn(null as any)
442
+
443
+ // projection
444
+ // GetSomething2 uses FiniteFromString, that means Codec is String -> Number
445
+ // when we project that to S.String, it should work as the encoded shapes are identical
446
+ // aka, when we project, we skip decoding with the original codec, and instead use the provided one
447
+ // we have to make sure the Encoded shape of the provided projection schema matches the Encoded Shape of the original codec.
448
+ const projected = client.GetSomething2.project(S.String)
449
+ // @ts-expect-error encoded type mismatch: original encodes to string, S.Number encodes to number
450
+ client.GetSomething2.project(S.Number)
451
+ const p0 = projected.request(null as any)
452
+
453
+ // struct example: success schema encodes to { a: string | null }
454
+ // good: projection schema also expects { a: string | null } on the encoded side
455
+ const projectedStruct = client.GetStructNullable.project(S.Struct({ a: S.NullOr(S.String) }))
456
+ // bad: { a: S.String } has encoded type { a: string } — does not accept null
457
+ // @ts-expect-error encoded type mismatch: original encodes to { a: string | null }, projection expects { a: string }
458
+ client.GetStructNullable.project(S.Struct({ a: S.String }))
459
+
460
+ const p00 = projected.query(null as any)
461
+ const p = projected.suspense(null as any)
73
462
 
74
463
  expect(true).toBe(true)
75
464
  console.log({
76
465
  a,
77
466
  a0,
78
467
  a00,
79
- c0,
80
468
  b,
81
- c,
82
- d,
83
469
  e,
470
+ de,
471
+ de2,
472
+ de3,
84
473
  e0,
85
474
  e00,
86
475
  e000,
87
- e0000,
88
476
  e1,
89
477
  e2,
90
478
  f,
91
479
  f0,
480
+ g0,
92
481
  g,
93
- h
482
+ g1,
483
+ g2,
484
+ g3,
485
+ g4,
486
+ g5,
487
+ g6,
488
+ h,
489
+ p0,
490
+ p00,
491
+ p,
492
+ projectedStruct
94
493
  })
95
494
  })
@@ -0,0 +1,92 @@
1
+ import { defaultRegistry, registryKey } from "@effect/atom-vue"
2
+ import { QueryClient, VueQueryPlugin } from "@tanstack/vue-query"
3
+ import type { RequestHandlerWithInput } from "effect-app/client/clientFor"
4
+ import * as Context from "effect-app/Context"
5
+ import * as Effect from "effect-app/Effect"
6
+ import * as Layer from "effect-app/Layer"
7
+ import * as Option from "effect-app/Option"
8
+ import type * as Cause from "effect/Cause"
9
+ import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
10
+ import { createApp, effectScope, nextTick } from "vue"
11
+ import { buildQueryFamily, makeAtomClientRuntime } from "../src/atomQuery.js"
12
+ import { makeTanstackQuery } from "../src/internal/tanstackQuery.js"
13
+ import { useAtomQuery } from "../src/query.js"
14
+
15
+ const fakeHandler = <A, E>(
16
+ id: string,
17
+ run: (input: void) => Effect.Effect<A, E, never>
18
+ ): RequestHandlerWithInput<void, A, E, never, never, string> => ({
19
+ id,
20
+ handler: run,
21
+ Request: undefined as never
22
+ })
23
+
24
+ const querySpanParentName = Effect.currentSpan.pipe(
25
+ Effect.map((span) =>
26
+ Option.match(span.parent, {
27
+ onNone: () => "none",
28
+ onSome: (parent) => parent._tag === "Span" ? parent.name : "external"
29
+ })
30
+ )
31
+ )
32
+
33
+ function makeTanstackContext(queryClient: QueryClient) {
34
+ const app = createApp({ render: () => null })
35
+ app.use(VueQueryPlugin, { queryClient })
36
+ const scope = effectScope(true)
37
+ const run = <T>(fn: () => T): T => {
38
+ let out!: T
39
+ app.runWithContext(() => {
40
+ scope.run(() => {
41
+ out = fn()
42
+ })
43
+ })
44
+ return out
45
+ }
46
+ return { run, dispose: () => scope.stop() }
47
+ }
48
+
49
+ it("passes the current parent span through legacy TanStack refetch", async () => {
50
+ const queryClient = new QueryClient({
51
+ defaultOptions: { queries: { retry: false, gcTime: Infinity, staleTime: 0 } }
52
+ })
53
+ const query = makeTanstackQuery(() => Context.empty(), queryClient)(
54
+ fakeHandler("Test/TanStackSpan", () => querySpanParentName)
55
+ )
56
+ const ctx = makeTanstackContext(queryClient)
57
+
58
+ const [, , , handle] = ctx.run(() => query(undefined, {}))
59
+ await Effect.runPromise(handle.awaitResult())
60
+
61
+ const parentName = await Effect.runPromise(handle.refetch().pipe(Effect.withSpan("trigger")))
62
+
63
+ expect(parentName).toBe("trigger")
64
+ ctx.dispose()
65
+ })
66
+
67
+ it("passes the current parent span through atom query refetch", async () => {
68
+ defaultRegistry.reset()
69
+ const rt = makeAtomClientRuntime(() => Layer.empty, Layer.makeMemoMapUnsafe())
70
+ const family = buildQueryFamily(rt, fakeHandler("Test/AtomSpan", () => querySpanParentName))
71
+ const atom = family(undefined)
72
+ let view!: ReturnType<typeof useAtomQuery<string, Cause.NoSuchElementError>>
73
+ const host = document.createElement("div")
74
+ const app = createApp({
75
+ setup() {
76
+ view = useAtomQuery(() => atom)
77
+ return () => null
78
+ }
79
+ })
80
+ app.provide(registryKey, defaultRegistry)
81
+ app.mount(host)
82
+
83
+ await Effect.runPromise(view.awaitResult())
84
+ const parentName = await Effect.runPromise(view.refetch().pipe(Effect.withSpan("trigger")))
85
+
86
+ expect(parentName).toBe("trigger")
87
+ expect(AsyncResult.isSuccess(view.result.value)).toBe(true)
88
+
89
+ app.unmount()
90
+ await nextTick()
91
+ defaultRegistry.reset()
92
+ })