@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.
- package/CHANGELOG.md +2707 -0
- package/dist/atomQuery.d.ts +120 -0
- package/dist/atomQuery.d.ts.map +1 -0
- package/dist/atomQuery.js +434 -0
- package/dist/commander.d.ts +318 -43
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +593 -81
- package/dist/confirm.d.ts +5 -3
- package/dist/confirm.d.ts.map +1 -1
- package/dist/confirm.js +12 -14
- package/dist/dependencyMetadata.d.ts +18 -0
- package/dist/dependencyMetadata.d.ts.map +1 -0
- package/dist/dependencyMetadata.js +67 -0
- package/dist/errorReporter.d.ts +6 -4
- package/dist/errorReporter.d.ts.map +1 -1
- package/dist/errorReporter.js +14 -19
- package/dist/form.d.ts +5 -5
- package/dist/form.d.ts.map +1 -1
- package/dist/form.js +16 -10
- package/dist/internal/tanstackQuery.d.ts +9 -0
- package/dist/internal/tanstackQuery.d.ts.map +1 -0
- package/dist/internal/tanstackQuery.js +204 -0
- package/dist/intl.d.ts +4 -4
- package/dist/intl.d.ts.map +1 -1
- package/dist/intl.js +2 -2
- package/dist/lib.d.ts +8 -10
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +37 -12
- package/dist/liveQueryInvalidation.d.ts +16 -0
- package/dist/liveQueryInvalidation.d.ts.map +1 -0
- package/dist/liveQueryInvalidation.js +115 -0
- package/dist/makeClient.d.ts +232 -109
- package/dist/makeClient.d.ts.map +1 -1
- package/dist/makeClient.js +416 -84
- package/dist/makeContext.d.ts.map +1 -1
- package/dist/makeIntl.d.ts.map +1 -1
- package/dist/makeUseCommand.d.ts +3 -2
- package/dist/makeUseCommand.d.ts.map +1 -1
- package/dist/makeUseCommand.js +2 -2
- package/dist/mutate.d.ts +110 -41
- package/dist/mutate.d.ts.map +1 -1
- package/dist/mutate.js +236 -59
- package/dist/query.d.ts +161 -44
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +382 -113
- package/dist/queryLifetime.d.ts +25 -0
- package/dist/queryLifetime.d.ts.map +1 -0
- package/dist/queryLifetime.js +54 -0
- package/dist/routeParams.d.ts +4 -4
- package/dist/routeParams.d.ts.map +1 -1
- package/dist/routeParams.js +4 -3
- package/dist/runtime.d.ts +1 -17
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +2 -38
- package/dist/suspense.d.ts +19 -0
- package/dist/suspense.d.ts.map +1 -0
- package/dist/suspense.js +38 -0
- package/dist/toast.d.ts +1 -45
- package/dist/toast.d.ts.map +1 -1
- package/dist/toast.js +2 -32
- package/dist/withToast.d.ts +1 -24
- package/dist/withToast.d.ts.map +1 -1
- package/dist/withToast.js +2 -45
- package/docs/atom-query-api-redesign.md +207 -0
- package/docs/mutation-command-atoms.md +40 -0
- package/docs/query-key-invalidation.md +107 -0
- package/examples/streamMutation.ts +72 -0
- package/package.json +35 -94
- package/src/atomQuery.ts +600 -0
- package/src/commander.ts +1064 -116
- package/src/confirm.ts +12 -14
- package/src/dependencyMetadata.ts +84 -0
- package/src/errorReporter.ts +65 -75
- package/src/form.ts +22 -15
- package/src/index.ts +7 -7
- package/src/internal/tanstackQuery.ts +308 -0
- package/src/intl.ts +2 -2
- package/src/lib.ts +51 -23
- package/src/liveQueryInvalidation.ts +138 -0
- package/src/makeClient.ts +972 -263
- package/src/makeIntl.ts +2 -2
- package/src/makeUseCommand.ts +8 -5
- package/src/mutate.ts +407 -154
- package/src/query.ts +758 -242
- package/src/queryLifetime.ts +74 -0
- package/src/routeParams.ts +7 -7
- package/src/runtime.ts +1 -54
- package/src/suspense.ts +43 -0
- package/src/toast.ts +1 -52
- package/src/withToast.ts +1 -99
- package/test/Mutation.test.ts +104 -17
- package/test/dependencyInvalidation.test.ts +444 -0
- package/test/dist/dependencyInvalidation.test.d.ts.map +1 -0
- package/test/dist/form.test.d.ts.map +1 -1
- package/test/dist/inactiveInvalidation.test.d.ts.map +1 -0
- package/test/dist/interrupt-refetch-repro.test.d.ts.map +1 -0
- package/test/dist/lib.test.d.ts.map +1 -0
- package/test/dist/liveQueryInvalidation.test.d.ts.map +1 -0
- package/test/dist/query-span.test.d.ts.map +1 -0
- package/test/dist/queryOptions.test.d.ts.map +1 -0
- package/test/dist/streamFinal.test.d.ts.map +1 -0
- package/test/dist/streamFn.test.d.ts.map +1 -0
- package/test/dist/stubs.d.ts +3681 -256
- package/test/dist/stubs.d.ts.map +1 -1
- package/test/dist/stubs.js +166 -25
- package/test/dist/suspense-regression.test.d.ts.map +1 -0
- package/test/dist/suspense.test.d.ts.map +1 -0
- package/test/form-validation-errors.test.ts +2 -1
- package/test/form.test.ts +2 -1
- package/test/inactiveInvalidation.test.ts +172 -0
- package/test/interrupt-refetch-repro.test.ts +625 -0
- package/test/lib.test.ts +240 -0
- package/test/liveQueryInvalidation.test.ts +108 -0
- package/test/makeClient.test.ts +414 -32
- package/test/query-span.test.ts +92 -0
- package/test/queryOptions.test.ts +88 -0
- package/test/streamFinal.test.ts +64 -0
- package/test/streamFn.test.ts +457 -0
- package/test/stubs.ts +194 -36
- package/test/suspense-regression.test.ts +168 -0
- package/test/suspense.test.ts +160 -0
- package/tsconfig.examples.json +20 -0
- package/tsconfig.json +9 -1
- package/tsconfig.src.json +34 -34
- package/tsconfig.test.json +2 -2
- package/vitest.config.ts +5 -5
- package/eslint.config.mjs +0 -24
- package/tsconfig.json.bak +0 -12
package/test/makeClient.test.ts
CHANGED
|
@@ -1,34 +1,313 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import {
|
|
3
|
-
import { makeQueryKey } from "
|
|
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(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
filters: {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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.
|
|
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
|
|
75
|
-
const e0 = client.GetSomething2WithDependencies.wrap
|
|
76
|
-
// @ts-expect-error
|
|
77
|
-
const e000 = Command.wrap(client.GetSomething2WithDependencies)
|
|
78
|
-
const e00 = client.GetSomething2WithDependencies.
|
|
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
|
|
90
|
-
const
|
|
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
|
-
|
|
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
|
+
})
|