fix: harden upgrade publication and refresh

This commit is contained in:
golem
2026-08-25 00:16:20 -06:00
parent d29dc86383
commit eb7c2c18b3
8 changed files with 267 additions and 32 deletions
+35
View File
@@ -222,4 +222,39 @@ describe("useBankDashboard", () => {
expect(result.current.status === "ready" && result.current.snapshot.blockNumber).toBe(13n);
});
});
it("refetches a changed manifest on a watched block and isolates its implementation snapshot without remounting", async () => {
const v2ManifestJson = { ...manifestJson, implementation: address("6") };
const v2Snapshot = {
...snapshot,
blockNumber: 13n,
version: 2 as const,
implementation: address("6"),
synchronizedAt: new Date("2026-08-21T10:01:00.000Z"),
};
const pendingV2Snapshot = deferred<DashboardSnapshot>();
const loadManifest = vi.fn()
.mockResolvedValueOnce(manifestJson)
.mockResolvedValueOnce(v2ManifestJson);
const loader = vi.fn((current: DeploymentManifest) => current.implementation === manifest.implementation
? Promise.resolve(snapshot)
: pendingV2Snapshot.promise);
const { result } = renderHook(() => useBankDashboard({ loadManifest, loader }), {
wrapper: createWrapper(),
});
await waitFor(() => expect(result.current.status).toBe("ready"));
act(() => notifyBlock?.(13n));
await waitFor(() => {
expect(loadManifest).toHaveBeenCalledTimes(2);
expect(loader).toHaveBeenCalledTimes(2);
});
expect(result.current.status).toBe("loading");
pendingV2Snapshot.resolve(v2Snapshot);
await waitFor(() => expect(result.current.status).toBe("ready"));
expect(result.current.status === "ready" && result.current.manifest.implementation).toBe(address("6"));
expect(result.current.status === "ready" && result.current.snapshot).toBe(v2Snapshot);
});
});
+25 -4
View File
@@ -43,6 +43,15 @@ function isChainMismatch(error: unknown): boolean {
return /endpoint chain ID \d+ does not match manifest chain ID \d+/i.test(message(error));
}
function manifestIdentity(manifest: DeploymentManifest | undefined): string | undefined {
if (!manifest) return undefined;
return JSON.stringify({
...manifest,
deploymentBlock: manifest.deploymentBlock.toString(),
actors: manifest.actors.map(({ label, address }) => ({ label, address })),
});
}
export function useBankDashboard(options: BankDashboardOptions = {}): DashboardState {
const loadManifest = options.loadManifest ?? fetchManifest;
const loader = options.loader ?? fetchSnapshot;
@@ -70,9 +79,10 @@ export function useBankDashboard(options: BankDashboardOptions = {}): DashboardS
}
}, [manifestQuery.data]);
const manifest = parsedManifest && "manifest" in parsedManifest ? parsedManifest.manifest : undefined;
const snapshotIdentity = useMemo(() => manifestIdentity(manifest), [manifest]);
const snapshotQueryKey = useMemo(
() => ["bank-dashboard", manifest?.chainId, manifest?.proxy] as const,
[manifest?.chainId, manifest?.proxy],
() => ["bank-dashboard", snapshotIdentity] as const,
[snapshotIdentity],
);
const snapshotQuery = useQuery({
@@ -97,8 +107,19 @@ export function useBankDashboard(options: BankDashboardOptions = {}): DashboardS
const reconcileBlock = useCallback((blockNumber: bigint) => {
if (!manifest || mismatch || latestSnapshot.current?.blockNumber === blockNumber || lastWatchedBlock.current === blockNumber) return;
lastWatchedBlock.current = blockNumber;
void queryClient.invalidateQueries({ queryKey: snapshotQueryKey, exact: true });
}, [manifest, mismatch, queryClient, snapshotQueryKey]);
void manifestQuery.refetch().then((result) => {
if (result.error !== null || result.data === undefined) return;
let refreshedManifest: DeploymentManifest;
try {
refreshedManifest = parseDeploymentManifest(result.data);
} catch {
return;
}
if (manifestIdentity(refreshedManifest) === snapshotIdentity) {
void queryClient.invalidateQueries({ queryKey: snapshotQueryKey, exact: true });
}
});
}, [manifest, manifestQuery, mismatch, queryClient, snapshotIdentity, snapshotQueryKey]);
useWatchBlockNumber({
chainId: manifest?.chainId,