@effect-app/vue 4.0.0-beta.98 → 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 +2707 -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
@@ -1,34 +1,313 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { type Effect } from "effect-app"
3
- import { makeQueryKey } from "../src/lib.js"
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 * as Atom from "effect/reactivity/Atom"
10
+ import { TestClock } from "effect/testing"
11
+ import type { CommandFromRequest } from "../src/makeClient.js"
12
+ import { combineQueryInvalidators, invalidateQueries } from "../src/mutate.js"
13
+ import { useAtomQuery, useAtomSuspense } from "../src/query.js"
4
14
  import { Something, SomethingElse, SomethingElseReq, SomethingReq, useClient, useExperimental } from "./stubs.js"
5
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
+
6
25
  it("TaggedRequestFor .moduleName and request .id / .moduleName", () => {
7
26
  expectTypeOf(SomethingReq.moduleName).toEqualTypeOf<"Something">()
8
27
  expectTypeOf(SomethingElseReq.moduleName).toEqualTypeOf<"SomethingElse">()
9
28
 
10
29
  expectTypeOf(Something.GetSomething2.moduleName).toEqualTypeOf<"Something">()
11
30
  expectTypeOf(Something.GetSomething2.id).toEqualTypeOf<"Something.GetSomething2">()
31
+ expectTypeOf(Something.GetSomething2.type).toEqualTypeOf<"query">()
32
+ expectTypeOf(Something.DoSomething.type).toEqualTypeOf<"command">()
12
33
 
13
34
  expectTypeOf(SomethingElse.GetSomething2.moduleName).toEqualTypeOf<"SomethingElse">()
14
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 []
55
+ })
56
+
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
+ }
15
290
  })
16
291
 
17
292
  it.skip("query type tests", () => {
18
293
  const { clientFor } = useClient()
19
- const client = clientFor(Something, () => ({
20
- GetSomething2WithDependencies: (queryKey) => [
21
- { filters: { queryKey } },
22
- {
23
- filters: {
24
- queryKey: makeQueryKey(
25
- SomethingElse
26
- .GetSomething2
27
- )
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
+ }
28
306
  }
29
- }
30
- ]
31
- }))
307
+ ]
308
+ }),
309
+ somethingInvalidationResources
310
+ )
32
311
 
33
312
  const q = client.GetSomething2.query
34
313
 
@@ -57,37 +336,127 @@ it.skip("query type tests", () => {
57
336
  expectTypeOf(val4).toEqualTypeOf<string>()
58
337
  })
59
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
+ void 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
+ void 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
+ void 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
+ void 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
+
60
399
  it.skip("works", () => {
61
400
  const { clientFor } = useClient()
62
- const client = clientFor(Something)
401
+ const client = clientFor(Something, undefined, somethingInvalidationResources)
63
402
  const Command = useExperimental()
64
403
 
65
404
  // just for jsdoc / type testing.
66
- const a0 = client.GetSomething2(null as any)
67
- 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)
68
407
  const a = client.GetSomething2.suspense(null as any)
69
408
  const b = client.GetSomething2.query(null as any)
70
409
 
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
415
+
416
+ // @ts-expect-error query requests no longer expose command helpers
71
417
  const e = client.GetSomething2.wrap(null as any)
418
+ // @ts-expect-error query requests no longer expose command helpers
72
419
  const f = client.GetSomething2.fn(null as any)
73
420
 
74
- // @ts-expect-error dependencies required that are not provided
75
- const e0 = client.GetSomething2WithDependencies.wrap().handle // not available as we require dependencies not provided by the runtime
76
- // @ts-expect-error dependencies required that are not provided
77
- const e000 = Command.wrap(client.GetSomething2WithDependencies)().handle // not available as we require dependencies not provided by the runtime
78
- const e00 = client.GetSomething2WithDependencies.wrap((_) => _ as Effect.Effect<number, never, never>).handle(
79
- null as any
80
- )
81
- const e0000 =
82
- Command.wrap(client.GetSomething2WithDependencies)((_) => _ as Effect.Effect<number, never, never>).handle
83
- // @ts-expect-error dependencies required that are not provided
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)
84
426
  const e1 = client.GetSomething2WithDependencies.suspense(null as any)
85
- // @ts-expect-error dependencies required that are not provided
86
427
  const e2 = client.GetSomething2WithDependencies.query(null as any)
428
+ // @ts-expect-error query requests no longer expose command helpers
87
429
  const f0 = client.GetSomething2WithDependencies.fn(null as any)
88
430
 
89
- const g = client.GetSomething2.mutate.wrap(null as any)
90
- const h = client.GetSomething2.mutate.fn(null as any)
431
+ const g0 = client.DoSomething.wrap(null as any)
432
+ const g = client.DoSomething.mutate.wrap(null as any)
433
+ const g1 = client.DoSomething.mutate.project(S.String)
434
+ const g2 = g1(null as any)
435
+ const g3 = g1.wrap(null as any)
436
+ const g4 = client.helpers.doSomethingMutation.project(S.String)
437
+ const g5 = g4(null as any)
438
+ const g6 = g4.wrap(null as any)
439
+ const h = client.DoSomething.mutate.fn(null as any)
440
+
441
+ // projection
442
+ // GetSomething2 uses FiniteFromString, that means Codec is String -> Number
443
+ // when we project that to S.String, it should work as the encoded shapes are identical
444
+ // aka, when we project, we skip decoding with the original codec, and instead use the provided one
445
+ // we have to make sure the Encoded shape of the provided projection schema matches the Encoded Shape of the original codec.
446
+ const projected = client.GetSomething2.project(S.String)
447
+ // @ts-expect-error encoded type mismatch: original encodes to string, S.Number encodes to number
448
+ client.GetSomething2.project(S.Number)
449
+ const p0 = projected.request(null as any)
450
+
451
+ // struct example: success schema encodes to { a: string | null }
452
+ // good: projection schema also expects { a: string | null } on the encoded side
453
+ const projectedStruct = client.GetStructNullable.project(S.Struct({ a: S.NullOr(S.String) }))
454
+ // bad: { a: S.String } has encoded type { a: string } — does not accept null
455
+ // @ts-expect-error encoded type mismatch: original encodes to { a: string | null }, projection expects { a: string }
456
+ client.GetStructNullable.project(S.Struct({ a: S.String }))
457
+
458
+ const p00 = projected.query(null as any)
459
+ const p = projected.suspense(null as any)
91
460
 
92
461
  expect(true).toBe(true)
93
462
  console.log({
@@ -96,15 +465,28 @@ it.skip("works", () => {
96
465
  a00,
97
466
  b,
98
467
  e,
468
+ de,
469
+ de2,
470
+ de3,
99
471
  e0,
100
472
  e00,
101
473
  e000,
102
- e0000,
103
474
  e1,
104
475
  e2,
105
476
  f,
106
477
  f0,
478
+ g0,
107
479
  g,
108
- h
480
+ g1,
481
+ g2,
482
+ g3,
483
+ g4,
484
+ g5,
485
+ g6,
486
+ h,
487
+ p0,
488
+ p00,
489
+ p,
490
+ projectedStruct
109
491
  })
110
492
  })
@@ -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/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
+ })
@@ -0,0 +1,88 @@
1
+ import { expect, it } from "@effect/vitest"
2
+ import * as Cause from "effect/Cause"
3
+ import * as Option from "effect/Option"
4
+ import * as AsyncResult from "effect/reactivity/AsyncResult"
5
+ import { shallowRef } from "vue"
6
+ import { withDataFallback } from "../src/query.js"
7
+
8
+ it("withDataFallback: returns the raw ref untouched when neither option is set", () => {
9
+ const raw = shallowRef(AsyncResult.initial<number, never>(false))
10
+ expect(withDataFallback(raw, undefined)).toBe(raw)
11
+ expect(withDataFallback(raw, {})).toBe(raw)
12
+ })
13
+
14
+ it("withDataFallback: placeholderData is provisional (waiting) and dropped once real data exists", () => {
15
+ const raw = shallowRef<AsyncResult.AsyncResult<number, never>>(AsyncResult.initial(true))
16
+ const out = withDataFallback(raw, { placeholderData: 1 })
17
+
18
+ // while pending with no value, surface the placeholder as provisional
19
+ expect(out.value).toMatchObject({ _tag: "Success", value: 1, waiting: true })
20
+
21
+ // real data replaces the placeholder (display-only — raw was never written)
22
+ raw.value = AsyncResult.success(2)
23
+ expect(out.value).toMatchObject({ _tag: "Success", value: 2, waiting: false })
24
+ })
25
+
26
+ it("withDataFallback: placeholderData function form receives the last seen concrete value", () => {
27
+ const raw = shallowRef<AsyncResult.AsyncResult<number, never>>(AsyncResult.success(10))
28
+ const out = withDataFallback(raw, { placeholderData: (prev: number | undefined) => (prev ?? 0) + 1 })
29
+
30
+ // concrete value present -> shown as-is, recorded as "previous"
31
+ expect(out.value).toMatchObject({ _tag: "Success", value: 10 })
32
+
33
+ // input changed -> back to Initial: placeholder fn sees the previous value (10)
34
+ raw.value = AsyncResult.initial(true)
35
+ expect(out.value).toMatchObject({ _tag: "Success", value: 11, waiting: true })
36
+ })
37
+
38
+ it("withDataFallback: initialData shows as resolved data while Initial, dropped on real success", () => {
39
+ const raw = shallowRef<AsyncResult.AsyncResult<number, never>>(AsyncResult.initial(false))
40
+ const out = withDataFallback(raw, { initialData: 7 })
41
+
42
+ expect(out.value).toMatchObject({ _tag: "Success", value: 7, waiting: false })
43
+
44
+ const out2 = withDataFallback(raw, { initialData: () => 9 })
45
+ expect(out2.value).toMatchObject({ _tag: "Success", value: 9 })
46
+
47
+ raw.value = AsyncResult.success(42)
48
+ expect(out.value).toMatchObject({ _tag: "Success", value: 42 })
49
+ })
50
+
51
+ it("withDataFallback: initialData takes precedence over placeholderData", () => {
52
+ const raw = shallowRef<AsyncResult.AsyncResult<number, never>>(AsyncResult.initial(false))
53
+ const out = withDataFallback(raw, { initialData: 1, placeholderData: 99 })
54
+ expect(out.value).toMatchObject({ _tag: "Success", value: 1 })
55
+ })
56
+
57
+ it("withDataFallback: select applies to the placeholder when layered after the fallback", () => {
58
+ // mirrors makeQueryView: fallback pre-select, then AsyncResult.map(select)
59
+ const raw = shallowRef<AsyncResult.AsyncResult<number, never>>(AsyncResult.initial(true))
60
+ const fallback = withDataFallback(raw, { placeholderData: 123 })
61
+ const selected = () => AsyncResult.map(fallback.value, (n) => n.toString())
62
+
63
+ expect(selected()).toMatchObject({ _tag: "Success", value: "123", waiting: true })
64
+
65
+ raw.value = AsyncResult.success(456)
66
+ expect(selected()).toMatchObject({ _tag: "Success", value: "456", waiting: false })
67
+ })
68
+
69
+ it("withDataFallback: does not mask Failure without previousSuccess", () => {
70
+ const fail = AsyncResult.failure<number, string>(Cause.fail("boom"))
71
+ const raw = shallowRef<AsyncResult.AsyncResult<number, string>>(fail)
72
+ const out = withDataFallback(raw, { placeholderData: 1, initialData: 2 })
73
+
74
+ // Failure has no value — old gate would have fabricated Success; must pass Failure through
75
+ expect(out.value).toBe(fail)
76
+ expect(AsyncResult.isFailure(out.value)).toBe(true)
77
+ })
78
+
79
+ it("withDataFallback: Failure with previousSuccess still surfaces as Failure", () => {
80
+ const fail = AsyncResult.failureWithPrevious<number, string>(Cause.fail("boom"), {
81
+ previous: Option.some(AsyncResult.success(10))
82
+ })
83
+ const raw = shallowRef<AsyncResult.AsyncResult<number, string>>(fail)
84
+ const out = withDataFallback(raw, { placeholderData: 99 })
85
+
86
+ expect(out.value).toBe(fail)
87
+ expect(AsyncResult.isFailure(out.value)).toBe(true)
88
+ })