@effect-app/vue 4.0.0-beta.33 → 4.0.0-beta.331
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2463 -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 +634 -0
- package/dist/commander.d.ts.map +1 -0
- package/dist/commander.js +1068 -0
- package/dist/{experimental/confirm.d.ts → confirm.d.ts} +6 -4
- package/dist/confirm.d.ts.map +1 -0
- package/dist/confirm.js +26 -0
- 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 +42 -16
- 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 +15 -0
- package/dist/intl.d.ts.map +1 -0
- package/dist/intl.js +9 -0
- package/dist/lib.d.ts +7 -9
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +35 -10
- 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 +236 -336
- package/dist/makeClient.d.ts.map +1 -1
- package/dist/makeClient.js +417 -408
- package/dist/makeContext.d.ts.map +1 -1
- package/dist/makeIntl.d.ts.map +1 -1
- package/dist/{experimental/makeUseCommand.d.ts → makeUseCommand.d.ts} +4 -3
- package/dist/makeUseCommand.d.ts.map +1 -0
- package/dist/makeUseCommand.js +13 -0
- package/dist/mutate.d.ts +109 -40
- package/dist/mutate.d.ts.map +1 -1
- package/dist/mutate.js +235 -58
- package/dist/query.d.ts +158 -45
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +380 -122
- 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 -14
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +2 -26
- 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 +2 -0
- package/dist/toast.d.ts.map +1 -0
- package/dist/toast.js +2 -0
- package/dist/withToast.d.ts +2 -0
- package/dist/withToast.d.ts.map +1 -0
- package/dist/withToast.js +2 -0
- 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 +36 -95
- package/src/atomQuery.ts +600 -0
- package/src/commander.ts +3408 -0
- package/src/{experimental/confirm.ts → confirm.ts} +13 -15
- package/src/dependencyMetadata.ts +84 -0
- package/src/errorReporter.ts +65 -75
- package/src/form.ts +57 -21
- package/src/index.ts +7 -7
- package/src/internal/tanstackQuery.ts +308 -0
- package/src/intl.ts +12 -0
- package/src/lib.ts +49 -21
- package/src/liveQueryInvalidation.ts +138 -0
- package/src/makeClient.ts +971 -1159
- package/src/makeIntl.ts +2 -2
- package/src/{experimental/makeUseCommand.ts → makeUseCommand.ts} +9 -6
- package/src/mutate.ts +406 -153
- package/src/query.ts +763 -260
- package/src/queryLifetime.ts +74 -0
- package/src/routeParams.ts +7 -7
- package/src/runtime.ts +1 -31
- package/src/suspense.ts +43 -0
- package/src/toast.ts +1 -0
- package/src/withToast.ts +1 -0
- package/test/Mutation.test.ts +181 -24
- 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 +3751 -120
- package/test/dist/stubs.d.ts.map +1 -1
- package/test/dist/stubs.js +190 -30
- 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 +24 -19
- package/test/form.test.ts +25 -6
- 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 +437 -38
- 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 +227 -42
- 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 -2
- package/tsconfig.src.json +34 -34
- package/tsconfig.test.json +2 -2
- package/vitest.config.ts +5 -5
- package/dist/experimental/commander.d.ts +0 -359
- package/dist/experimental/commander.d.ts.map +0 -1
- package/dist/experimental/commander.js +0 -557
- package/dist/experimental/confirm.d.ts.map +0 -1
- package/dist/experimental/confirm.js +0 -28
- package/dist/experimental/intl.d.ts +0 -16
- package/dist/experimental/intl.d.ts.map +0 -1
- package/dist/experimental/intl.js +0 -5
- package/dist/experimental/makeUseCommand.d.ts.map +0 -1
- package/dist/experimental/makeUseCommand.js +0 -13
- package/dist/experimental/toast.d.ts +0 -47
- package/dist/experimental/toast.d.ts.map +0 -1
- package/dist/experimental/toast.js +0 -41
- package/dist/experimental/withToast.d.ts +0 -25
- package/dist/experimental/withToast.d.ts.map +0 -1
- package/dist/experimental/withToast.js +0 -45
- package/eslint.config.mjs +0 -24
- package/src/experimental/commander.ts +0 -1835
- package/src/experimental/intl.ts +0 -9
- package/src/experimental/toast.ts +0 -66
- package/src/experimental/withToast.ts +0 -99
- package/tsconfig.json.bak +0 -12
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
|
|
2
|
+
import * as Atom from "effect/unstable/reactivity/Atom"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Per-query observer count + invalidation flag. Lives on the family atom so
|
|
6
|
+
* invalidate can mark stale and refetch only when someone is looking
|
|
7
|
+
* (TanStack refetchType: "active").
|
|
8
|
+
*/
|
|
9
|
+
export interface QueryLifetime {
|
|
10
|
+
observers: number
|
|
11
|
+
invalidated: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const lifetimes = new WeakMap<Atom.Atom<any>, QueryLifetime>()
|
|
15
|
+
|
|
16
|
+
const rootOf = (atom: Atom.Atom<any>): Atom.Atom<any> => {
|
|
17
|
+
let target = atom
|
|
18
|
+
while (target.initialValueTarget !== undefined) {
|
|
19
|
+
target = target.initialValueTarget
|
|
20
|
+
}
|
|
21
|
+
return target
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const queryLifetimeOf = (atom: Atom.Atom<any>): QueryLifetime | undefined =>
|
|
25
|
+
lifetimes.get(rootOf(atom)) ?? lifetimes.get(atom)
|
|
26
|
+
|
|
27
|
+
export const attachQueryLifetime = <A, E>(
|
|
28
|
+
atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>
|
|
29
|
+
): QueryLifetime => {
|
|
30
|
+
const existing = queryLifetimeOf(atom)
|
|
31
|
+
if (existing !== undefined) return existing
|
|
32
|
+
const created: QueryLifetime = { observers: 0, invalidated: false }
|
|
33
|
+
lifetimes.set(rootOf(atom), created)
|
|
34
|
+
lifetimes.set(atom, created)
|
|
35
|
+
return created
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Count this subscription as an observer of `family`. On remount, consume a
|
|
40
|
+
* pending invalidation and refresh the family atom.
|
|
41
|
+
*/
|
|
42
|
+
export const observeQueryAtom = <A, E>(
|
|
43
|
+
family: Atom.Atom<AsyncResult.AsyncResult<A, E>>
|
|
44
|
+
): Atom.Atom<AsyncResult.AsyncResult<A, E>> => {
|
|
45
|
+
const life = attachQueryLifetime(family)
|
|
46
|
+
return Atom.transform(family, (get) => {
|
|
47
|
+
life.observers++
|
|
48
|
+
get.addFinalizer(() => {
|
|
49
|
+
life.observers = Math.max(0, life.observers - 1)
|
|
50
|
+
})
|
|
51
|
+
if (life.invalidated) {
|
|
52
|
+
life.invalidated = false
|
|
53
|
+
get.refresh(family)
|
|
54
|
+
}
|
|
55
|
+
return get(family)
|
|
56
|
+
}, { initialValueTarget: family })
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Idle queries with a lifetime are marked stale. Queries that still have
|
|
61
|
+
* observers (or have no lifetime, e.g. ad-hoc registry mounts of a raw
|
|
62
|
+
* effect atom) are refetched now.
|
|
63
|
+
*/
|
|
64
|
+
export const atomsToRefetch = <A, E>(
|
|
65
|
+
atoms: ReadonlyArray<Atom.Atom<AsyncResult.AsyncResult<A, E>>>
|
|
66
|
+
): ReadonlyArray<Atom.Atom<AsyncResult.AsyncResult<A, E>>> => {
|
|
67
|
+
const active: Array<Atom.Atom<AsyncResult.AsyncResult<A, E>>> = []
|
|
68
|
+
for (const atom of atoms) {
|
|
69
|
+
const life = queryLifetimeOf(atom)
|
|
70
|
+
if (life === undefined || life.observers > 0) active.push(atom)
|
|
71
|
+
else life.invalidated = true
|
|
72
|
+
}
|
|
73
|
+
return active
|
|
74
|
+
}
|
package/src/routeParams.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import type { Schema } from "effect-app/Schema"
|
|
1
|
+
import * as Option from "effect-app/Option"
|
|
2
|
+
import * as S from "effect-app/Schema"
|
|
4
3
|
import { typedKeysOf } from "effect-app/utils"
|
|
4
|
+
import { flow } from "effect/Function"
|
|
5
5
|
import type { ParsedQuery } from "query-string"
|
|
6
6
|
|
|
7
7
|
export function getQueryParam(search: ParsedQuery, param: string) {
|
|
@@ -16,7 +16,7 @@ export const getQueryParamO = flow(getQueryParam, Option.fromNullishOr)
|
|
|
16
16
|
|
|
17
17
|
export function parseRouteParamsOption<NER extends Record<string, S.Codec<any, any>>>(query: Record<string, any>, t: NER // enforce non empty
|
|
18
18
|
): {
|
|
19
|
-
[K in keyof NER]: Option.Option<
|
|
19
|
+
[K in keyof NER]: Option.Option<NER[K]["Type"]>
|
|
20
20
|
} {
|
|
21
21
|
return typedKeysOf(t).reduce(
|
|
22
22
|
(prev, cur) => {
|
|
@@ -28,7 +28,7 @@ export function parseRouteParamsOption<NER extends Record<string, S.Codec<any, a
|
|
|
28
28
|
return prev
|
|
29
29
|
},
|
|
30
30
|
{} as {
|
|
31
|
-
[K in keyof NER]: Option.Option<
|
|
31
|
+
[K in keyof NER]: Option.Option<NER[K]["Type"]>
|
|
32
32
|
}
|
|
33
33
|
)
|
|
34
34
|
}
|
|
@@ -37,7 +37,7 @@ export function parseRouteParams<NER extends Record<string, S.Codec<any, any>>>(
|
|
|
37
37
|
query: Record<string, any>,
|
|
38
38
|
t: NER // enforce non empty
|
|
39
39
|
): {
|
|
40
|
-
[K in keyof NER]:
|
|
40
|
+
[K in keyof NER]: NER[K]["Type"]
|
|
41
41
|
} {
|
|
42
42
|
return typedKeysOf(t).reduce(
|
|
43
43
|
(prev, cur) => {
|
|
@@ -49,7 +49,7 @@ export function parseRouteParams<NER extends Record<string, S.Codec<any, any>>>(
|
|
|
49
49
|
return prev
|
|
50
50
|
},
|
|
51
51
|
{} as {
|
|
52
|
-
[K in keyof NER]:
|
|
52
|
+
[K in keyof NER]: NER[K]["Type"]
|
|
53
53
|
}
|
|
54
54
|
)
|
|
55
55
|
}
|
package/src/runtime.ts
CHANGED
|
@@ -1,31 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { Effect, Layer, Logger } from "effect-app"
|
|
3
|
-
|
|
4
|
-
export function makeAppRuntime<A, E>(layer: Layer.Layer<A, E>) {
|
|
5
|
-
return Effect.gen(function*() {
|
|
6
|
-
const l = layer.pipe(
|
|
7
|
-
Layer.provide(Logger.layer([Logger.consolePretty()]))
|
|
8
|
-
) as Layer.Layer<A, never>
|
|
9
|
-
const mrt = ManagedRuntime.make(l)
|
|
10
|
-
yield* mrt.servicesEffect
|
|
11
|
-
return Object.assign(mrt, {
|
|
12
|
-
[Symbol.dispose]() {
|
|
13
|
-
return Effect.runSync(mrt.disposeEffect)
|
|
14
|
-
},
|
|
15
|
-
|
|
16
|
-
[Symbol.asyncDispose]() {
|
|
17
|
-
return mrt.dispose()
|
|
18
|
-
}
|
|
19
|
-
}) // as we initialise here, there is no more error left.
|
|
20
|
-
})
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function initializeSync<A, E>(layer: Layer.Layer<A, E, never>) {
|
|
24
|
-
const runtime = Effect.runSync(makeAppRuntime(layer))
|
|
25
|
-
return runtime
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function initializeAsync<A, E>(layer: Layer.Layer<A, E, never>) {
|
|
29
|
-
return Effect
|
|
30
|
-
.runPromise(makeAppRuntime(layer))
|
|
31
|
-
}
|
|
1
|
+
export * from "effect-app/runtime"
|
package/src/suspense.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Effect } from "effect-app"
|
|
2
|
+
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
|
|
3
|
+
import { computed, type ComputedRef, nextTick } from "vue"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The always-defined `data` ref behind a resolved suspense query.
|
|
7
|
+
*
|
|
8
|
+
* A reactive arg can re-point the view at a DIFFERENT cache atom after the suspense promise
|
|
9
|
+
* resolved; the fresh atom starts `Initial` (no `previousSuccess` carried over), so the raw data
|
|
10
|
+
* briefly reads `undefined` while it fetches. The suspense contract ("after the await there is
|
|
11
|
+
* always a value") is kept by serving the last defined value across that transition — TanStack's
|
|
12
|
+
* keepPreviousData, which Vue needs because it cannot re-suspend after mount. Waiting/failure of
|
|
13
|
+
* the new fetch stays observable on the view's `result` ref.
|
|
14
|
+
*
|
|
15
|
+
* Throws only if there has never been a value, which is unreachable once the suspense await
|
|
16
|
+
* resolved successfully.
|
|
17
|
+
*/
|
|
18
|
+
export const latestDefined = <TData>(source: () => TData | undefined, label: string): ComputedRef<TData> => {
|
|
19
|
+
let last: TData | undefined
|
|
20
|
+
return computed<TData>(() => {
|
|
21
|
+
const latest = source()
|
|
22
|
+
if (latest !== undefined) {
|
|
23
|
+
last = latest
|
|
24
|
+
return latest
|
|
25
|
+
}
|
|
26
|
+
if (last !== undefined) return last
|
|
27
|
+
throw new Error(`Internal Error: ${label} resolved without a latest value`)
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const awaitResolvedSuspenseResult = <A, E>(
|
|
32
|
+
resultRef: ComputedRef<AsyncResult.AsyncResult<A, E>>
|
|
33
|
+
) =>
|
|
34
|
+
Effect.gen(function*() {
|
|
35
|
+
let result = resultRef.value
|
|
36
|
+
|
|
37
|
+
for (let remainingTicks = 3; remainingTicks > 0 && AsyncResult.isInitial(result); remainingTicks--) {
|
|
38
|
+
yield* Effect.promise(() => nextTick())
|
|
39
|
+
result = resultRef.value
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return result
|
|
43
|
+
})
|
package/src/toast.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "effect-app/toast"
|
package/src/withToast.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "effect-app/withToast"
|
package/test/Mutation.test.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import { it } from "@effect/vitest"
|
|
3
|
-
import
|
|
4
|
-
import
|
|
3
|
+
import * as Effect from "effect-app/Effect"
|
|
4
|
+
import * as Option from "effect-app/Option"
|
|
5
|
+
import * as Cause from "effect/Cause"
|
|
6
|
+
import * as Exit from "effect/Exit"
|
|
7
|
+
import * as Fiber from "effect/Fiber"
|
|
8
|
+
import { TestClock } from "effect/testing"
|
|
9
|
+
import { CommandContext, DefaultIntl } from "../src/commander.js"
|
|
5
10
|
import { AsyncResult } from "../src/lib.js"
|
|
6
|
-
import { useExperimental } from "./stubs.js"
|
|
11
|
+
import { useExperimental, useExperimentalE } from "./stubs.js"
|
|
7
12
|
|
|
8
13
|
const unwrap = <A, E>(r: Fiber.Fiber<Exit.Exit<A, E>, never>) => Fiber.join(r).pipe(Effect.flatten)
|
|
9
14
|
|
|
@@ -71,7 +76,10 @@ describe("alt2", () => {
|
|
|
71
76
|
expect(yield* Effect.currentSpan.pipe(Effect.map((_) => _.name))).toBe("Test Action")
|
|
72
77
|
})),
|
|
73
78
|
Effect.tap(() =>
|
|
74
|
-
Effect.currentSpan.pipe(
|
|
79
|
+
Effect.currentSpan.pipe(
|
|
80
|
+
Effect.map((_) => _.name),
|
|
81
|
+
Effect.tap((_) => Effect.sync(() => expect(_).toBe("Test Action")))
|
|
82
|
+
)
|
|
75
83
|
),
|
|
76
84
|
Effect.tap(() => Effect.sync(() => executed = true))
|
|
77
85
|
)
|
|
@@ -125,7 +133,10 @@ it.live("works", () =>
|
|
|
125
133
|
expect(yield* Effect.currentSpan.pipe(Effect.map((_) => _.name))).toBe("Test Action")
|
|
126
134
|
})),
|
|
127
135
|
Effect.tap(() =>
|
|
128
|
-
Effect.currentSpan.pipe(
|
|
136
|
+
Effect.currentSpan.pipe(
|
|
137
|
+
Effect.map((_) => _.name),
|
|
138
|
+
Effect.tap((_) => Effect.sync(() => expect(_).toBe("Test Action")))
|
|
139
|
+
)
|
|
129
140
|
),
|
|
130
141
|
Effect.tap(() => Effect.sync(() => executed = true))
|
|
131
142
|
)
|
|
@@ -175,7 +186,10 @@ it.live("works non-gen", () =>
|
|
|
175
186
|
expect(yield* Effect.currentSpan.pipe(Effect.map((_) => _.name))).toBe("Test Action")
|
|
176
187
|
})),
|
|
177
188
|
Effect.tap(() =>
|
|
178
|
-
Effect.currentSpan.pipe(
|
|
189
|
+
Effect.currentSpan.pipe(
|
|
190
|
+
Effect.map((_) => _.name),
|
|
191
|
+
Effect.tap((_) => Effect.sync(() => expect(_).toBe("Test Action")))
|
|
192
|
+
)
|
|
179
193
|
),
|
|
180
194
|
Effect.tap(() => Effect.sync(() => executed = true))
|
|
181
195
|
)
|
|
@@ -308,8 +322,8 @@ it.live("with toasts", () =>
|
|
|
308
322
|
function*() {
|
|
309
323
|
expect(yield* Effect.currentSpan.pipe(Effect.map((_) => _.name))).toBe("Test Action")
|
|
310
324
|
|
|
311
|
-
|
|
312
|
-
expect(toasts
|
|
325
|
+
// fast actions complete before the 1s waiting-toast delay, so no waiting toast is shown
|
|
326
|
+
expect(toasts.length).toBe(0)
|
|
313
327
|
|
|
314
328
|
return "test-value"
|
|
315
329
|
},
|
|
@@ -317,7 +331,10 @@ it.live("with toasts", () =>
|
|
|
317
331
|
expect(yield* Effect.currentSpan.pipe(Effect.map((_) => _.name))).toBe("Test Action")
|
|
318
332
|
})),
|
|
319
333
|
Effect.tap(() =>
|
|
320
|
-
Effect.currentSpan.pipe(
|
|
334
|
+
Effect.currentSpan.pipe(
|
|
335
|
+
Effect.map((_) => _.name),
|
|
336
|
+
Effect.tap((_) => Effect.sync(() => expect(_).toBe("Test Action")))
|
|
337
|
+
)
|
|
321
338
|
),
|
|
322
339
|
// WithToast.handle({
|
|
323
340
|
// onFailure: "failed",
|
|
@@ -346,7 +363,7 @@ it.live("interrupted", () =>
|
|
|
346
363
|
|
|
347
364
|
const command = Command.fn("Test Action")(
|
|
348
365
|
function*() {
|
|
349
|
-
expect(toasts.length).toBe(
|
|
366
|
+
expect(toasts.length).toBe(0)
|
|
350
367
|
yield* Effect.interrupt
|
|
351
368
|
return "test-value"
|
|
352
369
|
},
|
|
@@ -373,7 +390,7 @@ it.live("fail", () =>
|
|
|
373
390
|
|
|
374
391
|
const command = Command.fn("Test Action")(
|
|
375
392
|
function*() {
|
|
376
|
-
expect(toasts.length).toBe(
|
|
393
|
+
expect(toasts.length).toBe(0)
|
|
377
394
|
return yield* Effect.fail({ message: "Boom!" })
|
|
378
395
|
},
|
|
379
396
|
Command.withDefaultToast(),
|
|
@@ -388,9 +405,30 @@ it.live("fail", () =>
|
|
|
388
405
|
expect(command.waiting).toBe(false)
|
|
389
406
|
expect(Exit.isFailure(AsyncResult.toExit(command.result))).toBe(true)
|
|
390
407
|
expect(toasts.length).toBe(1) // toast should show error
|
|
391
|
-
expect(toasts[0].
|
|
408
|
+
expect(toasts[0].type).toBe("warning")
|
|
409
|
+
expect(toasts[0].message).toContain("Test Action Failed:\nBoom!")
|
|
410
|
+
expect(toasts[0].message).toMatch(/Trace: [a-f0-9]{32}/)
|
|
411
|
+
expect(toasts[0].message).toMatch(/Span: [a-f0-9]{16}/)
|
|
392
412
|
}))
|
|
393
413
|
|
|
414
|
+
it.live("fail with showSpanInfo disabled", () =>
|
|
415
|
+
Effect
|
|
416
|
+
.gen(function*() {
|
|
417
|
+
const toasts: any[] = []
|
|
418
|
+
const Command = useExperimental({ toasts, messages: DefaultIntl.en })
|
|
419
|
+
|
|
420
|
+
const command = Command.fn("Test Action")(
|
|
421
|
+
function*() {
|
|
422
|
+
return yield* Effect.fail({ message: "Boom!" })
|
|
423
|
+
},
|
|
424
|
+
Command.withDefaultToast({ showSpanInfo: false })
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
yield* Fiber.join(command.handle())
|
|
428
|
+
|
|
429
|
+
expect(toasts.length).toBe(1)
|
|
430
|
+
expect(toasts[0].message).toBe("Test Action Failed:\nBoom!")
|
|
431
|
+
}))
|
|
394
432
|
it.live("fail and recover", () =>
|
|
395
433
|
Effect
|
|
396
434
|
.gen(function*() {
|
|
@@ -400,7 +438,7 @@ it.live("fail and recover", () =>
|
|
|
400
438
|
|
|
401
439
|
const command = Command.fn("Test Action")(
|
|
402
440
|
function*() {
|
|
403
|
-
expect(toasts.length).toBe(
|
|
441
|
+
expect(toasts.length).toBe(0)
|
|
404
442
|
return yield* Effect.fail({ message: "Boom!" })
|
|
405
443
|
},
|
|
406
444
|
Effect.orElseSucceed(() => "recovered"), // we recover from the error here, so the final result is success
|
|
@@ -428,7 +466,7 @@ it.live("defect", () =>
|
|
|
428
466
|
|
|
429
467
|
const command = Command.fn("Test Action")(
|
|
430
468
|
function*() {
|
|
431
|
-
expect(toasts.length).toBe(
|
|
469
|
+
expect(toasts.length).toBe(0)
|
|
432
470
|
return yield* Effect.die({ message: "Boom!" })
|
|
433
471
|
},
|
|
434
472
|
Command.withDefaultToast(),
|
|
@@ -444,7 +482,8 @@ it.live("defect", () =>
|
|
|
444
482
|
expect(command.waiting).toBe(false)
|
|
445
483
|
expect(Exit.isFailure(AsyncResult.toExit(command.result))).toBe(true)
|
|
446
484
|
expect(toasts.length).toBe(1) // toast should show error
|
|
447
|
-
expect(toasts[0].
|
|
485
|
+
expect(toasts[0].type).toBe("error")
|
|
486
|
+
expect(toasts[0].message).toContain("Test Action unexpected error, please try again shortly.")
|
|
448
487
|
}))
|
|
449
488
|
|
|
450
489
|
it.live("works with alt", () =>
|
|
@@ -471,7 +510,10 @@ it.live("works with alt", () =>
|
|
|
471
510
|
expect(yield* Effect.currentSpan.pipe(Effect.map((_) => _.name))).toBe("Test Action")
|
|
472
511
|
})),
|
|
473
512
|
Effect.tap(() =>
|
|
474
|
-
Effect.currentSpan.pipe(
|
|
513
|
+
Effect.currentSpan.pipe(
|
|
514
|
+
Effect.map((_) => _.name),
|
|
515
|
+
Effect.tap((_) => Effect.sync(() => expect(_).toBe("Test Action")))
|
|
516
|
+
)
|
|
475
517
|
),
|
|
476
518
|
Effect.tap(() => Effect.sync(() => executed = true))
|
|
477
519
|
)
|
|
@@ -615,8 +657,8 @@ it.live("with toasts with alt", () =>
|
|
|
615
657
|
function*() {
|
|
616
658
|
expect(yield* Effect.currentSpan.pipe(Effect.map((_) => _.name))).toBe("Test Action")
|
|
617
659
|
|
|
618
|
-
|
|
619
|
-
expect(toasts
|
|
660
|
+
// fast actions complete before the 1s waiting-toast delay, so no waiting toast is shown
|
|
661
|
+
expect(toasts.length).toBe(0)
|
|
620
662
|
|
|
621
663
|
return "test-value"
|
|
622
664
|
},
|
|
@@ -624,7 +666,10 @@ it.live("with toasts with alt", () =>
|
|
|
624
666
|
expect(yield* Effect.currentSpan.pipe(Effect.map((_) => _.name))).toBe("Test Action")
|
|
625
667
|
})),
|
|
626
668
|
Effect.tap(() =>
|
|
627
|
-
Effect.currentSpan.pipe(
|
|
669
|
+
Effect.currentSpan.pipe(
|
|
670
|
+
Effect.map((_) => _.name),
|
|
671
|
+
Effect.tap((_) => Effect.sync(() => expect(_).toBe("Test Action")))
|
|
672
|
+
)
|
|
628
673
|
),
|
|
629
674
|
Command.withDefaultToast(),
|
|
630
675
|
Effect.tap(() => Effect.sync(() => executed = true))
|
|
@@ -650,7 +695,7 @@ it.live("interrupted with alt", () =>
|
|
|
650
695
|
const command = Command.alt("Test Action")(
|
|
651
696
|
Effect.fnUntraced(
|
|
652
697
|
function*() {
|
|
653
|
-
expect(toasts.length).toBe(
|
|
698
|
+
expect(toasts.length).toBe(0)
|
|
654
699
|
// @effect-diagnostics-next-line missingReturnYieldStar:off
|
|
655
700
|
yield* Effect.interrupt
|
|
656
701
|
return "test-value"
|
|
@@ -680,7 +725,7 @@ it.live("fail with alt", () =>
|
|
|
680
725
|
const command = Command.alt("Test Action")(
|
|
681
726
|
Effect.fnUntraced(
|
|
682
727
|
function*() {
|
|
683
|
-
expect(toasts.length).toBe(
|
|
728
|
+
expect(toasts.length).toBe(0)
|
|
684
729
|
return yield* Effect.fail({ message: "Boom!" })
|
|
685
730
|
},
|
|
686
731
|
Command.withDefaultToast(),
|
|
@@ -696,7 +741,8 @@ it.live("fail with alt", () =>
|
|
|
696
741
|
expect(command.waiting).toBe(false)
|
|
697
742
|
expect(Exit.isFailure(AsyncResult.toExit(command.result))).toBe(true)
|
|
698
743
|
expect(toasts.length).toBe(1) // toast should show error
|
|
699
|
-
expect(toasts[0].
|
|
744
|
+
expect(toasts[0].type).toBe("warning")
|
|
745
|
+
expect(toasts[0].message).toContain("Test Action Failed:\nBoom!")
|
|
700
746
|
}))
|
|
701
747
|
|
|
702
748
|
it.live("fail and recover with alt", () =>
|
|
@@ -709,7 +755,7 @@ it.live("fail and recover with alt", () =>
|
|
|
709
755
|
const command = Command.alt("Test Action")(
|
|
710
756
|
Effect.fnUntraced(
|
|
711
757
|
function*() {
|
|
712
|
-
expect(toasts.length).toBe(
|
|
758
|
+
expect(toasts.length).toBe(0)
|
|
713
759
|
return yield* Effect.fail({ message: "Boom!" })
|
|
714
760
|
},
|
|
715
761
|
Effect.orElseSucceed(() => "recovered"), // we recover from the error here, so the final result is success
|
|
@@ -729,6 +775,64 @@ it.live("fail and recover with alt", () =>
|
|
|
729
775
|
expect(toasts[0].message).toBe("Test Action Success")
|
|
730
776
|
}))
|
|
731
777
|
|
|
778
|
+
it.effect("slow action shows waiting toast after the 1s delay", () =>
|
|
779
|
+
Effect
|
|
780
|
+
.gen(function*() {
|
|
781
|
+
const toasts: any[] = []
|
|
782
|
+
const Command = yield* useExperimentalE({ toasts, messages: DefaultIntl.en })
|
|
783
|
+
|
|
784
|
+
const command = Command.fn("Test Action")(
|
|
785
|
+
function*() {
|
|
786
|
+
// before 1s the waiting toast must NOT have surfaced yet
|
|
787
|
+
expect(toasts.length).toBe(0)
|
|
788
|
+
yield* Effect.sleep("1100 millis")
|
|
789
|
+
// after the delay window the waiting info toast should be visible
|
|
790
|
+
expect(toasts.length).toBe(1)
|
|
791
|
+
expect(toasts[0].type).toBe("info")
|
|
792
|
+
expect(toasts[0].message).toBe("Test Action executing...")
|
|
793
|
+
return "test-value"
|
|
794
|
+
},
|
|
795
|
+
Command.withDefaultToast()
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
const fiber = command.handle()
|
|
799
|
+
yield* TestClock.adjust("2 seconds")
|
|
800
|
+
const r = yield* unwrap(fiber)
|
|
801
|
+
|
|
802
|
+
expect(r).toBe("test-value")
|
|
803
|
+
// waiting toast is replaced (same id) with the success toast
|
|
804
|
+
expect(toasts.length).toBe(1)
|
|
805
|
+
expect(toasts[0].type).toBe("success")
|
|
806
|
+
expect(toasts[0].message).toBe("Test Action Success")
|
|
807
|
+
}))
|
|
808
|
+
|
|
809
|
+
it.effect("slow failing action surfaces waiting toast then shows error", () =>
|
|
810
|
+
Effect
|
|
811
|
+
.gen(function*() {
|
|
812
|
+
const toasts: any[] = []
|
|
813
|
+
const Command = yield* useExperimentalE({ toasts, messages: DefaultIntl.en })
|
|
814
|
+
|
|
815
|
+
const command = Command.fn("Test Action")(
|
|
816
|
+
function*() {
|
|
817
|
+
expect(toasts.length).toBe(0)
|
|
818
|
+
yield* Effect.sleep("1100 millis")
|
|
819
|
+
expect(toasts.length).toBe(1)
|
|
820
|
+
expect(toasts[0].type).toBe("info")
|
|
821
|
+
return yield* Effect.fail({ message: "Boom!" })
|
|
822
|
+
},
|
|
823
|
+
Command.withDefaultToast()
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
const fiber = command.handle()
|
|
827
|
+
yield* TestClock.adjust("2 seconds")
|
|
828
|
+
const r = yield* Fiber.join(fiber)
|
|
829
|
+
|
|
830
|
+
expect(Exit.isFailure(r) && Cause.hasFails(r.cause)).toBe(true)
|
|
831
|
+
expect(toasts.length).toBe(1)
|
|
832
|
+
expect(toasts[0].type).toBe("warning")
|
|
833
|
+
expect(toasts[0].message).toContain("Test Action Failed:\nBoom!")
|
|
834
|
+
}))
|
|
835
|
+
|
|
732
836
|
it.live("defect with alt", () =>
|
|
733
837
|
Effect
|
|
734
838
|
.gen(function*() {
|
|
@@ -756,5 +860,58 @@ it.live("defect with alt", () =>
|
|
|
756
860
|
expect(command.waiting).toBe(false)
|
|
757
861
|
expect(Exit.isFailure(AsyncResult.toExit(command.result))).toBe(true)
|
|
758
862
|
expect(toasts.length).toBe(1) // toast should show error
|
|
759
|
-
expect(toasts[0].
|
|
863
|
+
expect(toasts[0].type).toBe("error")
|
|
864
|
+
expect(toasts[0].message).toContain("Test Action unexpected error, please try again shortly.")
|
|
760
865
|
}))
|
|
866
|
+
|
|
867
|
+
describe("state-in-toast", () => {
|
|
868
|
+
it("works", () => {
|
|
869
|
+
const toasts: any[] = []
|
|
870
|
+
const removeMutation = Object.assign(
|
|
871
|
+
Effect.fn(function*(_item: string) {
|
|
872
|
+
yield* Effect.sleep(1000)
|
|
873
|
+
}),
|
|
874
|
+
{ id: "remove_thing" }
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
const item = "x"
|
|
878
|
+
|
|
879
|
+
const Command = useExperimental({ toasts, messages: DefaultIntl.en })
|
|
880
|
+
|
|
881
|
+
Command.fn(removeMutation, {
|
|
882
|
+
state: () => ({ item }),
|
|
883
|
+
waitKey: (id) => `${id}.${item}`,
|
|
884
|
+
blockKey: () => `modify_thing.${item}`
|
|
885
|
+
// allowed: () => role.value === "admin"
|
|
886
|
+
})(
|
|
887
|
+
function*() {
|
|
888
|
+
// yield* Command.confirmOrInterrupt(yield* I18n.formatMessage({ id: "confirm.remove_item" }, { item }))
|
|
889
|
+
yield* removeMutation(item)
|
|
890
|
+
},
|
|
891
|
+
Command.withDefaultToast({
|
|
892
|
+
onSuccess: (a, b, c, d) => {
|
|
893
|
+
console.log("Success", { a, b, c, d })
|
|
894
|
+
expectTypeOf(d.state).toEqualTypeOf<{ readonly item: "x" }>()
|
|
895
|
+
}
|
|
896
|
+
})
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
Command.fn(removeMutation, {
|
|
900
|
+
state: () => ({ item }),
|
|
901
|
+
waitKey: (id) => `${id}.${item}`,
|
|
902
|
+
blockKey: () => `modify_thing.${item}`
|
|
903
|
+
// allowed: () => role.value === "admin"
|
|
904
|
+
})(
|
|
905
|
+
function*() {
|
|
906
|
+
// yield* Command.confirmOrInterrupt(yield* I18n.formatMessage({ id: "confirm.remove_item" }, { item }))
|
|
907
|
+
yield* removeMutation(item)
|
|
908
|
+
},
|
|
909
|
+
Command.withDefaultToast({
|
|
910
|
+
onSuccess: (a, b, c) => {
|
|
911
|
+
console.log("Success", { a, b, c })
|
|
912
|
+
expectTypeOf(c).toEqualTypeOf<undefined>()
|
|
913
|
+
}
|
|
914
|
+
})
|
|
915
|
+
)
|
|
916
|
+
})
|
|
917
|
+
})
|