@effect-app/vue 4.0.0-beta.13 → 4.0.0-beta.131

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 (70) hide show
  1. package/CHANGELOG.md +829 -0
  2. package/dist/{experimental/commander.d.ts → commander.d.ts} +62 -53
  3. package/dist/commander.d.ts.map +1 -0
  4. package/dist/commander.js +582 -0
  5. package/dist/{experimental/confirm.d.ts → confirm.d.ts} +4 -4
  6. package/dist/confirm.d.ts.map +1 -0
  7. package/dist/confirm.js +24 -0
  8. package/dist/errorReporter.d.ts +3 -3
  9. package/dist/errorReporter.d.ts.map +1 -1
  10. package/dist/errorReporter.js +12 -18
  11. package/dist/form.d.ts +10 -1
  12. package/dist/form.d.ts.map +1 -1
  13. package/dist/form.js +38 -9
  14. package/dist/intl.d.ts +15 -0
  15. package/dist/intl.d.ts.map +1 -0
  16. package/dist/intl.js +9 -0
  17. package/dist/makeClient.d.ts +72 -271
  18. package/dist/makeClient.d.ts.map +1 -1
  19. package/dist/makeClient.js +44 -351
  20. package/dist/makeUseCommand.d.ts.map +1 -0
  21. package/dist/makeUseCommand.js +13 -0
  22. package/dist/query.d.ts +9 -13
  23. package/dist/query.d.ts.map +1 -1
  24. package/dist/query.js +24 -24
  25. package/dist/runtime.d.ts +4 -1
  26. package/dist/runtime.d.ts.map +1 -1
  27. package/dist/runtime.js +27 -17
  28. package/dist/{experimental/toast.d.ts → toast.d.ts} +9 -10
  29. package/dist/toast.d.ts.map +1 -0
  30. package/dist/toast.js +32 -0
  31. package/dist/{experimental/withToast.d.ts → withToast.d.ts} +4 -3
  32. package/dist/withToast.d.ts.map +1 -0
  33. package/dist/withToast.js +49 -0
  34. package/package.json +43 -43
  35. package/src/{experimental/commander.ts → commander.ts} +902 -236
  36. package/src/{experimental/confirm.ts → confirm.ts} +10 -14
  37. package/src/errorReporter.ts +60 -72
  38. package/src/form.ts +51 -12
  39. package/src/intl.ts +12 -0
  40. package/src/makeClient.ts +172 -1006
  41. package/src/{experimental/makeUseCommand.ts → makeUseCommand.ts} +3 -3
  42. package/src/query.ts +45 -46
  43. package/src/runtime.ts +39 -18
  44. package/src/{experimental/toast.ts → toast.ts} +11 -25
  45. package/src/{experimental/withToast.ts → withToast.ts} +15 -6
  46. package/test/Mutation.test.ts +101 -10
  47. package/test/dist/form.test.d.ts.map +1 -1
  48. package/test/dist/stubs.d.ts +979 -114
  49. package/test/dist/stubs.d.ts.map +1 -1
  50. package/test/dist/stubs.js +42 -15
  51. package/test/form-validation-errors.test.ts +23 -19
  52. package/test/form.test.ts +20 -2
  53. package/test/makeClient.test.ts +54 -39
  54. package/test/stubs.ts +53 -18
  55. package/tsconfig.json +0 -1
  56. package/dist/experimental/commander.d.ts.map +0 -1
  57. package/dist/experimental/commander.js +0 -557
  58. package/dist/experimental/confirm.d.ts.map +0 -1
  59. package/dist/experimental/confirm.js +0 -28
  60. package/dist/experimental/intl.d.ts +0 -16
  61. package/dist/experimental/intl.d.ts.map +0 -1
  62. package/dist/experimental/intl.js +0 -5
  63. package/dist/experimental/makeUseCommand.d.ts.map +0 -1
  64. package/dist/experimental/makeUseCommand.js +0 -13
  65. package/dist/experimental/toast.d.ts.map +0 -1
  66. package/dist/experimental/toast.js +0 -41
  67. package/dist/experimental/withToast.d.ts.map +0 -1
  68. package/dist/experimental/withToast.js +0 -45
  69. package/src/experimental/intl.ts +0 -9
  70. /package/dist/{experimental/makeUseCommand.d.ts → makeUseCommand.d.ts} +0 -0
@@ -12,15 +12,15 @@ export interface CommanderResolved<RT, RTHooks>
12
12
  export const makeUseCommand = Effect.fnUntraced(
13
13
  function*<R = never, RTHooks = never>(rtHooks: Layer.Layer<RTHooks, never, R>) {
14
14
  const cmndr = yield* Commander
15
- const runtime = yield* Effect.services<R>()
15
+ const runtime = yield* Effect.context<R>()
16
16
 
17
17
  const comm = cmndr(runtime, rtHooks)
18
18
 
19
- const command = {
19
+ const command: CommanderResolved<R, RTHooks> = {
20
20
  ...comm,
21
21
  ...CommanderStatic
22
22
  }
23
23
 
24
- return command as CommanderResolved<R, RTHooks>
24
+ return command
25
25
  }
26
26
  )
package/src/query.ts CHANGED
@@ -3,15 +3,16 @@
3
3
  /* eslint-disable @typescript-eslint/no-unsafe-return */
4
4
  /* eslint-disable @typescript-eslint/no-unsafe-assignment */
5
5
  import { type DefaultError, type Enabled, type InitialDataFunction, type NonUndefinedGuard, type PlaceholderDataFunction, type QueryKey, type QueryObserverOptions, type QueryObserverResult, type RefetchOptions, useQuery as useTanstackQuery, useQueryClient, type UseQueryDefinedReturnType, type UseQueryReturnType } from "@tanstack/vue-query"
6
- import { Array, Cause, Effect, Exit, flow, Option, S, type ServiceMap } from "effect-app"
6
+ import { Array, Cause, type Context, Effect, Option, S } from "effect-app"
7
7
  import { type Req } from "effect-app/client"
8
8
  import type { RequestHandler, RequestHandlerWithInput } from "effect-app/client/clientFor"
9
- import { ServiceUnavailableError } from "effect-app/client/errors"
9
+ import { CauseException, ServiceUnavailableError } from "effect-app/client/errors"
10
10
  import { type Span } from "effect/Tracer"
11
11
  import { isHttpClientError } from "effect/unstable/http/HttpClientError"
12
12
  import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
13
13
  import { computed, type ComputedRef, type MaybeRefOrGetter, ref, shallowRef, watch, type WatchSource } from "vue"
14
14
  import { makeQueryKey, reportRuntimeError } from "./lib.js"
15
+ import { makeRunPromise } from "./runtime.js"
15
16
 
16
17
  // we must use interface extends, or we get the dreaded typescript error of isn't portable blabla @tanstack/vue-query/build/modern/types.js
17
18
  // but because how they are dealing with some extends clause, we loose all properties except initialData
@@ -74,15 +75,7 @@ export interface CustomDefinedPlaceholderQueryOptions<
74
75
  | PlaceholderDataFunction<NonFunctionGuard<TQueryData>, TError, NonFunctionGuard<TQueryData>, TQueryKey>
75
76
  }
76
77
 
77
- export class KnownFiberFailure<E> extends Error {
78
- readonly error: unknown
79
- constructor(public effectCause: Cause.Cause<E>) {
80
- super("Query failed with cause: " + Cause.squash(effectCause))
81
- this.error = Cause.squash(effectCause)
82
- }
83
- }
84
-
85
- export const makeQuery = <R>(getRuntime: () => ServiceMap.ServiceMap<R>) => {
78
+ export const makeQuery = <R>(getRuntime: () => Context.Context<R>) => {
86
79
  const useQuery_: {
87
80
  <I, A, E, Request extends Req, Name extends string>(
88
81
  q:
@@ -95,8 +88,8 @@ export const makeQuery = <R>(getRuntime: () => ServiceMap.ServiceMap<R>) => {
95
88
  ): readonly [
96
89
  ComputedRef<AsyncResult.AsyncResult<TData, E>>,
97
90
  ComputedRef<TData | undefined>,
98
- (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, KnownFiberFailure<E>>, never, never>,
99
- UseQueryDefinedReturnType<TData, KnownFiberFailure<E>>
91
+ (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, CauseException<E>>, never, never>,
92
+ UseQueryDefinedReturnType<TData, CauseException<E>>
100
93
  ]
101
94
 
102
95
  <TData = A>(
@@ -105,8 +98,8 @@ export const makeQuery = <R>(getRuntime: () => ServiceMap.ServiceMap<R>) => {
105
98
  ): readonly [
106
99
  ComputedRef<AsyncResult.AsyncResult<TData, E>>,
107
100
  ComputedRef<TData>,
108
- (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, KnownFiberFailure<E>>, never, never>,
109
- UseQueryDefinedReturnType<TData, KnownFiberFailure<E>>
101
+ (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, CauseException<E>>, never, never>,
102
+ UseQueryDefinedReturnType<TData, CauseException<E>>
110
103
  ]
111
104
 
112
105
  <TData = A>(
@@ -115,8 +108,8 @@ export const makeQuery = <R>(getRuntime: () => ServiceMap.ServiceMap<R>) => {
115
108
  ): readonly [
116
109
  ComputedRef<AsyncResult.AsyncResult<TData, E>>,
117
110
  ComputedRef<TData>,
118
- (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, KnownFiberFailure<E>>, never, never>,
119
- UseQueryDefinedReturnType<TData, KnownFiberFailure<E>>
111
+ (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, CauseException<E>>, never, never>,
112
+ UseQueryDefinedReturnType<TData, CauseException<E>>
120
113
  ]
121
114
  }
122
115
  } = <I, A, E, Request extends Req, Name extends string>(
@@ -130,34 +123,39 @@ export const makeQuery = <R>(getRuntime: () => ServiceMap.ServiceMap<R>) => {
130
123
  options?: any
131
124
  // TODO
132
125
  ) => {
133
- // we wrap into KnownFiberFailure because we want to keep the full cause of the failure.
134
- const runPromise = flow(Effect.runPromiseExitWith(getRuntime()), (_) =>
135
- _.then(
136
- Exit.match({
137
- onFailure: (cause) => Promise.reject(new KnownFiberFailure(cause)),
138
- onSuccess: (value) => Promise.resolve(value)
139
- })
140
- ))
126
+ // we wrap into CauseException because we want to keep the full cause of the failure.
127
+ const runPromise = makeRunPromise(getRuntime())
141
128
  const arr = arg
142
129
  const req: { value: I } = !arg
143
- ? undefined
130
+ ? undefined as any
144
131
  : typeof arr === "function"
145
132
  ? ({
146
133
  get value() {
147
134
  return (arr as any)()
148
135
  }
149
- } as any)
136
+ })
150
137
  : ref(arg)
151
138
  const queryKey = makeQueryKey(q)
152
139
  const handler = q.handler
153
140
 
154
- const r = useTanstackQuery<A, KnownFiberFailure<E>, TData>(
141
+ const defaultOptions = {
142
+ // we do not want to throw errors, because we turn the success and error responses into a Result type
143
+ // why don't we turn the error/success response into a Result type before returning to tanstack query? because we want to leverage tanstack query's retry and caching mechanism, which relies on throwing errors to trigger retries, and we don't want to interfere with that by catching the errors too early.
144
+ // but if we allow tanstack query to throw, it will trigger the error boundary in Vue - via a "watcher callback" error - which we currently report and log, which is not what we want.
145
+ // TODO: we might want to rethink the strategy of how to handle errors that happen after the initial load.
146
+ // For suspense, the initial load is captured by the suspense boundary.
147
+ // For subsequent loads (or non suspense use) we currently are required to use the QueryResult component to conditionally render error/loading/etc.
148
+ throwOnError: false
149
+ }
150
+
151
+ const r = useTanstackQuery<A, CauseException<E>, TData>(
155
152
  Effect.isEffect(handler)
156
153
  ? {
154
+ ...defaultOptions,
157
155
  ...options,
158
156
  retry: (retryCount, error) => {
159
- if (error instanceof KnownFiberFailure) {
160
- if (!isHttpClientError(error.error) && !S.is(ServiceUnavailableError)(error.error)) {
157
+ if (error instanceof CauseException) {
158
+ if (!isHttpClientError(error.cause) && !S.is(ServiceUnavailableError)(error.cause)) {
161
159
  return false
162
160
  }
163
161
  }
@@ -177,10 +175,11 @@ export const makeQuery = <R>(getRuntime: () => ServiceMap.ServiceMap<R>) => {
177
175
  )
178
176
  }
179
177
  : {
178
+ ...defaultOptions,
180
179
  ...options,
181
180
  retry: (retryCount, error) => {
182
- if (error instanceof KnownFiberFailure) {
183
- if (!isHttpClientError(error.error) && !S.is(ServiceUnavailableError)(error.error)) {
181
+ if (error instanceof CauseException) {
182
+ if (!isHttpClientError(error.cause) && !S.is(ServiceUnavailableError)(error.cause)) {
184
183
  return false
185
184
  }
186
185
  }
@@ -228,13 +227,13 @@ export const makeQuery = <R>(getRuntime: () => ServiceMap.ServiceMap<R>) => {
228
227
  }
229
228
 
230
229
  function swrToQuery<E, A>(r: {
231
- error: KnownFiberFailure<E> | undefined
230
+ error: CauseException<E> | undefined
232
231
  data: A | undefined
233
232
  isValidating: boolean
234
233
  }): AsyncResult.AsyncResult<A, E> {
235
234
  if (r.error !== undefined) {
236
235
  return AsyncResult.failureWithPrevious(
237
- r.error.effectCause,
236
+ r.error.originalCause,
238
237
  {
239
238
  previous: r.data === undefined ? Option.none() : Option.some(AsyncResult.success(r.data)),
240
239
  waiting: r.isValidating
@@ -261,11 +260,11 @@ export const makeQuery = <R>(getRuntime: () => ServiceMap.ServiceMap<R>) => {
261
260
  * Effect results are passed to the caller, including errors.
262
261
  */
263
262
  <TData = A>(
264
- options: CustomDefinedInitialQueryOptions<A, KnownFiberFailure<E>, TData>
263
+ options: CustomDefinedInitialQueryOptions<A, CauseException<E>, TData>
265
264
  ): readonly [
266
265
  ComputedRef<AsyncResult.AsyncResult<TData, E>>,
267
266
  ComputedRef<TData>,
268
- (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, KnownFiberFailure<E>>>,
267
+ (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, CauseException<E>>>,
269
268
  UseQueryReturnType<any, any>
270
269
  ]
271
270
  <TData = A>(
@@ -273,17 +272,17 @@ export const makeQuery = <R>(getRuntime: () => ServiceMap.ServiceMap<R>) => {
273
272
  ): readonly [
274
273
  ComputedRef<AsyncResult.AsyncResult<TData, E>>,
275
274
  ComputedRef<TData>,
276
- (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, KnownFiberFailure<E>>, never, never>,
277
- UseQueryDefinedReturnType<TData, KnownFiberFailure<E>>
275
+ (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, CauseException<E>>, never, never>,
276
+ UseQueryDefinedReturnType<TData, CauseException<E>>
278
277
  ]
279
278
  // optional options, optional A
280
279
  /**
281
280
  * Effect results are passed to the caller, including errors.
282
281
  */
283
- <TData = A>(options?: CustomUndefinedInitialQueryOptions<A, KnownFiberFailure<E>, TData>): readonly [
282
+ <TData = A>(options?: CustomUndefinedInitialQueryOptions<A, CauseException<E>, TData>): readonly [
284
283
  ComputedRef<AsyncResult.AsyncResult<A, E>>,
285
284
  ComputedRef<A | undefined>,
286
- (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, KnownFiberFailure<E>>>,
285
+ (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, CauseException<E>>>,
287
286
  UseQueryReturnType<any, any>
288
287
  ]
289
288
  }
@@ -300,11 +299,11 @@ export const makeQuery = <R>(getRuntime: () => ServiceMap.ServiceMap<R>) => {
300
299
  */
301
300
  <TData = A>(
302
301
  arg: Arg | WatchSource<Arg>,
303
- options: CustomDefinedInitialQueryOptions<A, KnownFiberFailure<E>, TData>
302
+ options: CustomDefinedInitialQueryOptions<A, CauseException<E>, TData>
304
303
  ): readonly [
305
304
  ComputedRef<AsyncResult.AsyncResult<TData, E>>,
306
305
  ComputedRef<TData>,
307
- (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, KnownFiberFailure<E>>>,
306
+ (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, CauseException<E>>>,
308
307
  UseQueryReturnType<any, any>
309
308
  ]
310
309
  // required options, with placeholderData
@@ -313,11 +312,11 @@ export const makeQuery = <R>(getRuntime: () => ServiceMap.ServiceMap<R>) => {
313
312
  */
314
313
  <TData = A>(
315
314
  arg: Arg | WatchSource<Arg>,
316
- options: CustomDefinedPlaceholderQueryOptions<A, KnownFiberFailure<E>, TData>
315
+ options: CustomDefinedPlaceholderQueryOptions<A, CauseException<E>, TData>
317
316
  ): readonly [
318
317
  ComputedRef<AsyncResult.AsyncResult<TData, E>>,
319
318
  ComputedRef<TData>,
320
- (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, KnownFiberFailure<E>>>,
319
+ (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, CauseException<E>>>,
321
320
  UseQueryReturnType<any, any>
322
321
  ]
323
322
  // optional options, optional A
@@ -326,11 +325,11 @@ export const makeQuery = <R>(getRuntime: () => ServiceMap.ServiceMap<R>) => {
326
325
  */
327
326
  <TData = A>(
328
327
  arg: Arg | WatchSource<Arg>,
329
- options?: CustomUndefinedInitialQueryOptions<A, KnownFiberFailure<E>, TData>
328
+ options?: CustomUndefinedInitialQueryOptions<A, CauseException<E>, TData>
330
329
  ): readonly [
331
330
  ComputedRef<AsyncResult.AsyncResult<TData, E>>,
332
331
  ComputedRef<TData | undefined>,
333
- (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, KnownFiberFailure<E>>>,
332
+ (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, CauseException<E>>>,
334
333
  UseQueryReturnType<any, any>
335
334
  ]
336
335
  }
package/src/runtime.ts CHANGED
@@ -1,24 +1,24 @@
1
- import { ManagedRuntime } from "effect"
1
+ import { Exit, flow, ManagedRuntime } from "effect"
2
2
  import { Effect, Layer, Logger } from "effect-app"
3
+ import { CauseException } from "effect-app/client/errors"
4
+ import { type Context } from "effect-app/Context"
3
5
 
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
- },
6
+ export const makeAppRuntime = Effect.fnUntraced(function*<A, E>(layer: Layer.Layer<A, E>) {
7
+ const l = layer.pipe(
8
+ Layer.provide(Logger.layer([Logger.consolePretty()]))
9
+ ) as Layer.Layer<A, never>
10
+ const mrt = ManagedRuntime.make(l)
11
+ yield* mrt.contextEffect
12
+ return Object.assign(mrt, {
13
+ [Symbol.dispose]() {
14
+ return Effect.runSync(mrt.disposeEffect)
15
+ },
15
16
 
16
- [Symbol.asyncDispose]() {
17
- return mrt.dispose()
18
- }
19
- }) // as we initialise here, there is no more error left.
20
- })
21
- }
17
+ [Symbol.asyncDispose]() {
18
+ return mrt.dispose()
19
+ }
20
+ }) // as we initialise here, there is no more error left.
21
+ })
22
22
 
23
23
  export function initializeSync<A, E>(layer: Layer.Layer<A, E, never>) {
24
24
  const runtime = Effect.runSync(makeAppRuntime(layer))
@@ -29,3 +29,24 @@ export function initializeAsync<A, E>(layer: Layer.Layer<A, E, never>) {
29
29
  return Effect
30
30
  .runPromise(makeAppRuntime(layer))
31
31
  }
32
+
33
+ // we wrap into CauseException because we want to keep the full cause of the failure.
34
+ export const makeRunPromise = <T>(services: Context<T>) =>
35
+ flow(Effect.runPromiseExitWith(services), (_) =>
36
+ _.then(
37
+ Exit.match({
38
+ onFailure: (cause) => Promise.reject(new CauseException(cause, "runPromise")),
39
+ onSuccess: (value) => Promise.resolve(value)
40
+ })
41
+ ))
42
+
43
+ export const makeRunSync = <T>(services: Context<T>) =>
44
+ flow(
45
+ Effect.runSyncExitWith(services),
46
+ Exit.match({
47
+ onFailure: (cause) => {
48
+ throw new CauseException(cause, "runSync")
49
+ },
50
+ onSuccess: (value) => value
51
+ })
52
+ )
@@ -1,5 +1,5 @@
1
- import { Effect, Option, ServiceMap } from "effect-app"
2
- import { proxify } from "effect-app/ServiceMap"
1
+ import { Context, Effect, Option } from "effect-app"
2
+ import { accessEffectFn } from "effect-app/Context"
3
3
 
4
4
  export type ToastId = string | number
5
5
  export type ToastOpts = { id?: ToastId; timeout?: number }
@@ -13,7 +13,7 @@ export type UseToast = () => {
13
13
  dismiss: (this: void, id: ToastId) => void
14
14
  }
15
15
 
16
- export class CurrentToastId extends ServiceMap.Opaque<CurrentToastId, { toastId: ToastId }>()("CurrentToastId") {}
16
+ export class CurrentToastId extends Context.Opaque<CurrentToastId, { toastId: ToastId }>()("CurrentToastId") {}
17
17
 
18
18
  /** fallback to CurrentToastId when available unless id is explicitly set to a value or null */
19
19
  export const wrap = (toast: ReturnType<UseToast>) => {
@@ -41,26 +41,12 @@ export const wrap = (toast: ReturnType<UseToast>) => {
41
41
  }
42
42
  }
43
43
 
44
- export class Toast
45
- extends proxify(ServiceMap.Opaque<Toast, ReturnType<typeof wrap>>()("Toast"))<Toast, ReturnType<typeof wrap>>()
46
- {
47
- }
48
-
49
- // const a = Layer.effect(Toast, Effect.sync(() => Toast.of(null as any)))
50
-
51
- // const A = Toast.of({
52
- // error: () => Effect.succeed(null as any),
53
- // info: () => Effect.succeed(null as any),
54
- // success: () => Effect.succeed(null as any),
55
- // warning: () => Effect.succeed(null as any),
56
- // dismiss: () => Effect.succeed(null as any)
57
- // })
44
+ type ToastShape = ReturnType<typeof wrap>
58
45
 
59
- // const b = Toast.info("test")
60
-
61
- // const a2 = Toast.use((_) => _.error("test"))
62
-
63
- // const b2 = Effect.gen(function*() {
64
- // const toast = yield* Toast
65
- // toast.error("test")
66
- // })
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,10 +1,11 @@
1
- import { Cause, Effect, Layer, type Option, ServiceMap } from "effect-app"
1
+ import { Cause, Context, Effect, Layer, type Option } from "effect-app"
2
2
  import { wrapEffect } from "effect-app/utils"
3
3
  import { CurrentToastId, Toast } from "./toast.js"
4
4
 
5
5
  export interface ToastOptions<A, E, Args extends ReadonlyArray<unknown>, WaiR, SucR, ErrR> {
6
6
  stableToastId?: undefined | string | ((...args: Args) => string | undefined)
7
7
  timeout?: number
8
+ showSpanInfo?: false
8
9
  onWaiting:
9
10
  | string
10
11
  | ((...args: Args) => string | null)
@@ -33,10 +34,10 @@ export interface ToastOptions<A, E, Args extends ReadonlyArray<unknown>, WaiR, S
33
34
  }
34
35
 
35
36
  // @effect-diagnostics-next-line missingEffectServiceDependency:off
36
- export class WithToast extends ServiceMap.Service<WithToast>()("WithToast", {
37
+ export class WithToast extends Context.Service<WithToast>()("WithToast", {
37
38
  make: Effect.gen(function*() {
38
39
  const toast = yield* Toast
39
- return <A, E, Args extends Array<unknown>, R, WaiR = never, SucR = never, ErrR = never>(
40
+ return <A, E, Args extends readonly unknown[], R, WaiR = never, SucR = never, ErrR = never>(
40
41
  options: ToastOptions<A, E, Args, WaiR, SucR, ErrR>
41
42
  ) =>
42
43
  Effect.fnUntraced(function*(self: Effect.Effect<A, E, R>, ...args: Args) {
@@ -74,15 +75,23 @@ export class WithToast extends ServiceMap.Service<WithToast>()("WithToast", {
74
75
  return
75
76
  }
76
77
 
78
+ const spanInfo = options.showSpanInfo !== false
79
+ ? yield* Effect.currentSpan.pipe(
80
+ Effect.map((span) => `\nTrace: ${span.traceId}\nSpan: ${span.spanId}`),
81
+ Effect.orElseSucceed(() => "")
82
+ )
83
+ : ""
84
+
77
85
  const t = yield* wrapEffect(options.onFailure)(Cause.findErrorOption(cause), ...args)
78
86
  const opts = { timeout: baseTimeout * 2 }
79
87
 
80
88
  if (typeof t === "object") {
89
+ const message = t.message + spanInfo
81
90
  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)
91
+ ? yield* toast.warning(message, toastId !== undefined ? { ...opts, id: toastId } : opts)
92
+ : yield* toast.error(message, toastId !== undefined ? { ...opts, id: toastId } : opts)
84
93
  }
85
- yield* toast.error(t, toastId !== undefined ? { ...opts, id: toastId } : opts)
94
+ yield* toast.error(t + spanInfo, toastId !== undefined ? { ...opts, id: toastId } : opts)
86
95
  }, Effect.uninterruptible)),
87
96
  toastId !== undefined ? Effect.provideService(CurrentToastId, CurrentToastId.of({ toastId })) : (_) => _
88
97
  )
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
  import { it } from "@effect/vitest"
3
3
  import { Cause, Effect, Exit, Fiber, Option } from "effect-app"
4
- import { CommandContext, DefaultIntl } from "../src/experimental/commander.js"
4
+ import { CommandContext, DefaultIntl } from "../src/commander.js"
5
5
  import { AsyncResult } from "../src/lib.js"
6
6
  import { useExperimental } from "./stubs.js"
7
7
 
@@ -71,7 +71,10 @@ describe("alt2", () => {
71
71
  expect(yield* Effect.currentSpan.pipe(Effect.map((_) => _.name))).toBe("Test Action")
72
72
  })),
73
73
  Effect.tap(() =>
74
- Effect.currentSpan.pipe(Effect.map((_) => _.name), Effect.tap((_) => Effect.sync(() => expect(_).toBe("Test Action"))))
74
+ Effect.currentSpan.pipe(
75
+ Effect.map((_) => _.name),
76
+ Effect.tap((_) => Effect.sync(() => expect(_).toBe("Test Action")))
77
+ )
75
78
  ),
76
79
  Effect.tap(() => Effect.sync(() => executed = true))
77
80
  )
@@ -125,7 +128,10 @@ it.live("works", () =>
125
128
  expect(yield* Effect.currentSpan.pipe(Effect.map((_) => _.name))).toBe("Test Action")
126
129
  })),
127
130
  Effect.tap(() =>
128
- Effect.currentSpan.pipe(Effect.map((_) => _.name), Effect.tap((_) => Effect.sync(() => expect(_).toBe("Test Action"))))
131
+ Effect.currentSpan.pipe(
132
+ Effect.map((_) => _.name),
133
+ Effect.tap((_) => Effect.sync(() => expect(_).toBe("Test Action")))
134
+ )
129
135
  ),
130
136
  Effect.tap(() => Effect.sync(() => executed = true))
131
137
  )
@@ -175,7 +181,10 @@ it.live("works non-gen", () =>
175
181
  expect(yield* Effect.currentSpan.pipe(Effect.map((_) => _.name))).toBe("Test Action")
176
182
  })),
177
183
  Effect.tap(() =>
178
- Effect.currentSpan.pipe(Effect.map((_) => _.name), Effect.tap((_) => Effect.sync(() => expect(_).toBe("Test Action"))))
184
+ Effect.currentSpan.pipe(
185
+ Effect.map((_) => _.name),
186
+ Effect.tap((_) => Effect.sync(() => expect(_).toBe("Test Action")))
187
+ )
179
188
  ),
180
189
  Effect.tap(() => Effect.sync(() => executed = true))
181
190
  )
@@ -317,7 +326,10 @@ it.live("with toasts", () =>
317
326
  expect(yield* Effect.currentSpan.pipe(Effect.map((_) => _.name))).toBe("Test Action")
318
327
  })),
319
328
  Effect.tap(() =>
320
- Effect.currentSpan.pipe(Effect.map((_) => _.name), Effect.tap((_) => Effect.sync(() => expect(_).toBe("Test Action"))))
329
+ Effect.currentSpan.pipe(
330
+ Effect.map((_) => _.name),
331
+ Effect.tap((_) => Effect.sync(() => expect(_).toBe("Test Action")))
332
+ )
321
333
  ),
322
334
  // WithToast.handle({
323
335
  // onFailure: "failed",
@@ -388,6 +400,27 @@ it.live("fail", () =>
388
400
  expect(command.waiting).toBe(false)
389
401
  expect(Exit.isFailure(AsyncResult.toExit(command.result))).toBe(true)
390
402
  expect(toasts.length).toBe(1) // toast should show error
403
+ expect(toasts[0].message).toContain("Test Action Failed:\nBoom!")
404
+ expect(toasts[0].message).toMatch(/Trace: [a-f0-9]{32}/)
405
+ expect(toasts[0].message).toMatch(/Span: [a-f0-9]{16}/)
406
+ }))
407
+
408
+ it.live("fail with showSpanInfo disabled", () =>
409
+ Effect
410
+ .gen(function*() {
411
+ const toasts: any[] = []
412
+ const Command = useExperimental({ toasts, messages: DefaultIntl.en })
413
+
414
+ const command = Command.fn("Test Action")(
415
+ function*() {
416
+ return yield* Effect.fail({ message: "Boom!" })
417
+ },
418
+ Command.withDefaultToast({ showSpanInfo: false })
419
+ )
420
+
421
+ yield* Fiber.join(command.handle())
422
+
423
+ expect(toasts.length).toBe(1)
391
424
  expect(toasts[0].message).toBe("Test Action Failed:\nBoom!")
392
425
  }))
393
426
 
@@ -444,7 +477,7 @@ it.live("defect", () =>
444
477
  expect(command.waiting).toBe(false)
445
478
  expect(Exit.isFailure(AsyncResult.toExit(command.result))).toBe(true)
446
479
  expect(toasts.length).toBe(1) // toast should show error
447
- expect(toasts[0].message).toBe("Test Action unexpected error, please try again shortly.")
480
+ expect(toasts[0].message).toContain("Test Action unexpected error, please try again shortly.")
448
481
  }))
449
482
 
450
483
  it.live("works with alt", () =>
@@ -471,7 +504,10 @@ it.live("works with alt", () =>
471
504
  expect(yield* Effect.currentSpan.pipe(Effect.map((_) => _.name))).toBe("Test Action")
472
505
  })),
473
506
  Effect.tap(() =>
474
- Effect.currentSpan.pipe(Effect.map((_) => _.name), Effect.tap((_) => Effect.sync(() => expect(_).toBe("Test Action"))))
507
+ Effect.currentSpan.pipe(
508
+ Effect.map((_) => _.name),
509
+ Effect.tap((_) => Effect.sync(() => expect(_).toBe("Test Action")))
510
+ )
475
511
  ),
476
512
  Effect.tap(() => Effect.sync(() => executed = true))
477
513
  )
@@ -624,7 +660,10 @@ it.live("with toasts with alt", () =>
624
660
  expect(yield* Effect.currentSpan.pipe(Effect.map((_) => _.name))).toBe("Test Action")
625
661
  })),
626
662
  Effect.tap(() =>
627
- Effect.currentSpan.pipe(Effect.map((_) => _.name), Effect.tap((_) => Effect.sync(() => expect(_).toBe("Test Action"))))
663
+ Effect.currentSpan.pipe(
664
+ Effect.map((_) => _.name),
665
+ Effect.tap((_) => Effect.sync(() => expect(_).toBe("Test Action")))
666
+ )
628
667
  ),
629
668
  Command.withDefaultToast(),
630
669
  Effect.tap(() => Effect.sync(() => executed = true))
@@ -696,7 +735,7 @@ it.live("fail with alt", () =>
696
735
  expect(command.waiting).toBe(false)
697
736
  expect(Exit.isFailure(AsyncResult.toExit(command.result))).toBe(true)
698
737
  expect(toasts.length).toBe(1) // toast should show error
699
- expect(toasts[0].message).toBe("Test Action Failed:\nBoom!")
738
+ expect(toasts[0].message).toContain("Test Action Failed:\nBoom!")
700
739
  }))
701
740
 
702
741
  it.live("fail and recover with alt", () =>
@@ -756,5 +795,57 @@ it.live("defect with alt", () =>
756
795
  expect(command.waiting).toBe(false)
757
796
  expect(Exit.isFailure(AsyncResult.toExit(command.result))).toBe(true)
758
797
  expect(toasts.length).toBe(1) // toast should show error
759
- expect(toasts[0].message).toBe("Test Action unexpected error, please try again shortly.")
798
+ expect(toasts[0].message).toContain("Test Action unexpected error, please try again shortly.")
760
799
  }))
800
+
801
+ describe("state-in-toast", () => {
802
+ it("works", () => {
803
+ const toasts: any[] = []
804
+ const removeMutation = Object.assign(
805
+ Effect.fn(function*(_item: string) {
806
+ yield* Effect.sleep(1000)
807
+ }),
808
+ { id: "remove_thing" }
809
+ )
810
+
811
+ const item = "x"
812
+
813
+ const Command = useExperimental({ toasts, messages: DefaultIntl.en })
814
+
815
+ Command.fn(removeMutation, {
816
+ state: () => ({ item }),
817
+ waitKey: (id) => `${id}.${item}`,
818
+ blockKey: () => `modify_thing.${item}`
819
+ // allowed: () => role.value === "admin"
820
+ })(
821
+ function*() {
822
+ // yield* Command.confirmOrInterrupt(yield* I18n.formatMessage({ id: "confirm.remove_item" }, { item }))
823
+ yield* removeMutation(item)
824
+ },
825
+ Command.withDefaultToast({
826
+ onSuccess: (a, b, c, d) => {
827
+ console.log("Success", { a, b, c, d })
828
+ expectTypeOf(d.state).toEqualTypeOf<{ readonly item: "x" }>()
829
+ }
830
+ })
831
+ )
832
+
833
+ Command.fn(removeMutation, {
834
+ state: () => ({ item }),
835
+ waitKey: (id) => `${id}.${item}`,
836
+ blockKey: () => `modify_thing.${item}`
837
+ // allowed: () => role.value === "admin"
838
+ })(
839
+ function*() {
840
+ // yield* Command.confirmOrInterrupt(yield* I18n.formatMessage({ id: "confirm.remove_item" }, { item }))
841
+ yield* removeMutation(item)
842
+ },
843
+ Command.withDefaultToast({
844
+ onSuccess: (a, b, c) => {
845
+ console.log("Success", { a, b, c })
846
+ expectTypeOf(c).toEqualTypeOf<undefined>()
847
+ }
848
+ })
849
+ )
850
+ })
851
+ })
@@ -1 +1 @@
1
- {"version":3,"file":"form.test.d.ts","sourceRoot":"","sources":["../form.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,CAAC,EAAE,MAAM,YAAY,CAAA;;;;;;;;;;;;;;;;;AAGtC,qBAAa,YAAa,SAAQ,iBAShC;CAAG;;;;AAEL,qBAAa,mBAAoB,SAAQ,wBAEvC;CAAG;;;;;;;;;;;AAEL,qBAAa,WAAY,SAAQ,gBAK/B;CAAG;;;;;;;;AAEL,cAAM,MAAO,SAAQ,WAEnB;CAAG;;;;;;;;AAEL,cAAM,MAAO,SAAQ,WAEnB;CAAG;;;;;;;;;;;AAEL,cAAM,QAAS,SAAQ,aAGrB;CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqBL,qBAAa,cAAe,SAAQ,mBAGlC;CAAG"}
1
+ {"version":3,"file":"form.test.d.ts","sourceRoot":"","sources":["../form.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,CAAC,EAAE,MAAM,YAAY,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAGtC,qBAAa,YAAa,SAAQ,iBAahC;CAAG;;;;AAEL,qBAAa,mBAAoB,SAAQ,wBAEvC;CAAG;;;;;;;;;;;;AAEL,qBAAa,WAAY,SAAQ,gBAK/B;CAAG;;;;;;;;;;;;;;;;;;;;;;;;AAEL,cAAM,MAAO,SAAQ,WAEnB;CAAG;;;;;;;;;;;;;;;;;;;;;;;;AAEL,cAAM,MAAO,SAAQ,WAEnB;CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAEL,cAAM,QAAS,SAAQ,aAGrB;CAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqBL,qBAAa,cAAe,SAAQ,mBAGlC;CAAG"}