@effect-app/vue 4.0.0-beta.26 → 4.0.0-beta.261

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 (112) hide show
  1. package/CHANGELOG.md +1929 -0
  2. package/dist/commander.d.ts +634 -0
  3. package/dist/commander.d.ts.map +1 -0
  4. package/dist/commander.js +1070 -0
  5. package/dist/confirm.d.ts +21 -0
  6. package/dist/confirm.d.ts.map +1 -0
  7. package/dist/confirm.js +26 -0
  8. package/dist/dependencyMetadata.d.ts +8 -0
  9. package/dist/dependencyMetadata.d.ts.map +1 -0
  10. package/dist/dependencyMetadata.js +6 -0
  11. package/dist/errorReporter.d.ts +7 -5
  12. package/dist/errorReporter.d.ts.map +1 -1
  13. package/dist/errorReporter.js +14 -19
  14. package/dist/form.d.ts +15 -6
  15. package/dist/form.d.ts.map +1 -1
  16. package/dist/form.js +46 -13
  17. package/dist/index.d.ts +1 -1
  18. package/dist/intl.d.ts +15 -0
  19. package/dist/intl.d.ts.map +1 -0
  20. package/dist/intl.js +9 -0
  21. package/dist/lib.d.ts +8 -10
  22. package/dist/lib.d.ts.map +1 -1
  23. package/dist/lib.js +35 -10
  24. package/dist/makeClient.d.ts +157 -343
  25. package/dist/makeClient.d.ts.map +1 -1
  26. package/dist/makeClient.js +216 -376
  27. package/dist/makeContext.d.ts +1 -1
  28. package/dist/makeContext.d.ts.map +1 -1
  29. package/dist/makeIntl.d.ts +1 -1
  30. package/dist/makeIntl.d.ts.map +1 -1
  31. package/dist/makeUseCommand.d.ts +9 -0
  32. package/dist/makeUseCommand.d.ts.map +1 -0
  33. package/dist/makeUseCommand.js +13 -0
  34. package/dist/mutate.d.ts +97 -39
  35. package/dist/mutate.d.ts.map +1 -1
  36. package/dist/mutate.js +177 -49
  37. package/dist/query.d.ts +24 -39
  38. package/dist/query.d.ts.map +1 -1
  39. package/dist/query.js +156 -78
  40. package/dist/routeParams.d.ts +5 -5
  41. package/dist/routeParams.d.ts.map +1 -1
  42. package/dist/routeParams.js +4 -3
  43. package/dist/runtime.d.ts +2 -15
  44. package/dist/runtime.d.ts.map +1 -1
  45. package/dist/runtime.js +2 -26
  46. package/dist/toast.d.ts +2 -0
  47. package/dist/toast.d.ts.map +1 -0
  48. package/dist/toast.js +2 -0
  49. package/dist/withToast.d.ts +2 -0
  50. package/dist/withToast.d.ts.map +1 -0
  51. package/dist/withToast.js +2 -0
  52. package/examples/streamMutation.ts +72 -0
  53. package/package.json +29 -90
  54. package/src/commander.ts +3406 -0
  55. package/src/{experimental/confirm.ts → confirm.ts} +12 -14
  56. package/src/errorReporter.ts +65 -75
  57. package/src/form.ts +61 -18
  58. package/src/intl.ts +12 -0
  59. package/src/lib.ts +48 -20
  60. package/src/makeClient.ts +581 -1138
  61. package/src/{experimental/makeUseCommand.ts → makeUseCommand.ts} +8 -5
  62. package/src/mutate.ts +335 -134
  63. package/src/query.ts +241 -183
  64. package/src/routeParams.ts +7 -7
  65. package/src/runtime.ts +1 -31
  66. package/src/toast.ts +1 -0
  67. package/src/withToast.ts +1 -0
  68. package/test/Mutation.test.ts +181 -24
  69. package/test/dist/dependencyInvalidation.test.d.ts.map +1 -0
  70. package/test/dist/form.test.d.ts.map +1 -1
  71. package/test/dist/lib.test.d.ts.map +1 -0
  72. package/test/dist/streamFinal.test.d.ts.map +1 -0
  73. package/test/dist/streamFn.test.d.ts.map +1 -0
  74. package/test/dist/stubs.d.ts +3527 -122
  75. package/test/dist/stubs.d.ts.map +1 -1
  76. package/test/dist/stubs.js +187 -32
  77. package/test/form-validation-errors.test.ts +25 -20
  78. package/test/form.test.ts +22 -3
  79. package/test/lib.test.ts +240 -0
  80. package/test/makeClient.test.ts +327 -38
  81. package/test/streamFinal.test.ts +64 -0
  82. package/test/streamFn.test.ts +457 -0
  83. package/test/stubs.ts +223 -43
  84. package/tsconfig.examples.json +20 -0
  85. package/tsconfig.json +2 -1
  86. package/tsconfig.json.bak +5 -2
  87. package/tsconfig.src.json +34 -34
  88. package/tsconfig.test.json +2 -2
  89. package/vitest.config.ts +5 -5
  90. package/dist/experimental/commander.d.ts +0 -359
  91. package/dist/experimental/commander.d.ts.map +0 -1
  92. package/dist/experimental/commander.js +0 -557
  93. package/dist/experimental/confirm.d.ts +0 -19
  94. package/dist/experimental/confirm.d.ts.map +0 -1
  95. package/dist/experimental/confirm.js +0 -28
  96. package/dist/experimental/intl.d.ts +0 -16
  97. package/dist/experimental/intl.d.ts.map +0 -1
  98. package/dist/experimental/intl.js +0 -5
  99. package/dist/experimental/makeUseCommand.d.ts +0 -8
  100. package/dist/experimental/makeUseCommand.d.ts.map +0 -1
  101. package/dist/experimental/makeUseCommand.js +0 -13
  102. package/dist/experimental/toast.d.ts +0 -47
  103. package/dist/experimental/toast.d.ts.map +0 -1
  104. package/dist/experimental/toast.js +0 -41
  105. package/dist/experimental/withToast.d.ts +0 -25
  106. package/dist/experimental/withToast.d.ts.map +0 -1
  107. package/dist/experimental/withToast.js +0 -45
  108. package/eslint.config.mjs +0 -24
  109. package/src/experimental/commander.ts +0 -1835
  110. package/src/experimental/intl.ts +0 -9
  111. package/src/experimental/toast.ts +0 -66
  112. package/src/experimental/withToast.ts +0 -99
@@ -0,0 +1,240 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { computed, isProxy, isReactive, isRef, reactive, ref } from "vue"
3
+ import { deepToRaw } from "../src/lib.js"
4
+
5
+ type DeepMapKey = { id: string } | "list"
6
+ type DeepMapValue = { nestedSet: Set<{ ok: boolean } | Date> } | Array<{ count: number }>
7
+ type DeepSetValue = Map<string, { value: number }> | Array<{ value: number }>
8
+
9
+ const expectPlainDeep = (value: unknown): void => {
10
+ expect(isRef(value)).toBe(false)
11
+ expect(isReactive(value)).toBe(false)
12
+ expect(isProxy(value)).toBe(false)
13
+
14
+ if (Array.isArray(value)) {
15
+ value.forEach(expectPlainDeep)
16
+ return
17
+ }
18
+
19
+ if (value instanceof Map) {
20
+ value.forEach((entryValue, entryKey) => {
21
+ expectPlainDeep(entryKey)
22
+ expectPlainDeep(entryValue)
23
+ })
24
+ return
25
+ }
26
+
27
+ if (value instanceof Set) {
28
+ value.forEach((entry) => {
29
+ expectPlainDeep(entry)
30
+ })
31
+ return
32
+ }
33
+
34
+ if (value instanceof Date) {
35
+ return
36
+ }
37
+
38
+ if (value && typeof value === "object") {
39
+ Object.values(value).forEach(expectPlainDeep)
40
+ }
41
+ }
42
+
43
+ describe("deepToRaw", () => {
44
+ it("supports non-object root inputs", () => {
45
+ expect(deepToRaw(1)).toBe(1)
46
+ expect(deepToRaw("x")).toBe("x")
47
+ expect(deepToRaw(null)).toBe(null)
48
+ expect(deepToRaw(undefined)).toBe(undefined)
49
+ expect(deepToRaw(ref(123))).toBe(123)
50
+
51
+ const rootArray = deepToRaw(reactive([reactive({ n: 1 }), ref(2)]))
52
+ expect(rootArray).toEqual([{ n: 1 }, 2])
53
+ expect(Array.isArray(rootArray)).toBe(true)
54
+
55
+ const rootMap = deepToRaw(
56
+ reactive(new Map<string, unknown>([["k", reactive({ n: 1 })], ["r", ref(2)]]))
57
+ )
58
+ expect(rootMap).toBeInstanceOf(Map)
59
+ expect(rootMap.get("k")).toEqual({ n: 1 })
60
+ expect(rootMap.get("r")).toBe(2)
61
+
62
+ const rootSet = deepToRaw(reactive(new Set([reactive({ n: 1 }), ref(2)])))
63
+ expect(rootSet).toBeInstanceOf(Set)
64
+ expect(Array.from(rootSet)).toEqual([{ n: 1 }, 2])
65
+
66
+ const date = new Date("2024-02-03T00:00:00.000Z")
67
+ const rootDate = deepToRaw(date)
68
+ expect(rootDate).toBeInstanceOf(Date)
69
+ expect(rootDate).not.toBe(date)
70
+ expect(rootDate.toISOString()).toBe(date.toISOString())
71
+ })
72
+
73
+ it("unwraps nested objects and arrays without leaving vue proxies behind", () => {
74
+ const source = reactive({
75
+ list: [
76
+ reactive({
77
+ nested: reactive({
78
+ count: 1,
79
+ items: [reactive({ label: "a" }), reactive({ label: "b" })]
80
+ })
81
+ })
82
+ ],
83
+ plain: reactive({ ok: true })
84
+ })
85
+
86
+ const result = deepToRaw(source)
87
+
88
+ expect(Array.isArray(result.list)).toBe(true)
89
+ expect(Array.isArray(result.list[0]?.nested.items)).toBe(true)
90
+ expect(result).toEqual({
91
+ list: [{ nested: { count: 1, items: [{ label: "a" }, { label: "b" }] } }],
92
+ plain: { ok: true }
93
+ })
94
+ expectPlainDeep(result)
95
+ })
96
+
97
+ it("preserves maps and sets while deeply unwrapping nested entries", () => {
98
+ const key = reactive({ id: "key" })
99
+ const nestedDate = new Date("2024-01-02T03:04:05.000Z")
100
+ const map = reactive(
101
+ new Map<DeepMapKey, DeepMapValue>([
102
+ [key, reactive({ nestedSet: reactive(new Set([{ ok: true }, nestedDate])) })],
103
+ ["list", reactive([{ count: 2 }])]
104
+ ])
105
+ )
106
+ const set = reactive(
107
+ new Set<DeepSetValue>([
108
+ reactive(new Map([["deep", reactive({ value: 3 })]])),
109
+ reactive([{ value: 4 }])
110
+ ])
111
+ )
112
+ const source = reactive({
113
+ map,
114
+ set
115
+ })
116
+
117
+ const result = deepToRaw(source)
118
+
119
+ expect(result.map).toBeInstanceOf(Map)
120
+ expect(result.set).toBeInstanceOf(Set)
121
+
122
+ const entries = Array.from(result.map.entries())
123
+ expect(entries[0]?.[0]).toEqual({ id: "key" })
124
+ expect(entries[0]?.[0]).not.toBe(key)
125
+ expect(entries[0]?.[1]).toEqual({ nestedSet: new Set([{ ok: true }, nestedDate]) })
126
+ expect(entries[1]?.[1]).toEqual([{ count: 2 }])
127
+
128
+ const setValues = Array.from(result.set.values())
129
+ expect(setValues[0]).toBeInstanceOf(Map)
130
+ expect(setValues[1]).toEqual([{ value: 4 }])
131
+ expect((setValues[0] as Map<string, { value: number }>).get("deep")).toEqual({ value: 3 })
132
+
133
+ expectPlainDeep(result)
134
+ })
135
+
136
+ it("keeps nested dates as dates, including dates reached through refs", () => {
137
+ const date = new Date("2025-06-07T08:09:10.000Z")
138
+ const source = reactive({
139
+ createdAt: date,
140
+ nested: reactive({
141
+ updatedAt: ref(date),
142
+ list: [ref(date)],
143
+ map: reactive(new Map([["at", ref(date)]])),
144
+ set: reactive(new Set([ref(date)]))
145
+ })
146
+ })
147
+
148
+ const result = deepToRaw(source)
149
+
150
+ expect(result.createdAt).toBeInstanceOf(Date)
151
+ expect(result.nested.updatedAt).toBeInstanceOf(Date)
152
+ expect(result.nested.list[0]).toBeInstanceOf(Date)
153
+ expect(result.nested.map).toBeInstanceOf(Map)
154
+ expect(result.nested.set).toBeInstanceOf(Set)
155
+
156
+ const updatedAt = result.nested.updatedAt
157
+ const firstListDate = result.nested.list[0]
158
+ const mappedDate = result.nested.map.get("at")
159
+ const firstSetDate = Array.from(result.nested.set)[0]
160
+
161
+ if (!(updatedAt instanceof Date)) {
162
+ throw new Error("expected updatedAt to be a Date")
163
+ }
164
+
165
+ if (!(firstListDate instanceof Date)) {
166
+ throw new Error("expected first list item to be a Date")
167
+ }
168
+
169
+ if (!(mappedDate instanceof Date)) {
170
+ throw new Error("expected mapped date to be a Date")
171
+ }
172
+
173
+ if (!(firstSetDate instanceof Date)) {
174
+ throw new Error("expected first set item to be a Date")
175
+ }
176
+
177
+ expect(result.createdAt.toISOString()).toBe(date.toISOString())
178
+ expect(updatedAt.toISOString()).toBe(date.toISOString())
179
+ expect(firstListDate.toISOString()).toBe(date.toISOString())
180
+ expect(mappedDate.toISOString()).toBe(date.toISOString())
181
+ expect(firstSetDate.toISOString()).toBe(date.toISOString())
182
+
183
+ expectPlainDeep(result)
184
+ })
185
+
186
+ it("unwraps computed values nested in refs/plain objects and deepToRawes the computed result", () => {
187
+ const source = {
188
+ innerRef: ref({
189
+ computedValue: computed(() =>
190
+ reactive({
191
+ list: [reactive({ n: 1 }), reactive({ n: 2 })],
192
+ map: reactive(new Map([["k", reactive({ nested: true })]])),
193
+ set: reactive(new Set([reactive({ fromSet: true })]))
194
+ })
195
+ )
196
+ }),
197
+ plainComputed: computed(() => reactive({ date: ref(new Date("2025-01-01T00:00:00.000Z")) }))
198
+ }
199
+
200
+ const result = deepToRaw(source)
201
+
202
+ expect(result).toEqual({
203
+ innerRef: {
204
+ computedValue: {
205
+ list: [{ n: 1 }, { n: 2 }],
206
+ map: new Map([["k", { nested: true }]]),
207
+ set: new Set([{ fromSet: true }])
208
+ }
209
+ },
210
+ plainComputed: {
211
+ date: new Date("2025-01-01T00:00:00.000Z")
212
+ }
213
+ })
214
+
215
+ const innerRefValue = Reflect.get(result, "innerRef")
216
+ if (!innerRefValue || typeof innerRefValue !== "object") {
217
+ throw new Error("expected innerRef to be an object")
218
+ }
219
+
220
+ const computedValue = Reflect.get(innerRefValue, "computedValue")
221
+ if (!computedValue || typeof computedValue !== "object") {
222
+ throw new Error("expected computedValue to be an object")
223
+ }
224
+
225
+ const computedMap = Reflect.get(computedValue, "map")
226
+ const computedSet = Reflect.get(computedValue, "set")
227
+
228
+ const plainComputedValue = Reflect.get(result, "plainComputed")
229
+ if (!plainComputedValue || typeof plainComputedValue !== "object") {
230
+ throw new Error("expected plainComputed to be an object")
231
+ }
232
+
233
+ const computedDate = Reflect.get(plainComputedValue, "date")
234
+
235
+ expect(computedMap).toBeInstanceOf(Map)
236
+ expect(computedSet).toBeInstanceOf(Set)
237
+ expect(computedDate).toBeInstanceOf(Date)
238
+ expectPlainDeep(result)
239
+ })
240
+ })
@@ -1,95 +1,384 @@
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 S from "effect-app/Schema"
6
+ import * as Exit from "effect/Exit"
7
+ import type { CommandFromRequest } from "../src/makeClient.js"
8
+ import { Something, SomethingElse, SomethingElseReq, SomethingReq, useClient, useExperimental } from "./stubs.js"
9
+
10
+ const somethingInvalidationResources = {
11
+ Something: {
12
+ GetSomething2: Something.GetSomething2,
13
+ GetSomething2WithDependencies: Something.GetSomething2WithDependencies,
14
+ GetSomething3: Something.GetSomething3,
15
+ GetSomething4: Something.GetSomething4
16
+ }
17
+ }
18
+
19
+ it("TaggedRequestFor .moduleName and request .id / .moduleName", () => {
20
+ expectTypeOf(SomethingReq.moduleName).toEqualTypeOf<"Something">()
21
+ expectTypeOf(SomethingElseReq.moduleName).toEqualTypeOf<"SomethingElse">()
22
+
23
+ expectTypeOf(Something.GetSomething2.moduleName).toEqualTypeOf<"Something">()
24
+ expectTypeOf(Something.GetSomething2.id).toEqualTypeOf<"Something.GetSomething2">()
25
+ expectTypeOf(Something.GetSomething2.type).toEqualTypeOf<"query">()
26
+ expectTypeOf(Something.DoSomething.type).toEqualTypeOf<"command">()
27
+
28
+ expectTypeOf(SomethingElse.GetSomething2.moduleName).toEqualTypeOf<"SomethingElse">()
29
+ expectTypeOf(SomethingElse.GetSomething2.id).toEqualTypeOf<"SomethingElse.GetSomething2">()
30
+
31
+ const invalidates = configureInvalidation<{
32
+ Something: typeof Something
33
+ SomethingElse: typeof SomethingElse
34
+ }>()((queryKey, { Something, SomethingElse }) => [
35
+ { filters: { queryKey } },
36
+ { filters: { queryKey: makeQueryKey(Something.GetSomething2) } },
37
+ { filters: { queryKey: makeQueryKey(SomethingElse.GetSomething2) } }
38
+ ])
39
+
40
+ expectTypeOf(invalidates.invalidatesQueries).toBeFunction()
41
+ configureInvalidation<{ Something: typeof Something }>()((_queryKey, { Something }) => {
42
+ // @ts-expect-error commands are intentionally excluded from configured resources
43
+ void Something.DoSomething
44
+ return []
11
45
  })
12
46
 
13
- const [, z] = n("a")
47
+ const { clientFor } = useClient()
48
+ const client = clientFor(
49
+ Something,
50
+ undefined,
51
+ somethingInvalidationResources
52
+ )
53
+
54
+ // only queries, no commands, and no commands who require resources; shouldn't require invalidation resources args!
55
+ clientFor({ GetSomething: Something.GetSomething2 })
56
+
57
+ // @ts-expect-error invalidation resources should be required when any command configures them
58
+ clientFor(Something)
59
+
60
+ // @ts-expect-error invalidation resources for this module reject extra top-level resources
61
+ clientFor(Something, undefined, { ...somethingInvalidationResources, SomethingElse })
62
+
63
+ const doSomethingInvalidation = client.DoSomething.Request.config["invalidatesQueries"]
64
+ if (doSomethingInvalidation) {
65
+ const entries = doSomethingInvalidation(
66
+ ["$Something"],
67
+ somethingInvalidationResources,
68
+ { id: "abc" },
69
+ Exit.succeed(123)
70
+ )
71
+ expect(Array.isArray(entries)).toBe(true)
72
+ }
73
+
74
+ const SomethingCommand = SomethingReq.Command
75
+
76
+ class TypeInferenceWithSuccess extends SomethingCommand<TypeInferenceWithSuccess>()("TypeInferenceWithSuccess", {
77
+ id: S.String
78
+ }, {
79
+ success: S.FiniteFromString
80
+ }, (_queryKey, _resources, input, result) => {
81
+ expectTypeOf(input).toEqualTypeOf<{ readonly id: string }>()
82
+ expectTypeOf(result).toEqualTypeOf<Exit.Exit<number, never>>()
83
+ return []
84
+ }) {}
85
+ void TypeInferenceWithSuccess
86
+
87
+ class TypeInferenceWithoutSuccess extends SomethingCommand<TypeInferenceWithoutSuccess>()(
88
+ "TypeInferenceWithoutSuccess",
89
+ {
90
+ id: S.String
91
+ },
92
+ {},
93
+ (_queryKey, _resources, input, result) => {
94
+ expectTypeOf(input).toEqualTypeOf<{ readonly id: string }>()
95
+ expectTypeOf(result).toEqualTypeOf<Exit.Exit<void, never>>()
96
+ return []
97
+ }
98
+ ) {}
99
+ void TypeInferenceWithoutSuccess
100
+
101
+ type MixedResources = {
102
+ Something: typeof Something
103
+ Misc: {
104
+ value: number
105
+ GetSomething2: typeof Something.GetSomething2
106
+ }
107
+ }
108
+
109
+ class TypeInferenceResourceFiltering extends SomethingCommand<
110
+ TypeInferenceResourceFiltering,
111
+ MixedResources
112
+ >()("TypeInferenceResourceFiltering", {
113
+ id: S.String
114
+ }, {
115
+ success: S.FiniteFromString
116
+ }, (_queryKey, resources, _input, _result) => {
117
+ expectTypeOf(resources.Something.GetSomething2).toEqualTypeOf<typeof Something.GetSomething2>()
118
+ expectTypeOf(resources.Misc.GetSomething2).toEqualTypeOf<typeof Something.GetSomething2>()
119
+
120
+ // @ts-expect-error commands must be filtered from invalidation resources
121
+ void resources.Something.DoSomething
122
+ // @ts-expect-error non-query values must be filtered from invalidation resources
123
+ void resources.Misc.value
124
+
125
+ return []
126
+ }) {}
127
+ void TypeInferenceResourceFiltering
128
+
129
+ type WithSuccessInvalidation = NonNullable<typeof TypeInferenceWithSuccess.config.invalidatesQueries> // @ts-expect-error input should be required when command payload is non-empty
130
+ ;((_queryKey, _resources) => []) satisfies WithSuccessInvalidation
131
+ })
132
+
133
+ it("clientFor handler shape — props variants", () => {
134
+ const { clientFor } = useClient()
135
+ const client = clientFor(
136
+ Something,
137
+ undefined,
138
+ somethingInvalidationResources
139
+ )
140
+ expect(client).toBeDefined()
141
+
142
+ // no-props (no fields): handler is (i: void) => Effect — callable without arg
143
+ expectTypeOf(client.DoNoProps.handler).toBeFunction()
144
+ client.DoNoProps.handler()
145
+
146
+ // no-props: request mirrors handler — (i: void) => Effect, callable without arg
147
+ expectTypeOf(client.DoNoProps.request).toBeFunction()
148
+ client.DoNoProps.request()
149
+ // optional-only: any fields → function handler. Input matches `make`, which for
150
+ // fully-optional payload is omittable.
151
+ expectTypeOf(client.DoOptionalOnly.handler).toBeFunction()
152
+ // arg may be omitted entirely
153
+ client.DoOptionalOnly.handler()
154
+ // or supplied with all-optional payload
155
+ client.DoOptionalOnly.handler({})
156
+ client.DoOptionalOnly.handler({ name: "x" })
157
+
158
+ // required-only: function, `id` required
159
+ expectTypeOf(client.DoRequiredOnly.handler).toBeFunction()
160
+ client.DoRequiredOnly.handler({ id: "x" })
161
+ // @ts-expect-error id is required
162
+ client.DoRequiredOnly.handler({})
163
+ // @ts-expect-error arg cannot be omitted
164
+ client.DoRequiredOnly.handler()
165
+
166
+ // mixed: id required, name optional
167
+ expectTypeOf(client.DoMixed.handler).toBeFunction()
168
+ client.DoMixed.handler({ id: "x" })
169
+ client.DoMixed.handler({ id: "x", name: "y" })
170
+ // @ts-expect-error id required
171
+ client.DoMixed.handler({ name: "y" })
172
+ })
173
+
174
+ it("client[Key].Input — extracted input type per props variant", () => {
175
+ const { clientFor } = useClient()
176
+ const client = clientFor(
177
+ Something,
178
+ undefined,
179
+ somethingInvalidationResources
180
+ )
181
+
182
+ // Input mirrors HandlerInput<typeof Request> — the same type the handler accepts.
183
+ expectTypeOf(client.GetSomething2.Input).toEqualTypeOf<HandlerInput<typeof Something.GetSomething2>>()
184
+ expectTypeOf(client.GetSomething3.Input).toEqualTypeOf<HandlerInput<typeof Something.GetSomething3>>()
185
+ // GetSomething4: no fields → void
186
+ expectTypeOf(client.GetSomething4.Input).toBeVoid()
187
+ expectTypeOf(client.GetSomething4.Input).toEqualTypeOf<HandlerInput<typeof Something.GetSomething4>>()
188
+
189
+ // commands — props variants
190
+ expectTypeOf(client.DoNoProps.Input).toBeVoid()
191
+ expectTypeOf(client.DoNoProps.Input).toEqualTypeOf<HandlerInput<typeof Something.DoNoProps>>()
192
+ expectTypeOf(client.DoOptionalOnly.Input).toEqualTypeOf<HandlerInput<typeof Something.DoOptionalOnly>>()
193
+ expectTypeOf(client.DoRequiredOnly.Input).toEqualTypeOf<HandlerInput<typeof Something.DoRequiredOnly>>()
194
+ expectTypeOf(client.DoMixed.Input).toEqualTypeOf<HandlerInput<typeof Something.DoMixed>>()
195
+ expectTypeOf(client.DoSomething.Input).toEqualTypeOf<HandlerInput<typeof Something.DoSomething>>()
196
+
197
+ // Sanity: a non-trivial handler Input matches the handler's parameter type
198
+ type HandlerArg = Parameters<typeof client.DoMixed.handler>[0]
199
+ expectTypeOf<typeof client.DoMixed.Input>().toEqualTypeOf<HandlerArg>()
200
+
201
+ // Stream handlers — Input now extracts via RequestStreamHandlerWithInput fallback.
202
+ expectTypeOf(client.StreamWithoutFinal.Input).toEqualTypeOf<
203
+ { readonly id: string; readonly _tag?: "StreamWithoutFinal" }
204
+ >()
205
+ expectTypeOf(client.StreamWithFinal.Input).toEqualTypeOf<{ readonly id: string; readonly _tag?: "StreamWithFinal" }>()
206
+ })
207
+
208
+ it("CommandFromRequest input shape — props variants", () => {
209
+ type NoPropsArg = Parameters<CommandFromRequest<typeof Something.DoNoProps>["handle"]>[0]
210
+
211
+ // no-props (no fields) → void input; void parameter is implicitly optional, so handle() works
212
+ expectTypeOf<NoPropsArg>().toBeVoid()
213
+
214
+ // type-only assignability checks for the remaining variants
215
+ if (false as boolean) {
216
+ const noProps = null as unknown as CommandFromRequest<typeof Something.DoNoProps>
217
+ const optOnly = null as unknown as CommandFromRequest<typeof Something.DoOptionalOnly>
218
+ const reqOnly = null as unknown as CommandFromRequest<typeof Something.DoRequiredOnly>
219
+ const mixed = null as unknown as CommandFromRequest<typeof Something.DoMixed>
220
+
221
+ // no-props → void param, calling without args is fine
222
+ noProps.handle()
223
+
224
+ // optional-only → matches `make` (fully optional, arg omittable)
225
+ optOnly.handle()
226
+ optOnly.handle({})
227
+ optOnly.handle({ name: "x" })
228
+
229
+ // required-only → id required
230
+ reqOnly.handle({ id: "x" })
231
+ // @ts-expect-error id required
232
+ reqOnly.handle({})
233
+
234
+ // mixed → id required, name optional
235
+ mixed.handle({ id: "x" })
236
+ mixed.handle({ id: "x", name: "y" })
237
+ // @ts-expect-error id required
238
+ mixed.handle({ name: "y" })
239
+ }
240
+ })
241
+
242
+ it.skip("query type tests", () => {
243
+ const { clientFor } = useClient()
244
+ const client = clientFor(
245
+ Something,
246
+ () => ({
247
+ GetSomething2WithDependencies: (queryKey) => [
248
+ { filters: { queryKey } },
249
+ {
250
+ filters: {
251
+ queryKey: makeQueryKey(
252
+ SomethingElse
253
+ .GetSomething2
254
+ )
255
+ }
256
+ }
257
+ ]
258
+ }),
259
+ somethingInvalidationResources
260
+ )
261
+
262
+ const q = client.GetSomething2.query
263
+
264
+ const [, z] = q({ id: "a" })
14
265
  const valz = z.value
15
266
  expectTypeOf(valz).toEqualTypeOf<number | undefined>()
16
267
 
17
- const [, a] = n("a", { placeholderData: () => 123 })
268
+ const [, a] = q({ id: "a" }, { placeholderData: () => 123 })
18
269
  const val1 = a.value
19
270
  expectTypeOf(val1).toEqualTypeOf<number>()
20
271
 
21
- const [, bbbb] = n("a", { select: (data) => data.toString() })
272
+ const [, bbbb] = q({ id: "a" }, { select: (data) => data.toString() })
22
273
  const val = bbbb.value
23
274
  expectTypeOf(val).toEqualTypeOf<string | undefined>()
24
275
 
25
- const [, ccc] = n("a", { placeholderData: () => 123, select: (data) => data.toString() })
276
+ const [, ccc] = q({ id: "a" }, { placeholderData: () => 123, select: (data) => data.toString() })
26
277
  const val2 = ccc.value
27
278
  expectTypeOf(val2).toEqualTypeOf<string>()
28
279
 
29
- const [, ddd] = n("a", { initialData: 123, select: (data) => data.toString() })
280
+ const [, ddd] = q({ id: "a" }, { initialData: 123, select: (data) => data.toString() })
30
281
  const val3 = ddd.value
31
282
  expectTypeOf(val3).toEqualTypeOf<string>()
32
283
 
33
- const [, eee] = n("a", { initialData: 123, placeholderData: () => 123, select: (data) => data.toString() })
284
+ const [, eee] = q({ id: "a" }, { initialData: 123, placeholderData: () => 123, select: (data) => data.toString() })
34
285
  const val4 = eee.value
35
286
  expectTypeOf(val4).toEqualTypeOf<string>()
36
287
  })
37
288
 
38
289
  it.skip("works", () => {
39
- const { clientFor, legacy } = useClient()
40
- const client = clientFor(Something)
290
+ const { clientFor } = useClient()
291
+ const client = clientFor(Something, undefined, somethingInvalidationResources)
41
292
  const Command = useExperimental()
42
293
 
43
294
  // just for jsdoc / type testing.
44
- const a0 = client.GetSomething2(null as any)
45
- const a00 = client.GetSomething2.mutate(null as any)
295
+ const a0 = client.GetSomething2.request(null as any)
296
+ const a00 = client.DoSomething.mutate(null as any)
46
297
  const a = client.GetSomething2.suspense(null as any)
47
298
  const b = client.GetSomething2.query(null as any)
48
299
 
49
- const c0 = legacy.useSafeMutation(null as any)
50
- const c = legacy.useQuery(null as any)
51
- const d = legacy.useSuspenseQuery(null as any)
300
+ const de = client.GetSomething3.handler(null as any)
301
+ const de2 = client.GetSomething3.handler({ id: null })
302
+
303
+ const de3 = client.GetSomething4.handler()
304
+ void client.GetSomething4.handler
52
305
 
306
+ // @ts-expect-error query requests no longer expose command helpers
53
307
  const e = client.GetSomething2.wrap(null as any)
308
+ // @ts-expect-error query requests no longer expose command helpers
54
309
  const f = client.GetSomething2.fn(null as any)
55
310
 
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
311
+ // @ts-expect-error query requests no longer expose command helpers
312
+ const e0 = client.GetSomething2WithDependencies.wrap
313
+ // @ts-expect-error query request does not match Command.wrap mutation signature
314
+ const e000 = Command.wrap(client.GetSomething2WithDependencies)
315
+ const e00 = client.GetSomething2WithDependencies.request(null as any)
65
316
  // @ts-expect-error dependencies required that are not provided
66
317
  const e1 = client.GetSomething2WithDependencies.suspense(null as any)
67
318
  // @ts-expect-error dependencies required that are not provided
68
319
  const e2 = client.GetSomething2WithDependencies.query(null as any)
320
+ // @ts-expect-error query requests no longer expose command helpers
69
321
  const f0 = client.GetSomething2WithDependencies.fn(null as any)
70
322
 
71
- const g = client.GetSomething2.mutate.wrap(null as any)
72
- const h = client.GetSomething2.mutate.fn(null as any)
323
+ const g0 = client.DoSomething.wrap(null as any)
324
+ const g = client.DoSomething.mutate.wrap(null as any)
325
+ const g1 = client.DoSomething.mutate.project(S.String)
326
+ const g2 = g1(null as any)
327
+ const g3 = g1.wrap(null as any)
328
+ const g4 = client.helpers.doSomethingMutation.project(S.String)
329
+ const g5 = g4(null as any)
330
+ const g6 = g4.wrap(null as any)
331
+ const h = client.DoSomething.mutate.fn(null as any)
332
+
333
+ // projection
334
+ // GetSomething2 uses FiniteFromString, that means Codec is String -> Number
335
+ // when we project that to S.String, it should work as the encoded shapes are identical
336
+ // aka, when we project, we skip decoding with the original codec, and instead use the provided one
337
+ // we have to make sure the Encoded shape of the provided projection schema matches the Encoded Shape of the original codec.
338
+ const projected = client.GetSomething2.project(S.String)
339
+ // @ts-expect-error encoded type mismatch: original encodes to string, S.Number encodes to number
340
+ client.GetSomething2.project(S.Number)
341
+ const p0 = projected.request(null as any)
342
+
343
+ // struct example: success schema encodes to { a: string | null }
344
+ // good: projection schema also expects { a: string | null } on the encoded side
345
+ const projectedStruct = client.GetStructNullable.project(S.Struct({ a: S.NullOr(S.String) }))
346
+ // bad: { a: S.String } has encoded type { a: string } — does not accept null
347
+ // @ts-expect-error encoded type mismatch: original encodes to { a: string | null }, projection expects { a: string }
348
+ client.GetStructNullable.project(S.Struct({ a: S.String }))
349
+
350
+ const p00 = projected.query(null as any)
351
+ const p = projected.suspense(null as any)
73
352
 
74
353
  expect(true).toBe(true)
75
354
  console.log({
76
355
  a,
77
356
  a0,
78
357
  a00,
79
- c0,
80
358
  b,
81
- c,
82
- d,
83
359
  e,
360
+ de,
361
+ de2,
362
+ de3,
84
363
  e0,
85
364
  e00,
86
365
  e000,
87
- e0000,
88
366
  e1,
89
367
  e2,
90
368
  f,
91
369
  f0,
370
+ g0,
92
371
  g,
93
- h
372
+ g1,
373
+ g2,
374
+ g3,
375
+ g4,
376
+ g5,
377
+ g6,
378
+ h,
379
+ p0,
380
+ p00,
381
+ p,
382
+ projectedStruct
94
383
  })
95
384
  })