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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/CHANGELOG.md +2707 -0
  2. package/dist/atomQuery.d.ts +120 -0
  3. package/dist/atomQuery.d.ts.map +1 -0
  4. package/dist/atomQuery.js +434 -0
  5. package/dist/commander.d.ts +318 -43
  6. package/dist/commander.d.ts.map +1 -1
  7. package/dist/commander.js +593 -81
  8. package/dist/confirm.d.ts +5 -3
  9. package/dist/confirm.d.ts.map +1 -1
  10. package/dist/confirm.js +12 -14
  11. package/dist/dependencyMetadata.d.ts +18 -0
  12. package/dist/dependencyMetadata.d.ts.map +1 -0
  13. package/dist/dependencyMetadata.js +67 -0
  14. package/dist/errorReporter.d.ts +6 -4
  15. package/dist/errorReporter.d.ts.map +1 -1
  16. package/dist/errorReporter.js +14 -19
  17. package/dist/form.d.ts +5 -5
  18. package/dist/form.d.ts.map +1 -1
  19. package/dist/form.js +16 -10
  20. package/dist/internal/tanstackQuery.d.ts +9 -0
  21. package/dist/internal/tanstackQuery.d.ts.map +1 -0
  22. package/dist/internal/tanstackQuery.js +204 -0
  23. package/dist/intl.d.ts +4 -4
  24. package/dist/intl.d.ts.map +1 -1
  25. package/dist/intl.js +2 -2
  26. package/dist/lib.d.ts +8 -10
  27. package/dist/lib.d.ts.map +1 -1
  28. package/dist/lib.js +37 -12
  29. package/dist/liveQueryInvalidation.d.ts +16 -0
  30. package/dist/liveQueryInvalidation.d.ts.map +1 -0
  31. package/dist/liveQueryInvalidation.js +115 -0
  32. package/dist/makeClient.d.ts +232 -109
  33. package/dist/makeClient.d.ts.map +1 -1
  34. package/dist/makeClient.js +416 -84
  35. package/dist/makeContext.d.ts.map +1 -1
  36. package/dist/makeIntl.d.ts.map +1 -1
  37. package/dist/makeUseCommand.d.ts +3 -2
  38. package/dist/makeUseCommand.d.ts.map +1 -1
  39. package/dist/makeUseCommand.js +2 -2
  40. package/dist/mutate.d.ts +110 -41
  41. package/dist/mutate.d.ts.map +1 -1
  42. package/dist/mutate.js +236 -59
  43. package/dist/query.d.ts +161 -44
  44. package/dist/query.d.ts.map +1 -1
  45. package/dist/query.js +382 -113
  46. package/dist/queryLifetime.d.ts +25 -0
  47. package/dist/queryLifetime.d.ts.map +1 -0
  48. package/dist/queryLifetime.js +54 -0
  49. package/dist/routeParams.d.ts +4 -4
  50. package/dist/routeParams.d.ts.map +1 -1
  51. package/dist/routeParams.js +4 -3
  52. package/dist/runtime.d.ts +1 -17
  53. package/dist/runtime.d.ts.map +1 -1
  54. package/dist/runtime.js +2 -38
  55. package/dist/suspense.d.ts +19 -0
  56. package/dist/suspense.d.ts.map +1 -0
  57. package/dist/suspense.js +38 -0
  58. package/dist/toast.d.ts +1 -45
  59. package/dist/toast.d.ts.map +1 -1
  60. package/dist/toast.js +2 -32
  61. package/dist/withToast.d.ts +1 -24
  62. package/dist/withToast.d.ts.map +1 -1
  63. package/dist/withToast.js +2 -45
  64. package/docs/atom-query-api-redesign.md +207 -0
  65. package/docs/mutation-command-atoms.md +40 -0
  66. package/docs/query-key-invalidation.md +107 -0
  67. package/examples/streamMutation.ts +72 -0
  68. package/package.json +35 -94
  69. package/src/atomQuery.ts +600 -0
  70. package/src/commander.ts +1064 -116
  71. package/src/confirm.ts +12 -14
  72. package/src/dependencyMetadata.ts +84 -0
  73. package/src/errorReporter.ts +65 -75
  74. package/src/form.ts +22 -15
  75. package/src/index.ts +7 -7
  76. package/src/internal/tanstackQuery.ts +308 -0
  77. package/src/intl.ts +2 -2
  78. package/src/lib.ts +51 -23
  79. package/src/liveQueryInvalidation.ts +138 -0
  80. package/src/makeClient.ts +972 -263
  81. package/src/makeIntl.ts +2 -2
  82. package/src/makeUseCommand.ts +8 -5
  83. package/src/mutate.ts +407 -154
  84. package/src/query.ts +758 -242
  85. package/src/queryLifetime.ts +74 -0
  86. package/src/routeParams.ts +7 -7
  87. package/src/runtime.ts +1 -54
  88. package/src/suspense.ts +43 -0
  89. package/src/toast.ts +1 -52
  90. package/src/withToast.ts +1 -99
  91. package/test/Mutation.test.ts +104 -17
  92. package/test/dependencyInvalidation.test.ts +444 -0
  93. package/test/dist/dependencyInvalidation.test.d.ts.map +1 -0
  94. package/test/dist/form.test.d.ts.map +1 -1
  95. package/test/dist/inactiveInvalidation.test.d.ts.map +1 -0
  96. package/test/dist/interrupt-refetch-repro.test.d.ts.map +1 -0
  97. package/test/dist/lib.test.d.ts.map +1 -0
  98. package/test/dist/liveQueryInvalidation.test.d.ts.map +1 -0
  99. package/test/dist/query-span.test.d.ts.map +1 -0
  100. package/test/dist/queryOptions.test.d.ts.map +1 -0
  101. package/test/dist/streamFinal.test.d.ts.map +1 -0
  102. package/test/dist/streamFn.test.d.ts.map +1 -0
  103. package/test/dist/stubs.d.ts +3681 -256
  104. package/test/dist/stubs.d.ts.map +1 -1
  105. package/test/dist/stubs.js +166 -25
  106. package/test/dist/suspense-regression.test.d.ts.map +1 -0
  107. package/test/dist/suspense.test.d.ts.map +1 -0
  108. package/test/form-validation-errors.test.ts +2 -1
  109. package/test/form.test.ts +2 -1
  110. package/test/inactiveInvalidation.test.ts +172 -0
  111. package/test/interrupt-refetch-repro.test.ts +625 -0
  112. package/test/lib.test.ts +240 -0
  113. package/test/liveQueryInvalidation.test.ts +108 -0
  114. package/test/makeClient.test.ts +414 -32
  115. package/test/query-span.test.ts +92 -0
  116. package/test/queryOptions.test.ts +88 -0
  117. package/test/streamFinal.test.ts +64 -0
  118. package/test/streamFn.test.ts +457 -0
  119. package/test/stubs.ts +194 -36
  120. package/test/suspense-regression.test.ts +168 -0
  121. package/test/suspense.test.ts +160 -0
  122. package/tsconfig.examples.json +20 -0
  123. package/tsconfig.json +9 -1
  124. package/tsconfig.src.json +34 -34
  125. package/tsconfig.test.json +2 -2
  126. package/vitest.config.ts +5 -5
  127. package/eslint.config.mjs +0 -24
  128. package/tsconfig.json.bak +0 -12
@@ -0,0 +1,308 @@
1
+ import { injectRegistry } from "@effect/atom-vue"
2
+ import { QueryClient, useQuery as useTanstackQuery } from "@tanstack/vue-query"
3
+ import { DataDependencies, makeQueryKey, type Req } from "effect-app/client"
4
+ import type { RequestHandlerWithInput } from "effect-app/client/clientFor"
5
+ import { CauseException, ServiceUnavailableError } from "effect-app/client/errors"
6
+ import type * as Context from "effect-app/Context"
7
+ import * as Effect from "effect-app/Effect"
8
+ import * as Option from "effect-app/Option"
9
+ import * as S from "effect-app/Schema"
10
+ import * as Cause from "effect/Cause"
11
+ import { isHttpClientError } from "effect/http/HttpClientError"
12
+ import * as AsyncResult from "effect/reactivity/AsyncResult"
13
+ import * as Atom from "effect/reactivity/Atom"
14
+ import * as Ref from "effect/Ref"
15
+ import type * as Tracer from "effect/Tracer"
16
+ import { computed, type MaybeRefOrGetter, shallowRef, toValue, watch, type WatchSource } from "vue"
17
+ import { replaceEqualDeep } from "../atomQuery.ts"
18
+ import { clearQueryReadDependencies, registerQueryInvalidationMode, setQueryReadDependencies } from "../dependencyMetadata.ts"
19
+ import { reportRuntimeError } from "../lib.ts"
20
+ import type { QueryInvalidator } from "../mutate.ts"
21
+ import type { CustomDefinedInitialQueryOptions, CustomDefinedPlaceholderQueryOptions, CustomUndefinedInitialQueryOptions, CustomUseQueryOptions, MakeQuery2, QueryCacheUpdater, QueryHandle, QueryObserverResult, RefetchOptions } from "../query.ts"
22
+ import { makeRunPromise } from "../runtime.ts"
23
+
24
+ const swrToQuery = <E, A>(r: {
25
+ readonly error: CauseException<E> | null | undefined
26
+ readonly data: A | undefined
27
+ readonly isValidating: boolean
28
+ }): AsyncResult.AsyncResult<A, E> => {
29
+ if (r.error !== undefined && r.error !== null) {
30
+ return AsyncResult.failureWithPrevious(
31
+ r.error.originalCause,
32
+ {
33
+ previous: r.data === undefined ? Option.none() : Option.some(AsyncResult.success(r.data)),
34
+ waiting: r.isValidating
35
+ }
36
+ )
37
+ }
38
+ if (r.data !== undefined) {
39
+ return AsyncResult.success<A, E>(r.data, { waiting: r.isValidating })
40
+ }
41
+
42
+ return AsyncResult.initial(r.isValidating)
43
+ }
44
+
45
+ const recoverCauseException = <A, E>(error: unknown): Effect.Effect<A, E> =>
46
+ error instanceof CauseException
47
+ ? Effect.failCause(error.originalCause)
48
+ : Effect.die(error)
49
+
50
+ const isRetryable = (error: unknown) => {
51
+ if (error instanceof CauseException) {
52
+ return isHttpClientError(error.cause) || S.is(ServiceUnavailableError)(error.cause)
53
+ }
54
+ return false
55
+ }
56
+
57
+ const isInputOption = <I>(value: I | Option.Option<I> | undefined): value is Option.Option<I> => Option.isOption(value)
58
+
59
+ const resolveInput = <I>(
60
+ arg: I | WatchSource<I> | undefined | WatchSource<Option.Option<I>>,
61
+ mode: "optional" | undefined
62
+ ): I | undefined => {
63
+ if (mode === "optional") {
64
+ const option = toValue(arg)
65
+ return isInputOption(option) && Option.isSome(option) ? option.value : undefined
66
+ }
67
+ const value = toValue(arg)
68
+ return isInputOption(value) ? undefined : value
69
+ }
70
+
71
+ const resolveEnabled = <I>(
72
+ arg: I | WatchSource<I> | undefined | WatchSource<Option.Option<I>>,
73
+ options: {
74
+ readonly mode?: "optional" | undefined
75
+ readonly enabled?: MaybeRefOrGetter<boolean | undefined> | undefined
76
+ } | undefined
77
+ ) => {
78
+ if (options?.mode === "optional") {
79
+ return computed(() => {
80
+ const option = toValue(arg)
81
+ return Option.isSome(option)
82
+ })
83
+ }
84
+ return computed(() => {
85
+ const enabled = options?.enabled
86
+ if (enabled === undefined) return true
87
+ return !!toValue(enabled)
88
+ })
89
+ }
90
+
91
+ type LegacyTanstackOptions<A, E, TData> =
92
+ & (
93
+ | CustomUndefinedInitialQueryOptions<A, E, TData>
94
+ | CustomDefinedInitialQueryOptions<A, E, TData>
95
+ | CustomDefinedPlaceholderQueryOptions<A, E, TData>
96
+ | CustomUseQueryOptions<A, E, TData>
97
+ )
98
+ & { readonly mode?: "optional" | undefined }
99
+
100
+ export const makeTanstackQueryClient = () => new QueryClient()
101
+
102
+ export const makeTanstackQueryInvalidator = (queryClient: QueryClient): QueryInvalidator => ({
103
+ invalidateAndAwait: (keys) =>
104
+ Effect
105
+ .gen(function*() {
106
+ const span = yield* Effect.currentParentSpan.pipe(Effect.orElseSucceed(() => undefined))
107
+ yield* Effect.forEach(
108
+ keys,
109
+ (queryKey) => {
110
+ const options = { updateMeta: { span } }
111
+ return Effect.promise(() => queryClient.invalidateQueries({ queryKey }, options))
112
+ },
113
+ { discard: true, concurrency: "unbounded" }
114
+ )
115
+ })
116
+ .pipe(Effect.orDie),
117
+ invalidateSoft: (keys) =>
118
+ Effect
119
+ .gen(function*() {
120
+ const span = yield* Effect.currentParentSpan.pipe(Effect.orElseSucceed(() => undefined))
121
+ yield* Effect.forEach(
122
+ keys,
123
+ (queryKey) => Effect.promise(() => queryClient.invalidateQueries({ queryKey }, { updateMeta: { span } })),
124
+ { discard: true, concurrency: "unbounded" }
125
+ )
126
+ })
127
+ .pipe(Effect.orDie)
128
+ })
129
+
130
+ const fullQueryKey = (
131
+ q: { readonly queryKeyProjectionHash?: string },
132
+ queryKey: ReadonlyArray<unknown>,
133
+ input: unknown
134
+ ) => q.queryKeyProjectionHash === undefined ? [...queryKey, input] : [...queryKey, q.queryKeyProjectionHash, input]
135
+
136
+ export const makeTanstackQueryCacheUpdater = (queryClient: QueryClient): QueryCacheUpdater => ({
137
+ update: <I, A, E, R, Request extends Req, Name extends string>(
138
+ _registry: ReturnType<typeof injectRegistry>,
139
+ query: RequestHandlerWithInput<I, A, E, R, Request, Name>,
140
+ input: I,
141
+ updater: (data: NoInfer<A>) => NoInfer<A>
142
+ ) => {
143
+ const queryKey = fullQueryKey(query, makeQueryKey(query), input)
144
+ if (queryClient.getQueryData(queryKey) === undefined) {
145
+ console.warn(`Query ${query.id} has not been used yet; nothing to update`)
146
+ return
147
+ }
148
+ queryClient.setQueryData(queryKey, (data: A | undefined) => data === undefined ? data : updater(data))
149
+ }
150
+ })
151
+
152
+ export const makeTanstackQuery = <R>(
153
+ getRuntime: () => Context.Context<R>,
154
+ queryClient: QueryClient
155
+ ): MakeQuery2<R> => {
156
+ // Drop a query's recorded read-dependencies when tanstack evicts it from the cache, so the
157
+ // registry mirrors the live queries (mirrors the atom engine's `trackReadDependencies` finalizer).
158
+ queryClient.getQueryCache().subscribe((event) => {
159
+ if (event.type === "removed") clearQueryReadDependencies(event.query.queryKey)
160
+ })
161
+
162
+ const useQuery: MakeQuery2<R> = <I, A, E, Request extends Req, Name extends string>(
163
+ q: RequestHandlerWithInput<I, A, E, R, Request, Name>
164
+ ) =>
165
+ <TData = A>(
166
+ arg: I | WatchSource<I> | undefined | WatchSource<Option.Option<I>>,
167
+ options?: LegacyTanstackOptions<A, CauseException<E>, TData>
168
+ ) => {
169
+ const runPromise = makeRunPromise(getRuntime())
170
+ const queryKey = makeQueryKey(q)
171
+ const enabled = resolveEnabled(arg, options)
172
+ const structuralSharing = options?.structuralSharing === false ? false : replaceEqualDeep
173
+ const tanstackOptions = {
174
+ ...(options?.staleTime !== undefined ? { staleTime: options.staleTime } : {}),
175
+ ...(typeof options?.gcTime === "number" ? { gcTime: options.gcTime } : {}),
176
+ ...(options?.refetchOnWindowFocus !== undefined ? { refetchOnWindowFocus: options.refetchOnWindowFocus } : {}),
177
+ structuralSharing,
178
+ ...(options?.refetchInterval !== undefined ? { refetchInterval: options.refetchInterval } : {}),
179
+ ...(options?.select !== undefined ? { select: options.select } : {})
180
+ }
181
+ const resolvedQueryKey = computed(() => {
182
+ const input = resolveInput(arg, options?.mode)
183
+ return fullQueryKey(q, queryKey, input)
184
+ })
185
+ watch(
186
+ resolvedQueryKey,
187
+ (key, _, onCleanup) => onCleanup(registerQueryInvalidationMode(key, options?.invalidation ?? "await")),
188
+ { immediate: true }
189
+ )
190
+ const tanstack = useTanstackQuery<A, CauseException<E>, TData>({
191
+ ...tanstackOptions,
192
+ enabled,
193
+ throwOnError: false,
194
+ retry: (retryCount: number, error: unknown) => isRetryable(error) && retryCount < 5,
195
+ queryKey: resolvedQueryKey,
196
+ queryFn: (
197
+ { meta, signal }: {
198
+ readonly meta?: { readonly span?: Tracer.AnySpan | undefined } | undefined
199
+ readonly signal: AbortSignal
200
+ }
201
+ ) =>
202
+ runPromise(
203
+ Effect.gen(function*() {
204
+ const input = resolveInput(arg, options?.mode)!
205
+ // Record the repository/server read-dependencies seen while fetching, keyed by the
206
+ // tanstack queryKey, so a later mutation whose writes intersect them derives this query
207
+ // as an invalidation target (the tanstack invalidator invalidates by this same key).
208
+ const readsRef = yield* Ref.make(DataDependencies.empty())
209
+ const writesRef = yield* Ref.make(DataDependencies.empty())
210
+ const recorder = DataDependencies.makeDataDependencyRecorder(readsRef, writesRef)
211
+ const result = yield* q
212
+ .handler(input)
213
+ .pipe(
214
+ Effect.provideService(DataDependencies.DataDependencyRecorder, recorder),
215
+ Effect.tapCauseIf(Cause.hasDies, (cause) => reportRuntimeError(cause)),
216
+ Effect.withSpan(`query ${q.id}`, {}, { captureStackTrace: false }),
217
+ meta?.span === undefined
218
+ ? (effect) => effect
219
+ : Effect.withParentSpan(meta.span, { captureStackTrace: false })
220
+ )
221
+ setQueryReadDependencies(fullQueryKey(q, queryKey, input), yield* Ref.get(readsRef))
222
+ return result
223
+ }),
224
+ { signal }
225
+ )
226
+ }, queryClient)
227
+
228
+ const latestSuccess = shallowRef<TData>()
229
+ const result = computed((): AsyncResult.AsyncResult<TData, E> =>
230
+ swrToQuery({
231
+ error: tanstack.error.value,
232
+ data: tanstack.data.value === undefined ? latestSuccess.value : tanstack.data.value,
233
+ isValidating: tanstack.isFetching.value
234
+ })
235
+ )
236
+ watch(result, (value) => latestSuccess.value = Option.getOrUndefined(AsyncResult.value(value)), { immediate: true })
237
+ const seedLatestSuccess = (data: TData) => {
238
+ if (latestSuccess.value === undefined) {
239
+ latestSuccess.value = data
240
+ }
241
+ }
242
+
243
+ const registry = injectRegistry()
244
+ const latestSuccessRef = computed(() => latestSuccess.value)
245
+ const atom = computed<Atom.Atom<AsyncResult.AsyncResult<TData, E>>>(() => Atom.readable(() => result.value))
246
+
247
+ const awaitResult = (): Effect.Effect<TData, E> =>
248
+ Effect
249
+ .tryPromise({
250
+ try: async () => {
251
+ const queryResult = await tanstack.suspense()
252
+ const data = queryResult.data
253
+ if (data === undefined) {
254
+ throw new Error("TanStack query resolved without data")
255
+ }
256
+ seedLatestSuccess(data)
257
+ return data
258
+ },
259
+ catch: (error) => error
260
+ })
261
+ .pipe(
262
+ Effect.catch((error) => recoverCauseException<TData, E>(error))
263
+ )
264
+
265
+ const refetch = (): Effect.Effect<TData, E> =>
266
+ Effect.gen(function*() {
267
+ const span = yield* Effect.currentParentSpan.pipe(Effect.orElseSucceed(() => undefined))
268
+ return yield* Effect
269
+ .tryPromise({
270
+ try: async () => {
271
+ const options = { throwOnError: true, updateMeta: { span } }
272
+ const queryResult = await tanstack.refetch(options)
273
+ const data = queryResult.data
274
+ if (data === undefined) {
275
+ throw new Error("TanStack query refetched without data")
276
+ }
277
+ return data
278
+ },
279
+ catch: (error) => error
280
+ })
281
+ .pipe(
282
+ Effect.catch((error) => recoverCauseException<TData, E>(error))
283
+ )
284
+ })
285
+
286
+ const handle: QueryHandle<TData, E> = {
287
+ awaitResult,
288
+ refetch,
289
+ refresh: () => {
290
+ void tanstack.refetch()
291
+ },
292
+ registry,
293
+ atom
294
+ }
295
+
296
+ const fetch = (_options?: RefetchOptions): Effect.Effect<QueryObserverResult<TData, CauseException<E>>> =>
297
+ refetch().pipe(Effect.exit, Effect.map(AsyncResult.fromExit))
298
+
299
+ return [
300
+ result,
301
+ latestSuccessRef,
302
+ fetch,
303
+ handle
304
+ ] as const
305
+ }
306
+
307
+ return useQuery
308
+ }
package/src/intl.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { Context } from "effect-app"
1
+ import * as Context from "effect-app/Context"
2
2
  import { accessCn, accessFn } from "effect-app/Context"
3
- import { type MakeIntlReturn } from "./makeIntl.js"
3
+ import { type MakeIntlReturn } from "./makeIntl.ts"
4
4
 
5
5
  type I18nShape = ReturnType<MakeIntlReturn<string>["useIntl"]>
6
6
 
package/src/lib.ts CHANGED
@@ -1,12 +1,13 @@
1
1
  import { type Pausable, useIntervalFn, type UseIntervalFnOptions } from "@vueuse/core"
2
- import { Cause, type Effect, pipe } from "effect-app"
3
- import { type Req } from "effect-app/client"
4
- import type { ClientForOptions, RequestHandler, RequestHandlerWithInput } from "effect-app/client/clientFor"
5
- import { isHttpClientError } from "effect/unstable/http/HttpClientError"
6
- import type { MaybeRefOrGetter } from "vue"
7
- import { reportError } from "./errorReporter.js"
2
+ import type { Req } from "effect-app/client"
3
+ import type { RequestHandlerWithInput } from "effect-app/client/clientFor"
4
+ import type * as Effect from "effect-app/Effect"
5
+ import * as Cause from "effect/Cause"
6
+ import { isHttpClientError } from "effect/http/HttpClientError"
7
+ import { isProxy, isReactive, isRef, type MaybeRefOrGetter, toRaw } from "vue"
8
+ import { reportError } from "./errorReporter.ts"
8
9
 
9
- export * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
10
+ export * as AsyncResult from "effect/reactivity/AsyncResult"
10
11
 
11
12
  const reportRuntimeError_ = reportError("Runtime")
12
13
 
@@ -31,15 +32,7 @@ const determineLevel = (cause: Cause.Cause<unknown>) => {
31
32
  export const reportRuntimeError = (cause: Cause.Cause<unknown>, extras?: Record<string, unknown>) =>
32
33
  reportRuntimeError_(cause, extras, determineLevel(cause))
33
34
 
34
- // $Project/$Configuration.Index
35
- // -> "$Project", "$Configuration", "Index"
36
- export const makeQueryKey = ({ id, options }: { id: string; options?: ClientForOptions }) =>
37
- pipe(
38
- id.split("/"),
39
- (split) => split.filter((_) => !options || !options?.skipQueryKey?.includes(_)).map((_) => "$" + _)
40
- )
41
- .join(".")
42
- .split(".")
35
+ export { makeQueryKey } from "effect-app/client"
43
36
 
44
37
  export function pauseWhileProcessing(
45
38
  iv: Pausable,
@@ -75,13 +68,48 @@ export const mapHandler: {
75
68
  self: RequestHandlerWithInput<I, A, E, R, Request, Name>,
76
69
  map: (handler: (i: I) => Effect.Effect<A, E, R>) => (i: I) => Effect.Effect<A2, E2, R2>
77
70
  ): RequestHandlerWithInput<I, A2, E2, R2, Request, Name>
78
- <E, A, R, E2, A2, R2, Request extends Req, Name extends string>(
79
- self: RequestHandler<A, E, R, Request, Name>,
80
- map: (handler: Effect.Effect<A, E, R>) => Effect.Effect<A2, E2, R2>
81
- ): RequestHandler<A2, E2, R2, Request, Name>
82
71
  } = (self: any, map: any): any => ({
83
72
  ...self,
84
- handler: typeof self.handler === "function"
85
- ? (i: any) => map(self.handler as (i: any) => Effect.Effect<any, any, any>)(i)
86
- : map(self.handler)
73
+ handler: (i: any) => map(self.handler as (i: any) => Effect.Effect<any, any, any>)(i)
87
74
  })
75
+
76
+ export function deepToRaw<T>(sourceObj: T): T {
77
+ const objectIterator = (input: any): any => {
78
+ if (isRef(input)) {
79
+ return objectIterator(input.value)
80
+ }
81
+
82
+ const rawInput = isReactive(input) || isProxy(input)
83
+ ? toRaw(input)
84
+ : input
85
+
86
+ if (Array.isArray(rawInput)) {
87
+ return rawInput.map((item) => objectIterator(item))
88
+ }
89
+
90
+ if (rawInput instanceof Map) {
91
+ return new Map(
92
+ Array.from(rawInput.entries(), ([key, value]) => [objectIterator(key), objectIterator(value)])
93
+ )
94
+ }
95
+
96
+ if (rawInput instanceof Set) {
97
+ return new Set(Array.from(rawInput.values(), (value) => objectIterator(value)))
98
+ }
99
+
100
+ if (rawInput instanceof Date) {
101
+ return new Date(rawInput)
102
+ }
103
+
104
+ if (rawInput && typeof rawInput === "object") {
105
+ return Object.keys(rawInput).reduce((acc, key) => {
106
+ acc[key] = objectIterator(rawInput[key])
107
+ return acc
108
+ }, {} as Record<string, unknown>)
109
+ }
110
+
111
+ return rawInput
112
+ }
113
+
114
+ return objectIterator(sourceObj)
115
+ }
@@ -0,0 +1,138 @@
1
+ import { DataDependencies } from "effect-app/client"
2
+ import * as Hash from "effect/Hash"
3
+
4
+ export interface LiveQueryOptions {
5
+ /** Fixed maximum delay from the first matching write. Zero reacts immediately. */
6
+ readonly maxDelayMs?: number
7
+ }
8
+
9
+ export interface LiveQueryInvalidationSource {
10
+ /** Resolve only after events received after this point can no longer be missed. */
11
+ readonly ready: () => Promise<void>
12
+ readonly subscribe: (
13
+ onWrites: (writes: DataDependencies.DataDependencies) => void,
14
+ onReset: () => void
15
+ ) => () => void
16
+ readonly invalidate: (keys: ReadonlyArray<ReadonlyArray<unknown>>) => void
17
+ }
18
+
19
+ interface LiveQueryEntry {
20
+ readonly key: ReadonlyArray<unknown>
21
+ readonly reads: () => DataDependencies.DataDependencies
22
+ observers: number
23
+ readonly delays: Map<number, number>
24
+ timer: ReturnType<typeof setTimeout> | undefined
25
+ }
26
+
27
+ const entries = new Map<number, LiveQueryEntry>()
28
+ let source: LiveQueryInvalidationSource | undefined
29
+ let unsubscribe: (() => void) | undefined
30
+ let discovering = 0
31
+ let bufferedWrites = DataDependencies.empty()
32
+
33
+ const invalidateEntry = (entry: LiveQueryEntry) => {
34
+ entry.timer = undefined
35
+ source?.invalidate([entry.key])
36
+ }
37
+
38
+ const schedule = (entry: LiveQueryEntry) => {
39
+ if (entry.timer !== undefined) return
40
+ const maxDelayMs = Math.min(...entry.delays.keys())
41
+ if (maxDelayMs <= 0) return invalidateEntry(entry)
42
+ entry.timer = setTimeout(() => invalidateEntry(entry), maxDelayMs)
43
+ }
44
+
45
+ const dispatch = (writes: DataDependencies.DataDependencies) => {
46
+ if (!DataDependencies.isNonEmpty(writes)) return
47
+ for (const entry of entries.values()) {
48
+ if (DataDependencies.intersects(entry.reads(), writes)) schedule(entry)
49
+ }
50
+ }
51
+
52
+ const receive = (writes: DataDependencies.DataDependencies) => {
53
+ if (discovering > 0) {
54
+ bufferedWrites = DataDependencies.merge(bufferedWrites, writes)
55
+ return
56
+ }
57
+ dispatch(writes)
58
+ }
59
+
60
+ const reset = () => {
61
+ bufferedWrites = DataDependencies.empty()
62
+ for (const entry of entries.values()) {
63
+ if (DataDependencies.isNonEmpty(entry.reads())) schedule(entry)
64
+ }
65
+ }
66
+
67
+ const ensureSubscribed = () => {
68
+ if (unsubscribe === undefined && source !== undefined && entries.size > 0) {
69
+ unsubscribe = source.subscribe(receive, reset)
70
+ }
71
+ }
72
+
73
+ export const configureLiveQueryInvalidation = (next: LiveQueryInvalidationSource) => {
74
+ unsubscribe?.()
75
+ unsubscribe = undefined
76
+ source = next
77
+ ensureSubscribed()
78
+ }
79
+
80
+ export const registerLiveQuery = (
81
+ key: ReadonlyArray<unknown>,
82
+ reads: () => DataDependencies.DataDependencies,
83
+ options: LiveQueryOptions
84
+ ) => {
85
+ const hash = Hash.hash(key)
86
+ const delay = options.maxDelayMs ?? 0
87
+ const current = entries.get(hash)
88
+ if (current !== undefined) {
89
+ current.observers++
90
+ current.delays.set(delay, (current.delays.get(delay) ?? 0) + 1)
91
+ } else {
92
+ entries.set(hash, {
93
+ key,
94
+ reads,
95
+ observers: 1,
96
+ delays: new Map([[delay, 1]]),
97
+ timer: undefined
98
+ })
99
+ }
100
+ ensureSubscribed()
101
+
102
+ return () => {
103
+ const entry = entries.get(hash)
104
+ if (entry === undefined) return
105
+ const delayObservers = entry.delays.get(delay) ?? 0
106
+ if (delayObservers <= 1) entry.delays.delete(delay)
107
+ else entry.delays.set(delay, delayObservers - 1)
108
+ if (--entry.observers > 0) return
109
+ if (entry.timer !== undefined) clearTimeout(entry.timer)
110
+ entries.delete(hash)
111
+ if (entries.size === 0) {
112
+ unsubscribe?.()
113
+ unsubscribe = undefined
114
+ bufferedWrites = DataDependencies.empty()
115
+ }
116
+ }
117
+ }
118
+
119
+ export const beginLiveQueryFetch = async (key: ReadonlyArray<unknown>) => {
120
+ if (!entries.has(Hash.hash(key)) || source === undefined) return false
121
+ discovering++
122
+ try {
123
+ await source.ready()
124
+ return true
125
+ } catch (error) {
126
+ discovering--
127
+ throw error
128
+ }
129
+ }
130
+
131
+ export const endLiveQueryFetch = (wasLive: boolean) => {
132
+ if (!wasLive) return
133
+ discovering = Math.max(0, discovering - 1)
134
+ if (discovering > 0 || !DataDependencies.isNonEmpty(bufferedWrites)) return
135
+ const writes = bufferedWrites
136
+ bufferedWrites = DataDependencies.empty()
137
+ dispatch(writes)
138
+ }