From 07c670c27271feddaeb69b1ce5eb80c7cc9602a0 Mon Sep 17 00:00:00 2001 From: hzhang Date: Fri, 10 Apr 2026 20:41:32 +0100 Subject: [PATCH] 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 --- plugin/index.ts | 50 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/plugin/index.ts b/plugin/index.ts index 33b02b8..dea52aa 100644 --- a/plugin/index.ts +++ b/plugin/index.ts @@ -34,12 +34,18 @@ import path from "node:path"; import fs from "node:fs"; import { validateYonexusServerConfig } from "./core/config.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 { createServerRuleRegistry } from "./core/rules.js"; +import { createServerRuleRegistry, YonexusServerRuleRegistry } from "./core/rules.js"; import { encodeRuleMessage } from "../../Yonexus.Protocol/src/index.js"; import type { ServerPersistenceData } from "./core/persistence.js"; +const _G = globalThis as Record; +const _STARTED_KEY = "_yonexusServerStarted"; +const _TRANSPORT_KEY = "_yonexusServerTransport"; +const _REGISTRY_KEY = "_yonexusServerRegistry"; +const _CALLBACKS_KEY = "_yonexusServerOnAuthCallbacks"; + export interface YonexusServerPluginManifest { readonly name: "Yonexus.Server"; readonly version: string; @@ -52,8 +58,6 @@ const manifest: YonexusServerPluginManifest = { description: "Yonexus central hub plugin for cross-instance OpenClaw communication" }; -let _serverStarted = false; - export function createYonexusServerPlugin(api: { rootDir: string; pluginConfig: unknown; @@ -138,15 +142,34 @@ export function createYonexusServerPlugin(api: { }); }, { commands: ["yonexus-server"] }); - if (_serverStarted) return; - _serverStarted = true; + // 1. Ensure shared state survives hot-reload — only initialise when absent + 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 store = createYonexusServerStore(stateFilePath); - const ruleRegistry = createServerRuleRegistry(); - const onClientAuthenticatedCallbacks: Array<(identifier: string) => void> = []; - + // runtimeRef is local; transport is stored in globalThis so sendRule closures stay valid let runtimeRef: ReturnType | null = null; const transport = createServerTransport({ config, @@ -161,14 +184,7 @@ export function createYonexusServerPlugin(api: { } } }); - - // Expose registry and helpers for other plugins loaded in the same process - (globalThis as Record)["__yonexusServer"] = { - ruleRegistry, - sendRule: (identifier: string, ruleId: string, content: string): boolean => - transport.send(identifier, encodeRuleMessage(ruleId, content)), - onClientAuthenticated: onClientAuthenticatedCallbacks - }; + _G[_TRANSPORT_KEY] = transport; const runtime = createYonexusServerRuntime({ config,