Skip to content
Source
Sign inSign up

revisionedQuery.ts

80 lines3.5 KB

1import * as Option from "effect/Option";
2import { Atom, type AtomRegistry } from "effect/reactivity";
3​
4// Outside the nodes: a node is dropped once unmounted, and the next read must
5// still compare against the revision the query was last read under.
6const readRevisions = new WeakMap<AtomRegistry.AtomRegistry, WeakMap<object, string | null>>();
7​
8// The registry keys serializable atoms' nodes by their key, not the atom.
9const hasNode = (registry: AtomRegistry.AtomRegistry, atom: Atom.Atom<unknown>) =>
10 registry.getNodes().has(Atom.isSerializable(atom) ? atom[Atom.SerializableTypeId].key : atom);
11​
12// A query's own node can go with its last reader while the atom it takes its value from
13// (an environment query's cache, on an idle TTL) still holds the value.
14const isCached = (registry: AtomRegistry.AtomRegistry, query: Atom.Atom<unknown>) =>
15 hasNode(registry, query) ||
16 (query.initialValueTarget !== undefined && hasNode(registry, query.initialValueTarget));
17​
18const makeRevisionedFamily = (query: Atom.Atom<unknown>) =>
19 Atom.family((revision: string | null) =>
20 Atom.readable(
21 (get) => {
22 // Only a node's first build compares: a node still mounted under the old
23 // revision rebuilds when the query refetches, and must not refetch it again.
24 if (Option.isNone(get.self())) {
25 let revisions = readRevisions.get(get.registry);
26 if (revisions === undefined) {
27 revisions = new WeakMap();
28 readRevisions.set(get.registry, revisions);
29 }
30 const stale = revisions.has(query)
31 ? revisions.get(query) !== revision
32 : // A reader without a revision may have cached it before the latest
33 // workspace change; a query nobody has read yet fetches fresh anyway.
34 revision !== null && isCached(get.registry, query);
35 // Deferred: this builds while React renders, and a refresh would synchronously
36 // re-render the query's other readers, which React rejects.
37 if (stale) {
38 const registry = get.registry;
39 queueMicrotask(() => {
40 // No node, as after the registry is disposed: nothing cached to refetch.
41 if (isCached(registry, query)) registry.refresh(query);
42 });
43 }
44 revisions.set(query, revision);
45 }
46 return get(query);
47 },
48 // Refreshing the reader refetches the query it reads.
49 (refresh) => refresh(query),
50 ),
51 );
52​
53// Held as long as the query lives, so one (query, revision) pair keeps one atom; an
54// `Atom.family` would hold the inner family only weakly and could swap the atom under a reader.
55const revisionedFamilies = new WeakMap<
56 Atom.Atom<unknown>,
57 ReturnType<typeof makeRevisionedFamily>
58>();
59const revisionedFamily = (query: Atom.Atom<unknown>) => {
60 let family = revisionedFamilies.get(query);
61 if (family === undefined) {
62 family = makeRevisionedFamily(query);
63 revisionedFamilies.set(query, family);
64 }
65 return family;
66};
67​
68/**
69 * Reads `query` under a workspace revision (latest mutation id); a different
70 * revision than the last read refetches it. The first read records the revision,
71 * and refetches only a query that a reader without a revision already cached.
72 * `undefined` reads without taking part, for readers that must not refetch now.
73 */
74export function revisionedQuery<A>(
75 query: Atom.Atom<A>,
76 revision: string | null | undefined,
77): Atom.Atom<A> {
78 if (revision === undefined) return query;
79 return revisionedFamily(query)(revision) as Atom.Atom<A>;