Skip to content

Commit c2aa191

Browse files
committed
fix(webapp): serialize registry reloads and clear readiness timeout
1 parent fb1004a commit c2aa191

2 files changed

Lines changed: 75 additions & 6 deletions

File tree

apps/webapp/app/utils/reloadingRegistry.server.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,17 @@ export type ReloadingRegistryOptions<T> = {
4848
export function createReloadingRegistry<T>(opts: ReloadingRegistryOptions<T>): ReloadingRegistry<T> {
4949
let snapshot: T | undefined;
5050
let loaded = false;
51+
let loadSeq = 0;
5152
let resolveReady!: () => void;
5253
const isReady = new Promise<void>((resolve) => {
5354
resolveReady = resolve;
5455
});
5556

5657
async function doLoad() {
57-
snapshot = await opts.load();
58+
const seq = ++loadSeq;
59+
const next = await opts.load();
60+
if (seq < loadSeq) return; // a newer load started while we were awaiting; don't clobber
61+
snapshot = next;
5862
lastSuccessfulLoadAt.set({ name: opts.name }, Date.now() / 1000);
5963
if (!loaded) {
6064
loaded = true;
@@ -110,10 +114,17 @@ export function createReloadingRegistry<T>(opts: ReloadingRegistryOptions<T>): R
110114
reload: doLoad,
111115
async waitUntilReady(timeoutMs: number) {
112116
if (loaded || timeoutMs <= 0) return;
113-
await Promise.race([
114-
isReady,
115-
new Promise<void>((resolve) => setTimeout(resolve, timeoutMs)),
116-
]);
117+
let timer: ReturnType<typeof setTimeout> | undefined;
118+
try {
119+
await Promise.race([
120+
isReady,
121+
new Promise<void>((resolve) => {
122+
timer = setTimeout(resolve, timeoutMs);
123+
}),
124+
]);
125+
} finally {
126+
if (timer) clearTimeout(timer);
127+
}
117128
},
118129
stop,
119130
};

apps/webapp/test/reloadingRegistry.test.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it, expect } from "vitest";
1+
import { describe, it, expect, vi } from "vitest";
22
import { createReloadingRegistry } from "~/utils/reloadingRegistry.server";
33

44
describe("createReloadingRegistry", () => {
@@ -55,4 +55,62 @@ describe("createReloadingRegistry", () => {
5555
expect(reg.current()).toBe(2);
5656
reg.stop();
5757
});
58+
59+
it("newer load wins even if an older load resolves later", async () => {
60+
// load hands the test a deferred resolver per call so completion order is controllable.
61+
const deferred: Array<(value: number) => void> = [];
62+
const reg = createReloadingRegistry({
63+
name: "test-e",
64+
intervalMs: 10_000,
65+
load: () =>
66+
new Promise<number>((resolve) => {
67+
deferred.push(resolve);
68+
}),
69+
});
70+
71+
// deferred[0] is the startup load; let it complete with an initial value.
72+
deferred[0](0);
73+
await reg.isReady;
74+
75+
// start two overlapping loads; don't await yet (deferred[1] older, deferred[2] newer)
76+
const older = reg.reload();
77+
const newer = reg.reload();
78+
79+
// resolve the NEWER load first, then the OLDER load last
80+
deferred[2](2);
81+
deferred[1](1);
82+
await Promise.all([older, newer]);
83+
84+
// the older load completing last must NOT clobber the newer snapshot
85+
expect(reg.current()).toBe(2);
86+
reg.stop();
87+
});
88+
89+
it("waitUntilReady clears its timeout when ready wins", async () => {
90+
const clearSpy = vi.spyOn(global, "clearTimeout");
91+
// load resolves only when the test releases it, so waitUntilReady runs the
92+
// race while still unloaded (it would return early if already loaded)
93+
let releaseLoad!: () => void;
94+
const loadGate = new Promise<void>((resolve) => {
95+
releaseLoad = resolve;
96+
});
97+
const reg = createReloadingRegistry({
98+
name: "test-f",
99+
intervalMs: 10_000,
100+
load: async () => {
101+
await loadGate;
102+
return 1;
103+
},
104+
});
105+
106+
// long timeout so isReady is what actually wins the race
107+
const waiting = reg.waitUntilReady(10_000);
108+
releaseLoad();
109+
await reg.isReady;
110+
await waiting;
111+
112+
expect(clearSpy).toHaveBeenCalled();
113+
clearSpy.mockRestore();
114+
reg.stop();
115+
});
58116
});

0 commit comments

Comments
 (0)