fix(executors): make registerLazyExecutor HMR-safe (#15792)

⚠️ base-red inherited: #15306

Validated in local merge-train _artifacts/merge-wave-14/train.log on devbox-vm-06-dev002 @ 9f6fb4713421394d33142e91521fb43887000b13 (FAST), base 97da13f773. Both #15792 and #15858 boarded. Static gates green except inherited translation drift of docs/reference/ENVIRONMENT.md, the same violations on origin/release/v3.8.52. The changed node:test (executors-registry-hmr-safe) passed. Vitest timeouts in autoCombo and MCP audit reproduced on the pure origin/release/v3.8.52 tip (5 files timed out there; 4 on the train). Those files are not in this train's diff.
This commit is contained in:
Junior-HJ 2026-10-08 09:31:59 -05:00 • committed by GitHub
parent 97da13f773
commit 363ed1bbae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 69 additions and 2 deletions

View file

@ -0,0 +1 @@
- **fix(executors):** make `registerLazyExecutor` HMR-safe so hot-reload no longer double-registers executors ([#15792](https://github.com/diegosouzapw/OmniRoute/pull/15792)) — thanks @Junior-HJ

View file

@ -37,11 +37,17 @@ export function getRegisteredExecutor(alias: string): BaseExecutor | undefined {
// static registration.
const lazyLoaders = new Map<string, () => Promise<BaseExecutor>>();
const lazyInFlight = new Map<string, Promise<BaseExecutor>>();
// Bumped on every re-register so a load already in flight cannot publish the
// old executor or delete the replacement loader (#15792).
const lazyGeneration = new Map<string, number>();
export function registerLazyExecutor(alias: string, load: () => Promise<BaseExecutor>): void {
if (registry.has(alias) || lazyLoaders.has(alias)) {
throw new Error(`executor alias already registered: "${alias}"`);
// If re-evaluated under HMR / dev reload, overwrite lazy loader and clear cached instance
if (registry.has(alias)) {
registry.delete(alias);
}
lazyInFlight.delete(alias);
lazyGeneration.set(alias, (lazyGeneration.get(alias) ?? 0) + 1);
lazyLoaders.set(alias, load);
}
@ -52,7 +58,9 @@ export function loadRegisteredExecutor(alias: string): Promise<BaseExecutor> | u
if (!load) return undefined;
let inFlight = lazyInFlight.get(alias);
if (!inFlight) {
const generation = lazyGeneration.get(alias) ?? 0;
inFlight = load().then((executor) => {
if ((lazyGeneration.get(alias) ?? 0) !== generation) return executor;
registerExecutor(alias, executor);
lazyLoaders.delete(alias);
lazyInFlight.delete(alias);

View file

@ -0,0 +1,58 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
registerLazyExecutor,
loadRegisteredExecutor,
hasRegisteredExecutor,
} from "../../open-sse/executors/registry.ts";
import { BaseExecutor } from "../../open-sse/executors/base.ts";
class DummyExecutor extends BaseExecutor {
constructor(public version: number) {
super("dummy");
}
async execute(): Promise<Response> {
return new Response("ok");
}
}
test("registerLazyExecutor is HMR-safe and re-registration overwrites cached instance", async () => {
const alias = `test-hmr-alias-${Date.now()}`;
// Initial lazy registration
registerLazyExecutor(alias, async () => new DummyExecutor(1));
assert.ok(hasRegisteredExecutor(alias), "lazy executor is registered");
// Load and cache instance v1
const v1 = (await loadRegisteredExecutor(alias)) as DummyExecutor;
assert.equal(v1.version, 1);
// Re-evaluation under HMR: re-register same alias must not throw
assert.doesNotThrow(() => {
registerLazyExecutor(alias, async () => new DummyExecutor(2));
}, "re-registration under HMR should not throw duplicate alias error");
// Loading again resolves new instance v2 (cached v1 was purged)
const v2 = (await loadRegisteredExecutor(alias)) as DummyExecutor;
assert.equal(v2.version, 2, "subsequent load resolves updated executor version");
});
test("an in-flight load cannot republish the stale executor after HMR", async () => {
const alias = `test-hmr-inflight-${Date.now()}`;
let release: (executor: DummyExecutor) => void = () => {};
const gate = new Promise<DummyExecutor>((resolve) => {
release = resolve;
});
registerLazyExecutor(alias, () => gate);
const first = loadRegisteredExecutor(alias);
assert.ok(first, "first load starts");
registerLazyExecutor(alias, async () => new DummyExecutor(2));
release(new DummyExecutor(1));
const stale = (await first) as DummyExecutor;
assert.equal(stale.version, 1, "the caller that started v1 still receives v1");
const current = (await loadRegisteredExecutor(alias)) as DummyExecutor;
assert.equal(current.version, 2, "a load started after HMR resolves the replacement");
});