feat: complete 2.0.7.12 platform overhaul
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createEventBatcher } from "./eventBatcher";
|
||||
import { createLatestRequest } from "./latestRequest";
|
||||
|
||||
describe("admin async coordination", () => {
|
||||
it("applies only the latest request result and aborts the previous request", async () => {
|
||||
const latest = createLatestRequest();
|
||||
let resolveFirst: (value: string) => void = () => undefined;
|
||||
let firstSignal: AbortSignal | undefined;
|
||||
const first = latest.run((signal) => {
|
||||
firstSignal = signal;
|
||||
return new Promise<string>((resolve) => { resolveFirst = resolve; });
|
||||
});
|
||||
const second = latest.run(async () => "second");
|
||||
resolveFirst("first");
|
||||
|
||||
await expect(first).resolves.toBeUndefined();
|
||||
await expect(second).resolves.toBe("second");
|
||||
expect(firstSignal?.aborted).toBe(true);
|
||||
});
|
||||
|
||||
it("coalesces rapid SSE events into one 250ms batch", () => {
|
||||
vi.useFakeTimers();
|
||||
const flush = vi.fn();
|
||||
const batcher = createEventBatcher(250, flush);
|
||||
batcher.push("source_check.item");
|
||||
batcher.push("source_check.progress");
|
||||
batcher.push("source_check.item");
|
||||
|
||||
vi.advanceTimersByTime(249);
|
||||
expect(flush).not.toHaveBeenCalled();
|
||||
vi.advanceTimersByTime(1);
|
||||
expect(flush).toHaveBeenCalledOnce();
|
||||
expect([...flush.mock.calls[0][0]]).toEqual(["source_check.item", "source_check.progress"]);
|
||||
batcher.cancel();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
export type EventBatcher = {
|
||||
push(kind: string): void;
|
||||
cancel(): void;
|
||||
};
|
||||
|
||||
export function createEventBatcher(delayMs: number, flush: (kinds: ReadonlySet<string>) => void): EventBatcher {
|
||||
const pending = new Set<string>();
|
||||
let timer: number | undefined;
|
||||
|
||||
return {
|
||||
push(kind: string) {
|
||||
pending.add(kind);
|
||||
if (timer !== undefined) return;
|
||||
timer = window.setTimeout(() => {
|
||||
timer = undefined;
|
||||
const batch = new Set(pending);
|
||||
pending.clear();
|
||||
flush(batch);
|
||||
}, Math.max(0, delayMs));
|
||||
},
|
||||
cancel() {
|
||||
if (timer !== undefined) window.clearTimeout(timer);
|
||||
timer = undefined;
|
||||
pending.clear();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
export type LatestRequest = {
|
||||
run<T>(task: (signal: AbortSignal) => Promise<T>): Promise<T | undefined>;
|
||||
cancel(reason?: string): void;
|
||||
};
|
||||
|
||||
export function createLatestRequest(): LatestRequest {
|
||||
let controller: AbortController | null = null;
|
||||
let serial = 0;
|
||||
|
||||
return {
|
||||
async run<T>(task: (signal: AbortSignal) => Promise<T>) {
|
||||
controller?.abort("superseded");
|
||||
const current = new AbortController();
|
||||
controller = current;
|
||||
const requestSerial = ++serial;
|
||||
try {
|
||||
const value = await task(current.signal);
|
||||
return requestSerial === serial ? value : undefined;
|
||||
} catch (error) {
|
||||
if (current.signal.aborted) return undefined;
|
||||
throw error;
|
||||
} finally {
|
||||
if (controller === current) controller = null;
|
||||
}
|
||||
},
|
||||
cancel(reason = "cancelled") {
|
||||
serial++;
|
||||
controller?.abort(reason);
|
||||
controller = null;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { adminResourceDiagnostic, adminResourceMarker, claimAdminResourceReload } from "./resourceRecovery";
|
||||
|
||||
describe("admin resource recovery", () => {
|
||||
beforeEach(() => sessionStorage.clear());
|
||||
|
||||
it("permits one automatic reload per build and route", () => {
|
||||
const marker = adminResourceMarker("build-7", { href: "https://example.test/admin/feedbacks?_admin_reload=1&status=new" });
|
||||
expect(marker).toBe("build-7:/admin/feedbacks?status=new");
|
||||
expect(claimAdminResourceReload(sessionStorage, "reload", marker)).toBe(true);
|
||||
expect(claimAdminResourceReload(sessionStorage, "reload", marker)).toBe(false);
|
||||
expect(claimAdminResourceReload(sessionStorage, "reload", marker.replace("build-7", "build-8"))).toBe(true);
|
||||
});
|
||||
|
||||
it("creates a stable diagnostic number without exposing the URL", () => {
|
||||
const first = adminResourceDiagnostic("build-7", { pathname: "/admin/feedbacks", search: "?status=new" });
|
||||
const second = adminResourceDiagnostic("build-7", { pathname: "/admin/feedbacks", search: "?status=new" });
|
||||
expect(first).toBe(second);
|
||||
expect(first).toMatch(/^ADMIN-ASSET-[A-Z0-9]+$/);
|
||||
expect(first).not.toContain("feedbacks");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
export function adminResourceMarker(buildId: string, locationValue: Pick<Location, "href">) {
|
||||
const canonical = new URL(locationValue.href);
|
||||
canonical.searchParams.delete("_admin_reload");
|
||||
return `${buildId}:${canonical.pathname}${canonical.search}`;
|
||||
}
|
||||
|
||||
export function claimAdminResourceReload(storage: Pick<Storage, "getItem" | "setItem">, key: string, marker: string) {
|
||||
if (storage.getItem(key) === marker) return false;
|
||||
storage.setItem(key, marker);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function adminResourceDiagnostic(buildId: string, locationValue: Pick<Location, "pathname" | "search">) {
|
||||
const route = `${locationValue.pathname}${locationValue.search}`;
|
||||
let hash = 2166136261;
|
||||
for (const character of `${buildId}:${route}`) {
|
||||
hash ^= character.charCodeAt(0);
|
||||
hash = Math.imul(hash, 16777619);
|
||||
}
|
||||
return `ADMIN-ASSET-${Math.abs(hash).toString(36).toUpperCase()}`;
|
||||
}
|
||||
Reference in New Issue
Block a user