@effect-app/vue 4.0.0-beta.15 → 4.0.0-beta.150
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +952 -0
- package/dist/commander.d.ts +370 -0
- package/dist/commander.d.ts.map +1 -0
- package/dist/commander.js +587 -0
- package/dist/confirm.d.ts +19 -0
- package/dist/confirm.d.ts.map +1 -0
- package/dist/confirm.js +24 -0
- package/dist/errorReporter.d.ts +4 -4
- package/dist/errorReporter.d.ts.map +1 -1
- package/dist/errorReporter.js +12 -18
- package/dist/form.d.ts +13 -4
- package/dist/form.d.ts.map +1 -1
- package/dist/form.js +38 -9
- package/dist/index.d.ts +1 -1
- package/dist/intl.d.ts +15 -0
- package/dist/intl.d.ts.map +1 -0
- package/dist/intl.js +9 -0
- package/dist/lib.d.ts +1 -1
- package/dist/lib.d.ts.map +1 -1
- package/dist/makeClient.d.ts +84 -275
- package/dist/makeClient.d.ts.map +1 -1
- package/dist/makeClient.js +44 -351
- package/dist/makeContext.d.ts +1 -1
- package/dist/makeContext.d.ts.map +1 -1
- package/dist/makeIntl.d.ts +1 -1
- package/dist/makeIntl.d.ts.map +1 -1
- package/dist/makeUseCommand.d.ts +8 -0
- package/dist/makeUseCommand.d.ts.map +1 -0
- package/dist/makeUseCommand.js +13 -0
- package/dist/mutate.d.ts +2 -2
- package/dist/mutate.d.ts.map +1 -1
- package/dist/mutate.js +1 -1
- package/dist/query.d.ts +11 -15
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +24 -24
- package/dist/routeParams.d.ts +1 -1
- package/dist/runtime.d.ts +5 -2
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +27 -17
- package/dist/toast.d.ts +46 -0
- package/dist/toast.d.ts.map +1 -0
- package/dist/toast.js +32 -0
- package/dist/withToast.d.ts +26 -0
- package/dist/withToast.d.ts.map +1 -0
- package/dist/withToast.js +49 -0
- package/package.json +46 -46
- package/src/{experimental/commander.ts → commander.ts} +922 -249
- package/src/{experimental/confirm.ts → confirm.ts} +10 -14
- package/src/errorReporter.ts +60 -72
- package/src/form.ts +51 -12
- package/src/intl.ts +12 -0
- package/src/makeClient.ts +177 -1008
- package/src/{experimental/makeUseCommand.ts → makeUseCommand.ts} +3 -3
- package/src/query.ts +45 -46
- package/src/runtime.ts +39 -18
- package/src/{experimental/toast.ts → toast.ts} +11 -25
- package/src/{experimental/withToast.ts → withToast.ts} +15 -6
- package/test/Mutation.test.ts +130 -10
- package/test/dist/form.test.d.ts.map +1 -1
- package/test/dist/stubs.d.ts +983 -118
- package/test/dist/stubs.d.ts.map +1 -1
- package/test/dist/stubs.js +50 -23
- package/test/form-validation-errors.test.ts +23 -19
- package/test/form.test.ts +20 -2
- package/test/makeClient.test.ts +54 -39
- package/test/stubs.ts +61 -26
- package/tsconfig.json +0 -1
- package/dist/experimental/commander.d.ts +0 -359
- package/dist/experimental/commander.d.ts.map +0 -1
- package/dist/experimental/commander.js +0 -557
- package/dist/experimental/confirm.d.ts +0 -19
- package/dist/experimental/confirm.d.ts.map +0 -1
- package/dist/experimental/confirm.js +0 -28
- package/dist/experimental/intl.d.ts +0 -16
- package/dist/experimental/intl.d.ts.map +0 -1
- package/dist/experimental/intl.js +0 -5
- package/dist/experimental/makeUseCommand.d.ts +0 -8
- package/dist/experimental/makeUseCommand.d.ts.map +0 -1
- package/dist/experimental/makeUseCommand.js +0 -13
- package/dist/experimental/toast.d.ts +0 -47
- package/dist/experimental/toast.d.ts.map +0 -1
- package/dist/experimental/toast.js +0 -41
- package/dist/experimental/withToast.d.ts +0 -25
- package/dist/experimental/withToast.d.ts.map +0 -1
- package/dist/experimental/withToast.js +0 -45
- package/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.
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
99
|
-
UseQueryDefinedReturnType<TData,
|
|
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,
|
|
109
|
-
UseQueryDefinedReturnType<TData,
|
|
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,
|
|
119
|
-
UseQueryDefinedReturnType<TData,
|
|
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
|
|
134
|
-
const runPromise =
|
|
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
|
-
}
|
|
136
|
+
})
|
|
150
137
|
: ref(arg)
|
|
151
138
|
const queryKey = makeQueryKey(q)
|
|
152
139
|
const handler = q.handler
|
|
153
140
|
|
|
154
|
-
const
|
|
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
|
|
160
|
-
if (!isHttpClientError(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
|
|
183
|
-
if (!isHttpClientError(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:
|
|
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.
|
|
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,
|
|
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,
|
|
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,
|
|
277
|
-
UseQueryDefinedReturnType<TData,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
2
|
-
import {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
83
|
-
: yield* toast.error(
|
|
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
|
)
|
package/test/Mutation.test.ts
CHANGED
|
@@ -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 {
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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].
|
|
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(
|
|
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(
|
|
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].
|
|
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].
|
|
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
|
|
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"}
|