Files
Yonexus.Client/plugin/index.ts
hzhang 4adb187331 fix: migrate startup guard and shared state to globalThis
Module-level _clientStarted / ruleRegistry / onAuthenticatedCallbacks
reset on hot-reload (new VM context), causing a second runtime to start
and the exposed __yonexusClient API to point at orphaned objects.

- Replace let _clientStarted with _G["_yonexusClientStarted"]
- Store ruleRegistry and onAuthenticatedCallbacks under globalThis keys,
  initialising only when absent (survives hot-reload)
- Store runtime under _G["_yonexusClientRuntime"]; sendRule / submitPairingCode
  closures read it from globalThis instead of capturing a module-local ref
- Re-write __yonexusClient every register() call so closures stay current,
  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:27 +01:00

135 lines
4.5 KiB
TypeScript

export { validateYonexusClientConfig, YonexusClientConfigError } from "./core/config.js";
export type { YonexusClientConfig } from "./core/config.js";
export {
CLIENT_STATE_VERSION,
YonexusClientStateError,
YonexusClientStateCorruptionError,
createYonexusClientStateStore,
loadYonexusClientState,
saveYonexusClientState,
createInitialClientState,
hasClientSecret,
hasClientKeyPair,
type YonexusClientState,
type YonexusClientStateFile,
type YonexusClientStateStore
} from "./core/state.js";
export {
createClientTransport,
YonexusClientTransport,
type ClientTransport,
type ClientTransportOptions,
type ClientConnectionState,
type ClientMessageHandler,
type ClientStateChangeHandler,
type ClientErrorHandler
} from "./core/transport.js";
export {
createYonexusClientRuntime,
YonexusClientRuntime,
type YonexusClientRuntimeOptions,
type YonexusClientRuntimeState,
type YonexusClientPhase
} from "./core/runtime.js";
export {
createClientRuleRegistry,
YonexusClientRuleRegistry,
ClientRuleRegistryError,
type ClientRuleRegistry,
type ClientRuleProcessor
} from "./core/rules.js";
import path from "node:path";
import { validateYonexusClientConfig } from "./core/config.js";
import { createYonexusClientStateStore } from "./core/state.js";
import { createClientTransport } from "./core/transport.js";
import { createYonexusClientRuntime, type YonexusClientRuntime } from "./core/runtime.js";
import { createClientRuleRegistry, YonexusClientRuleRegistry } from "./core/rules.js";
const _G = globalThis as Record<string, unknown>;
const _STARTED_KEY = "_yonexusClientStarted";
const _RUNTIME_KEY = "_yonexusClientRuntime";
const _REGISTRY_KEY = "_yonexusClientRegistry";
const _CALLBACKS_KEY = "_yonexusClientOnAuthCallbacks";
export interface YonexusClientPluginManifest {
readonly name: "Yonexus.Client";
readonly version: string;
readonly description: string;
}
const manifest: YonexusClientPluginManifest = {
name: "Yonexus.Client",
version: "0.1.0",
description: "Yonexus client plugin for cross-instance OpenClaw communication"
};
export function createYonexusClientPlugin(api: { rootDir: string; pluginConfig: unknown }): void {
// 1. Ensure shared state survives hot-reload — only initialise when absent
if (!(_G[_REGISTRY_KEY] instanceof YonexusClientRuleRegistry)) {
_G[_REGISTRY_KEY] = createClientRuleRegistry();
}
if (!Array.isArray(_G[_CALLBACKS_KEY])) {
_G[_CALLBACKS_KEY] = [];
}
const ruleRegistry = _G[_REGISTRY_KEY] as YonexusClientRuleRegistry;
const onAuthenticatedCallbacks = _G[_CALLBACKS_KEY] as Array<() => void>;
// 2. Refresh the cross-plugin API object every call so that sendRule / submitPairingCode
// closures always read the live runtime from globalThis.
_G["__yonexusClient"] = {
ruleRegistry,
sendRule: (ruleId: string, content: string): boolean =>
(_G[_RUNTIME_KEY] as YonexusClientRuntime | undefined)?.sendRuleMessage(ruleId, content) ?? false,
submitPairingCode: (code: string): boolean =>
(_G[_RUNTIME_KEY] as YonexusClientRuntime | undefined)?.submitPairingCode(code) ?? false,
onAuthenticated: onAuthenticatedCallbacks
};
// 3. Start the runtime only once — the globalThis flag survives hot-reload
if (_G[_STARTED_KEY]) return;
_G[_STARTED_KEY] = true;
const config = validateYonexusClientConfig(api.pluginConfig);
const stateStore = createYonexusClientStateStore(path.join(api.rootDir, "state.json"));
const transport = createClientTransport({
config,
onMessage: (msg) => {
(_G[_RUNTIME_KEY] as YonexusClientRuntime | undefined)?.handleMessage(msg).catch((err: unknown) => {
console.error("[yonexus-client] message handler error:", err);
});
},
onStateChange: (state) => {
(_G[_RUNTIME_KEY] as YonexusClientRuntime | undefined)?.handleTransportStateChange(state);
}
});
const runtime = createYonexusClientRuntime({
config,
transport,
stateStore,
ruleRegistry,
onAuthenticated: () => {
for (const cb of onAuthenticatedCallbacks) cb();
}
});
_G[_RUNTIME_KEY] = runtime;
const shutdown = (): void => {
runtime.stop().catch((err: unknown) => {
console.error("[yonexus-client] shutdown error:", err);
});
};
process.once("SIGTERM", shutdown);
process.once("SIGINT", shutdown);
runtime.start().catch((err: unknown) => {
console.error("[yonexus-client] failed to start:", err);
});
}
export default createYonexusClientPlugin;
export { manifest };