1import { Atom, AtomRegistry } from "effect/reactivity";
2import { describe, expect, it } from "vite-plus/test";
3
4import { revisionedQuery } from "./revisionedQuery";
5
6// A stand-in query that counts its fetches.
7function setup(readQuery: (cache: Atom.Atom<number>) => Atom.Atom<number> = (cache) => cache) {
8 let fetches = 0;
9 // Node removal runs on the registry's scheduler; releasing flushes it so the
10 // next mount of the same revision builds a fresh node, as a React remount does.
11 const scheduled: Array<() => void> = [];
12 const registry = AtomRegistry.make({
13 scheduleTask: (task) => {
14 scheduled.push(task);
15 return () => {};
16 },
17 });
18 const query = readQuery(
19 Atom.make(() => {
20 fetches += 1;
21 return fetches;
22 }).pipe(Atom.keepAlive),
23 );
24 const mount = async (revision: string | null | undefined) => {
25 const release = registry.mount(revisionedQuery(query, revision));
26 // A stale read refetches in a microtask, after the render that read it.
27 await Promise.resolve();
28 return () => {
29 release();
30 for (const task of scheduled.splice(0)) task();
31 };
32 };
33 return { registry, query, mount, fetches: () => fetches };
34}
35
36describe("revisionedQuery", () => {
37 it("refetches once per new revision, not on the first read or a remount", async () => {
38 const { mount, fetches } = setup();
39
40 (await mount("mutation-1"))();
41 expect(fetches()).toBe(1);
42
43 (await mount("mutation-1"))();
44 expect(fetches()).toBe(1);
45
46 (await mount("mutation-2"))();
47 expect(fetches()).toBe(2);
48 });
49
50 it("does not ping-pong while readers of the old and new revision overlap", async () => {
51 const { mount, fetches } = setup();
52
53 const releaseOld = await mount("mutation-1");
54 const releaseNew = await mount("mutation-2");
55 expect(fetches()).toBe(2);
56 releaseOld();
57 releaseNew();
58 });
59
60 it("refetches when an earlier revision is read again", async () => {
61 const { mount, fetches } = setup();
62
63 (await mount("mutation-1"))();
64 (await mount("mutation-2"))();
65 (await mount("mutation-1"))();
66 expect(fetches()).toBe(3);
67 });
68
69 it("refetches a query another reader already cached on its first revisioned read", async () => {
70 const { mount, fetches } = setup();
71
72 (await mount(undefined))();
73 expect(fetches()).toBe(1);
74
75 (await mount("mutation-1"))();
76 expect(fetches()).toBe(2);
77
78 (await mount("mutation-1"))();
79 expect(fetches()).toBe(2);
80 });
81
82 it("counts a query as cached while the atom it reads through keeps the value", async () => {
83 // Like an environment query: its own node goes with its last reader, the cache stays.
84 const { mount, fetches } = setup((cache) =>
85 Atom.transform(cache, (get) => get(cache), { initialValueTarget: cache }),
86 );
87
88 (await mount(undefined))();
89 (await mount("mutation-1"))();
90 expect(fetches()).toBe(2);
91 });
92
93 it("does not refetch a cached query on a first read before any workspace change", async () => {
94 const { mount, fetches } = setup();
95
96 (await mount(undefined))();
97 (await mount(null))();
98 expect(fetches()).toBe(1);
99 });
100
101 it("leaves the query alone when read without a revision", async () => {
102 const { mount, query, fetches } = setup();
103
104 (await mount("mutation-1"))();
105 expect(revisionedQuery(query, undefined)).toBe(query);
106 (await mount(undefined))();
107 (await mount("mutation-1"))();
108 expect(fetches()).toBe(1);
109 });
110
111 it("refetches the query when the reader is refreshed", async () => {
112 const { registry, query, mount, fetches } = setup();
113
114 const release = await mount("mutation-1");
115 registry.refresh(revisionedQuery(query, "mutation-1"));
116 expect(fetches()).toBe(2);
117 release();
118 });
119
120 it("drops a pending refetch when the registry is disposed before it runs", async () => {
121 const { registry, query, mount } = setup();
122
123 (await mount("mutation-1"))();
124 const release = registry.mount(revisionedQuery(query, "mutation-2"));
125 release();
126 registry.dispose();
127
128 // A refetch that ran now would throw "registry is disposed" as an uncaught error.
129 await Promise.resolve();
130 });
131});