Files
donutbrowser/src/lib/profile-list-store.test.mjs
T
2026-10-19 05:22:48 +04:00

186 lines
5.9 KiB
JavaScript

import assert from "node:assert/strict";
import test from "node:test";
import {
countBy,
createSharedResource,
REUSE_MS,
sameSetOr,
} from "./profile-list-store.ts";
/** A fetch whose calls the test resolves by hand. */
function controlledFetch() {
const calls = [];
const fetch = () =>
new Promise((resolve, reject) => {
calls.push({ resolve, reject });
});
return { calls, fetch };
}
/** An event bus standing in for the backend's events. */
function fakeEvents() {
const handlers = new Map();
return {
listenTo: async (event, onEvent) => {
const list = handlers.get(event) ?? new Set();
list.add(onEvent);
handlers.set(event, list);
return () => list.delete(onEvent);
},
emit: (event) => {
for (const handler of handlers.get(event) ?? []) handler();
},
count: (event) => handlers.get(event)?.size ?? 0,
};
}
const tick = () => new Promise((resolve) => setImmediate(resolve));
test("readers that load at the same time share one call", async () => {
const { calls, fetch } = controlledFetch();
const resource = createSharedResource(fetch, {
listenTo: fakeEvents().listenTo,
});
const reads = [resource.load(), resource.load(), resource.load()];
assert.equal(calls.length, 1);
calls[0].resolve(["a"]);
const values = await Promise.all(reads);
for (const value of values) assert.equal(value, values[0]);
});
test("a fresh result is reused, an old one is loaded again", async () => {
let clock = 1000;
const { calls, fetch } = controlledFetch();
const resource = createSharedResource(fetch, {
now: () => clock,
listenTo: fakeEvents().listenTo,
});
const first = resource.load();
calls[0].resolve(["a"]);
const value = await first;
clock += REUSE_MS - 1;
assert.equal(await resource.load(), value);
assert.equal(calls.length, 1);
clock += 2;
void resource.load();
assert.equal(calls.length, 2);
});
test("one change event loads once for every listener", async () => {
const events = fakeEvents();
const { calls, fetch } = controlledFetch();
const resource = createSharedResource(fetch, {
events: ["profiles-changed"],
listenTo: events.listenTo,
});
const heard = [];
for (let i = 0; i < 4; i++) {
resource.subscribe({ value: (value) => heard.push(value) });
}
await tick();
assert.equal(events.count("profiles-changed"), 1, "one backend listener");
events.emit("profiles-changed");
assert.equal(calls.length, 1);
calls[0].resolve(["b"]);
await tick();
assert.equal(heard.length, 4);
assert.ok(heard.every((value) => value === heard[0]));
});
test("a change during a load is never answered by that load alone", async () => {
const events = fakeEvents();
const { calls, fetch } = controlledFetch();
const resource = createSharedResource(fetch, {
events: ["profiles-changed"],
listenTo: events.listenTo,
});
const heard = [];
resource.subscribe({ value: (value) => heard.push(value) });
await tick();
void resource.load();
events.emit("profiles-changed");
events.emit("profiles-changed");
events.emit("profiles-changed");
assert.equal(calls.length, 1, "the burst waits for the running load");
calls[0].resolve(["old"]);
await tick();
assert.equal(calls.length, 2, "then loads once more");
calls[1].resolve(["new"]);
await tick();
assert.deepEqual(heard.at(-1), ["new"]);
// A reader after the change gets the newer value, not the older one.
assert.deepEqual(await resource.load(), ["new"]);
});
test("refresh never reuses what was loaded before it", async () => {
const { calls, fetch } = controlledFetch();
const resource = createSharedResource(fetch, {
listenTo: fakeEvents().listenTo,
});
const before = resource.load();
const after = resource.refresh();
assert.equal(calls.length, 2);
calls[1].resolve(["new"]);
calls[0].resolve(["old"]);
assert.deepEqual(await after, ["new"]);
assert.deepEqual(await before, ["new"], "its caller gets the newer value");
assert.deepEqual(resource.peek(), ["new"], "an older load does not win");
});
test("errors reach listeners and the caller", async () => {
const { calls, fetch } = controlledFetch();
const resource = createSharedResource(fetch, {
listenTo: fakeEvents().listenTo,
});
const errors = [];
resource.subscribe({ value: () => {}, error: (error) => errors.push(error) });
const read = resource.load();
calls[0].reject(new Error("boom"));
await assert.rejects(read, /boom/);
assert.equal(errors.length, 1);
// The failed load is not reused.
void resource.load();
assert.equal(calls.length, 2);
});
test("the last listener leaving stops listening and drops the cache", async () => {
const events = fakeEvents();
const { calls, fetch } = controlledFetch();
const resource = createSharedResource(fetch, {
events: ["profiles-changed"],
listenTo: events.listenTo,
});
const stop = resource.subscribe({ value: () => {} });
const read = resource.load();
calls[0].resolve(["a"]);
await read;
stop();
await tick();
assert.equal(events.count("profiles-changed"), 0);
void resource.load();
assert.equal(calls.length, 2, "an unwatched value is not trusted");
});
test("counts keep their identity when nothing changed", () => {
const profiles = [{ proxy_id: "p1" }, { proxy_id: "p1" }, { proxy_id: "p2" }];
const counts = countBy(profiles, (profile) => profile.proxy_id);
assert.deepEqual(counts, { p1: 2, p2: 1 });
assert.equal(
countBy([...profiles], (profile) => profile.proxy_id, counts),
counts,
);
const changed = countBy(
[...profiles, { proxy_id: "p2" }],
(profile) => profile.proxy_id,
counts,
);
assert.deepEqual(changed, { p1: 2, p2: 2 });
});
test("a set with the same members keeps its identity", () => {
const previous = new Set(["a", "b"]);
assert.equal(sameSetOr(previous, new Set(["b", "a"])), previous);
const next = new Set(["a", "c"]);
assert.equal(sameSetOr(previous, next), next);
});