@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/CHANGELOG.md +26 -0
- package/dist/atomQuery.d.ts.map +1 -1
- package/dist/atomQuery.js +48 -41
- package/dist/dependencyMetadata.d.ts.map +1 -1
- package/dist/dependencyMetadata.js +18 -16
- package/dist/liveQueryInvalidation.d.ts +13 -0
- package/dist/liveQueryInvalidation.d.ts.map +1 -1
- package/dist/liveQueryInvalidation.js +340 -63
- package/package.json +2 -2
- package/src/atomQuery.ts +61 -43
- package/src/dependencyMetadata.ts +19 -14
- package/src/liveQueryInvalidation.ts +337 -58
- package/test/dist/liveQueryLifecycle.test.d.ts.map +1 -0
- package/test/liveQueryInvalidation.test.ts +198 -5
- package/test/liveQueryLifecycle.test.ts +507 -0
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
|
|
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 {
|
|
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 =
|
|
48
|
+
const seen = MutableHashSet.empty<ReadonlyArray<unknown>>()
|
|
47
49
|
for (const key of keys) {
|
|
48
|
-
|
|
49
|
-
|
|
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 =
|
|
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
|
-
|
|
69
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
390
|
-
|
|
391
|
-
|
|
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 =
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
506
|
+
return yield* self
|
|
499
507
|
.handler(input)
|
|
500
|
-
.pipe(
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
-
.
|
|
508
|
-
|
|
509
|
-
|
|
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(
|
|
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,
|
|
562
|
-
atomQueryKeys.set(observed,
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
19
|
-
|
|
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
|
-
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
52
|
+
MutableHashMap.remove(readDependencies, key)
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
export const getQueryReadDependencies = (
|
|
52
56
|
key: ReadonlyArray<unknown>
|
|
53
|
-
): DataDependencies.DataDependencies =>
|
|
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
|
|
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
|