@effect-app/vue 4.0.0-beta.99 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/CHANGELOG.md +2701 -0
  2. package/dist/atomQuery.d.ts +120 -0
  3. package/dist/atomQuery.d.ts.map +1 -0
  4. package/dist/atomQuery.js +434 -0
  5. package/dist/commander.d.ts +318 -43
  6. package/dist/commander.d.ts.map +1 -1
  7. package/dist/commander.js +593 -81
  8. package/dist/confirm.d.ts +5 -3
  9. package/dist/confirm.d.ts.map +1 -1
  10. package/dist/confirm.js +12 -14
  11. package/dist/dependencyMetadata.d.ts +18 -0
  12. package/dist/dependencyMetadata.d.ts.map +1 -0
  13. package/dist/dependencyMetadata.js +67 -0
  14. package/dist/errorReporter.d.ts +6 -4
  15. package/dist/errorReporter.d.ts.map +1 -1
  16. package/dist/errorReporter.js +14 -19
  17. package/dist/form.d.ts +5 -5
  18. package/dist/form.d.ts.map +1 -1
  19. package/dist/form.js +16 -10
  20. package/dist/internal/tanstackQuery.d.ts +9 -0
  21. package/dist/internal/tanstackQuery.d.ts.map +1 -0
  22. package/dist/internal/tanstackQuery.js +204 -0
  23. package/dist/intl.d.ts +4 -4
  24. package/dist/intl.d.ts.map +1 -1
  25. package/dist/intl.js +2 -2
  26. package/dist/lib.d.ts +8 -10
  27. package/dist/lib.d.ts.map +1 -1
  28. package/dist/lib.js +37 -12
  29. package/dist/liveQueryInvalidation.d.ts +16 -0
  30. package/dist/liveQueryInvalidation.d.ts.map +1 -0
  31. package/dist/liveQueryInvalidation.js +115 -0
  32. package/dist/makeClient.d.ts +232 -109
  33. package/dist/makeClient.d.ts.map +1 -1
  34. package/dist/makeClient.js +416 -84
  35. package/dist/makeContext.d.ts.map +1 -1
  36. package/dist/makeIntl.d.ts.map +1 -1
  37. package/dist/makeUseCommand.d.ts +3 -2
  38. package/dist/makeUseCommand.d.ts.map +1 -1
  39. package/dist/makeUseCommand.js +2 -2
  40. package/dist/mutate.d.ts +110 -41
  41. package/dist/mutate.d.ts.map +1 -1
  42. package/dist/mutate.js +236 -59
  43. package/dist/query.d.ts +161 -44
  44. package/dist/query.d.ts.map +1 -1
  45. package/dist/query.js +382 -113
  46. package/dist/queryLifetime.d.ts +25 -0
  47. package/dist/queryLifetime.d.ts.map +1 -0
  48. package/dist/queryLifetime.js +54 -0
  49. package/dist/routeParams.d.ts +4 -4
  50. package/dist/routeParams.d.ts.map +1 -1
  51. package/dist/routeParams.js +4 -3
  52. package/dist/runtime.d.ts +1 -17
  53. package/dist/runtime.d.ts.map +1 -1
  54. package/dist/runtime.js +2 -38
  55. package/dist/suspense.d.ts +19 -0
  56. package/dist/suspense.d.ts.map +1 -0
  57. package/dist/suspense.js +38 -0
  58. package/dist/toast.d.ts +1 -45
  59. package/dist/toast.d.ts.map +1 -1
  60. package/dist/toast.js +2 -32
  61. package/dist/withToast.d.ts +1 -24
  62. package/dist/withToast.d.ts.map +1 -1
  63. package/dist/withToast.js +2 -45
  64. package/docs/atom-query-api-redesign.md +207 -0
  65. package/docs/mutation-command-atoms.md +40 -0
  66. package/docs/query-key-invalidation.md +107 -0
  67. package/examples/streamMutation.ts +72 -0
  68. package/package.json +35 -94
  69. package/src/atomQuery.ts +600 -0
  70. package/src/commander.ts +1064 -116
  71. package/src/confirm.ts +12 -14
  72. package/src/dependencyMetadata.ts +84 -0
  73. package/src/errorReporter.ts +65 -75
  74. package/src/form.ts +22 -15
  75. package/src/index.ts +7 -7
  76. package/src/internal/tanstackQuery.ts +308 -0
  77. package/src/intl.ts +2 -2
  78. package/src/lib.ts +51 -23
  79. package/src/liveQueryInvalidation.ts +138 -0
  80. package/src/makeClient.ts +972 -263
  81. package/src/makeIntl.ts +2 -2
  82. package/src/makeUseCommand.ts +8 -5
  83. package/src/mutate.ts +407 -154
  84. package/src/query.ts +758 -242
  85. package/src/queryLifetime.ts +74 -0
  86. package/src/routeParams.ts +7 -7
  87. package/src/runtime.ts +1 -54
  88. package/src/suspense.ts +43 -0
  89. package/src/toast.ts +1 -52
  90. package/src/withToast.ts +1 -99
  91. package/test/Mutation.test.ts +104 -17
  92. package/test/dependencyInvalidation.test.ts +444 -0
  93. package/test/dist/dependencyInvalidation.test.d.ts.map +1 -0
  94. package/test/dist/form.test.d.ts.map +1 -1
  95. package/test/dist/inactiveInvalidation.test.d.ts.map +1 -0
  96. package/test/dist/interrupt-refetch-repro.test.d.ts.map +1 -0
  97. package/test/dist/lib.test.d.ts.map +1 -0
  98. package/test/dist/liveQueryInvalidation.test.d.ts.map +1 -0
  99. package/test/dist/query-span.test.d.ts.map +1 -0
  100. package/test/dist/queryOptions.test.d.ts.map +1 -0
  101. package/test/dist/streamFinal.test.d.ts.map +1 -0
  102. package/test/dist/streamFn.test.d.ts.map +1 -0
  103. package/test/dist/stubs.d.ts +3681 -256
  104. package/test/dist/stubs.d.ts.map +1 -1
  105. package/test/dist/stubs.js +166 -25
  106. package/test/dist/suspense-regression.test.d.ts.map +1 -0
  107. package/test/dist/suspense.test.d.ts.map +1 -0
  108. package/test/form-validation-errors.test.ts +2 -1
  109. package/test/form.test.ts +2 -1
  110. package/test/inactiveInvalidation.test.ts +172 -0
  111. package/test/interrupt-refetch-repro.test.ts +625 -0
  112. package/test/lib.test.ts +240 -0
  113. package/test/liveQueryInvalidation.test.ts +108 -0
  114. package/test/makeClient.test.ts +414 -32
  115. package/test/query-span.test.ts +92 -0
  116. package/test/queryOptions.test.ts +88 -0
  117. package/test/streamFinal.test.ts +64 -0
  118. package/test/streamFn.test.ts +457 -0
  119. package/test/stubs.ts +194 -36
  120. package/test/suspense-regression.test.ts +168 -0
  121. package/test/suspense.test.ts +160 -0
  122. package/tsconfig.examples.json +20 -0
  123. package/tsconfig.json +9 -1
  124. package/tsconfig.src.json +34 -34
  125. package/tsconfig.test.json +2 -2
  126. package/vitest.config.ts +5 -5
  127. package/eslint.config.mjs +0 -24
  128. package/tsconfig.json.bak +0 -12
@@ -0,0 +1,74 @@
1
+ import type * as AsyncResult from "effect/reactivity/AsyncResult"
2
+ import * as Atom from "effect/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
+ }
@@ -1,7 +1,7 @@
1
- import { flow } from "effect"
2
- import { Option, S } from "effect-app"
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<Schema.Type<NER[K]>>
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<Schema.Type<NER[K]>>
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]: Schema.Type<NER[K]>
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]: Schema.Type<NER[K]>
52
+ [K in keyof NER]: NER[K]["Type"]
53
53
  }
54
54
  )
55
55
  }
package/src/runtime.ts CHANGED
@@ -1,54 +1 @@
1
- import { Exit, flow, ManagedRuntime } from "effect"
2
- import { Effect, Layer, Logger } from "effect-app"
3
- import { CauseException } from "effect-app/client/errors"
4
- import { type Context } from "effect-app/Context"
5
-
6
- export function makeAppRuntime<A, E>(layer: Layer.Layer<A, E>) {
7
- return Effect.gen(function*() {
8
- const l = layer.pipe(
9
- Layer.provide(Logger.layer([Logger.consolePretty()]))
10
- ) as Layer.Layer<A, never>
11
- const mrt = ManagedRuntime.make(l)
12
- yield* mrt.contextEffect
13
- return Object.assign(mrt, {
14
- [Symbol.dispose]() {
15
- return Effect.runSync(mrt.disposeEffect)
16
- },
17
-
18
- [Symbol.asyncDispose]() {
19
- return mrt.dispose()
20
- }
21
- }) // as we initialise here, there is no more error left.
22
- })
23
- }
24
-
25
- export function initializeSync<A, E>(layer: Layer.Layer<A, E, never>) {
26
- const runtime = Effect.runSync(makeAppRuntime(layer))
27
- return runtime
28
- }
29
-
30
- export function initializeAsync<A, E>(layer: Layer.Layer<A, E, never>) {
31
- return Effect
32
- .runPromise(makeAppRuntime(layer))
33
- }
34
-
35
- // we wrap into CauseException because we want to keep the full cause of the failure.
36
- export const makeRunPromise = <T>(services: Context<T>) =>
37
- flow(Effect.runPromiseExitWith(services), (_) =>
38
- _.then(
39
- Exit.match({
40
- onFailure: (cause) => Promise.reject(new CauseException(cause, "runPromise")),
41
- onSuccess: (value) => Promise.resolve(value)
42
- })
43
- ))
44
-
45
- export const makeRunSync = <T>(services: Context<T>) =>
46
- flow(
47
- Effect.runSyncExitWith(services),
48
- Exit.match({
49
- onFailure: (cause) => {
50
- throw new CauseException(cause, "runSync")
51
- },
52
- onSuccess: (value) => value
53
- })
54
- )
1
+ export * from "effect-app/runtime"
@@ -0,0 +1,43 @@
1
+ import { Effect } from "effect-app"
2
+ import * as AsyncResult from "effect/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 CHANGED
@@ -1,52 +1 @@
1
- import { Context, Effect, Option } from "effect-app"
2
- import { accessEffectFn } from "effect-app/Context"
3
-
4
- export type ToastId = string | number
5
- export type ToastOpts = { id?: ToastId; timeout?: number }
6
- export type ToastOptsInternal = { id?: ToastId | null; timeout?: number }
7
-
8
- export type UseToast = () => {
9
- error: (this: void, message: string, options?: ToastOpts) => ToastId
10
- warning: (this: void, message: string, options?: ToastOpts) => ToastId
11
- success: (this: void, message: string, options?: ToastOpts) => ToastId
12
- info: (this: void, message: string, options?: ToastOpts) => ToastId
13
- dismiss: (this: void, id: ToastId) => void
14
- }
15
-
16
- export class CurrentToastId extends Context.Opaque<CurrentToastId, { toastId: ToastId }>()("CurrentToastId") {}
17
-
18
- /** fallback to CurrentToastId when available unless id is explicitly set to a value or null */
19
- export const wrap = (toast: ReturnType<UseToast>) => {
20
- const wrap = (toastHandler: (message: string, options?: ToastOpts) => ToastId) => {
21
- return (message: string, options?: ToastOptsInternal) =>
22
- Effect.serviceOption(CurrentToastId).pipe(
23
- Effect.flatMap((currentToast) =>
24
- Effect.sync(() => {
25
- const { id: _id, ...rest } = options ?? {}
26
- const id = _id !== undefined
27
- ? _id ?? undefined
28
- : Option.getOrUndefined(Option.map(currentToast, (_) => _.toastId))
29
- // when id is undefined, we may end up with no toast at all..
30
- return toastHandler(message, id !== undefined ? { ...rest, id } : rest)
31
- })
32
- )
33
- )
34
- }
35
- return {
36
- error: wrap(toast.error),
37
- info: wrap(toast.info),
38
- success: wrap(toast.success),
39
- warning: wrap(toast.warning),
40
- dismiss: (toastId: ToastId) => Effect.sync(() => toast.dismiss(toastId))
41
- }
42
- }
43
-
44
- type ToastShape = ReturnType<typeof wrap>
45
-
46
- export class Toast extends Context.Opaque<Toast, ToastShape>()("Toast") {
47
- static readonly error = accessEffectFn(this, "error")
48
- static readonly info = accessEffectFn(this, "info")
49
- static readonly success = accessEffectFn(this, "success")
50
- static readonly warning = accessEffectFn(this, "warning")
51
- static readonly dismiss = accessEffectFn(this, "dismiss")
52
- }
1
+ export * from "effect-app/toast"
package/src/withToast.ts CHANGED
@@ -1,99 +1 @@
1
- import { Cause, Context, Effect, Layer, type Option } from "effect-app"
2
- import { wrapEffect } from "effect-app/utils"
3
- import { CurrentToastId, Toast } from "./toast.js"
4
-
5
- export interface ToastOptions<A, E, Args extends ReadonlyArray<unknown>, WaiR, SucR, ErrR> {
6
- stableToastId?: undefined | string | ((...args: Args) => string | undefined)
7
- timeout?: number
8
- onWaiting:
9
- | string
10
- | ((...args: Args) => string | null)
11
- | null
12
- | ((
13
- ...args: Args
14
- ) => Effect.Effect<string | null, never, WaiR>)
15
- onSuccess:
16
- | string
17
- | ((a: A, ...args: Args) => string | null)
18
- | null
19
- | ((
20
- a: A,
21
- ...args: Args
22
- ) => Effect.Effect<string | null, never, SucR>)
23
- onFailure:
24
- | string
25
- | ((
26
- error: Option.Option<E>,
27
- ...args: Args
28
- ) => string | { level: "warn" | "error"; message: string })
29
- | ((
30
- error: Option.Option<E>,
31
- ...args: Args
32
- ) => Effect.Effect<string | { level: "warn" | "error"; message: string }, never, ErrR>)
33
- }
34
-
35
- // @effect-diagnostics-next-line missingEffectServiceDependency:off
36
- export class WithToast extends Context.Service<WithToast>()("WithToast", {
37
- make: Effect.gen(function*() {
38
- const toast = yield* Toast
39
- return <A, E, Args extends readonly unknown[], R, WaiR = never, SucR = never, ErrR = never>(
40
- options: ToastOptions<A, E, Args, WaiR, SucR, ErrR>
41
- ) =>
42
- Effect.fnUntraced(function*(self: Effect.Effect<A, E, R>, ...args: Args) {
43
- const baseTimeout = options.timeout ?? 3_000
44
-
45
- const stableToastId = typeof options.stableToastId === "function"
46
- ? options.stableToastId(...args)
47
- : options.stableToastId
48
-
49
- const t = yield* wrapEffect(options.onWaiting)(...args)
50
- const toastId = t === null ? stableToastId : yield* toast.info(
51
- t,
52
- { id: stableToastId ?? null } // TODO: timeout forever?
53
- )
54
- return yield* self.pipe(
55
- Effect.tap(Effect.fnUntraced(function*(a) {
56
- const t = yield* wrapEffect(options.onSuccess)(a, ...args)
57
- if (t === null) {
58
- return
59
- }
60
- yield* toast.success(
61
- t,
62
- toastId !== undefined ? { id: toastId, timeout: baseTimeout } : { timeout: baseTimeout }
63
- )
64
- })),
65
- Effect.tapCause(Effect.fnUntraced(function*(cause) {
66
- yield* Effect.logDebug(
67
- "WithToast - caught error cause: " + Cause.squash(cause),
68
- Cause.hasInterruptsOnly(cause),
69
- cause
70
- )
71
-
72
- if (Cause.hasInterruptsOnly(cause)) {
73
- if (toastId) yield* toast.dismiss(toastId)
74
- return
75
- }
76
-
77
- const t = yield* wrapEffect(options.onFailure)(Cause.findErrorOption(cause), ...args)
78
- const opts = { timeout: baseTimeout * 2 }
79
-
80
- if (typeof t === "object") {
81
- return t.level === "warn"
82
- ? yield* toast.warning(t.message, toastId !== undefined ? { ...opts, id: toastId } : opts)
83
- : yield* toast.error(t.message, toastId !== undefined ? { ...opts, id: toastId } : opts)
84
- }
85
- yield* toast.error(t, toastId !== undefined ? { ...opts, id: toastId } : opts)
86
- }, Effect.uninterruptible)),
87
- toastId !== undefined ? Effect.provideService(CurrentToastId, CurrentToastId.of({ toastId })) : (_) => _
88
- )
89
- })
90
- })
91
- }) {
92
- static readonly DefaultWithoutDependencies = Layer.effect(this, this.make)
93
- static readonly Default = this.DefaultWithoutDependencies
94
-
95
- static readonly handle = <A, E, Args extends Array<unknown>, R, WaiR = never, SucR = never, ErrR = never>(
96
- options: ToastOptions<A, E, Args, WaiR, SucR, ErrR>
97
- ): (self: Effect.Effect<A, E, R>, ...args: Args) => Effect.Effect<A, E, R | WaiR | SucR | ErrR | WithToast> =>
98
- (self, ...args) => this.use((_) => _<A, E, Args, R, WaiR, SucR, ErrR>(options)(self, ...args))
99
- }
1
+ export * from "effect-app/withToast"
@@ -1,9 +1,14 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
  import { it } from "@effect/vitest"
3
- import { Cause, Effect, Exit, Fiber, Option } from "effect-app"
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"
4
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
 
@@ -317,8 +322,8 @@ it.live("with toasts", () =>
317
322
  function*() {
318
323
  expect(yield* Effect.currentSpan.pipe(Effect.map((_) => _.name))).toBe("Test Action")
319
324
 
320
- expect(toasts.length).toBe(1)
321
- expect(toasts[0].message).toBe("Test Action executing...")
325
+ // fast actions complete before the 1s waiting-toast delay, so no waiting toast is shown
326
+ expect(toasts.length).toBe(0)
322
327
 
323
328
  return "test-value"
324
329
  },
@@ -358,7 +363,7 @@ it.live("interrupted", () =>
358
363
 
359
364
  const command = Command.fn("Test Action")(
360
365
  function*() {
361
- expect(toasts.length).toBe(1)
366
+ expect(toasts.length).toBe(0)
362
367
  yield* Effect.interrupt
363
368
  return "test-value"
364
369
  },
@@ -385,7 +390,7 @@ it.live("fail", () =>
385
390
 
386
391
  const command = Command.fn("Test Action")(
387
392
  function*() {
388
- expect(toasts.length).toBe(1)
393
+ expect(toasts.length).toBe(0)
389
394
  return yield* Effect.fail({ message: "Boom!" })
390
395
  },
391
396
  Command.withDefaultToast(),
@@ -400,9 +405,30 @@ it.live("fail", () =>
400
405
  expect(command.waiting).toBe(false)
401
406
  expect(Exit.isFailure(AsyncResult.toExit(command.result))).toBe(true)
402
407
  expect(toasts.length).toBe(1) // toast should show error
403
- expect(toasts[0].message).toBe("Test Action Failed:\nBoom!")
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}/)
404
412
  }))
405
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
+ }))
406
432
  it.live("fail and recover", () =>
407
433
  Effect
408
434
  .gen(function*() {
@@ -412,7 +438,7 @@ it.live("fail and recover", () =>
412
438
 
413
439
  const command = Command.fn("Test Action")(
414
440
  function*() {
415
- expect(toasts.length).toBe(1)
441
+ expect(toasts.length).toBe(0)
416
442
  return yield* Effect.fail({ message: "Boom!" })
417
443
  },
418
444
  Effect.orElseSucceed(() => "recovered"), // we recover from the error here, so the final result is success
@@ -440,7 +466,7 @@ it.live("defect", () =>
440
466
 
441
467
  const command = Command.fn("Test Action")(
442
468
  function*() {
443
- expect(toasts.length).toBe(1)
469
+ expect(toasts.length).toBe(0)
444
470
  return yield* Effect.die({ message: "Boom!" })
445
471
  },
446
472
  Command.withDefaultToast(),
@@ -456,7 +482,8 @@ it.live("defect", () =>
456
482
  expect(command.waiting).toBe(false)
457
483
  expect(Exit.isFailure(AsyncResult.toExit(command.result))).toBe(true)
458
484
  expect(toasts.length).toBe(1) // toast should show error
459
- expect(toasts[0].message).toBe("Test Action unexpected error, please try again shortly.")
485
+ expect(toasts[0].type).toBe("error")
486
+ expect(toasts[0].message).toContain("Test Action unexpected error, please try again shortly.")
460
487
  }))
461
488
 
462
489
  it.live("works with alt", () =>
@@ -630,8 +657,8 @@ it.live("with toasts with alt", () =>
630
657
  function*() {
631
658
  expect(yield* Effect.currentSpan.pipe(Effect.map((_) => _.name))).toBe("Test Action")
632
659
 
633
- expect(toasts.length).toBe(1)
634
- expect(toasts[0].message).toBe("Test Action executing...")
660
+ // fast actions complete before the 1s waiting-toast delay, so no waiting toast is shown
661
+ expect(toasts.length).toBe(0)
635
662
 
636
663
  return "test-value"
637
664
  },
@@ -668,7 +695,7 @@ it.live("interrupted with alt", () =>
668
695
  const command = Command.alt("Test Action")(
669
696
  Effect.fnUntraced(
670
697
  function*() {
671
- expect(toasts.length).toBe(1)
698
+ expect(toasts.length).toBe(0)
672
699
  // @effect-diagnostics-next-line missingReturnYieldStar:off
673
700
  yield* Effect.interrupt
674
701
  return "test-value"
@@ -698,7 +725,7 @@ it.live("fail with alt", () =>
698
725
  const command = Command.alt("Test Action")(
699
726
  Effect.fnUntraced(
700
727
  function*() {
701
- expect(toasts.length).toBe(1)
728
+ expect(toasts.length).toBe(0)
702
729
  return yield* Effect.fail({ message: "Boom!" })
703
730
  },
704
731
  Command.withDefaultToast(),
@@ -714,7 +741,8 @@ it.live("fail with alt", () =>
714
741
  expect(command.waiting).toBe(false)
715
742
  expect(Exit.isFailure(AsyncResult.toExit(command.result))).toBe(true)
716
743
  expect(toasts.length).toBe(1) // toast should show error
717
- expect(toasts[0].message).toBe("Test Action Failed:\nBoom!")
744
+ expect(toasts[0].type).toBe("warning")
745
+ expect(toasts[0].message).toContain("Test Action Failed:\nBoom!")
718
746
  }))
719
747
 
720
748
  it.live("fail and recover with alt", () =>
@@ -727,7 +755,7 @@ it.live("fail and recover with alt", () =>
727
755
  const command = Command.alt("Test Action")(
728
756
  Effect.fnUntraced(
729
757
  function*() {
730
- expect(toasts.length).toBe(1)
758
+ expect(toasts.length).toBe(0)
731
759
  return yield* Effect.fail({ message: "Boom!" })
732
760
  },
733
761
  Effect.orElseSucceed(() => "recovered"), // we recover from the error here, so the final result is success
@@ -747,6 +775,64 @@ it.live("fail and recover with alt", () =>
747
775
  expect(toasts[0].message).toBe("Test Action Success")
748
776
  }))
749
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
+
750
836
  it.live("defect with alt", () =>
751
837
  Effect
752
838
  .gen(function*() {
@@ -774,7 +860,8 @@ it.live("defect with alt", () =>
774
860
  expect(command.waiting).toBe(false)
775
861
  expect(Exit.isFailure(AsyncResult.toExit(command.result))).toBe(true)
776
862
  expect(toasts.length).toBe(1) // toast should show error
777
- expect(toasts[0].message).toBe("Test Action unexpected error, please try again shortly.")
863
+ expect(toasts[0].type).toBe("error")
864
+ expect(toasts[0].message).toContain("Test Action unexpected error, please try again shortly.")
778
865
  }))
779
866
 
780
867
  describe("state-in-toast", () => {