@effect-app/vue 4.0.0-beta.2 → 4.0.0-beta.200

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 (103) hide show
  1. package/CHANGELOG.md +1611 -0
  2. package/dist/commander.d.ts +628 -0
  3. package/dist/commander.d.ts.map +1 -0
  4. package/dist/commander.js +1055 -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 +14 -5
  12. package/dist/form.d.ts.map +1 -1
  13. package/dist/form.js +41 -12
  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 +6 -9
  19. package/dist/lib.d.ts.map +1 -1
  20. package/dist/lib.js +35 -10
  21. package/dist/makeClient.d.ts +153 -340
  22. package/dist/makeClient.d.ts.map +1 -1
  23. package/dist/makeClient.js +222 -377
  24. package/dist/makeContext.d.ts +1 -1
  25. package/dist/makeContext.d.ts.map +1 -1
  26. package/dist/makeIntl.d.ts +1 -1
  27. package/dist/makeIntl.d.ts.map +1 -1
  28. package/dist/makeUseCommand.d.ts +8 -0
  29. package/dist/makeUseCommand.d.ts.map +1 -0
  30. package/dist/makeUseCommand.js +13 -0
  31. package/dist/mutate.d.ts +53 -35
  32. package/dist/mutate.d.ts.map +1 -1
  33. package/dist/mutate.js +138 -47
  34. package/dist/query.d.ts +20 -40
  35. package/dist/query.d.ts.map +1 -1
  36. package/dist/query.js +138 -71
  37. package/dist/routeParams.d.ts +1 -1
  38. package/dist/runtime.d.ts +7 -4
  39. package/dist/runtime.d.ts.map +1 -1
  40. package/dist/runtime.js +27 -17
  41. package/dist/toast.d.ts +46 -0
  42. package/dist/toast.d.ts.map +1 -0
  43. package/dist/toast.js +32 -0
  44. package/dist/withToast.d.ts +26 -0
  45. package/dist/withToast.d.ts.map +1 -0
  46. package/dist/withToast.js +54 -0
  47. package/examples/streamMutation.ts +70 -0
  48. package/package.json +48 -50
  49. package/src/commander.ts +3384 -0
  50. package/src/{experimental/confirm.ts → confirm.ts} +10 -14
  51. package/src/errorReporter.ts +62 -74
  52. package/src/form.ts +56 -17
  53. package/src/intl.ts +12 -0
  54. package/src/lib.ts +47 -20
  55. package/src/makeClient.ts +569 -1135
  56. package/src/{experimental/makeUseCommand.ts → makeUseCommand.ts} +6 -4
  57. package/src/mutate.ts +266 -128
  58. package/src/query.ts +207 -181
  59. package/src/runtime.ts +41 -20
  60. package/src/{experimental/toast.ts → toast.ts} +11 -25
  61. package/src/{experimental/withToast.ts → withToast.ts} +28 -10
  62. package/test/Mutation.test.ts +176 -23
  63. package/test/dist/form.test.d.ts.map +1 -1
  64. package/test/dist/lib.test.d.ts.map +1 -0
  65. package/test/dist/streamFinal.test.d.ts.map +1 -0
  66. package/test/dist/streamFn.test.d.ts.map +1 -0
  67. package/test/dist/stubs.d.ts +3274 -122
  68. package/test/dist/stubs.d.ts.map +1 -1
  69. package/test/dist/stubs.js +178 -31
  70. package/test/form-validation-errors.test.ts +23 -19
  71. package/test/form.test.ts +20 -2
  72. package/test/lib.test.ts +240 -0
  73. package/test/makeClient.test.ts +292 -38
  74. package/test/streamFinal.test.ts +63 -0
  75. package/test/streamFn.test.ts +455 -0
  76. package/test/stubs.ts +214 -42
  77. package/tsconfig.examples.json +20 -0
  78. package/tsconfig.json +0 -1
  79. package/tsconfig.json.bak +5 -2
  80. package/tsconfig.src.json +34 -34
  81. package/tsconfig.test.json +2 -2
  82. package/vitest.config.ts +5 -5
  83. package/dist/experimental/commander.d.ts +0 -359
  84. package/dist/experimental/commander.d.ts.map +0 -1
  85. package/dist/experimental/commander.js +0 -558
  86. package/dist/experimental/confirm.d.ts +0 -19
  87. package/dist/experimental/confirm.d.ts.map +0 -1
  88. package/dist/experimental/confirm.js +0 -28
  89. package/dist/experimental/intl.d.ts +0 -16
  90. package/dist/experimental/intl.d.ts.map +0 -1
  91. package/dist/experimental/intl.js +0 -5
  92. package/dist/experimental/makeUseCommand.d.ts +0 -8
  93. package/dist/experimental/makeUseCommand.d.ts.map +0 -1
  94. package/dist/experimental/makeUseCommand.js +0 -13
  95. package/dist/experimental/toast.d.ts +0 -47
  96. package/dist/experimental/toast.d.ts.map +0 -1
  97. package/dist/experimental/toast.js +0 -41
  98. package/dist/experimental/withToast.d.ts +0 -25
  99. package/dist/experimental/withToast.d.ts.map +0 -1
  100. package/dist/experimental/withToast.js +0 -45
  101. package/eslint.config.mjs +0 -24
  102. package/src/experimental/commander.ts +0 -1836
  103. package/src/experimental/intl.ts +0 -9
package/src/query.ts CHANGED
@@ -2,16 +2,22 @@
2
2
  /* eslint-disable @typescript-eslint/no-unsafe-call */
3
3
  /* eslint-disable @typescript-eslint/no-unsafe-return */
4
4
  /* eslint-disable @typescript-eslint/no-unsafe-assignment */
5
- import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
6
- 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"
7
- import { Array, Cause, Effect, Exit, flow, Option, S, type ServiceMap } from "effect-app"
8
- import { type Req } from "effect-app/client"
9
- import type { RequestHandler, RequestHandlerWithInput } from "effect-app/client/clientFor"
10
- import { ServiceUnavailableError } from "effect-app/client/errors"
5
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
6
+ import { type DefaultError, type Enabled, experimental_streamedQuery as streamedQuery, 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"
7
+ import { Array, Cause, type Context, Effect, Exit, Option, S } from "effect-app"
8
+ import { makeQueryKey, type Req } from "effect-app/client"
9
+ import type { RequestHandlerWithInput, RequestStreamHandlerWithInput } from "effect-app/client/clientFor"
10
+ import { CauseException, ServiceUnavailableError } from "effect-app/client/errors"
11
+ import * as Channel from "effect/Channel"
12
+ import * as Pull from "effect/Pull"
13
+ import * as Scope from "effect/Scope"
14
+ import type * as Stream from "effect/Stream"
11
15
  import { type Span } from "effect/Tracer"
12
16
  import { isHttpClientError } from "effect/unstable/http/HttpClientError"
17
+ import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
13
18
  import { computed, type ComputedRef, type MaybeRefOrGetter, ref, shallowRef, watch, type WatchSource } from "vue"
14
- import { makeQueryKey, reportRuntimeError } from "./lib.js"
19
+ import { reportRuntimeError } from "./lib.js"
20
+ import { makeRunPromise } from "./runtime.js"
15
21
 
16
22
  // 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
23
  // but because how they are dealing with some extends clause, we loose all properties except initialData
@@ -74,20 +80,68 @@ export interface CustomDefinedPlaceholderQueryOptions<
74
80
  | PlaceholderDataFunction<NonFunctionGuard<TQueryData>, TError, NonFunctionGuard<TQueryData>, TQueryKey>
75
81
  }
76
82
 
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)
83
+ function swrToQuery<E, A>(r: {
84
+ error: CauseException<E> | undefined
85
+ data: A | undefined
86
+ isValidating: boolean
87
+ }): AsyncResult.AsyncResult<A, E> {
88
+ if (r.error !== undefined) {
89
+ return AsyncResult.failureWithPrevious(
90
+ r.error.originalCause,
91
+ {
92
+ previous: r.data === undefined ? Option.none() : Option.some(AsyncResult.success(r.data)),
93
+ waiting: r.isValidating
94
+ }
95
+ )
82
96
  }
97
+ if (r.data !== undefined) {
98
+ return AsyncResult.success<A, E>(r.data, { waiting: r.isValidating })
99
+ }
100
+
101
+ return AsyncResult.initial(r.isValidating)
83
102
  }
84
103
 
85
- export const makeQuery = <R>(getRuntime: () => ServiceMap.ServiceMap<R>) => {
104
+ function streamToAsyncIterableWithCauseException<A, E, R>(
105
+ self: Stream.Stream<A, E, R>,
106
+ context: Context.Context<R>,
107
+ id: string
108
+ ): AsyncIterable<A> {
109
+ return {
110
+ [Symbol.asyncIterator]() {
111
+ const runPromise = Effect.runPromiseWith(context)
112
+ const runPromiseExit = Effect.runPromiseExitWith(context)
113
+ const scope = Scope.makeUnsafe()
114
+ let pull: any
115
+ let currentIter: Iterator<A> | undefined
116
+ return {
117
+ async next(): Promise<IteratorResult<A>> {
118
+ if (currentIter) {
119
+ const next = currentIter.next()
120
+ if (!next.done) return next
121
+ currentIter = undefined
122
+ }
123
+ pull ??= await runPromise(Channel.toPullScoped((self as any).channel, scope))
124
+ const exit = await runPromiseExit(pull)
125
+ if (Exit.isSuccess(exit)) {
126
+ currentIter = (exit.value as any)[Symbol.iterator]()
127
+ return currentIter!.next()
128
+ } else if (Pull.isDoneCause((exit as any).cause)) {
129
+ return { done: true, value: undefined }
130
+ }
131
+ throw new CauseException((exit as any).cause, id)
132
+ },
133
+ return(_) {
134
+ return runPromise(Effect.as(Scope.close(scope, Exit.void), { done: true, value: undefined }) as any)
135
+ }
136
+ }
137
+ }
138
+ }
139
+ }
140
+
141
+ export const makeQuery = <R>(getRuntime: () => Context.Context<R>) => {
86
142
  const useQuery_: {
87
143
  <I, A, E, Request extends Req, Name extends string>(
88
- q:
89
- | RequestHandlerWithInput<I, A, E, R, Request, Name>
90
- | RequestHandler<A, E, R, Request, Name>
144
+ q: RequestHandlerWithInput<I, A, E, R, Request, Name>
91
145
  ): {
92
146
  <TData = A>(
93
147
  arg: I | WatchSource<I> | undefined,
@@ -95,8 +149,8 @@ export const makeQuery = <R>(getRuntime: () => ServiceMap.ServiceMap<R>) => {
95
149
  ): readonly [
96
150
  ComputedRef<AsyncResult.AsyncResult<TData, E>>,
97
151
  ComputedRef<TData | undefined>,
98
- (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, KnownFiberFailure<E>>, never, never>,
99
- UseQueryDefinedReturnType<TData, KnownFiberFailure<E>>
152
+ (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, CauseException<E>>>,
153
+ UseQueryDefinedReturnType<TData, CauseException<E>>
100
154
  ]
101
155
 
102
156
  <TData = A>(
@@ -105,8 +159,8 @@ export const makeQuery = <R>(getRuntime: () => ServiceMap.ServiceMap<R>) => {
105
159
  ): readonly [
106
160
  ComputedRef<AsyncResult.AsyncResult<TData, E>>,
107
161
  ComputedRef<TData>,
108
- (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, KnownFiberFailure<E>>, never, never>,
109
- UseQueryDefinedReturnType<TData, KnownFiberFailure<E>>
162
+ (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, CauseException<E>>>,
163
+ UseQueryDefinedReturnType<TData, CauseException<E>>
110
164
  ]
111
165
 
112
166
  <TData = A>(
@@ -115,14 +169,12 @@ export const makeQuery = <R>(getRuntime: () => ServiceMap.ServiceMap<R>) => {
115
169
  ): readonly [
116
170
  ComputedRef<AsyncResult.AsyncResult<TData, E>>,
117
171
  ComputedRef<TData>,
118
- (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, KnownFiberFailure<E>>, never, never>,
119
- UseQueryDefinedReturnType<TData, KnownFiberFailure<E>>
172
+ (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, CauseException<E>>>,
173
+ UseQueryDefinedReturnType<TData, CauseException<E>>
120
174
  ]
121
175
  }
122
176
  } = <I, A, E, Request extends Req, Name extends string>(
123
- q:
124
- | RequestHandlerWithInput<I, A, E, R, Request, Name>
125
- | RequestHandler<A, E, R, Request, Name>
177
+ q: RequestHandlerWithInput<I, A, E, R, Request, Name>
126
178
  ) =>
127
179
  <TData = A>(
128
180
  arg: I | WatchSource<I> | undefined,
@@ -130,76 +182,56 @@ export const makeQuery = <R>(getRuntime: () => ServiceMap.ServiceMap<R>) => {
130
182
  options?: any
131
183
  // TODO
132
184
  ) => {
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
- ))
185
+ // we wrap into CauseException because we want to keep the full cause of the failure.
186
+ const runPromise = makeRunPromise(getRuntime())
141
187
  const arr = arg
142
- const req: { value: I } = !arg
188
+ const req: { value: I } | undefined = !arg
143
189
  ? undefined
144
190
  : typeof arr === "function"
145
191
  ? ({
146
192
  get value() {
147
193
  return (arr as any)()
148
194
  }
149
- } as any)
150
- : ref(arg)
195
+ })
196
+ : ref(arg) as any
151
197
  const queryKey = makeQueryKey(q)
152
- const handler = q.handler
198
+ const projectionHash = (q as { queryKeyProjectionHash?: string }).queryKeyProjectionHash
153
199
 
154
- const r = useTanstackQuery<A, KnownFiberFailure<E>, TData>(
155
- Effect.isEffect(handler)
156
- ? {
157
- ...options,
158
- retry: (retryCount, error) => {
159
- if (error instanceof KnownFiberFailure) {
160
- if (!isHttpClientError(error.error) && !S.is(ServiceUnavailableError)(error.error)) {
161
- return false
162
- }
163
- }
200
+ const defaultOptions = {
201
+ // we do not want to throw errors, because we turn the success and error responses into a Result type
202
+ // 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.
203
+ // 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.
204
+ // TODO: we might want to rethink the strategy of how to handle errors that happen after the initial load.
205
+ // For suspense, the initial load is captured by the suspense boundary.
206
+ // For subsequent loads (or non suspense use) we currently are required to use the QueryResult component to conditionally render error/loading/etc.
207
+ throwOnError: false
208
+ }
164
209
 
165
- return retryCount < 5
166
- },
167
- queryKey,
168
- queryFn: ({ meta, signal }) =>
169
- runPromise(
170
- handler
171
- .pipe(
172
- Effect.tapCauseIf(Cause.hasDies, (cause) => reportRuntimeError(cause)),
173
- Effect.withSpan(`query ${q.id}`, {}, { captureStackTrace: false }),
174
- meta?.["span"] ? Effect.withParentSpan(meta["span"] as Span) : (_) => _
175
- ),
176
- { signal }
177
- )
210
+ const r = useTanstackQuery<A, CauseException<E>, TData>({
211
+ ...defaultOptions,
212
+ ...options,
213
+ retry: (retryCount, error) => {
214
+ if (error instanceof CauseException) {
215
+ if (!isHttpClientError(error.cause) && !S.is(ServiceUnavailableError)(error.cause)) {
216
+ return false
217
+ }
178
218
  }
179
- : {
180
- ...options,
181
- retry: (retryCount, error) => {
182
- if (error instanceof KnownFiberFailure) {
183
- if (!isHttpClientError(error.error) && !S.is(ServiceUnavailableError)(error.error)) {
184
- return false
185
- }
186
- }
187
219
 
188
- return retryCount < 5
189
- },
190
- queryKey: [...queryKey, req],
191
- queryFn: ({ meta, signal }) =>
192
- runPromise(
193
- handler(req.value)
194
- .pipe(
195
- Effect.tapCauseIf(Cause.hasDies, (cause) => reportRuntimeError(cause)),
196
- Effect.withSpan(`query ${q.id}`, {}, { captureStackTrace: false }),
197
- meta?.["span"] ? Effect.withParentSpan(meta["span"] as Span) : (_) => _
198
- ),
199
- { signal }
200
- )
201
- }
202
- )
220
+ return retryCount < 5
221
+ },
222
+ queryKey: projectionHash === undefined ? [...queryKey, req] : [...queryKey, req, projectionHash],
223
+ queryFn: ({ meta, signal }) =>
224
+ runPromise(
225
+ q
226
+ .handler(req?.value as I)
227
+ .pipe(
228
+ Effect.tapCauseIf(Cause.hasDies, (cause) => reportRuntimeError(cause)),
229
+ Effect.withSpan(`query ${q.id}`, {}, { captureStackTrace: false }),
230
+ meta?.["span"] ? Effect.withParentSpan(meta["span"] as Span) : (_) => _
231
+ ),
232
+ { signal }
233
+ )
234
+ })
203
235
 
204
236
  const latestSuccess = shallowRef<TData>()
205
237
  const result = computed((): AsyncResult.AsyncResult<TData, E> =>
@@ -227,129 +259,130 @@ export const makeQuery = <R>(getRuntime: () => ServiceMap.ServiceMap<R>) => {
227
259
  ] as any
228
260
  }
229
261
 
230
- function swrToQuery<E, A>(r: {
231
- error: KnownFiberFailure<E> | undefined
232
- data: A | undefined
233
- isValidating: boolean
234
- }): AsyncResult.AsyncResult<A, E> {
235
- if (r.error !== undefined) {
236
- return AsyncResult.failureWithPrevious(
237
- r.error.effectCause,
238
- {
239
- previous: r.data === undefined ? Option.none() : Option.some(AsyncResult.success(r.data)),
240
- waiting: r.isValidating
241
- }
242
- )
243
- }
244
- if (r.data !== undefined) {
245
- return AsyncResult.success<A, E>(r.data, { waiting: r.isValidating })
246
- }
247
-
248
- return AsyncResult.initial(r.isValidating)
249
- }
250
-
251
262
  const useQuery: {
252
263
  /**
253
264
  * Effect results are passed to the caller, including errors.
265
+ * When `I = void` the input argument may be omitted.
254
266
  * @deprecated use client helpers instead (.query())
255
267
  */
256
- <E, A, Request extends Req, Name extends string>(
257
- self: RequestHandler<A, E, R, Request, Name>
258
- ): {
259
- // required options, with initialData
260
- /**
261
- * Effect results are passed to the caller, including errors.
262
- */
263
- <TData = A>(
264
- options: CustomDefinedInitialQueryOptions<A, KnownFiberFailure<E>, TData>
265
- ): readonly [
266
- ComputedRef<AsyncResult.AsyncResult<TData, E>>,
267
- ComputedRef<TData>,
268
- (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, KnownFiberFailure<E>>>,
269
- UseQueryReturnType<any, any>
270
- ]
271
- <TData = A>(
272
- options: CustomDefinedPlaceholderQueryOptions<A, E, TData>
273
- ): readonly [
274
- ComputedRef<AsyncResult.AsyncResult<TData, E>>,
275
- ComputedRef<TData>,
276
- (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, KnownFiberFailure<E>>, never, never>,
277
- UseQueryDefinedReturnType<TData, KnownFiberFailure<E>>
278
- ]
279
- // optional options, optional A
280
- /**
281
- * Effect results are passed to the caller, including errors.
282
- */
283
- <TData = A>(options?: CustomUndefinedInitialQueryOptions<A, KnownFiberFailure<E>, TData>): readonly [
284
- ComputedRef<AsyncResult.AsyncResult<A, E>>,
285
- ComputedRef<A | undefined>,
286
- (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, KnownFiberFailure<E>>>,
287
- UseQueryReturnType<any, any>
288
- ]
289
- }
290
- /**
291
- * Effect results are passed to the caller, including errors.
292
- * @deprecated use client helpers instead (.query())
293
- */
294
- <Arg, E, A, Request extends Req, Name extends string>(
295
- self: RequestHandlerWithInput<Arg, A, E, R, Request, Name>
268
+ <I, E, A, Request extends Req, Name extends string>(
269
+ self: RequestHandlerWithInput<I, A, E, R, Request, Name>
296
270
  ): {
297
- // required options, with initialData
298
- /**
299
- * Effect results are passed to the caller, including errors.
300
- */
301
271
  <TData = A>(
302
- arg: Arg | WatchSource<Arg>,
303
- options: CustomDefinedInitialQueryOptions<A, KnownFiberFailure<E>, TData>
272
+ arg: I | WatchSource<I>,
273
+ options: CustomDefinedInitialQueryOptions<A, CauseException<E>, TData>
304
274
  ): readonly [
305
275
  ComputedRef<AsyncResult.AsyncResult<TData, E>>,
306
276
  ComputedRef<TData>,
307
- (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, KnownFiberFailure<E>>>,
277
+ (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, CauseException<E>>>,
308
278
  UseQueryReturnType<any, any>
309
279
  ]
310
- // required options, with placeholderData
311
- /**
312
- * Effect results are passed to the caller, including errors.
313
- */
314
280
  <TData = A>(
315
- arg: Arg | WatchSource<Arg>,
316
- options: CustomDefinedPlaceholderQueryOptions<A, KnownFiberFailure<E>, TData>
281
+ arg: I | WatchSource<I>,
282
+ options: CustomDefinedPlaceholderQueryOptions<A, CauseException<E>, TData>
317
283
  ): readonly [
318
284
  ComputedRef<AsyncResult.AsyncResult<TData, E>>,
319
285
  ComputedRef<TData>,
320
- (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, KnownFiberFailure<E>>>,
286
+ (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, CauseException<E>>>,
321
287
  UseQueryReturnType<any, any>
322
288
  ]
323
- // optional options, optional A
324
- /**
325
- * Effect results are passed to the caller, including errors.
326
- */
327
289
  <TData = A>(
328
- arg: Arg | WatchSource<Arg>,
329
- options?: CustomUndefinedInitialQueryOptions<A, KnownFiberFailure<E>, TData>
290
+ arg: I | WatchSource<I>,
291
+ options?: CustomUndefinedInitialQueryOptions<A, CauseException<E>, TData>
330
292
  ): readonly [
331
293
  ComputedRef<AsyncResult.AsyncResult<TData, E>>,
332
294
  ComputedRef<TData | undefined>,
333
- (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, KnownFiberFailure<E>>>,
295
+ (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<TData, CauseException<E>>>,
334
296
  UseQueryReturnType<any, any>
335
297
  ]
336
298
  }
337
- } = (
299
+ } = ((
338
300
  self: any
339
301
  ) => {
340
302
  const q = useQuery_(self)
341
-
342
- return (argOrOptions?: any, options?: any) =>
343
- Effect.isEffect(self.handler)
344
- ? q(undefined, argOrOptions)
345
- : q(argOrOptions, options)
346
- }
303
+ return (arg?: any, options?: any) => q(arg, options)
304
+ }) as any
347
305
  return useQuery
348
306
  }
349
307
 
350
308
  // eslint-disable-next-line @typescript-eslint/no-empty-object-type
351
309
  export interface MakeQuery2<R> extends ReturnType<typeof makeQuery<R>> {}
352
310
 
311
+ type StreamQueryResult<A, E> = readonly [
312
+ ComputedRef<AsyncResult.AsyncResult<A[], E>>,
313
+ ComputedRef<A[] | undefined>,
314
+ (options?: RefetchOptions) => Effect.Effect<QueryObserverResult<A[], CauseException<E>>>,
315
+ UseQueryReturnType<any, any>
316
+ ]
317
+
318
+ export const makeStreamQuery = <R>(getRuntime: () => Context.Context<R>) => {
319
+ const streamQuery_: {
320
+ <I, E, A, Request extends Req, Name extends string>(
321
+ q: RequestStreamHandlerWithInput<I, A, E, R, Request, Name>
322
+ ): (arg: I | WatchSource<I>) => StreamQueryResult<A, E>
323
+ } = (q: any) => (arg?: any) => {
324
+ const context = getRuntime()
325
+ const arr = arg
326
+ const req: { value: any } | undefined = !arg
327
+ ? undefined
328
+ : typeof arr === "function"
329
+ ? ({
330
+ get value() {
331
+ return arr()
332
+ }
333
+ })
334
+ : ref(arg)
335
+ const queryKey = makeQueryKey(q)
336
+
337
+ const r = useTanstackQuery<any[], CauseException<any>, any[]>(
338
+ {
339
+ throwOnError: false,
340
+ retry: (retryCount: number, error: unknown) => {
341
+ if (error instanceof CauseException) {
342
+ if (!isHttpClientError(error.cause) && !S.is(ServiceUnavailableError)(error.cause)) {
343
+ return false
344
+ }
345
+ }
346
+ return retryCount < 5
347
+ },
348
+ queryKey: [...queryKey, req],
349
+ queryFn: streamedQuery({
350
+ streamFn: () => {
351
+ const stream = q.handler(req?.value)
352
+ return streamToAsyncIterableWithCauseException(stream, context, q.id)
353
+ }
354
+ })
355
+ }
356
+ )
357
+
358
+ const latestSuccess = shallowRef<any[]>()
359
+ const result = computed((): AsyncResult.AsyncResult<any[], any> =>
360
+ swrToQuery({
361
+ error: r.error.value ?? undefined,
362
+ data: r.data.value === undefined ? latestSuccess.value : r.data.value,
363
+ isValidating: r.isFetching.value
364
+ })
365
+ )
366
+ watch(result, (value) => latestSuccess.value = Option.getOrUndefined(AsyncResult.value(value)), { immediate: true })
367
+
368
+ return [
369
+ result,
370
+ computed(() => latestSuccess.value),
371
+ (options?: RefetchOptions) =>
372
+ Effect.currentSpan.pipe(
373
+ Effect.orElseSucceed(() => null),
374
+ Effect.flatMap((span) => Effect.promise(() => r.refetch({ ...options, updateMeta: { span } })))
375
+ ),
376
+ r
377
+ ] as any
378
+ }
379
+
380
+ return streamQuery_
381
+ }
382
+
383
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
384
+ export interface MakeStreamQuery2<R> extends ReturnType<typeof makeStreamQuery<R>> {}
385
+
353
386
  function orPrevious<E, A>(result: AsyncResult.AsyncResult<A, E>) {
354
387
  return AsyncResult.isFailure(result) && Option.isSome(result.previousSuccess)
355
388
  ? AsyncResult.success(result.previousSuccess.value, { waiting: result.waiting })
@@ -400,20 +433,13 @@ export const useUpdateQuery = () => {
400
433
  const queryClient = useQueryClient()
401
434
 
402
435
  const f: {
403
- <A>(
404
- query: RequestHandler<A, any, any, any, any>,
405
- updater: (data: NoInfer<A>) => NoInfer<A>
406
- ): void
407
436
  <I, A>(
408
437
  query: RequestHandlerWithInput<I, A, any, any, any, any>,
409
438
  input: I,
410
439
  updater: (data: NoInfer<A>) => NoInfer<A>
411
440
  ): void
412
- } = (query: any, updateOrInput: any, updaterMaybe?: any) => {
413
- const updater = updaterMaybe !== undefined ? updaterMaybe : updateOrInput
414
- const key = updaterMaybe !== undefined
415
- ? [...makeQueryKey(query), updateOrInput]
416
- : makeQueryKey(query)
441
+ } = (query: any, input: any, updater: any) => {
442
+ const key = [...makeQueryKey(query), input]
417
443
  const data = queryClient.getQueryData(key)
418
444
  if (data) {
419
445
  queryClient.setQueryData(key, updater)
package/src/runtime.ts CHANGED
@@ -1,31 +1,52 @@
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>
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
- export function initializeSync<A, E>(layer: Layer.Layer<A, E, never>) {
23
+ export function initializeSync<A, E>(layer: Layer.Layer<A, E>) {
24
24
  const runtime = Effect.runSync(makeAppRuntime(layer))
25
25
  return runtime
26
26
  }
27
27
 
28
- export function initializeAsync<A, E>(layer: Layer.Layer<A, E, never>) {
28
+ export function initializeAsync<A, E>(layer: Layer.Layer<A, E>) {
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
+ }