refactor(gateway): split server runtime
This commit is contained in:
171
src/gateway/server-node-bridge.ts
Normal file
171
src/gateway/server-node-bridge.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import type { NodeBridgeServer } from "../infra/bridge/server.js";
|
||||
import { startNodeBridgeServer } from "../infra/bridge/server.js";
|
||||
import {
|
||||
listSystemPresence,
|
||||
upsertPresence,
|
||||
} from "../infra/system-presence.js";
|
||||
import { loadVoiceWakeConfig } from "../infra/voicewake.js";
|
||||
import { isLoopbackAddress } from "./net.js";
|
||||
import {
|
||||
getHealthVersion,
|
||||
getPresenceVersion,
|
||||
incrementPresenceVersion,
|
||||
} from "./server/health-state.js";
|
||||
import type {
|
||||
BridgeEvent,
|
||||
BridgeRequest,
|
||||
BridgeResponse,
|
||||
} from "./server-bridge-types.js";
|
||||
|
||||
export type GatewayNodeBridgeRuntime = {
|
||||
bridge: NodeBridgeServer | null;
|
||||
nodePresenceTimers: Map<string, ReturnType<typeof setInterval>>;
|
||||
};
|
||||
|
||||
export async function startGatewayNodeBridge(params: {
|
||||
bridgeEnabled: boolean;
|
||||
bridgePort: number;
|
||||
bridgeHost: string | null;
|
||||
machineDisplayName: string;
|
||||
canvasHostPort?: number;
|
||||
canvasHostHost?: string;
|
||||
broadcast: (
|
||||
event: string,
|
||||
payload: unknown,
|
||||
opts?: {
|
||||
dropIfSlow?: boolean;
|
||||
stateVersion?: { presence?: number; health?: number };
|
||||
},
|
||||
) => void;
|
||||
bridgeUnsubscribeAll: (nodeId: string) => void;
|
||||
handleBridgeRequest: (
|
||||
nodeId: string,
|
||||
req: BridgeRequest,
|
||||
) => Promise<BridgeResponse>;
|
||||
handleBridgeEvent: (nodeId: string, evt: BridgeEvent) => Promise<void> | void;
|
||||
logBridge: { info: (msg: string) => void; warn: (msg: string) => void };
|
||||
}): Promise<GatewayNodeBridgeRuntime> {
|
||||
const nodePresenceTimers = new Map<string, ReturnType<typeof setInterval>>();
|
||||
|
||||
const stopNodePresenceTimer = (nodeId: string) => {
|
||||
const timer = nodePresenceTimers.get(nodeId);
|
||||
if (timer) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
nodePresenceTimers.delete(nodeId);
|
||||
};
|
||||
|
||||
const beaconNodePresence = (
|
||||
node: {
|
||||
nodeId: string;
|
||||
displayName?: string;
|
||||
remoteIp?: string;
|
||||
version?: string;
|
||||
platform?: string;
|
||||
deviceFamily?: string;
|
||||
modelIdentifier?: string;
|
||||
},
|
||||
reason: string,
|
||||
) => {
|
||||
const host = node.displayName?.trim() || node.nodeId;
|
||||
const rawIp = node.remoteIp?.trim();
|
||||
const ip = rawIp && !isLoopbackAddress(rawIp) ? rawIp : undefined;
|
||||
const version = node.version?.trim() || "unknown";
|
||||
const platform = node.platform?.trim() || undefined;
|
||||
const deviceFamily = node.deviceFamily?.trim() || undefined;
|
||||
const modelIdentifier = node.modelIdentifier?.trim() || undefined;
|
||||
const text = `Node: ${host}${ip ? ` (${ip})` : ""} · app ${version} · last input 0s ago · mode remote · reason ${reason}`;
|
||||
upsertPresence(node.nodeId, {
|
||||
host,
|
||||
ip,
|
||||
version,
|
||||
platform,
|
||||
deviceFamily,
|
||||
modelIdentifier,
|
||||
mode: "remote",
|
||||
reason,
|
||||
lastInputSeconds: 0,
|
||||
instanceId: node.nodeId,
|
||||
text,
|
||||
});
|
||||
incrementPresenceVersion();
|
||||
params.broadcast(
|
||||
"presence",
|
||||
{ presence: listSystemPresence() },
|
||||
{
|
||||
dropIfSlow: true,
|
||||
stateVersion: {
|
||||
presence: getPresenceVersion(),
|
||||
health: getHealthVersion(),
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const startNodePresenceTimer = (node: { nodeId: string }) => {
|
||||
stopNodePresenceTimer(node.nodeId);
|
||||
nodePresenceTimers.set(
|
||||
node.nodeId,
|
||||
setInterval(() => {
|
||||
beaconNodePresence(node, "periodic");
|
||||
}, 180_000),
|
||||
);
|
||||
};
|
||||
|
||||
if (params.bridgeEnabled && params.bridgePort > 0 && params.bridgeHost) {
|
||||
try {
|
||||
const started = await startNodeBridgeServer({
|
||||
host: params.bridgeHost,
|
||||
port: params.bridgePort,
|
||||
serverName: params.machineDisplayName,
|
||||
canvasHostPort: params.canvasHostPort,
|
||||
canvasHostHost: params.canvasHostHost,
|
||||
onRequest: (nodeId, req) => params.handleBridgeRequest(nodeId, req),
|
||||
onAuthenticated: async (node) => {
|
||||
beaconNodePresence(node, "node-connected");
|
||||
startNodePresenceTimer(node);
|
||||
|
||||
try {
|
||||
const cfg = await loadVoiceWakeConfig();
|
||||
started.sendEvent({
|
||||
nodeId: node.nodeId,
|
||||
event: "voicewake.changed",
|
||||
payloadJSON: JSON.stringify({ triggers: cfg.triggers }),
|
||||
});
|
||||
} catch {
|
||||
// Best-effort only.
|
||||
}
|
||||
},
|
||||
onDisconnected: (node) => {
|
||||
params.bridgeUnsubscribeAll(node.nodeId);
|
||||
stopNodePresenceTimer(node.nodeId);
|
||||
beaconNodePresence(node, "node-disconnected");
|
||||
},
|
||||
onEvent: params.handleBridgeEvent,
|
||||
onPairRequested: (request) => {
|
||||
params.broadcast("node.pair.requested", request, {
|
||||
dropIfSlow: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
if (started.port > 0) {
|
||||
params.logBridge.info(
|
||||
`listening on tcp://${params.bridgeHost}:${started.port} (node)`,
|
||||
);
|
||||
return { bridge: started, nodePresenceTimers };
|
||||
}
|
||||
} catch (err) {
|
||||
params.logBridge.warn(`failed to start: ${String(err)}`);
|
||||
}
|
||||
} else if (
|
||||
params.bridgeEnabled &&
|
||||
params.bridgePort > 0 &&
|
||||
!params.bridgeHost
|
||||
) {
|
||||
params.logBridge.warn(
|
||||
"bind policy requested tailnet IP, but no tailnet interface was found; refusing to start bridge",
|
||||
);
|
||||
}
|
||||
|
||||
return { bridge: null, nodePresenceTimers };
|
||||
}
|
||||
Reference in New Issue
Block a user