@effect-app/vue 4.0.0-beta.15 → 4.0.0-beta.151

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 (86) hide show
  1. package/CHANGELOG.md +958 -0
  2. package/dist/commander.d.ts +370 -0
  3. package/dist/commander.d.ts.map +1 -0
  4. package/dist/commander.js +587 -0
  5. package/dist/confirm.d.ts +19 -0
  6. package/dist/confirm.d.ts.map +1 -0
  7. package/dist/confirm.js +24 -0
  8. package/dist/errorReporter.d.ts +4 -4
  9. package/dist/errorReporter.d.ts.map +1 -1
  10. package/dist/errorReporter.js +12 -18
  11. package/dist/form.d.ts +13 -4
  12. package/dist/form.d.ts.map +1 -1
  13. package/dist/form.js +38 -9
  14. package/dist/index.d.ts +1 -1
  15. package/dist/intl.d.ts +15 -0
  16. package/dist/intl.d.ts.map +1 -0
  17. package/dist/intl.js +9 -0
  18. package/dist/lib.d.ts +1 -1
  19. package/dist/lib.d.ts.map +1 -1
  20. package/dist/makeClient.d.ts +84 -275
  21. package/dist/makeClient.d.ts.map +1 -1
  22. package/dist/makeClient.js +44 -351
  23. package/dist/makeContext.d.ts +1 -1
  24. package/dist/makeContext.d.ts.map +1 -1
  25. package/dist/makeIntl.d.ts +1 -1
  26. package/dist/makeIntl.d.ts.map +1 -1
  27. package/dist/makeUseCommand.d.ts +8 -0
  28. package/dist/makeUseCommand.d.ts.map +1 -0
  29. package/dist/makeUseCommand.js +13 -0
  30. package/dist/mutate.d.ts +2 -2
  31. package/dist/mutate.d.ts.map +1 -1
  32. package/dist/mutate.js +1 -1
  33. package/dist/query.d.ts +11 -15
  34. package/dist/query.d.ts.map +1 -1
  35. package/dist/query.js +24 -24
  36. package/dist/routeParams.d.ts +1 -1
  37. package/dist/runtime.d.ts +5 -2
  38. package/dist/runtime.d.ts.map +1 -1
  39. package/dist/runtime.js +27 -17
  40. package/dist/toast.d.ts +46 -0
  41. package/dist/toast.d.ts.map +1 -0
  42. package/dist/toast.js +32 -0
  43. package/dist/withToast.d.ts +26 -0
  44. package/dist/withToast.d.ts.map +1 -0
  45. package/dist/withToast.js +49 -0
  46. package/package.json +46 -46
  47. package/src/{experimental/commander.ts → commander.ts} +922 -249
  48. package/src/{experimental/confirm.ts → confirm.ts} +10 -14
  49. package/src/errorReporter.ts +60 -72
  50. package/src/form.ts +51 -12
  51. package/src/intl.ts +12 -0
  52. package/src/makeClient.ts +177 -1008
  53. package/src/{experimental/makeUseCommand.ts → makeUseCommand.ts} +3 -3
  54. package/src/query.ts +45 -46
  55. package/src/runtime.ts +39 -18
  56. package/src/{experimental/toast.ts → toast.ts} +11 -25
  57. package/src/{experimental/withToast.ts → withToast.ts} +15 -6
  58. package/test/Mutation.test.ts +130 -10
  59. package/test/dist/form.test.d.ts.map +1 -1
  60. package/test/dist/stubs.d.ts +1053 -118
  61. package/test/dist/stubs.d.ts.map +1 -1
  62. package/test/dist/stubs.js +50 -23
  63. package/test/form-validation-errors.test.ts +23 -19
  64. package/test/form.test.ts +20 -2
  65. package/test/makeClient.test.ts +54 -39
  66. package/test/stubs.ts +61 -26
  67. package/tsconfig.json +0 -1
  68. package/dist/experimental/commander.d.ts +0 -359
  69. package/dist/experimental/commander.d.ts.map +0 -1
  70. package/dist/experimental/commander.js +0 -557
  71. package/dist/experimental/confirm.d.ts +0 -19
  72. package/dist/experimental/confirm.d.ts.map +0 -1
  73. package/dist/experimental/confirm.js +0 -28
  74. package/dist/experimental/intl.d.ts +0 -16
  75. package/dist/experimental/intl.d.ts.map +0 -1
  76. package/dist/experimental/intl.js +0 -5
  77. package/dist/experimental/makeUseCommand.d.ts +0 -8
  78. package/dist/experimental/makeUseCommand.d.ts.map +0 -1
  79. package/dist/experimental/makeUseCommand.js +0 -13
  80. package/dist/experimental/toast.d.ts +0 -47
  81. package/dist/experimental/toast.d.ts.map +0 -1
  82. package/dist/experimental/toast.js +0 -41
  83. package/dist/experimental/withToast.d.ts +0 -25
  84. package/dist/experimental/withToast.d.ts.map +0 -1
  85. package/dist/experimental/withToast.js +0 -45
  86. package/src/experimental/intl.ts +0 -9
@@ -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,8 @@
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 { OperationFailure } from "effect-app/Operations"
5
+ import { CommandContext, DefaultIntl } from "../src/commander.js"
5
6
  import { AsyncResult } from "../src/lib.js"
6
7
  import { useExperimental } from "./stubs.js"
7
8
 
@@ -71,7 +72,10 @@ describe("alt2", () => {
71
72
  expect(yield* Effect.currentSpan.pipe(Effect.map((_) => _.name))).toBe("Test Action")
72
73
  })),
73
74
  Effect.tap(() =>
74
- Effect.currentSpan.pipe(Effect.map((_) => _.name), Effect.tap((_) => Effect.sync(() => expect(_).toBe("Test Action"))))
75
+ Effect.currentSpan.pipe(
76
+ Effect.map((_) => _.name),
77
+ Effect.tap((_) => Effect.sync(() => expect(_).toBe("Test Action")))
78
+ )
75
79
  ),
76
80
  Effect.tap(() => Effect.sync(() => executed = true))
77
81
  )
@@ -125,7 +129,10 @@ it.live("works", () =>
125
129
  expect(yield* Effect.currentSpan.pipe(Effect.map((_) => _.name))).toBe("Test Action")
126
130
  })),
127
131
  Effect.tap(() =>
128
- Effect.currentSpan.pipe(Effect.map((_) => _.name), Effect.tap((_) => Effect.sync(() => expect(_).toBe("Test Action"))))
132
+ Effect.currentSpan.pipe(
133
+ Effect.map((_) => _.name),
134
+ Effect.tap((_) => Effect.sync(() => expect(_).toBe("Test Action")))
135
+ )
129
136
  ),
130
137
  Effect.tap(() => Effect.sync(() => executed = true))
131
138
  )
@@ -175,7 +182,10 @@ it.live("works non-gen", () =>
175
182
  expect(yield* Effect.currentSpan.pipe(Effect.map((_) => _.name))).toBe("Test Action")
176
183
  })),
177
184
  Effect.tap(() =>
178
- Effect.currentSpan.pipe(Effect.map((_) => _.name), Effect.tap((_) => Effect.sync(() => expect(_).toBe("Test Action"))))
185
+ Effect.currentSpan.pipe(
186
+ Effect.map((_) => _.name),
187
+ Effect.tap((_) => Effect.sync(() => expect(_).toBe("Test Action")))
188
+ )
179
189
  ),
180
190
  Effect.tap(() => Effect.sync(() => executed = true))
181
191
  )
@@ -317,7 +327,10 @@ it.live("with toasts", () =>
317
327
  expect(yield* Effect.currentSpan.pipe(Effect.map((_) => _.name))).toBe("Test Action")
318
328
  })),
319
329
  Effect.tap(() =>
320
- Effect.currentSpan.pipe(Effect.map((_) => _.name), Effect.tap((_) => Effect.sync(() => expect(_).toBe("Test Action"))))
330
+ Effect.currentSpan.pipe(
331
+ Effect.map((_) => _.name),
332
+ Effect.tap((_) => Effect.sync(() => expect(_).toBe("Test Action")))
333
+ )
321
334
  ),
322
335
  // WithToast.handle({
323
336
  // onFailure: "failed",
@@ -388,9 +401,55 @@ it.live("fail", () =>
388
401
  expect(command.waiting).toBe(false)
389
402
  expect(Exit.isFailure(AsyncResult.toExit(command.result))).toBe(true)
390
403
  expect(toasts.length).toBe(1) // toast should show error
404
+ expect(toasts[0].type).toBe("warning")
405
+ expect(toasts[0].message).toContain("Test Action Failed:\nBoom!")
406
+ expect(toasts[0].message).toMatch(/Trace: [a-f0-9]{32}/)
407
+ expect(toasts[0].message).toMatch(/Span: [a-f0-9]{16}/)
408
+ }))
409
+
410
+ it.live("fail with showSpanInfo disabled", () =>
411
+ Effect
412
+ .gen(function*() {
413
+ const toasts: any[] = []
414
+ const Command = useExperimental({ toasts, messages: DefaultIntl.en })
415
+
416
+ const command = Command.fn("Test Action")(
417
+ function*() {
418
+ return yield* Effect.fail({ message: "Boom!" })
419
+ },
420
+ Command.withDefaultToast({ showSpanInfo: false })
421
+ )
422
+
423
+ yield* Fiber.join(command.handle())
424
+
425
+ expect(toasts.length).toBe(1)
391
426
  expect(toasts[0].message).toBe("Test Action Failed:\nBoom!")
392
427
  }))
393
428
 
429
+ it.live("fail with custom errorRenderer uses warning toast", () =>
430
+ Effect
431
+ .gen(function*() {
432
+ const toasts: any[] = []
433
+ const Command = useExperimental({ toasts, messages: DefaultIntl.en })
434
+
435
+ const command = Command.fn("Test Action")(
436
+ function*() {
437
+ return yield* Effect.fail(OperationFailure.make({ message: null }))
438
+ },
439
+ Command.withDefaultToast({
440
+ errorRenderer: () => "Rendered Boom!"
441
+ })
442
+ )
443
+
444
+ yield* Fiber.join(command.handle())
445
+
446
+ expect(toasts.length).toBe(1)
447
+ expect(toasts[0].type).toBe("warning")
448
+ expect(toasts[0].message).toContain("Test Action, with warnings\nRendered Boom!")
449
+ expect(toasts[0].message).toMatch(/Trace: [a-f0-9]{32}/)
450
+ expect(toasts[0].message).toMatch(/Span: [a-f0-9]{16}/)
451
+ }))
452
+
394
453
  it.live("fail and recover", () =>
395
454
  Effect
396
455
  .gen(function*() {
@@ -444,7 +503,8 @@ it.live("defect", () =>
444
503
  expect(command.waiting).toBe(false)
445
504
  expect(Exit.isFailure(AsyncResult.toExit(command.result))).toBe(true)
446
505
  expect(toasts.length).toBe(1) // toast should show error
447
- expect(toasts[0].message).toBe("Test Action unexpected error, please try again shortly.")
506
+ expect(toasts[0].type).toBe("error")
507
+ expect(toasts[0].message).toContain("Test Action unexpected error, please try again shortly.")
448
508
  }))
449
509
 
450
510
  it.live("works with alt", () =>
@@ -471,7 +531,10 @@ it.live("works with alt", () =>
471
531
  expect(yield* Effect.currentSpan.pipe(Effect.map((_) => _.name))).toBe("Test Action")
472
532
  })),
473
533
  Effect.tap(() =>
474
- Effect.currentSpan.pipe(Effect.map((_) => _.name), Effect.tap((_) => Effect.sync(() => expect(_).toBe("Test Action"))))
534
+ Effect.currentSpan.pipe(
535
+ Effect.map((_) => _.name),
536
+ Effect.tap((_) => Effect.sync(() => expect(_).toBe("Test Action")))
537
+ )
475
538
  ),
476
539
  Effect.tap(() => Effect.sync(() => executed = true))
477
540
  )
@@ -624,7 +687,10 @@ it.live("with toasts with alt", () =>
624
687
  expect(yield* Effect.currentSpan.pipe(Effect.map((_) => _.name))).toBe("Test Action")
625
688
  })),
626
689
  Effect.tap(() =>
627
- Effect.currentSpan.pipe(Effect.map((_) => _.name), Effect.tap((_) => Effect.sync(() => expect(_).toBe("Test Action"))))
690
+ Effect.currentSpan.pipe(
691
+ Effect.map((_) => _.name),
692
+ Effect.tap((_) => Effect.sync(() => expect(_).toBe("Test Action")))
693
+ )
628
694
  ),
629
695
  Command.withDefaultToast(),
630
696
  Effect.tap(() => Effect.sync(() => executed = true))
@@ -696,7 +762,8 @@ it.live("fail with alt", () =>
696
762
  expect(command.waiting).toBe(false)
697
763
  expect(Exit.isFailure(AsyncResult.toExit(command.result))).toBe(true)
698
764
  expect(toasts.length).toBe(1) // toast should show error
699
- expect(toasts[0].message).toBe("Test Action Failed:\nBoom!")
765
+ expect(toasts[0].type).toBe("warning")
766
+ expect(toasts[0].message).toContain("Test Action Failed:\nBoom!")
700
767
  }))
701
768
 
702
769
  it.live("fail and recover with alt", () =>
@@ -756,5 +823,58 @@ it.live("defect with alt", () =>
756
823
  expect(command.waiting).toBe(false)
757
824
  expect(Exit.isFailure(AsyncResult.toExit(command.result))).toBe(true)
758
825
  expect(toasts.length).toBe(1) // toast should show error
759
- expect(toasts[0].message).toBe("Test Action unexpected error, please try again shortly.")
826
+ expect(toasts[0].type).toBe("error")
827
+ expect(toasts[0].message).toContain("Test Action unexpected error, please try again shortly.")
760
828
  }))
829
+
830
+ describe("state-in-toast", () => {
831
+ it("works", () => {
832
+ const toasts: any[] = []
833
+ const removeMutation = Object.assign(
834
+ Effect.fn(function*(_item: string) {
835
+ yield* Effect.sleep(1000)
836
+ }),
837
+ { id: "remove_thing" }
838
+ )
839
+
840
+ const item = "x"
841
+
842
+ const Command = useExperimental({ toasts, messages: DefaultIntl.en })
843
+
844
+ Command.fn(removeMutation, {
845
+ state: () => ({ item }),
846
+ waitKey: (id) => `${id}.${item}`,
847
+ blockKey: () => `modify_thing.${item}`
848
+ // allowed: () => role.value === "admin"
849
+ })(
850
+ function*() {
851
+ // yield* Command.confirmOrInterrupt(yield* I18n.formatMessage({ id: "confirm.remove_item" }, { item }))
852
+ yield* removeMutation(item)
853
+ },
854
+ Command.withDefaultToast({
855
+ onSuccess: (a, b, c, d) => {
856
+ console.log("Success", { a, b, c, d })
857
+ expectTypeOf(d.state).toEqualTypeOf<{ readonly item: "x" }>()
858
+ }
859
+ })
860
+ )
861
+
862
+ Command.fn(removeMutation, {
863
+ state: () => ({ item }),
864
+ waitKey: (id) => `${id}.${item}`,
865
+ blockKey: () => `modify_thing.${item}`
866
+ // allowed: () => role.value === "admin"
867
+ })(
868
+ function*() {
869
+ // yield* Command.confirmOrInterrupt(yield* I18n.formatMessage({ id: "confirm.remove_item" }, { item }))
870
+ yield* removeMutation(item)
871
+ },
872
+ Command.withDefaultToast({
873
+ onSuccess: (a, b, c) => {
874
+ console.log("Success", { a, b, c })
875
+ expectTypeOf(c).toEqualTypeOf<undefined>()
876
+ }
877
+ })
878
+ )
879
+ })
880
+ })
@@ -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"}