@mulmobridge/chat-service 1.1.0 → 1.2.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/index.d.ts CHANGED
@@ -14,6 +14,13 @@ export interface ChatService {
14
14
  * before `attachSocket`: the message is queued and flushes on
15
15
  * the next bridge connection for that transport. */
16
16
  pushToBridge: PushFn;
17
+ /** Subscribe an IN-PROCESS bridge to this transport's pushes (#3080).
18
+ * Returns the unsubscribe function. An in-process bridge is always live, so
19
+ * its pushes are never queued. */
20
+ registerInProcessBridge(transportId: string, handler: (event: {
21
+ chatId: string;
22
+ message: string;
23
+ }) => void): () => void;
17
24
  }
18
25
  export declare function createChatService(deps: ChatServiceDeps): ChatService;
19
26
  export type { Attachment, ChatServiceDeps, StartChatFn, StartChatParams, OnSessionEventFn } from "./types.js";
package/dist/index.js CHANGED
@@ -60,7 +60,42 @@ export function createChatService(deps) {
60
60
  // The queue is shared with the socket layer so any pushes
61
61
  // enqueued during the pre-attach window flush on first connect.
62
62
  let livePush = null;
63
+ // In-process bridges (#3080). Keyed by transportId, at most one each: two
64
+ // bridges claiming one transportId is what #3079 exists to stop, and the
65
+ // registry refuses the second rather than silently fanning a reply out twice.
66
+ const inProcessBridges = new Map();
67
+ const registerInProcessBridge = (transportId, handler) => {
68
+ if (inProcessBridges.has(transportId)) {
69
+ throw new Error(`an in-process bridge is already registered for transport "${transportId}"`);
70
+ }
71
+ inProcessBridges.set(transportId, handler);
72
+ return () => {
73
+ if (inProcessBridges.get(transportId) === handler)
74
+ inProcessBridges.delete(transportId);
75
+ };
76
+ };
63
77
  const pushToBridge = (transportId, chatId, message) => {
78
+ // An in-process bridge is in this very process, so it is live by
79
+ // construction — deliver and return rather than consulting the socket
80
+ // layer, whose miss path would queue a message that has been delivered.
81
+ const inProcess = inProcessBridges.get(transportId);
82
+ if (inProcess) {
83
+ try {
84
+ // Runs on WHATEVER stack called `pushToBridge` — a route handler, a
85
+ // scheduler tick. A socket bridge cannot reach us here because its
86
+ // handler runs in its own process; an in-process one can, so the
87
+ // isolation has to be written rather than assumed.
88
+ inProcess({ chatId, message });
89
+ }
90
+ catch (err) {
91
+ logger.error("chat-service", "in-process bridge push handler threw", {
92
+ transportId,
93
+ chatId,
94
+ error: err instanceof Error ? err.message : String(err),
95
+ });
96
+ }
97
+ return;
98
+ }
64
99
  if (livePush) {
65
100
  livePush(transportId, chatId, message);
66
101
  return;
@@ -152,6 +187,7 @@ export function createChatService(deps) {
152
187
  livePush = handle.pushToBridge;
153
188
  },
154
189
  pushToBridge,
190
+ registerInProcessBridge,
155
191
  };
156
192
  }
157
193
  export { writeFileAtomic } from "./atomic-write.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mulmobridge/chat-service",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Server-side chat service for MulmoBridge — socket.io + REST bridge to Claude Code agents",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",