@ggui-ai/mcp-server 0.2.0-alpha.4 → 0.4.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin-blueprints-transport.d.ts.map +1 -1
- package/dist/admin-blueprints-transport.js +2 -1
- package/dist/admin-oauth-providers-transport.d.ts.map +1 -1
- package/dist/admin-oauth-providers-transport.js +7 -5
- package/dist/api-renders-routes.d.ts +85 -0
- package/dist/api-renders-routes.d.ts.map +1 -0
- package/dist/api-renders-routes.js +372 -0
- package/dist/build-mcp.d.ts +2 -2
- package/dist/build-mcp.d.ts.map +1 -1
- package/dist/build-mcp.js +64 -5
- package/dist/code-routes.d.ts +47 -0
- package/dist/code-routes.d.ts.map +1 -0
- package/dist/code-routes.js +81 -0
- package/dist/code-store-fs.js +2 -2
- package/dist/console-auth.d.ts +10 -10
- package/dist/console-auth.d.ts.map +1 -1
- package/dist/console-auth.js +5 -5
- package/dist/console-blueprint-routes.d.ts +71 -0
- package/dist/console-blueprint-routes.d.ts.map +1 -0
- package/dist/console-blueprint-routes.js +348 -0
- package/dist/console-chat-routes.d.ts +80 -0
- package/dist/console-chat-routes.d.ts.map +1 -0
- package/dist/console-chat-routes.js +204 -0
- package/dist/console-config-routes.d.ts +37 -0
- package/dist/console-config-routes.d.ts.map +1 -0
- package/dist/console-config-routes.js +91 -0
- package/dist/console-headers.d.ts +1 -1
- package/dist/console-info-routes.d.ts +84 -0
- package/dist/console-info-routes.d.ts.map +1 -0
- package/dist/console-info-routes.js +135 -0
- package/dist/console-keys-routes.d.ts +50 -0
- package/dist/console-keys-routes.d.ts.map +1 -0
- package/dist/console-keys-routes.js +222 -0
- package/dist/console-llm-keys-routes.d.ts +47 -0
- package/dist/console-llm-keys-routes.d.ts.map +1 -0
- package/dist/console-llm-keys-routes.js +443 -0
- package/dist/console-mcp-tools-routes.d.ts +41 -0
- package/dist/console-mcp-tools-routes.d.ts.map +1 -0
- package/dist/console-mcp-tools-routes.js +60 -0
- package/dist/console-registry-routes.d.ts +66 -0
- package/dist/console-registry-routes.d.ts.map +1 -0
- package/dist/console-registry-routes.js +276 -0
- package/dist/console-session-routes.d.ts +89 -0
- package/dist/console-session-routes.d.ts.map +1 -0
- package/dist/console-session-routes.js +385 -0
- package/dist/console-sessions-routes.d.ts +52 -0
- package/dist/console-sessions-routes.d.ts.map +1 -0
- package/dist/console-sessions-routes.js +106 -0
- package/dist/console-static-routes.d.ts +54 -0
- package/dist/console-static-routes.d.ts.map +1 -0
- package/dist/console-static-routes.js +190 -0
- package/dist/console-theme-routes.d.ts +3 -3
- package/dist/console-theme-routes.js +1 -1
- package/dist/console-timeline.d.ts +5 -5
- package/dist/console-timeline.d.ts.map +1 -1
- package/dist/console-timeline.js +27 -26
- package/dist/console-welcome.js +2 -2
- package/dist/email-login.d.ts.map +1 -1
- package/dist/email-login.js +2 -3
- package/dist/ggui-session-channel/action-ingress.d.ts +54 -0
- package/dist/ggui-session-channel/action-ingress.d.ts.map +1 -0
- package/dist/ggui-session-channel/action-ingress.js +228 -0
- package/dist/ggui-session-channel/channel-subscriptions.d.ts +97 -0
- package/dist/ggui-session-channel/channel-subscriptions.d.ts.map +1 -0
- package/dist/ggui-session-channel/channel-subscriptions.js +224 -0
- package/dist/ggui-session-channel/internal-types.d.ts +102 -0
- package/dist/ggui-session-channel/internal-types.d.ts.map +1 -0
- package/dist/ggui-session-channel/internal-types.js +6 -0
- package/dist/ggui-session-channel/outbound.d.ts +81 -0
- package/dist/ggui-session-channel/outbound.d.ts.map +1 -0
- package/dist/ggui-session-channel/outbound.js +174 -0
- package/dist/ggui-session-channel/socket-router.d.ts +38 -0
- package/dist/ggui-session-channel/socket-router.d.ts.map +1 -0
- package/dist/ggui-session-channel/socket-router.js +213 -0
- package/dist/ggui-session-channel/subscribe.d.ts +165 -0
- package/dist/ggui-session-channel/subscribe.d.ts.map +1 -0
- package/dist/ggui-session-channel/subscribe.js +370 -0
- package/dist/ggui-session-channel/subscriber-lifecycle.d.ts +40 -0
- package/dist/ggui-session-channel/subscriber-lifecycle.d.ts.map +1 -0
- package/dist/ggui-session-channel/subscriber-lifecycle.js +123 -0
- package/dist/ggui-session-channel.d.ts +425 -0
- package/dist/ggui-session-channel.d.ts.map +1 -0
- package/dist/ggui-session-channel.js +262 -0
- package/dist/health-routes.d.ts +76 -0
- package/dist/health-routes.d.ts.map +1 -0
- package/dist/health-routes.js +145 -0
- package/dist/index.d.ts +10 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -9
- package/dist/instructions-presets.d.ts +3 -3
- package/dist/instructions-presets.js +24 -24
- package/dist/llm-backed-negotiator.d.ts +68 -67
- package/dist/llm-backed-negotiator.d.ts.map +1 -1
- package/dist/llm-backed-negotiator.js +82 -221
- package/dist/mcp-apps-outbound.d.ts +47 -48
- package/dist/mcp-apps-outbound.d.ts.map +1 -1
- package/dist/mcp-apps-outbound.js +154 -177
- package/dist/mcp-endpoint-routes.d.ts +88 -0
- package/dist/mcp-endpoint-routes.d.ts.map +1 -0
- package/dist/mcp-endpoint-routes.js +359 -0
- package/dist/mcp-mounts.d.ts +2 -76
- package/dist/mcp-mounts.d.ts.map +1 -1
- package/dist/mcp-mounts.js +0 -76
- package/dist/oauth-as-routes.d.ts +60 -0
- package/dist/oauth-as-routes.d.ts.map +1 -0
- package/dist/oauth-as-routes.js +82 -0
- package/dist/oauth-clients-routes.d.ts +39 -0
- package/dist/oauth-clients-routes.d.ts.map +1 -0
- package/dist/oauth-clients-routes.js +87 -0
- package/dist/oauth-login-types.d.ts +1 -20
- package/dist/oauth-login-types.d.ts.map +1 -1
- package/dist/oauth-login-types.js +30 -7
- package/dist/oauth-login.d.ts.map +1 -1
- package/dist/oauth-login.js +3 -2
- package/dist/oauth-providers-store.d.ts.map +1 -1
- package/dist/oauth-providers-store.js +5 -5
- package/dist/oauth.d.ts +9 -8
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +41 -19
- package/dist/pairing-transport.d.ts.map +1 -1
- package/dist/pairing-transport.js +2 -1
- package/dist/request-context.d.ts +2 -2
- package/dist/request-context.js +2 -2
- package/dist/reserved-validators.d.ts.map +1 -1
- package/dist/reserved-validators.js +9 -1
- package/dist/route-param.d.ts +9 -0
- package/dist/route-param.d.ts.map +1 -0
- package/dist/route-param.js +10 -0
- package/dist/runtime-bundle-route.d.ts +43 -0
- package/dist/runtime-bundle-route.d.ts.map +1 -0
- package/dist/runtime-bundle-route.js +80 -0
- package/dist/schema-compat.d.ts +64 -62
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +23 -51
- package/dist/server.d.ts +179 -193
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +644 -3759
- package/dist/storage.d.ts +5 -5
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +5 -5
- package/dist/thread-transport.d.ts.map +1 -1
- package/dist/thread-transport.js +4 -3
- package/dist/user-session-auth.d.ts +7 -21
- package/dist/user-session-auth.d.ts.map +1 -1
- package/dist/user-session-auth.js +7 -28
- package/package.json +16 -15
- package/dist/mcp-apps-inbound.d.ts +0 -86
- package/dist/mcp-apps-inbound.d.ts.map +0 -1
- package/dist/mcp-apps-inbound.js +0 -283
- package/dist/render-channel.d.ts +0 -694
- package/dist/render-channel.d.ts.map +0 -1
- package/dist/render-channel.js +0 -1775
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outbound send / fan-out family for the live channel — the wire-write
|
|
3
|
+
* primitives every handler module shares (`send`, `sendError`,
|
|
4
|
+
* `sendChannelError`) plus the public fan-out surfaces
|
|
5
|
+
* (`sendToGguiSession`, `notifyGguiSessionCommit`, `sendPropsUpdate`,
|
|
6
|
+
* `sendDrainAck`, `externalBroadcast`) the channel server object
|
|
7
|
+
* delegates to.
|
|
8
|
+
*
|
|
9
|
+
* The behavior contracts for the public surfaces (validation posture,
|
|
10
|
+
* replay-buffer stamping, best-effort delivery) are documented on the
|
|
11
|
+
* `GguiSessionChannelServer` interface in `../ggui-session-channel.ts`;
|
|
12
|
+
* inline comments here cover impl-level decisions only.
|
|
13
|
+
*/
|
|
14
|
+
import type { GguiSessionStore, GguiSessionStreamBuffer, StreamEnvelopeInput, StreamFanout } from "@ggui-ai/mcp-server-core";
|
|
15
|
+
import type { ErrorPayload, GguiSession, JsonObject, ReservedChannelValidator } from "@ggui-ai/protocol";
|
|
16
|
+
import type { WebSocketMessage } from "@ggui-ai/protocol/transport/websocket";
|
|
17
|
+
import type { WebSocket } from "ws";
|
|
18
|
+
import type { Logger } from "../logger.js";
|
|
19
|
+
import type { Subscriber } from "./internal-types.js";
|
|
20
|
+
/** Channel-scoped error codes carried on `channel_error` frames. */
|
|
21
|
+
export type ChannelErrorCode = "CHANNEL_UNKNOWN" | "CHANNEL_NOT_LOCAL" | "SESSION_NOT_FOUND" | "SUBSCRIBE_UNAUTHORIZED" | "POLL_FAILED";
|
|
22
|
+
export interface OutboundDeps {
|
|
23
|
+
readonly logger: Logger;
|
|
24
|
+
/** GguiSession backing store — streamSpec lookups for fan-out validation. */
|
|
25
|
+
readonly renderStore: GguiSessionStore;
|
|
26
|
+
/** Replay buffer — owns seq assignment + bounded replay storage. */
|
|
27
|
+
readonly streamBuffer: GguiSessionStreamBuffer;
|
|
28
|
+
/** Live-tail pub/sub seam for stream-envelope fan-out. */
|
|
29
|
+
readonly streamFanout: StreamFanout;
|
|
30
|
+
/**
|
|
31
|
+
* Flat set of all live WS subscribers — shared with the
|
|
32
|
+
* subscriber-lifecycle module (which owns membership); read-only
|
|
33
|
+
* here. Direct-to-WS frames (`props_update`, `render`, `drain_ack`,
|
|
34
|
+
* external broadcasts) iterate + filter by `sessionId`.
|
|
35
|
+
*/
|
|
36
|
+
readonly wsSubscribers: ReadonlySet<Subscriber>;
|
|
37
|
+
/**
|
|
38
|
+
* Reserved-channel payload validators threaded from
|
|
39
|
+
* `GguiSessionChannelOptions.extraReservedValidators` — consulted by
|
|
40
|
+
* `assertStreamContract` ahead of the protocol built-ins.
|
|
41
|
+
*/
|
|
42
|
+
readonly extraReservedValidators?: ReadonlyMap<string, ReservedChannelValidator>;
|
|
43
|
+
}
|
|
44
|
+
/** The send/fan-out helpers, bound to one channel instance. */
|
|
45
|
+
export interface Outbound {
|
|
46
|
+
/** Low-level wire write — skips closed sockets, warn-logs send failures. */
|
|
47
|
+
send(ws: WebSocket, msg: WebSocketMessage): void;
|
|
48
|
+
/** Emit an `error` frame with the canonical payload shape. */
|
|
49
|
+
sendError(ws: WebSocket, code: string, message: string, requestId?: string, details?: ErrorPayload["details"]): void;
|
|
50
|
+
/**
|
|
51
|
+
* Emit a `channel_error` frame to a specific subscriber. Used by the
|
|
52
|
+
* `channel_subscribe` handler for both subscribe-time rejections
|
|
53
|
+
* (`CHANNEL_UNKNOWN`, `CHANNEL_NOT_LOCAL`, `SESSION_NOT_FOUND`,
|
|
54
|
+
* `SUBSCRIBE_UNAUTHORIZED`) AND poll-time failures (`POLL_FAILED`).
|
|
55
|
+
*
|
|
56
|
+
* Direct-to-WS, not via fanOut — channel_error frames are
|
|
57
|
+
* per-subscriber and not stored in the replay buffer. A new
|
|
58
|
+
* subscriber on the same render will re-subscribe and discover the
|
|
59
|
+
* same error itself.
|
|
60
|
+
*/
|
|
61
|
+
sendChannelError(ws: WebSocket, sessionId: string, channelName: string, code: ChannelErrorCode, message: string, requestId?: string, details?: ErrorPayload["details"]): void;
|
|
62
|
+
/** Impl behind {@link GguiSessionChannelServer.sendToGguiSession}. */
|
|
63
|
+
sendToGguiSession(delivery: StreamEnvelopeInput): Promise<{
|
|
64
|
+
seq: number;
|
|
65
|
+
}>;
|
|
66
|
+
/** Impl behind {@link GguiSessionChannelServer.notifyGguiSessionCommit}. */
|
|
67
|
+
notifyGguiSessionCommit(sessionId: string, render: GguiSession, matchType?: string): void;
|
|
68
|
+
/** Impl behind {@link GguiSessionChannelServer.sendPropsUpdate}. */
|
|
69
|
+
sendPropsUpdate(sessionId: string, props: JsonObject): Promise<void>;
|
|
70
|
+
/** Impl behind {@link GguiSessionChannelServer.sendDrainAck}. */
|
|
71
|
+
sendDrainAck(args: {
|
|
72
|
+
readonly sessionId: string;
|
|
73
|
+
readonly appId: string;
|
|
74
|
+
readonly eventId: string;
|
|
75
|
+
readonly drainedAt: string;
|
|
76
|
+
}): void;
|
|
77
|
+
/** Impl behind {@link GguiSessionChannelServer.externalBroadcast}. */
|
|
78
|
+
externalBroadcast(sessionId: string, frame: WebSocketMessage): void;
|
|
79
|
+
}
|
|
80
|
+
export declare function createOutbound(deps: OutboundDeps): Outbound;
|
|
81
|
+
//# sourceMappingURL=outbound.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outbound.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/outbound.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EACV,gBAAgB,EAChB,uBAAuB,EACvB,mBAAmB,EACnB,YAAY,EACb,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EACV,YAAY,EACZ,WAAW,EACX,UAAU,EACV,wBAAwB,EAEzB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEtD,oEAAoE;AACpE,MAAM,MAAM,gBAAgB,GACxB,iBAAiB,GACjB,mBAAmB,GACnB,mBAAmB,GACnB,wBAAwB,GACxB,aAAa,CAAC;AAElB,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC,oEAAoE;IACpE,QAAQ,CAAC,YAAY,EAAE,uBAAuB,CAAC;IAC/C,0DAA0D;IAC1D,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,EAAE,WAAW,CAAC,UAAU,CAAC,CAAC;IAChD;;;;OAIG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAAC;CAClF;AAED,+DAA+D;AAC/D,MAAM,WAAW,QAAQ;IACvB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACjD,8DAA8D;IAC9D,SAAS,CACP,EAAE,EAAE,SAAS,EACb,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,YAAY,CAAC,SAAS,CAAC,GAChC,IAAI,CAAC;IACR;;;;;;;;;;OAUG;IACH,gBAAgB,CACd,EAAE,EAAE,SAAS,EACb,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,gBAAgB,EACtB,OAAO,EAAE,MAAM,EACf,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,YAAY,CAAC,SAAS,CAAC,GAChC,IAAI,CAAC;IACR,sEAAsE;IACtE,iBAAiB,CAAC,QAAQ,EAAE,mBAAmB,GAAG,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3E,4EAA4E;IAC5E,uBAAuB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1F,oEAAoE;IACpE,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrE,iEAAiE;IACjE,YAAY,CAAC,IAAI,EAAE;QACjB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;KAC5B,GAAG,IAAI,CAAC;IACT,sEAAsE;IACtE,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAAC;CACrE;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,QAAQ,CAsM3D"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outbound send / fan-out family for the live channel — the wire-write
|
|
3
|
+
* primitives every handler module shares (`send`, `sendError`,
|
|
4
|
+
* `sendChannelError`) plus the public fan-out surfaces
|
|
5
|
+
* (`sendToGguiSession`, `notifyGguiSessionCommit`, `sendPropsUpdate`,
|
|
6
|
+
* `sendDrainAck`, `externalBroadcast`) the channel server object
|
|
7
|
+
* delegates to.
|
|
8
|
+
*
|
|
9
|
+
* The behavior contracts for the public surfaces (validation posture,
|
|
10
|
+
* replay-buffer stamping, best-effort delivery) are documented on the
|
|
11
|
+
* `GguiSessionChannelServer` interface in `../ggui-session-channel.ts`;
|
|
12
|
+
* inline comments here cover impl-level decisions only.
|
|
13
|
+
*/
|
|
14
|
+
import { assertStreamContract } from "@ggui-ai/mcp-server-handlers/renders";
|
|
15
|
+
export function createOutbound(deps) {
|
|
16
|
+
function send(ws, msg) {
|
|
17
|
+
if (ws.readyState !== ws.OPEN)
|
|
18
|
+
return;
|
|
19
|
+
try {
|
|
20
|
+
ws.send(JSON.stringify(msg));
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
deps.logger.warn("render_channel_send_failed", { error: String(err) });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function sendError(ws, code, message, requestId, details) {
|
|
27
|
+
send(ws, {
|
|
28
|
+
type: "error",
|
|
29
|
+
payload: { code, message, ...(details !== undefined ? { details } : {}) },
|
|
30
|
+
...(requestId ? { requestId } : {}),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function sendChannelError(ws, sessionId, channelName, code, message, requestId, details) {
|
|
34
|
+
send(ws, {
|
|
35
|
+
type: "channel_error",
|
|
36
|
+
payload: {
|
|
37
|
+
sessionId,
|
|
38
|
+
channelName,
|
|
39
|
+
code,
|
|
40
|
+
message,
|
|
41
|
+
...(details !== undefined ? { details } : {}),
|
|
42
|
+
},
|
|
43
|
+
...(requestId ? { requestId } : {}),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Stamp a delivery through the replay buffer and fan it out to every
|
|
48
|
+
* subscriber of the render, honoring the per-subscriber replay
|
|
49
|
+
* cursor. Backs the public `sendToGguiSession` entry point.
|
|
50
|
+
*
|
|
51
|
+
* Caller is responsible for validating `delivery.payload` against
|
|
52
|
+
* the active streamSpec BEFORE calling — the fan-out here trusts
|
|
53
|
+
* its input. Reserved-channel deliveries (e.g. `_ggui:lifecycle`)
|
|
54
|
+
* bypass the streamSpec check upstream via
|
|
55
|
+
* `assertStreamContract`.
|
|
56
|
+
*/
|
|
57
|
+
async function fanOut(delivery, activeStreamSpec) {
|
|
58
|
+
const { envelope } = await deps.streamBuffer.record(delivery, activeStreamSpec);
|
|
59
|
+
// Publish to the seam — InProcessStreamFanout walks its subscriber
|
|
60
|
+
// queues synchronously inside publish(), so no real async hop. The
|
|
61
|
+
// pump loop on each WS subscriber yields the envelope, applies the
|
|
62
|
+
// per-sub replay-cursor filter, and sends to the WS. Fire-and-forget
|
|
63
|
+
// because publish() never throws on the in-process impl, and an
|
|
64
|
+
// external pubsub-fanout failure here would already be persisted to
|
|
65
|
+
// the GguiSessionStreamBuffer for replay-recovery on reconnect.
|
|
66
|
+
void deps.streamFanout.publish({ sessionId: envelope.sessionId, envelope });
|
|
67
|
+
return { seq: envelope.seq };
|
|
68
|
+
}
|
|
69
|
+
async function sendToGguiSession(delivery) {
|
|
70
|
+
// Outbound fan-out enforcement (defense-in-depth parity with
|
|
71
|
+
// hosted `handle-data.ts`). Re-validates the delivery's payload
|
|
72
|
+
// against the render's streamSpec BEFORE delivery — so a future
|
|
73
|
+
// OSS mutation handler that bypasses the emit-side check can't
|
|
74
|
+
// fan out malformed data to subscribers. Throws
|
|
75
|
+
// ContractViolationError{tool:'ggui_emit'} on violation;
|
|
76
|
+
// caller decides what to do (log, rethrow, wrap).
|
|
77
|
+
const stored = await deps.renderStore.get(delivery.sessionId);
|
|
78
|
+
const activeEntry = stored?.render;
|
|
79
|
+
const streamSpec = activeEntry !== undefined && activeEntry.type !== "mcpApps" && activeEntry.type !== "system"
|
|
80
|
+
? activeEntry.streamSpec
|
|
81
|
+
: undefined;
|
|
82
|
+
assertStreamContract(streamSpec, delivery.channel, delivery.payload, deps.extraReservedValidators);
|
|
83
|
+
return fanOut(delivery, streamSpec);
|
|
84
|
+
}
|
|
85
|
+
function notifyGguiSessionCommit(sessionId, render, matchType) {
|
|
86
|
+
// Best-effort fan-out to every live subscriber bound to this
|
|
87
|
+
// render. NOT routed through the replay buffer — see the
|
|
88
|
+
// `notifyGguiSessionCommit` JSDoc on the public interface for why
|
|
89
|
+
// fresh subscribers rely on `ack.render` instead of a replay frame.
|
|
90
|
+
// NOT routed through StreamFanout either — `type: 'render'` is a
|
|
91
|
+
// distinct WebSocket message type. Filter the flat WS-subscriber
|
|
92
|
+
// set by sessionId; N is typically 1-2 (multi-tab render sharing).
|
|
93
|
+
const payload = matchType !== undefined ? { session: render, matchType } : { session: render };
|
|
94
|
+
for (const sub of deps.wsSubscribers) {
|
|
95
|
+
if (sub.sessionId !== sessionId)
|
|
96
|
+
continue;
|
|
97
|
+
send(sub.ws, { type: "render", payload });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Best-effort + orphan-tolerant per the docstring on the public
|
|
102
|
+
* `sendPropsUpdate` method.
|
|
103
|
+
*/
|
|
104
|
+
async function sendPropsUpdate(sessionId, props) {
|
|
105
|
+
let stored;
|
|
106
|
+
try {
|
|
107
|
+
stored = await deps.renderStore.get(sessionId);
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
deps.logger.warn("render_channel_props_update_lookup_failed", {
|
|
111
|
+
sessionId,
|
|
112
|
+
error: String(err),
|
|
113
|
+
});
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (!stored) {
|
|
117
|
+
deps.logger.warn("render_channel_props_update_orphan", {
|
|
118
|
+
sessionId,
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
// Filter the flat WS-subscriber set by sessionId; same posture as
|
|
123
|
+
// `notifyGguiSessionCommit`. `send()` already silently skips closed sockets
|
|
124
|
+
// and logs (but doesn't throw on) per-subscriber send failures, so
|
|
125
|
+
// the calling handler can't be made to fail by a dead WebSocket.
|
|
126
|
+
for (const sub of deps.wsSubscribers) {
|
|
127
|
+
if (sub.sessionId !== sessionId)
|
|
128
|
+
continue;
|
|
129
|
+
send(sub.ws, {
|
|
130
|
+
type: "props_update",
|
|
131
|
+
payload: { sessionId, props },
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function sendDrainAck({ sessionId, appId, eventId, drainedAt, }) {
|
|
136
|
+
// Server-side fan-out for the action-drain ack.
|
|
137
|
+
// Filter the flat WS-subscriber set by sessionId (same posture
|
|
138
|
+
// as `sendPropsUpdate`). No persistence; subscribers that
|
|
139
|
+
// missed the frame fall back to their 10s claim timer, which
|
|
140
|
+
// the atomic pop resolves cleanly.
|
|
141
|
+
for (const sub of deps.wsSubscribers) {
|
|
142
|
+
if (sub.sessionId !== sessionId)
|
|
143
|
+
continue;
|
|
144
|
+
send(sub.ws, {
|
|
145
|
+
type: "drain_ack",
|
|
146
|
+
payload: { sessionId, appId, eventId, drainedAt },
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function externalBroadcast(sessionId, frame) {
|
|
151
|
+
// Walk the flat subscriber set; filter to matching sessionId.
|
|
152
|
+
// `send()` already guards closed sockets and logs (but doesn't
|
|
153
|
+
// throw on) per-subscriber failures, so the caller (an external
|
|
154
|
+
// pubsub on-message handler) can't be made to fail by a dead
|
|
155
|
+
// WebSocket. No GguiSessionStore lookup — the publisher already
|
|
156
|
+
// validated; this seam is the cross-process delivery path, not
|
|
157
|
+
// the re-validation point.
|
|
158
|
+
for (const sub of deps.wsSubscribers) {
|
|
159
|
+
if (sub.sessionId !== sessionId)
|
|
160
|
+
continue;
|
|
161
|
+
send(sub.ws, frame);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
send,
|
|
166
|
+
sendError,
|
|
167
|
+
sendChannelError,
|
|
168
|
+
sendToGguiSession,
|
|
169
|
+
notifyGguiSessionCommit,
|
|
170
|
+
sendPropsUpdate,
|
|
171
|
+
sendDrainAck,
|
|
172
|
+
externalBroadcast,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-socket message routing + WS lifecycle wiring for the live
|
|
3
|
+
* channel — the `connection` handler (with the per-socket inbound
|
|
4
|
+
* ordering chain), the pre-subscribe identity / cookie bindings, the
|
|
5
|
+
* `onMessage` type dispatcher, and the observation-message ingress
|
|
6
|
+
* (`host_context_observed`) it routes.
|
|
7
|
+
*/
|
|
8
|
+
import type { GguiSessionStore } from "@ggui-ai/mcp-server-core";
|
|
9
|
+
import type { WebSocket, WebSocketServer } from "ws";
|
|
10
|
+
import type { Logger } from "../logger.js";
|
|
11
|
+
import type { ActionIngress } from "./action-ingress.js";
|
|
12
|
+
import type { ChannelSubscriptions } from "./channel-subscriptions.js";
|
|
13
|
+
import type { Subscriber } from "./internal-types.js";
|
|
14
|
+
import type { Outbound } from "./outbound.js";
|
|
15
|
+
import type { SubscribeHandlers } from "./subscribe.js";
|
|
16
|
+
import type { SubscriberLifecycle } from "./subscriber-lifecycle.js";
|
|
17
|
+
export interface SocketRouterDeps {
|
|
18
|
+
readonly logger: Logger;
|
|
19
|
+
/** GguiSession backing store — observation-message patches persist here. */
|
|
20
|
+
readonly renderStore: GguiSessionStore;
|
|
21
|
+
/** ws → subscriber reverse index — the dispatcher's per-frame lookup. */
|
|
22
|
+
readonly subscribersByWs: WeakMap<WebSocket, Subscriber>;
|
|
23
|
+
readonly send: Outbound["send"];
|
|
24
|
+
readonly sendError: Outbound["sendError"];
|
|
25
|
+
readonly unregister: SubscriberLifecycle["unregister"];
|
|
26
|
+
readonly handleSubscribe: SubscribeHandlers["handleSubscribe"];
|
|
27
|
+
readonly handleInboundAction: ActionIngress["handleInboundAction"];
|
|
28
|
+
readonly handleChannelSubscribe: ChannelSubscriptions["handleChannelSubscribe"];
|
|
29
|
+
readonly handleChannelUnsubscribe: ChannelSubscriptions["handleChannelUnsubscribe"];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Wire the channel's `connection` handling onto `wss`. Reads the
|
|
33
|
+
* upgrade-time identity / cookie bindings the upgrade phase stashed on
|
|
34
|
+
* the request (see {@link UpgradeBindings}) and serializes inbound
|
|
35
|
+
* frame processing per socket.
|
|
36
|
+
*/
|
|
37
|
+
export declare function attachSocketRouter(wss: WebSocketServer, deps: SocketRouterDeps): void;
|
|
38
|
+
//# sourceMappingURL=socket-router.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"socket-router.d.ts","sourceRoot":"","sources":["../../src/ggui-session-channel/socket-router.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAoB,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAInF,OAAO,KAAK,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,IAAI,CAAC;AACrD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AACvE,OAAO,KAAK,EAAE,UAAU,EAAmB,MAAM,qBAAqB,CAAC;AACvE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAErE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,4EAA4E;IAC5E,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC,yEAAyE;IACzE,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IACzD,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC;IAC1C,QAAQ,CAAC,UAAU,EAAE,mBAAmB,CAAC,YAAY,CAAC,CAAC;IACvD,QAAQ,CAAC,eAAe,EAAE,iBAAiB,CAAC,iBAAiB,CAAC,CAAC;IAC/D,QAAQ,CAAC,mBAAmB,EAAE,aAAa,CAAC,qBAAqB,CAAC,CAAC;IACnE,QAAQ,CAAC,sBAAsB,EAAE,oBAAoB,CAAC,wBAAwB,CAAC,CAAC;IAChF,QAAQ,CAAC,wBAAwB,EAAE,oBAAoB,CAAC,0BAA0B,CAAC,CAAC;CACrF;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,eAAe,EAAE,IAAI,EAAE,gBAAgB,GAAG,IAAI,CAkQrF"}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-socket message routing + WS lifecycle wiring for the live
|
|
3
|
+
* channel — the `connection` handler (with the per-socket inbound
|
|
4
|
+
* ordering chain), the pre-subscribe identity / cookie bindings, the
|
|
5
|
+
* `onMessage` type dispatcher, and the observation-message ingress
|
|
6
|
+
* (`host_context_observed`) it routes.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Wire the channel's `connection` handling onto `wss`. Reads the
|
|
10
|
+
* upgrade-time identity / cookie bindings the upgrade phase stashed on
|
|
11
|
+
* the request (see {@link UpgradeBindings}) and serializes inbound
|
|
12
|
+
* frame processing per socket.
|
|
13
|
+
*/
|
|
14
|
+
export function attachSocketRouter(wss, deps) {
|
|
15
|
+
/**
|
|
16
|
+
* Tenancy guard for client-emitted observation messages
|
|
17
|
+
* (`host_context_observed` today). Returns `false`
|
|
18
|
+
* AND emits the appropriate error frame when:
|
|
19
|
+
*
|
|
20
|
+
* - the socket has no bound subscriber (NOT_SUBSCRIBED)
|
|
21
|
+
* - payload.sessionId doesn't match the subscriber binding
|
|
22
|
+
* (SESSION_MISMATCH)
|
|
23
|
+
*
|
|
24
|
+
* Subscriber binding is the authoritative tenancy scope. The wire
|
|
25
|
+
* payload's sessionId is belt-and-suspenders so the error message
|
|
26
|
+
* can be specific; appId narrows transparently via the binding.
|
|
27
|
+
*/
|
|
28
|
+
function checkSubscriberTenancy(ws, sub, payload, messageType, requestId) {
|
|
29
|
+
if (!sub) {
|
|
30
|
+
deps.sendError(ws, "NOT_SUBSCRIBED", `Send a 'subscribe' message first before '${messageType}'`, requestId);
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
if (payload.sessionId !== sub.sessionId) {
|
|
34
|
+
deps.sendError(ws, "SESSION_MISMATCH", `${messageType} payload id '${payload.sessionId ?? "<missing>"}' does not match subscriber render '${sub.sessionId}'`, requestId);
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Persist an observation-message-driven render patch. Fire-and-
|
|
41
|
+
* forget at the wire layer (no response frame); warn-logs persistence
|
|
42
|
+
* errors so transient store failures stay observable without
|
|
43
|
+
* disrupting the iframe. The iframe's local state is already in the
|
|
44
|
+
* new shape; the next round-trip re-emits whatever the persistence
|
|
45
|
+
* layer lost.
|
|
46
|
+
*/
|
|
47
|
+
async function applyGguiSessionPatch(sessionId, appId, messageType, patch) {
|
|
48
|
+
try {
|
|
49
|
+
await deps.renderStore.update(sessionId, patch);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
deps.logger.warn("render_channel_observation_persist_failed", {
|
|
53
|
+
messageType,
|
|
54
|
+
sessionId,
|
|
55
|
+
appId,
|
|
56
|
+
error: err instanceof Error ? err.message : String(err),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function onMessage(ws, raw) {
|
|
61
|
+
const sub = deps.subscribersByWs.get(ws);
|
|
62
|
+
let message;
|
|
63
|
+
try {
|
|
64
|
+
message = JSON.parse(raw);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
deps.sendError(ws, "INVALID_JSON", "Message is not valid JSON");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (!message || typeof message !== "object" || typeof message.type !== "string") {
|
|
71
|
+
deps.sendError(ws, "INVALID_MESSAGE", "Message is missing a `type` discriminator");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
switch (message.type) {
|
|
75
|
+
case "subscribe": {
|
|
76
|
+
// `subscribe` is the only message allowed before identity is
|
|
77
|
+
// bound to a render. Identity was already resolved at upgrade
|
|
78
|
+
// time; we just need to register the subscriber.
|
|
79
|
+
const identity = pendingIdentity.get(ws);
|
|
80
|
+
if (!identity) {
|
|
81
|
+
deps.sendError(ws, "UNAUTHENTICATED", "No identity bound to this socket", message.requestId);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// Cookie-scope enforcement: when the upgrade was authenticated
|
|
85
|
+
// via an console cookie, the subscribe payload MUST target
|
|
86
|
+
// the render the cookie was issued for. A valid cookie for
|
|
87
|
+
// render A can't be used to open render B.
|
|
88
|
+
const cookieBound = pendingCookieBinding.get(ws);
|
|
89
|
+
if (cookieBound) {
|
|
90
|
+
if (message.payload.sessionId !== cookieBound.sessionId) {
|
|
91
|
+
deps.sendError(ws, "DEVTOOL_COOKIE_SESSION_MISMATCH", `Embedded-ui cookie is bound to render '${cookieBound.sessionId}' but subscribe targets '${message.payload.sessionId}'`, message.requestId);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// `appId` is optional on the wire (SPEC §12.2): absent
|
|
95
|
+
// resolves to the cookie's bound appId inside
|
|
96
|
+
// `handleSubscribe` — only a PRESENT contradicting value is
|
|
97
|
+
// a mismatch.
|
|
98
|
+
if (message.payload.appId !== undefined && message.payload.appId !== cookieBound.appId) {
|
|
99
|
+
deps.sendError(ws, "DEVTOOL_COOKIE_APP_MISMATCH", `Embedded-ui cookie is bound to app '${cookieBound.appId}' but subscribe targets '${message.payload.appId}'`, message.requestId);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
await deps.handleSubscribe(ws, identity, message, cookieBound);
|
|
104
|
+
pendingIdentity.delete(ws);
|
|
105
|
+
pendingCookieBinding.delete(ws);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
case "ping":
|
|
109
|
+
deps.send(ws, {
|
|
110
|
+
type: "pong",
|
|
111
|
+
payload: {},
|
|
112
|
+
...(message.requestId ? { requestId: message.requestId } : {}),
|
|
113
|
+
});
|
|
114
|
+
return;
|
|
115
|
+
case "action":
|
|
116
|
+
if (!sub) {
|
|
117
|
+
deps.sendError(ws, "NOT_SUBSCRIBED", "Send a 'subscribe' message first before 'action'", message.requestId);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
await deps.handleInboundAction(ws, sub, message);
|
|
121
|
+
return;
|
|
122
|
+
case "channel_subscribe":
|
|
123
|
+
if (!sub) {
|
|
124
|
+
deps.sendError(ws, "NOT_SUBSCRIBED", "Send a 'subscribe' message first before 'channel_subscribe'", message.requestId);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
await deps.handleChannelSubscribe(ws, sub, message);
|
|
128
|
+
return;
|
|
129
|
+
case "channel_unsubscribe":
|
|
130
|
+
if (!sub) {
|
|
131
|
+
// No subscriber → nothing was subscribed → no-op silently.
|
|
132
|
+
// Returning an error would leak "is this socket subscribed"
|
|
133
|
+
// state for unauthenticated clients.
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
deps.handleChannelUnsubscribe(ws, sub, message);
|
|
137
|
+
return;
|
|
138
|
+
case "host_context_observed":
|
|
139
|
+
// The iframe-runtime echoes its captured `McpUiHostContext`
|
|
140
|
+
// after `ui/initialize` resolves and on every
|
|
141
|
+
// `ui/notifications/host-context-changed` notification. Persist
|
|
142
|
+
// on `GguiSession.hostContext` so `ggui_handshake` and
|
|
143
|
+
// `ggui_consume` can surface it to the agent on subsequent
|
|
144
|
+
// turns. Fire-and-forget on the client side; no response.
|
|
145
|
+
if (!checkSubscriberTenancy(ws, sub, message.payload, message.type, message.requestId)) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
await applyGguiSessionPatch(sub.sessionId, sub.appId, message.type, {
|
|
149
|
+
hostContext: message.payload.hostContext,
|
|
150
|
+
lastActivityAt: Date.now(),
|
|
151
|
+
});
|
|
152
|
+
return;
|
|
153
|
+
default:
|
|
154
|
+
deps.sendError(ws, "UNSUPPORTED_MESSAGE", `Unsupported message type: ${String(message.type)}`, message.requestId);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* During the pre-subscribe window, a ws has a resolved identity but
|
|
159
|
+
* no render-bound subscriber yet. We hold the identity here until
|
|
160
|
+
* the first `subscribe` lands; once it does, the subscriber record
|
|
161
|
+
* owns the identity and this entry is cleared.
|
|
162
|
+
*/
|
|
163
|
+
const pendingIdentity = new WeakMap();
|
|
164
|
+
/**
|
|
165
|
+
* Embedded-ui cookie binding established at upgrade. When present,
|
|
166
|
+
* `handleSubscribe` enforces `subscribe.sessionId === bound.sessionId`
|
|
167
|
+
* so a valid cookie can't be used to open a render it wasn't
|
|
168
|
+
* issued for. Parallel to {@link pendingIdentity} — same lifetime,
|
|
169
|
+
* same WeakMap rationale.
|
|
170
|
+
*/
|
|
171
|
+
const pendingCookieBinding = new WeakMap();
|
|
172
|
+
wss.on("connection", (ws, req) => {
|
|
173
|
+
// Bind the resolved identity from the upgrade phase. It was
|
|
174
|
+
// attached to the request object in handleUpgrade.
|
|
175
|
+
const identity = req.__gguiIdentity;
|
|
176
|
+
if (identity)
|
|
177
|
+
pendingIdentity.set(ws, identity);
|
|
178
|
+
// Likewise for any cookie binding.
|
|
179
|
+
const cookieBound = req.__gguiCookieBound;
|
|
180
|
+
if (cookieBound)
|
|
181
|
+
pendingCookieBinding.set(ws, cookieBound);
|
|
182
|
+
// Per-socket inbound processing chain. The WebSocket wire is an
|
|
183
|
+
// ORDERED frame stream, so inbound handling must observe arrival
|
|
184
|
+
// order even though the handlers are async: without the chain, a
|
|
185
|
+
// client that pipelines `subscribe` + `action` in one TCP segment
|
|
186
|
+
// (both `message` events fire in the same macrotask) gets the
|
|
187
|
+
// action handled while `handleSubscribe` is parked at its first
|
|
188
|
+
// `await` — the socket has no bound subscriber yet, and a
|
|
189
|
+
// correctly-ordered client is rejected with NOT_SUBSCRIBED.
|
|
190
|
+
// Serializing per socket restores the wire's ordering on the
|
|
191
|
+
// processing side; distinct sockets stay fully concurrent.
|
|
192
|
+
let inboundChain = Promise.resolve();
|
|
193
|
+
ws.on("message", (raw) => {
|
|
194
|
+
// `ws.on('message')` delivers Buffer/ArrayBuffer/Buffer[] depending
|
|
195
|
+
// on frame type; normalize to string.
|
|
196
|
+
const text = typeof raw === "string" ? raw : raw.toString("utf8");
|
|
197
|
+
inboundChain = inboundChain.then(() => onMessage(ws, text).catch((err) => {
|
|
198
|
+
// Catch INSIDE the chain link so one failed message never
|
|
199
|
+
// poisons the chain for subsequent frames.
|
|
200
|
+
deps.logger.error("render_channel_message_failed", {
|
|
201
|
+
error: String(err),
|
|
202
|
+
});
|
|
203
|
+
}));
|
|
204
|
+
});
|
|
205
|
+
ws.on("close", () => {
|
|
206
|
+
deps.unregister(ws);
|
|
207
|
+
pendingIdentity.delete(ws);
|
|
208
|
+
});
|
|
209
|
+
ws.on("error", (err) => {
|
|
210
|
+
deps.logger.warn("render_channel_socket_error", { error: String(err) });
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
}
|