@effect-app/vue 4.0.0-beta.22 → 4.0.0-beta.221

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 (106) hide show
  1. package/CHANGELOG.md +1613 -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/errorReporter.d.ts +7 -5
  9. package/dist/errorReporter.d.ts.map +1 -1
  10. package/dist/errorReporter.js +14 -19
  11. package/dist/form.d.ts +15 -6
  12. package/dist/form.d.ts.map +1 -1
  13. package/dist/form.js +46 -13
  14. package/dist/index.d.ts +1 -1
  15. package/dist/intl.d.ts +15 -0
  16. package/dist/intl.d.ts.map +1 -0
  17. package/dist/intl.js +9 -0
  18. package/dist/lib.d.ts +8 -10
  19. package/dist/lib.d.ts.map +1 -1
  20. package/dist/lib.js +35 -10
  21. package/dist/makeClient.d.ts +156 -339
  22. package/dist/makeClient.d.ts.map +1 -1
  23. package/dist/makeClient.js +225 -376
  24. package/dist/makeContext.d.ts +1 -1
  25. package/dist/makeContext.d.ts.map +1 -1
  26. package/dist/makeIntl.d.ts +1 -1
  27. package/dist/makeIntl.d.ts.map +1 -1
  28. package/dist/makeUseCommand.d.ts +9 -0
  29. package/dist/makeUseCommand.d.ts.map +1 -0
  30. package/dist/makeUseCommand.js +13 -0
  31. package/dist/mutate.d.ts +54 -34
  32. package/dist/mutate.d.ts.map +1 -1
  33. package/dist/mutate.js +139 -46
  34. package/dist/query.d.ts +20 -39
  35. package/dist/query.d.ts.map +1 -1
  36. package/dist/query.js +133 -72
  37. package/dist/routeParams.d.ts +3 -2
  38. package/dist/routeParams.d.ts.map +1 -1
  39. package/dist/routeParams.js +4 -3
  40. package/dist/runtime.d.ts +10 -6
  41. package/dist/runtime.d.ts.map +1 -1
  42. package/dist/runtime.js +32 -18
  43. package/dist/toast.d.ts +51 -0
  44. package/dist/toast.d.ts.map +1 -0
  45. package/dist/toast.js +34 -0
  46. package/dist/withToast.d.ts +30 -0
  47. package/dist/withToast.d.ts.map +1 -0
  48. package/dist/withToast.js +64 -0
  49. package/examples/streamMutation.ts +72 -0
  50. package/package.json +48 -50
  51. package/src/commander.ts +3406 -0
  52. package/src/{experimental/confirm.ts → confirm.ts} +12 -14
  53. package/src/errorReporter.ts +65 -75
  54. package/src/form.ts +61 -18
  55. package/src/intl.ts +12 -0
  56. package/src/lib.ts +48 -20
  57. package/src/makeClient.ts +574 -1134
  58. package/src/{experimental/makeUseCommand.ts → makeUseCommand.ts} +8 -5
  59. package/src/mutate.ts +268 -127
  60. package/src/query.ts +203 -183
  61. package/src/routeParams.ts +3 -2
  62. package/src/runtime.ts +46 -21
  63. package/src/{experimental/toast.ts → toast.ts} +15 -27
  64. package/src/{experimental/withToast.ts → withToast.ts} +46 -12
  65. package/test/Mutation.test.ts +181 -24
  66. package/test/dist/form.test.d.ts.map +1 -1
  67. package/test/dist/lib.test.d.ts.map +1 -0
  68. package/test/dist/streamFinal.test.d.ts.map +1 -0
  69. package/test/dist/streamFn.test.d.ts.map +1 -0
  70. package/test/dist/stubs.d.ts +3531 -122
  71. package/test/dist/stubs.d.ts.map +1 -1
  72. package/test/dist/stubs.js +187 -32
  73. package/test/form-validation-errors.test.ts +25 -20
  74. package/test/form.test.ts +22 -3
  75. package/test/lib.test.ts +240 -0
  76. package/test/makeClient.test.ts +292 -38
  77. package/test/streamFinal.test.ts +64 -0
  78. package/test/streamFn.test.ts +457 -0
  79. package/test/stubs.ts +223 -43
  80. package/tsconfig.examples.json +20 -0
  81. package/tsconfig.json +0 -1
  82. package/tsconfig.json.bak +5 -2
  83. package/tsconfig.src.json +34 -34
  84. package/tsconfig.test.json +2 -2
  85. package/vitest.config.ts +5 -5
  86. package/dist/experimental/commander.d.ts +0 -359
  87. package/dist/experimental/commander.d.ts.map +0 -1
  88. package/dist/experimental/commander.js +0 -557
  89. package/dist/experimental/confirm.d.ts +0 -19
  90. package/dist/experimental/confirm.d.ts.map +0 -1
  91. package/dist/experimental/confirm.js +0 -28
  92. package/dist/experimental/intl.d.ts +0 -16
  93. package/dist/experimental/intl.d.ts.map +0 -1
  94. package/dist/experimental/intl.js +0 -5
  95. package/dist/experimental/makeUseCommand.d.ts +0 -8
  96. package/dist/experimental/makeUseCommand.d.ts.map +0 -1
  97. package/dist/experimental/makeUseCommand.js +0 -13
  98. package/dist/experimental/toast.d.ts +0 -47
  99. package/dist/experimental/toast.d.ts.map +0 -1
  100. package/dist/experimental/toast.js +0 -41
  101. package/dist/experimental/withToast.d.ts +0 -25
  102. package/dist/experimental/withToast.d.ts.map +0 -1
  103. package/dist/experimental/withToast.js +0 -45
  104. package/eslint.config.mjs +0 -24
  105. package/src/experimental/commander.ts +0 -1835
  106. package/src/experimental/intl.ts +0 -9
@@ -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,349 @@
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 * as S from "effect-app/Schema"
5
+ import * as Exit from "effect/Exit"
6
+ import type { CommandFromRequest } from "../src/makeClient.js"
7
+ import { Something, SomethingElse, SomethingElseReq, SomethingReq, useClient, useExperimental } from "./stubs.js"
8
+
9
+ const somethingInvalidationResources = {
10
+ Something: {
11
+ GetSomething2: Something.GetSomething2,
12
+ GetSomething2WithDependencies: Something.GetSomething2WithDependencies,
13
+ GetSomething3: Something.GetSomething3,
14
+ GetSomething4: Something.GetSomething4
15
+ }
16
+ }
17
+
18
+ it("TaggedRequestFor .moduleName and request .id / .moduleName", () => {
19
+ expectTypeOf(SomethingReq.moduleName).toEqualTypeOf<"Something">()
20
+ expectTypeOf(SomethingElseReq.moduleName).toEqualTypeOf<"SomethingElse">()
21
+
22
+ expectTypeOf(Something.GetSomething2.moduleName).toEqualTypeOf<"Something">()
23
+ expectTypeOf(Something.GetSomething2.id).toEqualTypeOf<"Something.GetSomething2">()
24
+ expectTypeOf(Something.GetSomething2.type).toEqualTypeOf<"query">()
25
+ expectTypeOf(Something.DoSomething.type).toEqualTypeOf<"command">()
26
+
27
+ expectTypeOf(SomethingElse.GetSomething2.moduleName).toEqualTypeOf<"SomethingElse">()
28
+ expectTypeOf(SomethingElse.GetSomething2.id).toEqualTypeOf<"SomethingElse.GetSomething2">()
29
+
30
+ const invalidates = configureInvalidation<{
31
+ Something: typeof Something
32
+ SomethingElse: typeof SomethingElse
33
+ }>()((queryKey, { Something, SomethingElse }) => [
34
+ { filters: { queryKey } },
35
+ { filters: { queryKey: makeQueryKey(Something.GetSomething2) } },
36
+ { filters: { queryKey: makeQueryKey(SomethingElse.GetSomething2) } }
37
+ ])
38
+
39
+ expectTypeOf(invalidates.invalidatesQueries).toBeFunction()
40
+ configureInvalidation<{ Something: typeof Something }>()((_queryKey, { Something }) => {
41
+ // @ts-expect-error commands are intentionally excluded from configured resources
42
+ void Something.DoSomething
43
+ return []
11
44
  })
12
45
 
13
- const [, z] = n("a")
46
+ const { clientFor } = useClient()
47
+ const client = clientFor(
48
+ Something,
49
+ undefined,
50
+ somethingInvalidationResources
51
+ )
52
+
53
+ // only queries, no commands, and no commands who require resources; shouldn't require invalidation resources args!
54
+ clientFor({ GetSomething: Something.GetSomething2 })
55
+
56
+ // @ts-expect-error invalidation resources should be required when any command configures them
57
+ clientFor(Something)
58
+
59
+ // @ts-expect-error invalidation resources for this module reject extra top-level resources
60
+ clientFor(Something, undefined, { ...somethingInvalidationResources, SomethingElse })
61
+
62
+ const doSomethingInvalidation = client.DoSomething.Request.config["invalidatesQueries"]
63
+ if (doSomethingInvalidation) {
64
+ const entries = doSomethingInvalidation(
65
+ ["$Something"],
66
+ somethingInvalidationResources,
67
+ { id: "abc" },
68
+ Exit.succeed(123)
69
+ )
70
+ expect(Array.isArray(entries)).toBe(true)
71
+ }
72
+
73
+ const SomethingCommand = SomethingReq.Command
74
+
75
+ class TypeInferenceWithSuccess extends SomethingCommand<TypeInferenceWithSuccess>()("TypeInferenceWithSuccess", {
76
+ id: S.String
77
+ }, {
78
+ success: S.FiniteFromString
79
+ }, (_queryKey, _resources, input, result) => {
80
+ expectTypeOf(input).toEqualTypeOf<{ readonly id: string }>()
81
+ expectTypeOf(result).toEqualTypeOf<Exit.Exit<number, never>>()
82
+ return []
83
+ }) {}
84
+ void TypeInferenceWithSuccess
85
+
86
+ class TypeInferenceWithoutSuccess extends SomethingCommand<TypeInferenceWithoutSuccess>()(
87
+ "TypeInferenceWithoutSuccess",
88
+ {
89
+ id: S.String
90
+ },
91
+ {},
92
+ (_queryKey, _resources, input, result) => {
93
+ expectTypeOf(input).toEqualTypeOf<{ readonly id: string }>()
94
+ expectTypeOf(result).toEqualTypeOf<Exit.Exit<void, never>>()
95
+ return []
96
+ }
97
+ ) {}
98
+ void TypeInferenceWithoutSuccess
99
+
100
+ type MixedResources = {
101
+ Something: typeof Something
102
+ Misc: {
103
+ value: number
104
+ GetSomething2: typeof Something.GetSomething2
105
+ }
106
+ }
107
+
108
+ class TypeInferenceResourceFiltering extends SomethingCommand<
109
+ TypeInferenceResourceFiltering,
110
+ MixedResources
111
+ >()("TypeInferenceResourceFiltering", {
112
+ id: S.String
113
+ }, {
114
+ success: S.FiniteFromString
115
+ }, (_queryKey, resources, _input, _result) => {
116
+ expectTypeOf(resources.Something.GetSomething2).toEqualTypeOf<typeof Something.GetSomething2>()
117
+ expectTypeOf(resources.Misc.GetSomething2).toEqualTypeOf<typeof Something.GetSomething2>()
118
+
119
+ // @ts-expect-error commands must be filtered from invalidation resources
120
+ void resources.Something.DoSomething
121
+ // @ts-expect-error non-query values must be filtered from invalidation resources
122
+ void resources.Misc.value
123
+
124
+ return []
125
+ }) {}
126
+ void TypeInferenceResourceFiltering
127
+
128
+ type WithSuccessInvalidation = NonNullable<typeof TypeInferenceWithSuccess.config.invalidatesQueries> // @ts-expect-error input should be required when command payload is non-empty
129
+ ;((_queryKey, _resources) => []) satisfies WithSuccessInvalidation
130
+ })
131
+
132
+ it("clientFor handler shape — props variants", () => {
133
+ const { clientFor } = useClient()
134
+ const client = clientFor(
135
+ Something,
136
+ undefined,
137
+ somethingInvalidationResources
138
+ )
139
+ expect(client).toBeDefined()
140
+
141
+ // no-props (no fields): handler is (i: void) => Effect — callable without arg
142
+ expectTypeOf(client.DoNoProps.handler).toBeFunction()
143
+ client.DoNoProps.handler()
144
+
145
+ // no-props: request mirrors handler — (i: void) => Effect, callable without arg
146
+ expectTypeOf(client.DoNoProps.request).toBeFunction()
147
+ client.DoNoProps.request()
148
+ // optional-only: any fields → function handler. Input matches `make`, which for
149
+ // fully-optional payload is omittable.
150
+ expectTypeOf(client.DoOptionalOnly.handler).toBeFunction()
151
+ // arg may be omitted entirely
152
+ client.DoOptionalOnly.handler()
153
+ // or supplied with all-optional payload
154
+ client.DoOptionalOnly.handler({})
155
+ client.DoOptionalOnly.handler({ name: "x" })
156
+
157
+ // required-only: function, `id` required
158
+ expectTypeOf(client.DoRequiredOnly.handler).toBeFunction()
159
+ client.DoRequiredOnly.handler({ id: "x" })
160
+ // @ts-expect-error id is required
161
+ client.DoRequiredOnly.handler({})
162
+ // @ts-expect-error arg cannot be omitted
163
+ client.DoRequiredOnly.handler()
164
+
165
+ // mixed: id required, name optional
166
+ expectTypeOf(client.DoMixed.handler).toBeFunction()
167
+ client.DoMixed.handler({ id: "x" })
168
+ client.DoMixed.handler({ id: "x", name: "y" })
169
+ // @ts-expect-error id required
170
+ client.DoMixed.handler({ name: "y" })
171
+ })
172
+
173
+ it("CommandFromRequest input shape — props variants", () => {
174
+ type NoPropsArg = Parameters<CommandFromRequest<typeof Something.DoNoProps>["handle"]>[0]
175
+
176
+ // no-props (no fields) → void input; void parameter is implicitly optional, so handle() works
177
+ expectTypeOf<NoPropsArg>().toBeVoid()
178
+
179
+ // type-only assignability checks for the remaining variants
180
+ if (false as boolean) {
181
+ const noProps = null as unknown as CommandFromRequest<typeof Something.DoNoProps>
182
+ const optOnly = null as unknown as CommandFromRequest<typeof Something.DoOptionalOnly>
183
+ const reqOnly = null as unknown as CommandFromRequest<typeof Something.DoRequiredOnly>
184
+ const mixed = null as unknown as CommandFromRequest<typeof Something.DoMixed>
185
+
186
+ // no-props → void param, calling without args is fine
187
+ noProps.handle()
188
+
189
+ // optional-only → matches `make` (fully optional, arg omittable)
190
+ optOnly.handle()
191
+ optOnly.handle({})
192
+ optOnly.handle({ name: "x" })
193
+
194
+ // required-only → id required
195
+ reqOnly.handle({ id: "x" })
196
+ // @ts-expect-error id required
197
+ reqOnly.handle({})
198
+
199
+ // mixed → id required, name optional
200
+ mixed.handle({ id: "x" })
201
+ mixed.handle({ id: "x", name: "y" })
202
+ // @ts-expect-error id required
203
+ mixed.handle({ name: "y" })
204
+ }
205
+ })
206
+
207
+ it.skip("query type tests", () => {
208
+ const { clientFor } = useClient()
209
+ const client = clientFor(
210
+ Something,
211
+ () => ({
212
+ GetSomething2WithDependencies: (queryKey) => [
213
+ { filters: { queryKey } },
214
+ {
215
+ filters: {
216
+ queryKey: makeQueryKey(
217
+ SomethingElse
218
+ .GetSomething2
219
+ )
220
+ }
221
+ }
222
+ ]
223
+ }),
224
+ somethingInvalidationResources
225
+ )
226
+
227
+ const q = client.GetSomething2.query
228
+
229
+ const [, z] = q({ id: "a" })
14
230
  const valz = z.value
15
231
  expectTypeOf(valz).toEqualTypeOf<number | undefined>()
16
232
 
17
- const [, a] = n("a", { placeholderData: () => 123 })
233
+ const [, a] = q({ id: "a" }, { placeholderData: () => 123 })
18
234
  const val1 = a.value
19
235
  expectTypeOf(val1).toEqualTypeOf<number>()
20
236
 
21
- const [, bbbb] = n("a", { select: (data) => data.toString() })
237
+ const [, bbbb] = q({ id: "a" }, { select: (data) => data.toString() })
22
238
  const val = bbbb.value
23
239
  expectTypeOf(val).toEqualTypeOf<string | undefined>()
24
240
 
25
- const [, ccc] = n("a", { placeholderData: () => 123, select: (data) => data.toString() })
241
+ const [, ccc] = q({ id: "a" }, { placeholderData: () => 123, select: (data) => data.toString() })
26
242
  const val2 = ccc.value
27
243
  expectTypeOf(val2).toEqualTypeOf<string>()
28
244
 
29
- const [, ddd] = n("a", { initialData: 123, select: (data) => data.toString() })
245
+ const [, ddd] = q({ id: "a" }, { initialData: 123, select: (data) => data.toString() })
30
246
  const val3 = ddd.value
31
247
  expectTypeOf(val3).toEqualTypeOf<string>()
32
248
 
33
- const [, eee] = n("a", { initialData: 123, placeholderData: () => 123, select: (data) => data.toString() })
249
+ const [, eee] = q({ id: "a" }, { initialData: 123, placeholderData: () => 123, select: (data) => data.toString() })
34
250
  const val4 = eee.value
35
251
  expectTypeOf(val4).toEqualTypeOf<string>()
36
252
  })
37
253
 
38
254
  it.skip("works", () => {
39
- const { clientFor, legacy } = useClient()
40
- const client = clientFor(Something)
255
+ const { clientFor } = useClient()
256
+ const client = clientFor(Something, undefined, somethingInvalidationResources)
41
257
  const Command = useExperimental()
42
258
 
43
259
  // just for jsdoc / type testing.
44
- const a0 = client.GetSomething2(null as any)
45
- const a00 = client.GetSomething2.mutate(null as any)
260
+ const a0 = client.GetSomething2.request(null as any)
261
+ const a00 = client.DoSomething.mutate(null as any)
46
262
  const a = client.GetSomething2.suspense(null as any)
47
263
  const b = client.GetSomething2.query(null as any)
48
264
 
49
- const c0 = legacy.useSafeMutation(null as any)
50
- const c = legacy.useQuery(null as any)
51
- const d = legacy.useSuspenseQuery(null as any)
265
+ const de = client.GetSomething3.handler(null as any)
266
+ const de2 = client.GetSomething3.handler({ id: null })
267
+
268
+ const de3 = client.GetSomething4.handler()
269
+ void client.GetSomething4.handler
52
270
 
271
+ // @ts-expect-error query requests no longer expose command helpers
53
272
  const e = client.GetSomething2.wrap(null as any)
273
+ // @ts-expect-error query requests no longer expose command helpers
54
274
  const f = client.GetSomething2.fn(null as any)
55
275
 
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
276
+ // @ts-expect-error query requests no longer expose command helpers
277
+ const e0 = client.GetSomething2WithDependencies.wrap
278
+ // @ts-expect-error query request does not match Command.wrap mutation signature
279
+ const e000 = Command.wrap(client.GetSomething2WithDependencies)
280
+ const e00 = client.GetSomething2WithDependencies.request(null as any)
65
281
  // @ts-expect-error dependencies required that are not provided
66
282
  const e1 = client.GetSomething2WithDependencies.suspense(null as any)
67
283
  // @ts-expect-error dependencies required that are not provided
68
284
  const e2 = client.GetSomething2WithDependencies.query(null as any)
285
+ // @ts-expect-error query requests no longer expose command helpers
69
286
  const f0 = client.GetSomething2WithDependencies.fn(null as any)
70
287
 
71
- const g = client.GetSomething2.mutate.wrap(null as any)
72
- const h = client.GetSomething2.mutate.fn(null as any)
288
+ const g0 = client.DoSomething.wrap(null as any)
289
+ const g = client.DoSomething.mutate.wrap(null as any)
290
+ const g1 = client.DoSomething.mutate.project(S.String)
291
+ const g2 = g1(null as any)
292
+ const g3 = g1.wrap(null as any)
293
+ const g4 = client.helpers.doSomethingMutation.project(S.String)
294
+ const g5 = g4(null as any)
295
+ const g6 = g4.wrap(null as any)
296
+ const h = client.DoSomething.mutate.fn(null as any)
297
+
298
+ // projection
299
+ // GetSomething2 uses FiniteFromString, that means Codec is String -> Number
300
+ // when we project that to S.String, it should work as the encoded shapes are identical
301
+ // aka, when we project, we skip decoding with the original codec, and instead use the provided one
302
+ // we have to make sure the Encoded shape of the provided projection schema matches the Encoded Shape of the original codec.
303
+ const projected = client.GetSomething2.project(S.String)
304
+ // @ts-expect-error encoded type mismatch: original encodes to string, S.Number encodes to number
305
+ client.GetSomething2.project(S.Number)
306
+ const p0 = projected.request(null as any)
307
+
308
+ // struct example: success schema encodes to { a: string | null }
309
+ // good: projection schema also expects { a: string | null } on the encoded side
310
+ const projectedStruct = client.GetStructNullable.project(S.Struct({ a: S.NullOr(S.String) }))
311
+ // bad: { a: S.String } has encoded type { a: string } — does not accept null
312
+ // @ts-expect-error encoded type mismatch: original encodes to { a: string | null }, projection expects { a: string }
313
+ client.GetStructNullable.project(S.Struct({ a: S.String }))
314
+
315
+ const p00 = projected.query(null as any)
316
+ const p = projected.suspense(null as any)
73
317
 
74
318
  expect(true).toBe(true)
75
319
  console.log({
76
320
  a,
77
321
  a0,
78
322
  a00,
79
- c0,
80
323
  b,
81
- c,
82
- d,
83
324
  e,
325
+ de,
326
+ de2,
327
+ de3,
84
328
  e0,
85
329
  e00,
86
330
  e000,
87
- e0000,
88
331
  e1,
89
332
  e2,
90
333
  f,
91
334
  f0,
335
+ g0,
92
336
  g,
93
- h
337
+ g1,
338
+ g2,
339
+ g3,
340
+ g4,
341
+ g5,
342
+ g6,
343
+ h,
344
+ p0,
345
+ p00,
346
+ p,
347
+ projectedStruct
94
348
  })
95
349
  })
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Runtime and type tests for the `final` schema on stream requests.
3
+ *
4
+ * The `final` option on a stream request schema lets callers model which type
5
+ * the last emitted stream element is.
6
+ */
7
+ import { expect, it } from "@effect/vitest"
8
+ import * as Effect from "effect-app/Effect"
9
+ import * as S from "effect-app/Schema"
10
+ import * as Stream from "effect/Stream"
11
+ import { asStreamResult } from "../src/mutate.js"
12
+ import { ExportComplete, OperationProgress, Something } from "./stubs.js"
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // asStreamResult — low-level primitive, always returns void
16
+ // ---------------------------------------------------------------------------
17
+
18
+ it.live("asStreamResult returns void and updates ref with each element", () =>
19
+ Effect.gen(function*() {
20
+ const events: number[] = [1, 2, 3]
21
+ const [ref, execute] = asStreamResult(() => Stream.fromIterable(events))
22
+
23
+ yield* execute()
24
+
25
+ // ref should hold the last emitted value
26
+ expect(ref.value._tag).toBe("Success")
27
+ if (ref.value._tag === "Success") {
28
+ expect(ref.value.value).toBe(3)
29
+ expect(ref.value.waiting).toBe(false)
30
+ }
31
+ }))
32
+
33
+ it("stream request without final: .final is undefined", () => {
34
+ const req = Something.StreamWithoutFinal
35
+ expect((req as any).final).toBeUndefined()
36
+ })
37
+
38
+ it("stream request with final: .final holds the ExportComplete schema", () => {
39
+ const req = Something.StreamWithFinal
40
+ expect((req as any).final).toBeDefined()
41
+ // Verify the schema decodes correctly
42
+ const decoded = S.decodeUnknownSync((req as any).final)({ _tag: "ExportComplete", fileUrl: "https://x.com" })
43
+ expect(decoded).toBeInstanceOf(ExportComplete)
44
+ })
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Runtime: last stream value is accessible via the reactive ref after stream ends
48
+ // ---------------------------------------------------------------------------
49
+
50
+ it.live("last stream value is accessible via reactive ref after stream ends", () =>
51
+ Effect.gen(function*() {
52
+ const progress = new OperationProgress({ completed: 1 as S.NonNegativeInt, total: 2 as S.NonNegativeInt })
53
+ const complete = new ExportComplete({ fileUrl: "https://example.com/file.csv" as S.NonEmptyString })
54
+
55
+ const [ref, execute] = asStreamResult(() => Stream.make(progress, complete))
56
+
57
+ yield* execute()
58
+
59
+ expect(ref.value._tag).toBe("Success")
60
+ if (ref.value._tag === "Success") {
61
+ expect(ref.value.value).toBe(complete)
62
+ expect(ref.value.waiting).toBe(false)
63
+ }
64
+ }))