


























@@ -19,13 +19,15 @@ import {
1919DEFAULT_DANGEROUS_NODE_COMMANDS,
2020resolveNodeCommandAllowlist,
2121} from "./node-command-policy.js";
22+import type { SerializedEventPayload } from "./node-registry.js";
2223import type { RequestFrame } from "./protocol/index.js";
2324import { createGatewayBroadcaster } from "./server-broadcast.js";
2425import { createChatRunRegistry } from "./server-chat.js";
2526import { MAX_BUFFERED_BYTES } from "./server-constants.js";
2627import { handleNodeInvokeResult } from "./server-methods/nodes.handlers.invoke-result.js";
2728import type { GatewayClient as GatewayMethodClient } from "./server-methods/types.js";
2829import type { GatewayRequestContext, RespondFn } from "./server-methods/types.js";
30+import { createGatewayNodeSessionRuntime } from "./server-node-session-runtime.js";
2931import { createNodeSubscriptionManager } from "./server-node-subscriptions.js";
3032import { formatError, normalizeVoiceWakeTriggers } from "./server-utils.js";
3133import type { GatewayWsClient } from "./server/ws-types.js";
@@ -572,6 +574,47 @@ describe("gateway broadcaster", () => {
572574]);
573575});
574576577+it("reuses the same payload shape while assigning per-client seq values", () => {
578+const firstSocket = makeRecordingSocket();
579+const secondSocket = makeRecordingSocket();
580+const thirdSocket = makeRecordingSocket();
581+const clients = new Set<GatewayWsClient>([
582+makeGatewayWsClient("c-1", firstSocket, {
583+role: "operator",
584+scopes: ["operator.read"],
585+} as GatewayWsClient["connect"]),
586+makeGatewayWsClient("c-2", secondSocket, {
587+role: "operator",
588+scopes: ["operator.write"],
589+} as GatewayWsClient["connect"]),
590+makeGatewayWsClient("c-3", thirdSocket, {
591+role: "operator",
592+scopes: ["operator.admin"],
593+} as GatewayWsClient["connect"]),
594+]);
595+const payloadKeys: string[] = [];
596+const payload = {
597+toJSON(key: string) {
598+payloadKeys.push(key);
599+return { foo: key };
600+},
601+};
602+603+const { broadcast } = createGatewayBroadcaster({ clients });
604+broadcast("talk.mode", { enabled: true });
605+broadcast("chat", payload);
606+607+expect(payloadKeys).toEqual(["payload"]);
608+expect(firstSocket.sent.at(-1)?.payload).toEqual({ foo: "payload" });
609+expect(secondSocket.sent.at(-1)?.payload).toEqual({ foo: "payload" });
610+expect(thirdSocket.sent.at(-1)?.payload).toEqual({ foo: "payload" });
611+expect([
612+firstSocket.sent.at(-1)?.seq,
613+secondSocket.sent.at(-1)?.seq,
614+thirdSocket.sent.at(-1)?.seq,
615+]).toEqual([1, 2, 2]);
616+});
617+575618it("preserves seq gaps when dropIfSlow skips an eligible broadcast", () => {
576619const slowReadSocket = makeRecordingSocket();
577620slowReadSocket.bufferedAmount = Number.MAX_SAFE_INTEGER;
@@ -708,10 +751,13 @@ describe("node subscription manager", () => {
708751const sent: Array<{
709752nodeId: string;
710753event: string;
711-payloadJSON?: string | null;
754+payloadJSON?: SerializedEventPayload | null;
712755}> = [];
713-const sendEvent = (evt: { nodeId: string; event: string; payloadJSON?: string | null }) =>
714-sent.push(evt);
756+const sendEvent = (evt: {
757+nodeId: string;
758+event: string;
759+payloadJSON?: SerializedEventPayload | null;
760+}) => sent.push(evt);
715761716762manager.subscribe("node-a", "main");
717763manager.subscribe("node-b", "main");
@@ -722,6 +768,45 @@ describe("node subscription manager", () => {
722768expect(sent[0].event).toBe("chat");
723769});
724770771+test("runtime forwards subscribed node payload json without parsing it again", () => {
772+const frames: string[] = [];
773+const socket: TestSocket = {
774+bufferedAmount: 0,
775+send: vi.fn((payload: string) => frames.push(payload)),
776+close: vi.fn(),
777+};
778+const parseSpy = vi.spyOn(JSON, "parse");
779+try {
780+const runtime = createGatewayNodeSessionRuntime({ broadcast: vi.fn() });
781+runtime.nodeRegistry.register(
782+makeGatewayWsClient("conn-node-a", socket, {
783+role: "node",
784+scopes: [],
785+client: {
786+id: "node-client",
787+version: "1.0.0",
788+platform: "darwin",
789+mode: "node",
790+},
791+device: { id: "node-a" },
792+} as unknown as GatewayWsClient["connect"]),
793+{},
794+);
795+runtime.nodeSubscribe("node-a", "main");
796+797+runtime.nodeSendToSession("main", "chat", { ok: true });
798+799+expect(parseSpy).not.toHaveBeenCalled();
800+} finally {
801+parseSpy.mockRestore();
802+}
803+expect(JSON.parse(frames[0] ?? "{}")).toEqual({
804+type: "event",
805+event: "chat",
806+payload: { ok: true },
807+});
808+});
809+725810test("unsubscribeAll clears session mappings", () => {
726811const manager = createNodeSubscriptionManager();
727812const sent: string[] = [];
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。