From 0f1fb27cf964dc02dfa40e3345b472486db07ed1 Mon Sep 17 00:00:00 2001 From: hanghang zhang Date: Thu, 21 May 2026 08:08:33 +0100 Subject: [PATCH] feat(inbound): listen for backend-pushed channel.joined/left events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Companion to nav/Fabric.Backend.Guild# which adds the server-side emitToUser broadcast on channel membership changes. Before, the inbound only learned about new channels via the 60s polling resync (worst-case 60s lag). Now the backend tells us directly so sub/unsub is realtime. socket.on('channel.joined', evt) → join the socket.io room for evt.channelId and add to the local 'joined' set. socket.on('channel.left', evt) → leave + remove from 'joined'. Both events are idempotent (`if (joined.has(id))` / `if (!joined.has(id))`) so duplicate emits from server are safe. Polling resync still runs every 60s as a safety net for transient socket drops between emit and reconnect, partial server failures, etc. When backend lacks this support (older deployments), nothing breaks — the event simply never fires and polling carries the load as before. Co-Authored-By: Claude Opus 4.7 (1M context) --- dist/fabric/src/inbound.js | 22 ++++++++++++++++++++++ src/inbound.ts | 20 ++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/dist/fabric/src/inbound.js b/dist/fabric/src/inbound.js index a313823..ac56400 100644 --- a/dist/fabric/src/inbound.js +++ b/dist/fabric/src/inbound.js @@ -154,6 +154,28 @@ export class FabricInbound { joined.clear(); void syncChannels('initial'); }); + // Push-based membership events from the backend (companion to + // Fabric.Backend.Guild's RealtimeGateway.emitToUser). When the + // server tells us this user was added to / removed from a + // channel, we sub/unsub the socket.io room immediately — no + // 60s wait for the polling resync. Polling remains as a safety + // net for missed events. + socket.on('channel.joined', (evt) => { + const id = evt?.channelId; + if (!id || joined.has(id)) + return; + socket.emit('join_channel', { channelId: id }); + joined.add(id); + this.log.info(`fabric: agent ${agentId} channel.joined push on ${g.nodeId}: ${id} (now ${joined.size})`); + }); + socket.on('channel.left', (evt) => { + const id = evt?.channelId; + if (!id || !joined.has(id)) + return; + socket.emit('leave_channel', { channelId: id }); + joined.delete(id); + this.log.info(`fabric: agent ${agentId} channel.left push on ${g.nodeId}: ${id} (now ${joined.size})`); + }); const syncTimer = setInterval(() => void syncChannels('resync'), FabricInbound.CHANNEL_SYNC_INTERVAL_MS); this.channelSyncTimers.push(syncTimer); socket.on('message.created', (m) => { diff --git a/src/inbound.ts b/src/inbound.ts index 267527f..415c58a 100644 --- a/src/inbound.ts +++ b/src/inbound.ts @@ -199,6 +199,26 @@ export class FabricInbound { joined.clear(); void syncChannels('initial'); }); + // Push-based membership events from the backend (companion to + // Fabric.Backend.Guild's RealtimeGateway.emitToUser). When the + // server tells us this user was added to / removed from a + // channel, we sub/unsub the socket.io room immediately — no + // 60s wait for the polling resync. Polling remains as a safety + // net for missed events. + socket.on('channel.joined', (evt: { channelId?: string }) => { + const id = evt?.channelId; + if (!id || joined.has(id)) return; + socket.emit('join_channel', { channelId: id }); + joined.add(id); + this.log.info(`fabric: agent ${agentId} channel.joined push on ${g.nodeId}: ${id} (now ${joined.size})`); + }); + socket.on('channel.left', (evt: { channelId?: string }) => { + const id = evt?.channelId; + if (!id || !joined.has(id)) return; + socket.emit('leave_channel', { channelId: id }); + joined.delete(id); + this.log.info(`fabric: agent ${agentId} channel.left push on ${g.nodeId}: ${id} (now ${joined.size})`); + }); const syncTimer = setInterval( () => void syncChannels('resync'), FabricInbound.CHANNEL_SYNC_INTERVAL_MS,