@effect-app/vue 4.0.4 → 4.0.6

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
@@ -23,9 +23,11 @@ import * as S from "effect-app/Schema"
23
23
  import * as Cause from "effect/Cause"
24
24
  import * as Duration from "effect/Duration"
25
25
  import * as Equal from "effect/Equal"
26
- import * as Hash from "effect/Hash"
26
+ import * as Exit from "effect/Exit"
27
27
  import { isHttpClientError } from "effect/http/HttpClientError"
28
28
  import type * as Layer from "effect/Layer"
29
+ import * as MutableHashMap from "effect/MutableHashMap"
30
+ import * as MutableHashSet from "effect/MutableHashSet"
29
31
  import * as AsyncResult from "effect/reactivity/AsyncResult"
30
32
  import * as Atom from "effect/reactivity/Atom"
31
33
  import * as AtomRegistry from "effect/reactivity/AtomRegistry"
@@ -34,7 +36,7 @@ import * as Stream from "effect/Stream"
34
36
  import type * as Tracer from "effect/Tracer"
35
37
  import { clearQueryReadDependencies, getQueryReadDependencies, type QueryInvalidationMode, registerQueryInvalidationMode, setQueryReadDependencies } from "./dependencyMetadata.ts"
36
38
  import { reportRuntimeError } from "./lib.ts"
37
- import { beginLiveQueryFetch, endLiveQueryFetch, type LiveQueryOptions, registerLiveQuery } from "./liveQueryInvalidation.ts"
39
+ import { acquireLiveQueryFetch, awaitLiveQueryFetch, type LiveQueryOptions, registerLiveQuery, releaseLiveQueryFetch } from "./liveQueryInvalidation.ts"
38
40
  import { atomsToRefetch, observeQueryAtom } from "./queryLifetime.ts"
39
41
 
40
42
  /** All non-empty prefixes of a key, longest last. `[a,b,c]` -> `[[a],[a,b],[a,b,c]]`. */
@@ -43,11 +45,10 @@ const prefixesOf = (key: ReadonlyArray<unknown>): ReadonlyArray<ReadonlyArray<un
43
45
 
44
46
  const uniqueKeys = (keys: ReadonlyArray<ReadonlyArray<unknown>>): ReadonlyArray<ReadonlyArray<unknown>> => {
45
47
  const out: Array<ReadonlyArray<unknown>> = []
46
- const seen = new Set<number>()
48
+ const seen = MutableHashSet.empty<ReadonlyArray<unknown>>()
47
49
  for (const key of keys) {
48
- const hash = Hash.hash(key)
49
- if (seen.has(hash)) continue
50
- seen.add(hash)
50
+ if (MutableHashSet.has(seen, key)) continue
51
+ MutableHashSet.add(seen, key)
51
52
  out.push(key)
52
53
  }
53
54
  return out
@@ -58,20 +59,21 @@ const uniqueKeys = (keys: ReadonlyArray<ReadonlyArray<unknown>>): ReadonlyArray<
58
59
  // alive in the registry (mounted OR cached within idle-ttl). Invalidation *marks* every matching
59
60
  // atom stale; it only *refetches* atoms that currently have observers (TanStack refetchType:
60
61
  // "active"). Idle cache entries refetch on the next mount.
61
- const keyAtoms = new Map<number, Set<Atom.Atom<AsyncResult.AsyncResult<any, any>>>>()
62
+ const keyAtoms = MutableHashMap.empty<ReadonlyArray<unknown>, Set<Atom.Atom<AsyncResult.AsyncResult<any, any>>>>()
62
63
 
63
64
  const trackByKeys =
64
65
  (keys: ReadonlyArray<ReadonlyArray<unknown>>) =>
65
66
  <A, E>(atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>): Atom.Atom<AsyncResult.AsyncResult<A, E>> =>
66
67
  Atom.transform(atom, (get) => {
67
68
  for (const key of keys) {
68
- const h = Hash.hash(key)
69
- let set = keyAtoms.get(h)
70
- if (!set) keyAtoms.set(h, set = new Set())
69
+ let set = Option.getOrUndefined(MutableHashMap.get(keyAtoms, key))
70
+ if (!set) MutableHashMap.set(keyAtoms, key, set = new Set())
71
71
  set.add(atom)
72
72
  get.addFinalizer(() => {
73
73
  set.delete(atom)
74
- if (set.size === 0) keyAtoms.delete(h)
74
+ if (set.size === 0 && Option.getOrUndefined(MutableHashMap.get(keyAtoms, key)) === set) {
75
+ MutableHashMap.remove(keyAtoms, key)
76
+ }
75
77
  })
76
78
  }
77
79
  return get(atom)
@@ -111,7 +113,7 @@ const trackReadDependencies =
111
113
  const atomsForKeys = (keys: ReadonlyArray<unknown>): ReadonlyArray<Atom.Atom<AsyncResult.AsyncResult<any, any>>> => {
112
114
  const atoms = new Set<Atom.Atom<AsyncResult.AsyncResult<any, any>>>()
113
115
  for (const key of keys) {
114
- const set = keyAtoms.get(Hash.hash(key))
116
+ const set = Option.getOrUndefined(MutableHashMap.get(keyAtoms, key))
115
117
  if (set) { for (const a of set) atoms.add(a) }
116
118
  }
117
119
  return [...atoms]
@@ -377,6 +379,16 @@ const recoverStuckWaitingOnMount =
377
379
  return current
378
380
  }, { initialValueTarget: wrapped })
379
381
 
382
+ /** Runs `setup` once per mount (not per result change) and forwards the wrapped atom's results. */
383
+ const withMountLifetime =
384
+ <A, E>(setup: (get: Atom.AtomContext) => void) =>
385
+ (atom: Atom.Atom<AsyncResult.AsyncResult<A, E>>): Atom.Atom<AsyncResult.AsyncResult<A, E>> =>
386
+ Atom.transform(atom, (get) => {
387
+ setup(get)
388
+ get.subscribe(atom, (value) => get.setSelf(value))
389
+ return get.once(atom)
390
+ }, { initialValueTarget: atom })
391
+
380
392
  export const withQueryOptions = <A, E>(
381
393
  self: Atom.Atom<AsyncResult.AsyncResult<A, E>>,
382
394
  opts: AtomQueryOptions = {},
@@ -386,23 +398,15 @@ export const withQueryOptions = <A, E>(
386
398
  const staleTime: Duration.Input = opts.staleTime ?? defaults.staleTime
387
399
  let atom = self
388
400
  if (liveKey !== undefined) {
389
- atom = Atom.transform(atom, (get) => {
390
- const unregister = registerQueryInvalidationMode(liveKey, opts.invalidation ?? "await")
391
- get.addFinalizer(unregister)
392
- return get(self)
393
- }, { initialValueTarget: self })
401
+ atom = withMountLifetime<A, E>((get) => {
402
+ get.addFinalizer(registerQueryInvalidationMode(liveKey, opts.invalidation ?? "await"))
403
+ })(atom)
394
404
  }
395
405
  if (opts.live && liveKey !== undefined) {
396
406
  const liveOptions = opts.live === true ? {} : opts.live
397
- atom = Atom.transform(atom, (get) => {
398
- const unregister = registerLiveQuery(
399
- liveKey,
400
- () => getQueryReadDependencies(liveKey),
401
- liveOptions
402
- )
403
- get.addFinalizer(unregister)
404
- return get(self)
405
- }, { initialValueTarget: self })
407
+ atom = withMountLifetime<A, E>((get) => {
408
+ get.addFinalizer(registerLiveQuery(liveKey, () => getQueryReadDependencies(liveKey), liveOptions))
409
+ })(atom)
406
410
  }
407
411
  const revalidateOnFocus = opts.revalidateOnFocus ?? true
408
412
  atom = Atom.swr({
@@ -475,6 +479,10 @@ export const buildQueryFamily = <I, A, E>(
475
479
 
476
480
  return Atom.family((input: I) => {
477
481
  const fullKey = [...baseKey, input]
482
+ // metadata/live identity: projections of one handler read different data
483
+ const projectedFullKey = self.queryKeyProjectionHash === undefined
484
+ ? fullKey
485
+ : [...baseKey, self.queryKeyProjectionHash, input]
478
486
  // Record the repository/server read-dependencies seen while fetching, keyed by `fullKey`, so a
479
487
  // later mutation whose writes intersect them can derive this query as an invalidation target.
480
488
  // The last recorded reads are retained on the (memoized) family atom so `trackReadDependencies`
@@ -495,21 +503,34 @@ export const buildQueryFamily = <I, A, E>(
495
503
  const readsRef = yield* Ref.make(DataDependencies.empty())
496
504
  const writesRef = yield* Ref.make(DataDependencies.empty())
497
505
  const recorder = DataDependencies.makeDataDependencyRecorder(readsRef, writesRef)
498
- const result = yield* self
506
+ return yield* self
499
507
  .handler(input)
500
- .pipe(Effect.provideService(DataDependencies.DataDependencyRecorder, recorder))
501
- lastReads = yield* Ref.get(readsRef)
502
- setQueryReadDependencies(fullKey, lastReads)
503
- return result
508
+ .pipe(
509
+ Effect.provideService(DataDependencies.DataDependencyRecorder, recorder),
510
+ // a failure (e.g. not found) still depends on what it read; keep a displayed success's reads
511
+ Effect.onExit((exit) =>
512
+ Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)
513
+ ? Effect.void
514
+ : Ref.get(readsRef).pipe(Effect.map((reads) => {
515
+ lastReads = Exit.isSuccess(exit) ? reads : DataDependencies.merge(lastReads, reads)
516
+ setQueryReadDependencies(projectedFullKey, lastReads)
517
+ }))
518
+ )
519
+ )
504
520
  })
505
- let liveFetch = false
506
521
  const effect = Effect
507
- .gen(function*() {
508
- liveFetch = yield* Effect.promise(() => beginLiveQueryFetch(fullKey))
509
- return yield* recordReads.pipe(Effect.retry({ times: 5, while: isRetryable }))
510
- })
522
+ .acquireUseRelease(
523
+ Effect.sync(() => acquireLiveQueryFetch(projectedFullKey)),
524
+ (live) =>
525
+ (live === undefined ? Effect.void : Effect.promise((signal) => awaitLiveQueryFetch(live, signal))).pipe(
526
+ Effect.andThen(recordReads.pipe(Effect.retry({ times: 5, while: isRetryable })))
527
+ ),
528
+ (live) =>
529
+ Effect.sync(() => {
530
+ if (live !== undefined) releaseLiveQueryFetch(live)
531
+ })
532
+ )
511
533
  .pipe(
512
- Effect.ensuring(Effect.sync(() => endLiveQueryFetch(liveFetch))),
513
534
  Effect.tapCauseIf(Cause.hasDies, (cause) => reportRuntimeError(cause)),
514
535
  // On exit, the compute is no longer in-flight. An interrupt (subscriber lost interest / a
515
536
  // superseding refresh) may leave the result at `waiting`; with `inFlight` back at 0 that
@@ -531,13 +552,10 @@ export const buildQueryFamily = <I, A, E>(
531
552
  // Register under every prefix of the full key => hierarchical (prefix) invalidation. Two roles:
532
553
  // - withReactivity: `Reactivity.invalidate(key)` refreshes this atom (the actual refetch).
533
554
  // - trackByKeys: records the atom in `keyAtoms` so the mutation can AWAIT its settle.
534
- const projectedFullKey = self.queryKeyProjectionHash === undefined
535
- ? fullKey
536
- : [...baseKey, self.queryKeyProjectionHash, input]
537
555
  const reactivityKeys = uniqueKeys([...prefixesOf(fullKey), ...prefixesOf(projectedFullKey)])
538
556
  atom = rt.factory.withReactivity(reactivityKeys)(atom)
539
557
  atom = trackByKeys(reactivityKeys)(atom)
540
- atom = trackReadDependencies(fullKey, () => lastReads)(atom)
558
+ atom = trackReadDependencies(projectedFullKey, () => lastReads)(atom)
541
559
  // gcTime LAST so the whole chain (incl. the registration + tracking) stays alive through the
542
560
  // idle window. Invalidation still finds the cached atom and marks it stale; it does not
543
561
  // refetch until an observer remounts (TanStack refetchType: "active").
@@ -558,8 +576,8 @@ export const buildQueryFamily = <I, A, E>(
558
576
  const observed = observeQueryAtom(result)
559
577
  queryFetchStates.set(result, fetchState)
560
578
  queryFetchStates.set(observed, fetchState)
561
- atomQueryKeys.set(result, fullKey)
562
- atomQueryKeys.set(observed, fullKey)
579
+ atomQueryKeys.set(result, projectedFullKey)
580
+ atomQueryKeys.set(observed, projectedFullKey)
563
581
  return observed
564
582
  })
565
583
  }
@@ -1,37 +1,42 @@
1
1
  import { DataDependencies } from "effect-app/client"
2
- import * as Hash from "effect/Hash"
2
+ import * as MutableHashMap from "effect/MutableHashMap"
3
+ import * as Option from "effect/Option"
3
4
 
4
5
  // Maps a live query's reactivity key (its full `[...queryKey, input]` key) to the
5
6
  // data dependencies it read while fetching. Populated when the query handler resolves and
6
7
  // cleared when the query atom is GC'd, so the set mirrors the live query atoms (mounted or
7
8
  // cached-within-ttl) — the atom equivalent of the former tanstack query cache.
8
9
  type Entry = { readonly key: ReadonlyArray<unknown>; readonly reads: DataDependencies.DataDependencies }
9
- const readDependencies = new Map<number, Entry>()
10
+ const readDependencies = MutableHashMap.empty<ReadonlyArray<unknown>, Entry>()
10
11
  export type QueryInvalidationMode = "await" | "soft"
11
12
  type InvalidationModeEntry = { awaitSubscribers: number; softSubscribers: number }
12
- const invalidationModes = new Map<number, InvalidationModeEntry>()
13
+ const invalidationModes = MutableHashMap.empty<ReadonlyArray<unknown>, InvalidationModeEntry>()
13
14
 
14
15
  export const registerQueryInvalidationMode = (
15
16
  key: ReadonlyArray<unknown>,
16
17
  mode: QueryInvalidationMode
17
18
  ): () => void => {
18
- const hash = Hash.hash(key)
19
- const entry = invalidationModes.get(hash) ?? { awaitSubscribers: 0, softSubscribers: 0 }
19
+ const entry = Option.getOrUndefined(MutableHashMap.get(invalidationModes, key))
20
+ ?? { awaitSubscribers: 0, softSubscribers: 0 }
20
21
  if (mode === "soft") entry.softSubscribers++
21
22
  else entry.awaitSubscribers++
22
- invalidationModes.set(hash, entry)
23
+ MutableHashMap.set(invalidationModes, key, entry)
23
24
  let active = true
24
25
  return () => {
25
26
  if (!active) return
26
27
  active = false
27
28
  if (mode === "soft") entry.softSubscribers--
28
29
  else entry.awaitSubscribers--
29
- if (entry.awaitSubscribers === 0 && entry.softSubscribers === 0) invalidationModes.delete(hash)
30
+ if (
31
+ entry.awaitSubscribers === 0
32
+ && entry.softSubscribers === 0
33
+ && Option.getOrUndefined(MutableHashMap.get(invalidationModes, key)) === entry
34
+ ) MutableHashMap.remove(invalidationModes, key)
30
35
  }
31
36
  }
32
37
 
33
38
  export const getQueryInvalidationMode = (key: ReadonlyArray<unknown>): QueryInvalidationMode => {
34
- const entry = invalidationModes.get(Hash.hash(key))
39
+ const entry = Option.getOrUndefined(MutableHashMap.get(invalidationModes, key))
35
40
  return entry !== undefined && entry.awaitSubscribers === 0 && entry.softSubscribers > 0 ? "soft" : "await"
36
41
  }
37
42
 
@@ -39,18 +44,18 @@ export const setQueryReadDependencies = (
39
44
  key: ReadonlyArray<unknown>,
40
45
  reads: DataDependencies.DataDependencies
41
46
  ) => {
42
- const h = Hash.hash(key)
43
- if (!DataDependencies.isNonEmpty(reads)) readDependencies.delete(h)
44
- else readDependencies.set(h, { key, reads })
47
+ if (!DataDependencies.isNonEmpty(reads)) MutableHashMap.remove(readDependencies, key)
48
+ else MutableHashMap.set(readDependencies, key, { key, reads })
45
49
  }
46
50
 
47
51
  export const clearQueryReadDependencies = (key: ReadonlyArray<unknown>) => {
48
- readDependencies.delete(Hash.hash(key))
52
+ MutableHashMap.remove(readDependencies, key)
49
53
  }
50
54
 
51
55
  export const getQueryReadDependencies = (
52
56
  key: ReadonlyArray<unknown>
53
- ): DataDependencies.DataDependencies => readDependencies.get(Hash.hash(key))?.reads ?? DataDependencies.empty()
57
+ ): DataDependencies.DataDependencies =>
58
+ Option.getOrUndefined(MutableHashMap.get(readDependencies, key))?.reads ?? DataDependencies.empty()
54
59
 
55
60
  /**
56
61
  * Reactivity keys of every live query whose recorded read-dependencies intersect this
@@ -62,7 +67,7 @@ export const getDerivedInvalidationKeys = (
62
67
  ): ReadonlyArray<ReadonlyArray<unknown>> => {
63
68
  if (!DataDependencies.isNonEmpty(writeDependencies)) return []
64
69
  const keys: Array<ReadonlyArray<unknown>> = []
65
- for (const { key, reads } of readDependencies.values()) {
70
+ for (const { key, reads } of MutableHashMap.values(readDependencies)) {
66
71
  if (DataDependencies.intersects(reads, writeDependencies)) keys.push(key)
67
72
  }
68
73
  return keys