@effect-app/vue 4.0.0-beta.273 → 4.0.0-beta.275

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/src/atomQuery.ts CHANGED
@@ -206,6 +206,28 @@ export interface AtomQueryOptions {
206
206
 
207
207
  const defaults = { staleTime: Duration.seconds(5), gcTime: Duration.minutes(5) }
208
208
 
209
+ export interface AtomQueryMetadata {
210
+ readonly staleTimeMs: number
211
+ }
212
+
213
+ const atomQueryMetadata = new WeakMap<Atom.Atom<AsyncResult.AsyncResult<any, any>>, AtomQueryMetadata>()
214
+
215
+ const setAtomQueryMetadata = <A, E>(
216
+ atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>,
217
+ opts: AtomQueryOptions = {}
218
+ ) => {
219
+ const staleTimeMs = staleTimeMsOf(opts)
220
+ const previous = atomQueryMetadata.get(atom)
221
+ atomQueryMetadata.set(atom, {
222
+ staleTimeMs: previous === undefined ? staleTimeMs : Math.min(previous.staleTimeMs, staleTimeMs)
223
+ })
224
+ return atom
225
+ }
226
+
227
+ export const getAtomQueryMetadata = <A, E>(
228
+ atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>
229
+ ): AtomQueryMetadata | undefined => atomQueryMetadata.get(atom)
230
+
209
231
  /** Exported so the vue hook can do refetch-on-mount-per-observer with the same rule as swr. */
210
232
  export const isStaleResult = (r: AsyncResult.AsyncResult<any, any>, staleTimeMs: number): boolean => {
211
233
  if (r.waiting) return false
@@ -225,10 +247,8 @@ export const withQueryOptions = <A, E>(
225
247
  self: Atom.Atom<AsyncResult.AsyncResult<A, E>>,
226
248
  opts: AtomQueryOptions = {}
227
249
  ): Atom.Atom<AsyncResult.AsyncResult<A, E>> => {
250
+ setAtomQueryMetadata(self, opts)
228
251
  const staleTime: Duration.Input = opts.staleTime ?? defaults.staleTime
229
- const gcTime = opts.gcTime === "infinity"
230
- ? "infinity" as const
231
- : Duration.fromInputUnsafe(opts.gcTime ?? defaults.gcTime)
232
252
  let atom = self
233
253
  const revalidateOnFocus = opts.revalidateOnFocus ?? true
234
254
  atom = Atom.swr({
@@ -238,7 +258,7 @@ export const withQueryOptions = <A, E>(
238
258
  })(atom)
239
259
  if (opts.refetchInterval) atom = Atom.withRefresh(Duration.millis(opts.refetchInterval))(atom)
240
260
  if (opts.structuralSharing ?? true) atom = structuralShare(atom)
241
- return gcTime === "infinity" ? Atom.keepAlive(atom) : Atom.setIdleTTL(atom, gcTime)
261
+ return atom
242
262
  }
243
263
 
244
264
  /** Constant atom for disabled / `mode:"optional"`-None queries: stays Initial, never fetches. */
@@ -321,7 +341,7 @@ export const buildQueryFamily = <I, A, E>(
321
341
  // gcTime LAST so the whole chain (incl. the registration + tracking) stays alive through the
322
342
  // idle window, letting invalidation reach a cached-but-unmounted query.
323
343
  atom = Atom.setIdleTTL(atom, defaults.gcTime)
324
- return Atom.withLabel(`query:${self.id}`)(atom)
344
+ return setAtomQueryMetadata(Atom.withLabel(`query:${self.id}`)(atom))
325
345
  })
326
346
  }
327
347
 
@@ -350,7 +370,7 @@ export const buildStreamQueryFamily = <I, A, E>(
350
370
  atom = rt.factory.withReactivity(reactivityKeys)(atom)
351
371
  atom = trackWritableByKeys(reactivityKeys)(atom)
352
372
  atom = Atom.setIdleTTL(atom, defaults.gcTime)
353
- return Atom.withLabel(`stream-query:${self.id}`)(atom)
373
+ return setAtomQueryMetadata(Atom.withLabel(`stream-query:${self.id}`)(atom))
354
374
  })
355
375
  }
356
376
 
@@ -14,7 +14,7 @@ import * as Atom from "effect/unstable/reactivity/Atom"
14
14
  import { computed, type MaybeRefOrGetter, shallowRef, toValue, watch, type WatchSource } from "vue"
15
15
  import { reportRuntimeError } from "../lib.ts"
16
16
  import type { QueryInvalidator } from "../mutate.ts"
17
- import type { CustomDefinedInitialQueryOptions, CustomDefinedPlaceholderQueryOptions, CustomUndefinedInitialQueryOptions, CustomUseQueryOptions, MakeQuery2, QueryHandle, QueryObserverResult, RefetchOptions } from "../query.ts"
17
+ import type { CustomDefinedInitialQueryOptions, CustomDefinedPlaceholderQueryOptions, CustomUndefinedInitialQueryOptions, CustomUseQueryOptions, MakeQuery2, QueryCacheUpdater, QueryHandle, QueryObserverResult, RefetchOptions } from "../query.ts"
18
18
  import { makeRunPromise } from "../runtime.ts"
19
19
 
20
20
  const swrToQuery = <E, A>(r: {
@@ -104,6 +104,28 @@ export const makeTanstackQueryInvalidator = (queryClient: QueryClient): QueryInv
104
104
  )
105
105
  })
106
106
 
107
+ const fullQueryKey = (
108
+ q: { readonly queryKeyProjectionHash?: string },
109
+ queryKey: ReadonlyArray<unknown>,
110
+ input: unknown
111
+ ) => q.queryKeyProjectionHash === undefined ? [...queryKey, input] : [...queryKey, q.queryKeyProjectionHash, input]
112
+
113
+ export const makeTanstackQueryCacheUpdater = (queryClient: QueryClient): QueryCacheUpdater => ({
114
+ update: <I, A, E, R, Request extends Req, Name extends string>(
115
+ _registry: ReturnType<typeof injectRegistry>,
116
+ query: RequestHandlerWithInput<I, A, E, R, Request, Name>,
117
+ input: I,
118
+ updater: (data: NoInfer<A>) => NoInfer<A>
119
+ ) => {
120
+ const queryKey = fullQueryKey(query, makeQueryKey(query), input)
121
+ if (queryClient.getQueryData(queryKey) === undefined) {
122
+ console.warn(`Query ${query.id} has not been used yet; nothing to update`)
123
+ return
124
+ }
125
+ queryClient.setQueryData(queryKey, (data: A | undefined) => data === undefined ? data : updater(data))
126
+ }
127
+ })
128
+
107
129
  export const makeTanstackQuery = <R>(
108
130
  getRuntime: () => Context.Context<R>,
109
131
  queryClient: QueryClient
@@ -117,7 +139,6 @@ export const makeTanstackQuery = <R>(
117
139
  ) => {
118
140
  const runPromise = makeRunPromise(getRuntime())
119
141
  const queryKey = makeQueryKey(q)
120
- const projectionHash = q.queryKeyProjectionHash
121
142
  const enabled = resolveEnabled(arg, options)
122
143
  const tanstackOptions = {
123
144
  ...(options?.staleTime !== undefined ? { staleTime: options.staleTime } : {}),
@@ -134,7 +155,7 @@ export const makeTanstackQuery = <R>(
134
155
  retry: (retryCount: number, error: unknown) => isRetryable(error) && retryCount < 5,
135
156
  queryKey: computed(() => {
136
157
  const input = resolveInput(arg, options?.mode)
137
- return projectionHash === undefined ? [...queryKey, input] : [...queryKey, projectionHash, input]
158
+ return fullQueryKey(q, queryKey, input)
138
159
  }),
139
160
  queryFn: ({ signal }: { readonly signal: AbortSignal }) =>
140
161
  runPromise(
package/src/makeClient.ts CHANGED
@@ -16,28 +16,58 @@ import type * as Stream from "effect/Stream"
16
16
  import * as Struct from "effect/Struct"
17
17
  import type * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
18
18
  import * as Reactivity from "effect/unstable/reactivity/Reactivity"
19
- import { computed, type ComputedRef, onBeforeUnmount, ref, type WatchSource } from "vue"
19
+ import { computed, type ComputedRef, effectScope, onBeforeUnmount, onScopeDispose, ref, type WatchSource } from "vue"
20
20
  import { type AtomClientRuntime, invalidateAndAwait, makeAtomClientRuntime } from "./atomQuery.ts"
21
21
  import { type Commander, CommanderStatic, type Progress } from "./commander.ts"
22
- import { makeTanstackQuery, makeTanstackQueryClient, makeTanstackQueryInvalidator } from "./internal/tanstackQuery.ts"
22
+ import { makeTanstackQuery, makeTanstackQueryCacheUpdater, makeTanstackQueryClient, makeTanstackQueryInvalidator } from "./internal/tanstackQuery.ts"
23
23
  import { type I18n } from "./intl.ts"
24
24
  import { type CommanderResolved, makeUseCommand } from "./makeUseCommand.ts"
25
25
  import { atomQueryInvalidator, combineQueryInvalidators, type InvalidationEntry, makeMutation, makeStreamMutation2, type MutationOptionsBase, type QueryInvalidator, useMakeMutation } from "./mutate.ts"
26
- import { type AtomQueryNewOptions, type CustomUndefinedInitialQueryOptions, makeQuery, makeQueryAtom, makeQueryFamily, makeQueryNew, makeStreamQuery, makeStreamQueryAtom, makeStreamQueryFamily, makeStreamQueryNew, type QueryObserverResult, type RefetchOptions, type StreamQueryAtomFamily, type SuspenseQueryView, type UseQueryReturnType } from "./query.ts"
26
+ import { atomQueryCacheUpdater, type AtomQueryNewOptions, combineQueryCacheUpdaters, type CustomUndefinedInitialQueryOptions, makeQuery, makeQueryAtom, makeQueryFamily, makeQueryNew, makeStreamQuery, makeStreamQueryAtom, makeStreamQueryFamily, makeStreamQueryNew, optionalAtomQueryCacheUpdater, type QueryObserverResult, type RefetchOptions, setQueryCacheUpdater, type StreamQueryAtomFamily, type SuspenseQueryView, type UseQueryReturnType } from "./query.ts"
27
27
  import { makeRunPromise } from "./runtime.ts"
28
28
  import { type Toast } from "./toast.ts"
29
29
 
30
30
  export type { Progress }
31
31
 
32
+ const useScopedSuspenseSetup = <A>(setup: () => A) => {
33
+ const scope = effectScope()
34
+ const controller = new AbortController()
35
+ const value = scope.run(setup)
36
+ if (value === undefined) {
37
+ throw new Error("Internal Error: suspense setup scope did not initialize")
38
+ }
39
+
40
+ const isMounted = ref(true)
41
+ let stopped = false
42
+ const stop = () => {
43
+ if (stopped) return
44
+ stopped = true
45
+ isMounted.value = false
46
+ controller.abort()
47
+ scope.stop()
48
+ }
49
+ onBeforeUnmount(stop)
50
+ onScopeDispose(stop)
51
+
52
+ return [value, isMounted, controller.signal] as const
53
+ }
54
+
32
55
  // TODO: optimize - work from encoded shape directly
33
- const projectHandler = <I, A, E, R, ProjSchema extends S.Decoder<unknown, never>>(
56
+ const projectHandler = <
57
+ I,
58
+ A,
59
+ E,
60
+ R,
61
+ SuccessSchema extends S.Top & { readonly "EncodingServices": R },
62
+ ProjSchema extends S.Top & { readonly "DecodingServices": R }
63
+ >(
34
64
  handler: (i: I) => Effect.Effect<A, E, R>,
35
- successSchema: S.Top,
65
+ successSchema: SuccessSchema,
36
66
  projectionSchema: ProjSchema
37
67
  ) => {
38
- const encode = S.encodeSync(successSchema as S.Encoder<A>)
39
- const decode = S.decodeSync(projectionSchema)
40
- return (i: I) => handler(i).pipe(Effect.map((value) => decode(encode(value))))
68
+ const encode = S.encodeUnknownEffect(successSchema)
69
+ const decode = S.decodeUnknownEffect(projectionSchema)
70
+ return (i: I) => handler(i).pipe(Effect.flatMap(encode), Effect.flatMap(decode))
41
71
  }
42
72
 
43
73
  const projectionSchemaHash = (schema: S.Top) => String(Hash.hash(schema.ast))
@@ -484,18 +514,20 @@ export class QueryImpl<R> {
484
514
  argOrOptions: I | WatchSource<I>,
485
515
  options?: CustomUndefinedInitialQueryOptions<A, CauseException<E>, TData>
486
516
  ) => {
487
- const [resultRef, latestRef, fetch, uqrt] = q<TData>(argOrOptions, options)
488
- const latestDefinedRef = computed<TData>(() => {
489
- const latest = latestRef.value
490
- if (latest === undefined) {
491
- throw new Error("Internal Error: suspense resolved without a latest value")
492
- }
493
- return latest
494
- })
495
-
496
- const isMounted = ref(true)
497
- onBeforeUnmount(() => {
498
- isMounted.value = false
517
+ const [
518
+ [resultRef, latestDefinedRef, fetch, uqrt],
519
+ isMounted,
520
+ signal
521
+ ] = useScopedSuspenseSetup(() => {
522
+ const [resultRef, latestRef, fetch, uqrt] = q<TData>(argOrOptions, options)
523
+ const latestDefinedRef = computed<TData>(() => {
524
+ const latest = latestRef.value
525
+ if (latest === undefined) {
526
+ throw new Error("Internal Error: suspense resolved without a latest value")
527
+ }
528
+ return latest
529
+ })
530
+ return [resultRef, latestDefinedRef, fetch, uqrt] as const
499
531
  })
500
532
 
501
533
  // @effect-diagnostics effect/missingEffectError:off
@@ -511,7 +543,7 @@ export class QueryImpl<R> {
511
543
  return [resultRef, latestDefinedRef, fetch, uqrt] as const
512
544
  })
513
545
 
514
- return runPromise(eff)
546
+ return runPromise(eff, { signal })
515
547
  }
516
548
  }
517
549
 
@@ -539,18 +571,16 @@ export class QueryImpl<R> {
539
571
  argOrOptions: I | WatchSource<I>,
540
572
  options?: AtomQueryNewOptions<A, TData>
541
573
  ) => {
542
- const view = q<TData>(argOrOptions, options)
543
- const data = computed<TData>(() => {
544
- const latest = view.data.value
545
- if (latest === undefined) {
546
- throw new Error("Internal Error: suspenseNew resolved without a latest value")
547
- }
548
- return latest
549
- })
550
-
551
- const isMounted = ref(true)
552
- onBeforeUnmount(() => {
553
- isMounted.value = false
574
+ const [{ view, data }, isMounted, signal] = useScopedSuspenseSetup(() => {
575
+ const view = q<TData>(argOrOptions, options)
576
+ const data = computed<TData>(() => {
577
+ const latest = view.data.value
578
+ if (latest === undefined) {
579
+ throw new Error("Internal Error: suspenseNew resolved without a latest value")
580
+ }
581
+ return latest
582
+ })
583
+ return { view, data }
554
584
  })
555
585
 
556
586
  // @effect-diagnostics effect/missingEffectError:off
@@ -585,7 +615,7 @@ export class QueryImpl<R> {
585
615
  )
586
616
  })
587
617
 
588
- return runPromise(eff)
618
+ return runPromise(eff, { signal })
589
619
  }
590
620
  }
591
621
  }
@@ -694,6 +724,14 @@ export const makeClient = <RT_, RTHooks>(
694
724
  const queryInvalidator = legacyQueryEngine === "tanstack"
695
725
  ? combineQueryInvalidators(atomInvalidator, makeTanstackQueryInvalidator(getTanstackQueryClient()))
696
726
  : atomInvalidator
727
+ setQueryCacheUpdater(
728
+ legacyQueryEngine === "tanstack"
729
+ ? combineQueryCacheUpdaters(
730
+ makeTanstackQueryCacheUpdater(getTanstackQueryClient()),
731
+ optionalAtomQueryCacheUpdater
732
+ )
733
+ : atomQueryCacheUpdater
734
+ )
697
735
 
698
736
  let m: ReturnType<typeof useMutationInt>
699
737
  const useMutation = () => m ??= useMutationInt(queryInvalidator)
@@ -742,10 +780,16 @@ export const makeClient = <RT_, RTHooks>(
742
780
  queryNew: useStreamQueryNew(handler)
743
781
  })
744
782
 
745
- const projectQueryFor = <I, A, E, Request extends Req, Name extends string>(
783
+ const projectQueryFor = <
784
+ I,
785
+ A,
786
+ E,
787
+ Request extends Req & { readonly success: S.Top & { readonly "EncodingServices": RT } },
788
+ Name extends string
789
+ >(
746
790
  handler: RequestHandlerWithInput<I, A, E, RT, Request, Name>
747
791
  ) =>
748
- <ProjSchema extends S.Decoder<unknown, never>>(projectionSchema: ProjSchema) => {
792
+ <ProjSchema extends S.Top & { readonly "DecodingServices": RT }>(projectionSchema: ProjSchema) => {
749
793
  const successSchema = handler.Request.success
750
794
  const projectionHash = projectionSchemaHash(projectionSchema)
751
795
  const projected = projectHandler(handler.handler, successSchema, projectionSchema)
package/src/query.ts CHANGED
@@ -16,7 +16,7 @@ import * as Exit from "effect/Exit"
16
16
  import * as Stream from "effect/Stream"
17
17
  import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
18
18
  import * as Atom from "effect/unstable/reactivity/Atom"
19
- import { computed, type ComputedRef, type MaybeRefOrGetter, onBeforeUnmount, onMounted, ref, toValue, type WatchSource } from "vue"
19
+ import { computed, type ComputedRef, effectScope, type MaybeRefOrGetter, onBeforeUnmount, onMounted, onScopeDispose, ref, toValue, type WatchSource } from "vue"
20
20
  import { type AtomClientRuntime, type AtomQueryOptions, awaitAtomResult, buildQueryFamily, buildStreamQueryFamily, disabledQueryAtom, isStaleResult, staleTimeMsOf, withQueryOptions } from "./atomQuery.ts"
21
21
 
22
22
  // --- minimal local types (replacing the former @tanstack/vue-query type imports) ---
@@ -40,6 +40,38 @@ export interface QueryView<A, E> extends QueryHandle<A, E> {
40
40
  readonly data: ComputedRef<A | undefined>
41
41
  }
42
42
 
43
+ const useScopedSuspenseSetup = <A>(setup: () => A) => {
44
+ const scope = effectScope()
45
+ const controller = new AbortController()
46
+ const value = scope.run(setup)
47
+ if (value === undefined) {
48
+ throw new Error("Internal Error: atom suspense setup scope did not initialize")
49
+ }
50
+
51
+ const isMounted = ref(true)
52
+ let stopped = false
53
+ const stop = () => {
54
+ if (stopped) return
55
+ stopped = true
56
+ isMounted.value = false
57
+ controller.abort()
58
+ scope.stop()
59
+ }
60
+ onBeforeUnmount(stop)
61
+ onScopeDispose(stop)
62
+
63
+ return [value, isMounted, controller.signal] as const
64
+ }
65
+
66
+ export interface QueryCacheUpdater {
67
+ readonly update: <I, A, E, R, Request extends Req, Name extends string>(
68
+ registry: ReturnType<typeof injectRegistry>,
69
+ query: RequestHandlerWithInput<I, A, E, R, Request, Name>,
70
+ input: I,
71
+ updater: (data: NoInfer<A>) => NoInfer<A>
72
+ ) => void
73
+ }
74
+
43
75
  // retained generic aliases so the exported option-interface arity is unchanged for consumers
44
76
  export type UseQueryReturnType<A = any, E = any> = QueryHandle<A, E>
45
77
  export type UseQueryDefinedReturnType<A = any, E = any> = QueryHandle<A, E>
@@ -111,6 +143,35 @@ const getStreamQueryFamily = <I, A, E>(
111
143
  return f
112
144
  }
113
145
 
146
+ const makeAtomQueryCacheUpdater = (warnIfMissing: boolean): QueryCacheUpdater => ({
147
+ update: (registry, query, input) => {
148
+ const family = queryFamilyByKey.get(queryFamilyCacheKey(query))
149
+ if (!family) {
150
+ if (warnIfMissing) {
151
+ console.warn(`Query ${query.id} has not been used yet; nothing to update`)
152
+ }
153
+ return
154
+ }
155
+ registry.refresh(family(input))
156
+ }
157
+ })
158
+
159
+ export const atomQueryCacheUpdater = makeAtomQueryCacheUpdater(true)
160
+ export const optionalAtomQueryCacheUpdater = makeAtomQueryCacheUpdater(false)
161
+
162
+ export const combineQueryCacheUpdaters = (first: QueryCacheUpdater, second: QueryCacheUpdater): QueryCacheUpdater => ({
163
+ update: (registry, query, input, updater) => {
164
+ first.update(registry, query, input, updater)
165
+ second.update(registry, query, input, updater)
166
+ }
167
+ })
168
+
169
+ let activeQueryCacheUpdater = atomQueryCacheUpdater
170
+
171
+ export const setQueryCacheUpdater = (updater: QueryCacheUpdater) => {
172
+ activeQueryCacheUpdater = updater
173
+ }
174
+
114
175
  // Atom-engine query options (formerly reconstructed from @tanstack/vue-query types).
115
176
  // The generic arity is kept so the exported interface signatures are unchanged for consumers.
116
177
  export interface CustomUseQueryOptions<
@@ -252,18 +313,16 @@ export const useAtomQuery = <A, E>(
252
313
  export const useAtomSuspense = <A, E>(
253
314
  atom: () => Atom.Atom<AsyncResult.AsyncResult<A, E>>
254
315
  ): Promise<SuspenseQueryView<A, E>> => {
255
- const view = useAtomQuery(atom)
256
- const data = computed<A>(() => {
257
- const latest = view.data.value
258
- if (latest === undefined) {
259
- throw new Error("Internal Error: atom suspense resolved without a latest value")
260
- }
261
- return latest
262
- })
263
-
264
- const isMounted = ref(true)
265
- onBeforeUnmount(() => {
266
- isMounted.value = false
316
+ const [{ view, data }, isMounted, signal] = useScopedSuspenseSetup(() => {
317
+ const view = useAtomQuery(atom)
318
+ const data = computed<A>(() => {
319
+ const latest = view.data.value
320
+ if (latest === undefined) {
321
+ throw new Error("Internal Error: atom suspense resolved without a latest value")
322
+ }
323
+ return latest
324
+ })
325
+ return { view, data }
267
326
  })
268
327
 
269
328
  const eff = Effect.gen(function*() {
@@ -297,7 +356,7 @@ export const useAtomSuspense = <A, E>(
297
356
  )
298
357
  })
299
358
 
300
- return Effect.runPromise(eff)
359
+ return Effect.runPromise(eff, { signal })
301
360
  }
302
361
 
303
362
  export type StreamQueryPullValue<A> = {
@@ -795,22 +854,12 @@ export function composeQueries<
795
854
  export const useUpdateQuery = () => {
796
855
  const registry = injectRegistry()
797
856
 
798
- // NOTE: query atoms are derived (read-only) here, so unlike tanstack's `setQueryData` we can't
799
- // optimistically patch the cache in place — this refetches the query (the `updater` is ignored).
800
- // A first-class optimistic-update layer is planned for the atom-native redesign.
801
857
  const f: {
802
- <I, A>(
803
- query: RequestHandlerWithInput<I, A, any, any, any, any>,
858
+ <I, A, E, R, Request extends Req, Name extends string>(
859
+ query: RequestHandlerWithInput<I, A, E, R, Request, Name>,
804
860
  input: I,
805
861
  updater: (data: NoInfer<A>) => NoInfer<A>
806
862
  ): void
807
- } = (query: any, input: any, _updater: any) => {
808
- const family = queryFamilyByKey.get(queryFamilyCacheKey(query))
809
- if (!family) {
810
- console.warn(`Query ${query.id} has not been used yet; nothing to update`)
811
- return
812
- }
813
- registry.refresh(family(input))
814
- }
863
+ } = (query, input, updater) => activeQueryCacheUpdater.update(registry, query, input, updater)
815
864
  return f
816
865
  }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patchableQueryAtom.test.d.ts","sourceRoot":"","sources":["../patchableQueryAtom.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queryOptions.test.d.ts","sourceRoot":"","sources":["../queryOptions.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retrySchedule.test.d.ts","sourceRoot":"","sources":["../retrySchedule.test.ts"],"names":[],"mappings":""}
@@ -1,8 +1,14 @@
1
+ import { defaultRegistry, registryKey } from "@effect/atom-vue"
1
2
  import { Effect, Option } from "effect-app"
2
3
  import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
3
- import { computed, ref } from "vue"
4
+ import * as Atom from "effect/unstable/reactivity/Atom"
5
+ import { computed, createApp, nextTick, ref } from "vue"
6
+ import { withQueryOptions } from "../src/atomQuery.js"
7
+ import { useAtomQuery, useAtomSuspense } from "../src/query.js"
4
8
  import { awaitResolvedSuspenseResult } from "../src/suspense.js"
5
9
 
10
+ const listenerCount = (atom: Atom.Atom<unknown>) => defaultRegistry.getNodes().get(atom)?.listeners.size ?? 0
11
+
6
12
  it("waits for the query result ref after suspense resolves", async () => {
7
13
  const result = ref<AsyncResult.AsyncResult<number, never>>(AsyncResult.initial(true))
8
14
  const promise = Effect.runPromise(awaitResolvedSuspenseResult(computed(() => result.value)))
@@ -18,3 +24,137 @@ it("keeps unresolved query results initial", async () => {
18
24
 
19
25
  expect(AsyncResult.isInitial(settled)).toBe(true)
20
26
  })
27
+
28
+ it("stops atom suspense subscriptions when the setup scope is disposed", async () => {
29
+ defaultRegistry.reset()
30
+ const atom = Atom.make(Effect.succeed(123))
31
+ let promise: ReturnType<typeof useAtomSuspense<number, never>> | undefined
32
+ const host = document.createElement("div")
33
+ const app = createApp({
34
+ setup() {
35
+ promise = useAtomSuspense(() => atom)
36
+ return () => null
37
+ }
38
+ })
39
+ app.provide(registryKey, defaultRegistry)
40
+ app.mount(host)
41
+
42
+ if (promise === undefined) {
43
+ throw new Error("suspense setup did not initialize")
44
+ }
45
+
46
+ await promise
47
+ expect(listenerCount(atom)).toBe(1)
48
+
49
+ app.unmount()
50
+ await nextTick()
51
+ await new Promise((resolve) => setTimeout(resolve, 0))
52
+ expect(listenerCount(atom)).toBe(0)
53
+ defaultRegistry.reset()
54
+ })
55
+
56
+ it("does not keep query option wrapper subscriptions after unmount", async () => {
57
+ defaultRegistry.reset()
58
+ const atom = Atom.make(Effect.succeed(123))
59
+ const observed = withQueryOptions(atom)
60
+ let promise: ReturnType<typeof useAtomSuspense<number, never>> | undefined
61
+ const host = document.createElement("div")
62
+ const app = createApp({
63
+ setup() {
64
+ promise = useAtomSuspense(() => observed)
65
+ return () => null
66
+ }
67
+ })
68
+ app.provide(registryKey, defaultRegistry)
69
+ app.mount(host)
70
+
71
+ if (promise === undefined) {
72
+ throw new Error("suspense setup did not initialize")
73
+ }
74
+
75
+ await promise
76
+ expect(listenerCount(atom)).toBe(1)
77
+
78
+ app.unmount()
79
+ await nextTick()
80
+ await new Promise((resolve) => setTimeout(resolve, 0))
81
+ expect(listenerCount(atom)).toBe(0)
82
+ defaultRegistry.reset()
83
+ })
84
+
85
+ it("aborts atom suspense promises when the component unmounts", async () => {
86
+ defaultRegistry.reset()
87
+ const atom = Atom.make(Effect.never)
88
+ let promise: ReturnType<typeof useAtomSuspense<number, never>> | undefined
89
+ const host = document.createElement("div")
90
+ const app = createApp({
91
+ setup() {
92
+ promise = useAtomSuspense(() => atom)
93
+ return () => null
94
+ }
95
+ })
96
+ app.provide(registryKey, defaultRegistry)
97
+ app.mount(host)
98
+
99
+ if (promise === undefined) {
100
+ throw new Error("suspense setup did not initialize")
101
+ }
102
+
103
+ app.unmount()
104
+ await nextTick()
105
+ const settled = await Promise.race([
106
+ promise.then(() => "resolved" as const, () => "rejected" as const),
107
+ new Promise<"pending">((resolve) => setTimeout(() => resolve("pending"), 20))
108
+ ])
109
+ expect(settled).toBe("rejected")
110
+ defaultRegistry.reset()
111
+ })
112
+
113
+ it("stops atom query subscriptions when the component unmounts", async () => {
114
+ defaultRegistry.reset()
115
+ const atom = Atom.make(Effect.succeed(123))
116
+ const host = document.createElement("div")
117
+ const app = createApp({
118
+ setup() {
119
+ useAtomQuery(() => atom)
120
+ return () => null
121
+ }
122
+ })
123
+ app.provide(registryKey, defaultRegistry)
124
+ app.mount(host)
125
+
126
+ await nextTick()
127
+ expect(listenerCount(atom)).toBe(1)
128
+
129
+ app.unmount()
130
+ await nextTick()
131
+ await new Promise((resolve) => setTimeout(resolve, 0))
132
+ expect(listenerCount(atom)).toBe(0)
133
+ defaultRegistry.reset()
134
+ })
135
+
136
+ it("does not keep non-suspense query option wrapper subscriptions after unmount", async () => {
137
+ defaultRegistry.reset()
138
+ const atom = Atom.make(Effect.succeed(123))
139
+ const observed = withQueryOptions(atom)
140
+ const host = document.createElement("div")
141
+ const app = createApp({
142
+ setup() {
143
+ useAtomQuery(() => observed)
144
+ return () => null
145
+ }
146
+ })
147
+ app.provide(registryKey, defaultRegistry)
148
+ app.mount(host)
149
+
150
+ await nextTick()
151
+ expect(listenerCount(observed)).toBe(1)
152
+ expect(listenerCount(atom)).toBe(1)
153
+
154
+ app.unmount()
155
+ await nextTick()
156
+ await new Promise((resolve) => setTimeout(resolve, 0))
157
+ expect(listenerCount(observed)).toBe(0)
158
+ expect(listenerCount(atom)).toBe(0)
159
+ defaultRegistry.reset()
160
+ })