feat(FN-3730): add action-gate reload endpoint for reloadable exempt-tools
Adds a reload endpoint for action-gate exempt tools in the engine, wiring it into the dashboard routing system and documenting the behavior in the agents guide, with a changeset for the published CLI package. Fusion-Task-Id: FN-3730
This commit is contained in:
5
.changeset/fix-sw-first-install-blank-page.md
Normal file
5
.changeset/fix-sw-first-install-blank-page.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix dashboard rendering blank on first load by skipping the service worker `controllerchange` reload on initial install — the page only reloads now when an existing controller is genuinely being replaced.
|
||||||
100
packages/dashboard/app/__tests__/swUpdate.test.ts
Normal file
100
packages/dashboard/app/__tests__/swUpdate.test.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
/**
|
||||||
|
* @vitest-environment jsdom
|
||||||
|
*/
|
||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||||
|
|
||||||
|
vi.mock("../versionCheck", () => ({
|
||||||
|
reloadOnce: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { reloadOnce } from "../versionCheck";
|
||||||
|
import { installSwUpdate } from "../swUpdate";
|
||||||
|
|
||||||
|
vi.stubGlobal("__BUILD_VERSION__", "test-build-abc123");
|
||||||
|
|
||||||
|
type Listener = () => void;
|
||||||
|
|
||||||
|
interface FakeServiceWorkerContainer {
|
||||||
|
controller: ServiceWorker | null;
|
||||||
|
listeners: Map<string, Set<Listener>>;
|
||||||
|
addEventListener: (type: string, fn: Listener) => void;
|
||||||
|
register: (url: string) => Promise<ServiceWorkerRegistration>;
|
||||||
|
dispatch: (type: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeContainer(controller: ServiceWorker | null): FakeServiceWorkerContainer {
|
||||||
|
const listeners = new Map<string, Set<Listener>>();
|
||||||
|
return {
|
||||||
|
controller,
|
||||||
|
listeners,
|
||||||
|
addEventListener(type, fn) {
|
||||||
|
if (!listeners.has(type)) listeners.set(type, new Set());
|
||||||
|
listeners.get(type)!.add(fn);
|
||||||
|
},
|
||||||
|
register: vi.fn().mockResolvedValue({
|
||||||
|
scope: "/",
|
||||||
|
installing: null,
|
||||||
|
waiting: null,
|
||||||
|
addEventListener: vi.fn(),
|
||||||
|
} as unknown as ServiceWorkerRegistration),
|
||||||
|
dispatch(type) {
|
||||||
|
listeners.get(type)?.forEach((fn) => fn());
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("installSwUpdate", () => {
|
||||||
|
const originalNavigator = globalThis.navigator;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.stubEnv("PROD", true);
|
||||||
|
vi.mocked(reloadOnce).mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
Object.defineProperty(globalThis, "navigator", {
|
||||||
|
value: originalNavigator,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function withContainer(container: FakeServiceWorkerContainer) {
|
||||||
|
Object.defineProperty(globalThis, "navigator", {
|
||||||
|
value: { serviceWorker: container },
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it("does NOT reload on first install (no prior controller)", () => {
|
||||||
|
const container = makeContainer(null);
|
||||||
|
withContainer(container);
|
||||||
|
|
||||||
|
installSwUpdate();
|
||||||
|
container.dispatch("controllerchange");
|
||||||
|
|
||||||
|
expect(reloadOnce).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reloads when an existing controller is replaced", () => {
|
||||||
|
const container = makeContainer({} as ServiceWorker);
|
||||||
|
withContainer(container);
|
||||||
|
|
||||||
|
installSwUpdate();
|
||||||
|
container.dispatch("controllerchange");
|
||||||
|
|
||||||
|
expect(reloadOnce).toHaveBeenCalledTimes(1);
|
||||||
|
expect(reloadOnce).toHaveBeenCalledWith("service worker activated new version");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reloads at most once across multiple controllerchange events", () => {
|
||||||
|
const container = makeContainer({} as ServiceWorker);
|
||||||
|
withContainer(container);
|
||||||
|
|
||||||
|
installSwUpdate();
|
||||||
|
container.dispatch("controllerchange");
|
||||||
|
container.dispatch("controllerchange");
|
||||||
|
|
||||||
|
expect(reloadOnce).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -15,9 +15,14 @@ function watchInstalling(installing: ServiceWorker): void {
|
|||||||
export function installSwUpdate(): void {
|
export function installSwUpdate(): void {
|
||||||
if (!import.meta.env.PROD || !("serviceWorker" in navigator)) return;
|
if (!import.meta.env.PROD || !("serviceWorker" in navigator)) return;
|
||||||
|
|
||||||
|
// controllerchange fires on first install too (sw.js calls clients.claim()),
|
||||||
|
// but there's no prior version to swap out — reloading there races with the
|
||||||
|
// in-flight chunk loads and strands the page on a blank shell. Only reload
|
||||||
|
// when an existing controller is being replaced by a new one.
|
||||||
|
const wasControlled = !!navigator.serviceWorker.controller;
|
||||||
let reloading = false;
|
let reloading = false;
|
||||||
navigator.serviceWorker.addEventListener("controllerchange", () => {
|
navigator.serviceWorker.addEventListener("controllerchange", () => {
|
||||||
if (reloading) return;
|
if (reloading || !wasControlled) return;
|
||||||
reloading = true;
|
reloading = true;
|
||||||
reloadOnce("service worker activated new version");
|
reloadOnce("service worker activated new version");
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user