Compare commits

...

2 Commits

Author SHA1 Message Date
a8748f8c55 fix: globalThis 2026-04-10 21:58:59 +01:00
07c670c272 fix: migrate startup guard and shared state to globalThis
Module-level _serverStarted / ruleRegistry / onClientAuthenticatedCallbacks
reset on hot-reload (new VM context). After hot-reload the second runtime
attempt would hit EADDRINUSE (silently swallowed) while __yonexusServer
was overwritten to point at a transport that never started, making every
sendRule() return false.

- Replace let _serverStarted with _G["_yonexusServerStarted"]
- Store ruleRegistry and onClientAuthenticatedCallbacks under globalThis
  keys, initialising only when absent
- Store transport under _G["_yonexusServerTransport"]; sendRule closure
  reads it from globalThis instead of a module-local capture
- Re-write __yonexusServer every register() call (updated closures),
  but skip runtime.start() when the globalThis flag is already set

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 20:41:32 +01:00
2 changed files with 34 additions and 18 deletions

View File

@@ -34,12 +34,18 @@ import path from "node:path";
import fs from "node:fs"; import fs from "node:fs";
import { validateYonexusServerConfig } from "./core/config.js"; import { validateYonexusServerConfig } from "./core/config.js";
import { createYonexusServerStore } from "./core/store.js"; import { createYonexusServerStore } from "./core/store.js";
import { createServerTransport } from "./core/transport.js"; import { createServerTransport, type ServerTransport } from "./core/transport.js";
import { createYonexusServerRuntime } from "./core/runtime.js"; import { createYonexusServerRuntime } from "./core/runtime.js";
import { createServerRuleRegistry } from "./core/rules.js"; import { createServerRuleRegistry, YonexusServerRuleRegistry } from "./core/rules.js";
import { encodeRuleMessage } from "../../Yonexus.Protocol/src/index.js"; import { encodeRuleMessage } from "../../Yonexus.Protocol/src/index.js";
import type { ServerPersistenceData } from "./core/persistence.js"; import type { ServerPersistenceData } from "./core/persistence.js";
const _G = globalThis as Record<string, unknown>;
const _STARTED_KEY = "_yonexusServerStarted";
const _TRANSPORT_KEY = "_yonexusServerTransport";
const _REGISTRY_KEY = "_yonexusServerRegistry";
const _CALLBACKS_KEY = "_yonexusServerOnAuthCallbacks";
export interface YonexusServerPluginManifest { export interface YonexusServerPluginManifest {
readonly name: "Yonexus.Server"; readonly name: "Yonexus.Server";
readonly version: string; readonly version: string;
@@ -52,8 +58,6 @@ const manifest: YonexusServerPluginManifest = {
description: "Yonexus central hub plugin for cross-instance OpenClaw communication" description: "Yonexus central hub plugin for cross-instance OpenClaw communication"
}; };
let _serverStarted = false;
export function createYonexusServerPlugin(api: { export function createYonexusServerPlugin(api: {
rootDir: string; rootDir: string;
pluginConfig: unknown; pluginConfig: unknown;
@@ -138,15 +142,34 @@ export function createYonexusServerPlugin(api: {
}); });
}, { commands: ["yonexus-server"] }); }, { commands: ["yonexus-server"] });
if (_serverStarted) return; // 1. Ensure shared state survives hot-reload — only initialise when absent
_serverStarted = true; if (!(_G[_REGISTRY_KEY] instanceof YonexusServerRuleRegistry)) {
_G[_REGISTRY_KEY] = createServerRuleRegistry();
}
if (!Array.isArray(_G[_CALLBACKS_KEY])) {
_G[_CALLBACKS_KEY] = [];
}
const ruleRegistry = _G[_REGISTRY_KEY] as YonexusServerRuleRegistry;
const onClientAuthenticatedCallbacks = _G[_CALLBACKS_KEY] as Array<(identifier: string) => void>;
// 2. Refresh the cross-plugin API object every call so that sendRule closure
// always reads the live transport from globalThis.
_G["__yonexusServer"] = {
ruleRegistry,
sendRule: (identifier: string, ruleId: string, content: string): boolean =>
(_G[_TRANSPORT_KEY] as ServerTransport | undefined)?.send(identifier, encodeRuleMessage(ruleId, content)) ?? false,
onClientAuthenticated: onClientAuthenticatedCallbacks
};
// 3. Start the runtime only once — the globalThis flag survives hot-reload
if (_G[_STARTED_KEY]) return;
_G[_STARTED_KEY] = true;
const config = validateYonexusServerConfig(api.pluginConfig); const config = validateYonexusServerConfig(api.pluginConfig);
const store = createYonexusServerStore(stateFilePath); const store = createYonexusServerStore(stateFilePath);
const ruleRegistry = createServerRuleRegistry(); // runtimeRef is local; transport is stored in globalThis so sendRule closures stay valid
const onClientAuthenticatedCallbacks: Array<(identifier: string) => void> = [];
let runtimeRef: ReturnType<typeof createYonexusServerRuntime> | null = null; let runtimeRef: ReturnType<typeof createYonexusServerRuntime> | null = null;
const transport = createServerTransport({ const transport = createServerTransport({
config, config,
@@ -161,14 +184,7 @@ export function createYonexusServerPlugin(api: {
} }
} }
}); });
_G[_TRANSPORT_KEY] = transport;
// Expose registry and helpers for other plugins loaded in the same process
(globalThis as Record<string, unknown>)["__yonexusServer"] = {
ruleRegistry,
sendRule: (identifier: string, ruleId: string, content: string): boolean =>
transport.send(identifier, encodeRuleMessage(ruleId, content)),
onClientAuthenticated: onClientAuthenticatedCallbacks
};
const runtime = createYonexusServerRuntime({ const runtime = createYonexusServerRuntime({
config, config,