You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
codev-sdk: message-bus WebSocket client absent from v1 (tracking; mobile chat + gate-event consumers need it) #1350
Finding (from the #1189 RN/Metro async review of PR #1346)
Scope note rather than defect: #1189's design listed a "WS/message-bus client" among the sdk's contents, and v1 ships without one. The sdk covers REST (tower-client) and SSE decoding (sse), but subscribing to /ws/messages (MessageFrame stream) still requires each consumer to hand-roll a WebSocket client, reconnect policy wiring, and frame parsing.
Consumers that will need it: the mobile chat + feed (first RN spike consumer), the web dashboard's eventual migration onto the sdk, and every consumer of #1194's typed gate-event frames (which ride the message bus as BusEventMeta).
Ask
Confirm this was a deliberate v1 cut (likely: no consumer exists yet, matching the extract-on-evidence principle), and keep this issue as the tracking point for adding it when the first consumer lands. Shape suggestion when it happens, mirroring the existing seams: injected WebSocket constructor (RN provides a global WebSocket; Node consumers inject ws), reconnect-policy for backoff, typed MessageFrame from codev-types, and lifecycle hooks compatible with RN AppState foreground reconnects (verified necessary on-device in the LAN spike, codev/research/mobile/spikes/expo-lan-reachability-2026-07.md).
Confirming from the merged plan (codev/plans/1189-introduce-packages-codev-sdk-c.md): the cut was deliberate in effect but under-documented — SSE was scoped in as the v1 event channel; the message-bus WS client was never in the v1 build list at all. The plan's only explicit event-channel decision (Design Decisions, ~line 306) scopes subscribeEvents (SSE) for the controller/streamdeck/mobile lanes and defers even in-repo SSE consolidation; a /ws/messages bus client has no consumer among the in-repo trio the PR migrated (dashboard/vscode consume terminals + SSE via their own paths), so under the plan's no-facades/single-source discipline it had nothing to migrate FROM and was implicitly out.
So: deliberate descope, correctly so for v1 — but the issue body's original design ("Tower API client, WS/message-bus client") promised it, and this issue is the right tracker for delivering it. Since mobile's feed/chat is the first consumer, I'd suggest mobile drafts the client's contract needs (frame types from #1194's BusEventMeta envelope, reconnect semantics vs the sdk's reconnect-policy, auth handshake) as input, and the implementation lands as an sdk addition (./message-bus subpath) with the same import-boundary discipline — WebSocket arrives as an injected constructor, same pattern as fetchFn, so RN's WebSocket global and Node's ws both slot in. Sequencing-wise it can ride ahead of or alongside the mobile feed spike; it does not block #1347 (streamdeck rides SSE + command relay).
Consumer contract from the first consumer (mobile feed/chat), per main's suggestion — what the ./message-bus subpath needs to satisfy, shaped to the sdk's existing seam idioms:
1. Injected socket, minimal interface.wsFactory?: (url: string) => MinimalWebSocket where the interface is only what the client uses (onopen/onmessage/onerror/onclose, close(), readyState). Defaults to globalThis.WebSocket (exists on RN and browsers); Node consumers inject ws. Mirrors fetchFn's default-global-inject-to-override pattern.
2. URL building must compose with #1349. The ws(s):// URL derives from the client's base (scheme flipped from http(s), path prefix preserved), so /ws/messages works behind the tunnel proxy (wss://<cloud>/t/<towerId>/ws/messages). If #1349's baseUrl lands first, this comes free; if not, this subpath becomes the second place host+port hardcoding hurts.
3. Typed frames, open-enum dispatch. Parse MessageFrame from codev-types; expose one onFrame callback plus a discriminated meta (BusEventMeta) so #1194gate-event frames and future members route through a single dispatcher. Unknown kind values are delivered as opaque frames, never dropped or thrown (the #1194 open-enum discipline).
4. projectFilter passthrough (the ?project= query the route already supports).
5. Reconnect semantics.reconnect-policy backoff; explicit start()/stop() lifecycle so hosts wire their own foreground signal (RN AppState foreground-reconnect is verified necessary on-device — spike note, codev/research/mobile/spikes/expo-lan-reachability-2026-07.md); reconnect is resubscribe-only with no replay expectation — the bus is fire-and-forget, and consumers refetch REST snapshots on reconnect per decisions/q3-offline-behavior.md. The client should surface a droppedConnection signal so consumers know to refetch.
6. Connection-state surface.'idle' | 'connecting' | 'open' | 'backoff' | 'closed' exposed as a readable state + change callback — feeds the presence tiers in decisions/q6-session-presence.md directly.
7. Auth seam now, enforcement later./ws/messages is unauthenticated today, but the constructor should take getAuthKey from day one (applied as query param or first-frame auth, whichever Tower lands) so adding WS auth is not a breaking change for consumers.
Happy to review the subpath's shape against this when it lands; the RN feed spike will exercise whatever exists at that point (and doubles as #1348's expo/fetch streaming verification).
Activity
amrmelsayed commentedon Aug 5, 2026
Confirming from the merged plan (
codev/plans/1189-introduce-packages-codev-sdk-c.md): the cut was deliberate in effect but under-documented — SSE was scoped in as the v1 event channel; the message-bus WS client was never in the v1 build list at all. The plan's only explicit event-channel decision (Design Decisions, ~line 306) scopessubscribeEvents(SSE) for the controller/streamdeck/mobile lanes and defers even in-repo SSE consolidation; a/ws/messagesbus client has no consumer among the in-repo trio the PR migrated (dashboard/vscode consume terminals + SSE via their own paths), so under the plan's no-facades/single-source discipline it had nothing to migrate FROM and was implicitly out.So: deliberate descope, correctly so for v1 — but the issue body's original design ("Tower API client, WS/message-bus client") promised it, and this issue is the right tracker for delivering it. Since mobile's feed/chat is the first consumer, I'd suggest mobile drafts the client's contract needs (frame types from #1194's
BusEventMetaenvelope, reconnect semantics vs the sdk'sreconnect-policy, auth handshake) as input, and the implementation lands as an sdk addition (./message-bussubpath) with the same import-boundary discipline — WebSocket arrives as an injected constructor, same pattern asfetchFn, so RN'sWebSocketglobal and Node'swsboth slot in. Sequencing-wise it can ride ahead of or alongside the mobile feed spike; it does not block #1347 (streamdeck rides SSE + command relay).amrmelsayed commentedon Aug 5, 2026
Consumer contract from the first consumer (mobile feed/chat), per main's suggestion — what the
./message-bussubpath needs to satisfy, shaped to the sdk's existing seam idioms:1. Injected socket, minimal interface.
wsFactory?: (url: string) => MinimalWebSocketwhere the interface is only what the client uses (onopen/onmessage/onerror/onclose,close(),readyState). Defaults toglobalThis.WebSocket(exists on RN and browsers); Node consumers injectws. MirrorsfetchFn's default-global-inject-to-override pattern.2. URL building must compose with #1349. The
ws(s)://URL derives from the client's base (scheme flipped from http(s), path prefix preserved), so/ws/messagesworks behind the tunnel proxy (wss://<cloud>/t/<towerId>/ws/messages). If #1349'sbaseUrllands first, this comes free; if not, this subpath becomes the second place host+port hardcoding hurts.3. Typed frames, open-enum dispatch. Parse
MessageFramefromcodev-types; expose oneonFramecallback plus a discriminatedmeta(BusEventMeta) so #1194gate-eventframes and future members route through a single dispatcher. Unknownkindvalues are delivered as opaque frames, never dropped or thrown (the #1194 open-enum discipline).4.
projectFilterpassthrough (the?project=query the route already supports).5. Reconnect semantics.
reconnect-policybackoff; explicitstart()/stop()lifecycle so hosts wire their own foreground signal (RNAppStateforeground-reconnect is verified necessary on-device — spike note,codev/research/mobile/spikes/expo-lan-reachability-2026-07.md); reconnect is resubscribe-only with no replay expectation — the bus is fire-and-forget, and consumers refetch REST snapshots on reconnect perdecisions/q3-offline-behavior.md. The client should surface adroppedConnectionsignal so consumers know to refetch.6. Connection-state surface.
'idle' | 'connecting' | 'open' | 'backoff' | 'closed'exposed as a readable state + change callback — feeds the presence tiers indecisions/q6-session-presence.mddirectly.7. Auth seam now, enforcement later.
/ws/messagesis unauthenticated today, but the constructor should takegetAuthKeyfrom day one (applied as query param or first-frame auth, whichever Tower lands) so adding WS auth is not a breaking change for consumers.Happy to review the subpath's shape against this when it lands; the RN feed spike will exercise whatever exists at that point (and doubles as #1348's expo/fetch streaming verification).