@essentialai/cogent-server 2.0.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.
Files changed (166) hide show
  1. package/.env.example +68 -0
  2. package/CHANGELOG.md +16 -0
  3. package/Caddyfile +8 -0
  4. package/Dockerfile +46 -0
  5. package/LICENSE +190 -0
  6. package/README.md +89 -0
  7. package/config.json.example +16 -0
  8. package/dist/__tests__/helpers.d.ts +56 -0
  9. package/dist/__tests__/helpers.d.ts.map +1 -0
  10. package/dist/__tests__/helpers.js +138 -0
  11. package/dist/__tests__/helpers.js.map +1 -0
  12. package/dist/app.d.ts +38 -0
  13. package/dist/app.d.ts.map +1 -0
  14. package/dist/app.js +60 -0
  15. package/dist/app.js.map +1 -0
  16. package/dist/config.d.ts +88 -0
  17. package/dist/config.d.ts.map +1 -0
  18. package/dist/config.js +148 -0
  19. package/dist/config.js.map +1 -0
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +102 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/middleware/auth.d.ts +15 -0
  25. package/dist/middleware/auth.d.ts.map +1 -0
  26. package/dist/middleware/auth.js +47 -0
  27. package/dist/middleware/auth.js.map +1 -0
  28. package/dist/middleware/error-handler.d.ts +14 -0
  29. package/dist/middleware/error-handler.d.ts.map +1 -0
  30. package/dist/middleware/error-handler.js +26 -0
  31. package/dist/middleware/error-handler.js.map +1 -0
  32. package/dist/middleware/not-found.d.ts +8 -0
  33. package/dist/middleware/not-found.d.ts.map +1 -0
  34. package/dist/middleware/not-found.js +12 -0
  35. package/dist/middleware/not-found.js.map +1 -0
  36. package/dist/middleware/request-logger.d.ts +17 -0
  37. package/dist/middleware/request-logger.d.ts.map +1 -0
  38. package/dist/middleware/request-logger.js +65 -0
  39. package/dist/middleware/request-logger.js.map +1 -0
  40. package/dist/middleware/ws-auth.d.ts +21 -0
  41. package/dist/middleware/ws-auth.d.ts.map +1 -0
  42. package/dist/middleware/ws-auth.js +59 -0
  43. package/dist/middleware/ws-auth.js.map +1 -0
  44. package/dist/routes/health.d.ts +11 -0
  45. package/dist/routes/health.d.ts.map +1 -0
  46. package/dist/routes/health.js +34 -0
  47. package/dist/routes/health.js.map +1 -0
  48. package/dist/routes/messages.d.ts +19 -0
  49. package/dist/routes/messages.d.ts.map +1 -0
  50. package/dist/routes/messages.js +154 -0
  51. package/dist/routes/messages.js.map +1 -0
  52. package/dist/routes/peers.d.ts +17 -0
  53. package/dist/routes/peers.d.ts.map +1 -0
  54. package/dist/routes/peers.js +169 -0
  55. package/dist/routes/peers.js.map +1 -0
  56. package/dist/routes/poll.d.ts +15 -0
  57. package/dist/routes/poll.d.ts.map +1 -0
  58. package/dist/routes/poll.js +97 -0
  59. package/dist/routes/poll.js.map +1 -0
  60. package/dist/routes/sessions.d.ts +14 -0
  61. package/dist/routes/sessions.d.ts.map +1 -0
  62. package/dist/routes/sessions.js +113 -0
  63. package/dist/routes/sessions.js.map +1 -0
  64. package/dist/routes/ui.d.ts +21 -0
  65. package/dist/routes/ui.d.ts.map +1 -0
  66. package/dist/routes/ui.js +173 -0
  67. package/dist/routes/ui.js.map +1 -0
  68. package/dist/routes/validation-hook.d.ts +18 -0
  69. package/dist/routes/validation-hook.d.ts.map +1 -0
  70. package/dist/routes/validation-hook.js +24 -0
  71. package/dist/routes/validation-hook.js.map +1 -0
  72. package/dist/services/auth-service.d.ts +48 -0
  73. package/dist/services/auth-service.d.ts.map +1 -0
  74. package/dist/services/auth-service.js +63 -0
  75. package/dist/services/auth-service.js.map +1 -0
  76. package/dist/services/connection-manager.d.ts +108 -0
  77. package/dist/services/connection-manager.d.ts.map +1 -0
  78. package/dist/services/connection-manager.js +216 -0
  79. package/dist/services/connection-manager.js.map +1 -0
  80. package/dist/services/message-queue.d.ts +56 -0
  81. package/dist/services/message-queue.d.ts.map +1 -0
  82. package/dist/services/message-queue.js +164 -0
  83. package/dist/services/message-queue.js.map +1 -0
  84. package/dist/services/peer-cleanup.d.ts +39 -0
  85. package/dist/services/peer-cleanup.d.ts.map +1 -0
  86. package/dist/services/peer-cleanup.js +96 -0
  87. package/dist/services/peer-cleanup.js.map +1 -0
  88. package/dist/services/session-cleanup.d.ts +44 -0
  89. package/dist/services/session-cleanup.d.ts.map +1 -0
  90. package/dist/services/session-cleanup.js +100 -0
  91. package/dist/services/session-cleanup.js.map +1 -0
  92. package/dist/services/session-store.d.ts +103 -0
  93. package/dist/services/session-store.d.ts.map +1 -0
  94. package/dist/services/session-store.js +292 -0
  95. package/dist/services/session-store.js.map +1 -0
  96. package/dist/services/stats-service.d.ts +48 -0
  97. package/dist/services/stats-service.d.ts.map +1 -0
  98. package/dist/services/stats-service.js +77 -0
  99. package/dist/services/stats-service.js.map +1 -0
  100. package/dist/types.d.ts +60 -0
  101. package/dist/types.d.ts.map +1 -0
  102. package/dist/types.js +2 -0
  103. package/dist/types.js.map +1 -0
  104. package/dist/ui/components/Footer.d.ts +7 -0
  105. package/dist/ui/components/Footer.d.ts.map +1 -0
  106. package/dist/ui/components/Footer.js +17 -0
  107. package/dist/ui/components/Footer.js.map +1 -0
  108. package/dist/ui/components/Layout.d.ts +13 -0
  109. package/dist/ui/components/Layout.d.ts.map +1 -0
  110. package/dist/ui/components/Layout.js +11 -0
  111. package/dist/ui/components/Layout.js.map +1 -0
  112. package/dist/ui/components/NavBar.d.ts +12 -0
  113. package/dist/ui/components/NavBar.d.ts.map +1 -0
  114. package/dist/ui/components/NavBar.js +60 -0
  115. package/dist/ui/components/NavBar.js.map +1 -0
  116. package/dist/ui/components/StatCard.d.ts +14 -0
  117. package/dist/ui/components/StatCard.d.ts.map +1 -0
  118. package/dist/ui/components/StatCard.js +32 -0
  119. package/dist/ui/components/StatCard.js.map +1 -0
  120. package/dist/ui/components/Terminal.d.ts +13 -0
  121. package/dist/ui/components/Terminal.d.ts.map +1 -0
  122. package/dist/ui/components/Terminal.js +37 -0
  123. package/dist/ui/components/Terminal.js.map +1 -0
  124. package/dist/ui/pages/AdminDashboard.d.ts +13 -0
  125. package/dist/ui/pages/AdminDashboard.d.ts.map +1 -0
  126. package/dist/ui/pages/AdminDashboard.js +59 -0
  127. package/dist/ui/pages/AdminDashboard.js.map +1 -0
  128. package/dist/ui/pages/HowToPage.d.ts +8 -0
  129. package/dist/ui/pages/HowToPage.d.ts.map +1 -0
  130. package/dist/ui/pages/HowToPage.js +312 -0
  131. package/dist/ui/pages/HowToPage.js.map +1 -0
  132. package/dist/ui/pages/LandingPage.d.ts +13 -0
  133. package/dist/ui/pages/LandingPage.d.ts.map +1 -0
  134. package/dist/ui/pages/LandingPage.js +160 -0
  135. package/dist/ui/pages/LandingPage.js.map +1 -0
  136. package/dist/ui/pages/MessageLog.d.ts +25 -0
  137. package/dist/ui/pages/MessageLog.d.ts.map +1 -0
  138. package/dist/ui/pages/MessageLog.js +146 -0
  139. package/dist/ui/pages/MessageLog.js.map +1 -0
  140. package/dist/ui/pages/SessionDetail.d.ts +14 -0
  141. package/dist/ui/pages/SessionDetail.d.ts.map +1 -0
  142. package/dist/ui/pages/SessionDetail.js +165 -0
  143. package/dist/ui/pages/SessionDetail.js.map +1 -0
  144. package/dist/ui/pages/SessionList.d.ts +22 -0
  145. package/dist/ui/pages/SessionList.d.ts.map +1 -0
  146. package/dist/ui/pages/SessionList.js +88 -0
  147. package/dist/ui/pages/SessionList.js.map +1 -0
  148. package/dist/ui/styles/theme.d.ts +35 -0
  149. package/dist/ui/styles/theme.d.ts.map +1 -0
  150. package/dist/ui/styles/theme.js +65 -0
  151. package/dist/ui/styles/theme.js.map +1 -0
  152. package/dist/ws/frames.d.ts +82 -0
  153. package/dist/ws/frames.d.ts.map +1 -0
  154. package/dist/ws/frames.js +68 -0
  155. package/dist/ws/frames.js.map +1 -0
  156. package/dist/ws/handler.d.ts +26 -0
  157. package/dist/ws/handler.d.ts.map +1 -0
  158. package/dist/ws/handler.js +72 -0
  159. package/dist/ws/handler.js.map +1 -0
  160. package/dist/ws/heartbeat.d.ts +18 -0
  161. package/dist/ws/heartbeat.d.ts.map +1 -0
  162. package/dist/ws/heartbeat.js +39 -0
  163. package/dist/ws/heartbeat.js.map +1 -0
  164. package/docker-compose.yml +38 -0
  165. package/nginx.conf.example +63 -0
  166. package/package.json +61 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-service.js","sourceRoot":"","sources":["../../src/services/auth-service.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,MAAM,MAAM,UAAU,CAAC;AAE9B;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,WAAW;IACL,YAAY,CAAS;IAEtC,YAAY,YAAoB;QAC9B,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;IACnC,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,UAAU,CAAC,WAAmB;QAClC,OAAO,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;IACrD,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,YAAY,CAAC,WAAmB,EAAE,IAAY;QAClD,OAAO,MAAM,CAAC,OAAO,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;IAC3C,CAAC;IAED;;;;OAIG;IACH,aAAa;QACX,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QACrD,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QACxC,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;IAC9B,CAAC;IAED;;;OAGG;IACH,SAAS,CAAC,KAAa;QACrB,OAAO,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACjE,CAAC;IAED;;;;;OAKG;IACH,iBAAiB,CAAC,CAAS,EAAE,CAAS;QACpC,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;QAC7D,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;QAC7D,OAAO,MAAM,CAAC,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IAC9C,CAAC;CACF"}
@@ -0,0 +1,108 @@
1
+ import type { WSContext } from "hono/ws";
2
+ import type WebSocket from "ws";
3
+ /**
4
+ * Represents a single WebSocket connection from a peer.
5
+ * A peer may have multiple simultaneous connections (all receive messages).
6
+ */
7
+ export interface PeerConnection {
8
+ /** Hono WSContext wrapper for sending application-level messages. */
9
+ ws: WSContext;
10
+ /** Underlying ws.WebSocket object for native ping/pong heartbeat. */
11
+ rawWs: WebSocket;
12
+ /** The peer this connection belongs to. */
13
+ peerId: string;
14
+ /** The session this connection belongs to. */
15
+ sessionId: string;
16
+ /** Timestamp (Date.now()) when this connection was established. */
17
+ connectedAt: number;
18
+ /** Heartbeat interval handle -- must be cleared on disconnect. */
19
+ heartbeatInterval: ReturnType<typeof setInterval>;
20
+ }
21
+ /**
22
+ * In-memory WebSocket connection tracker.
23
+ *
24
+ * Manages active connections per session and peer, supports multiple
25
+ * connections per peer, implements grace period timers for offline
26
+ * detection, and checks readyState before sending to avoid errors
27
+ * on closed sockets.
28
+ */
29
+ export declare class ConnectionManager {
30
+ /**
31
+ * Nested map: sessionId -> peerId -> PeerConnection[]
32
+ * Supports O(1) lookup for a specific peer's connections.
33
+ */
34
+ private readonly connections;
35
+ /**
36
+ * Grace period timers: `${sessionId}:${peerId}` -> timeout handle.
37
+ * Started when last connection for a peer closes; cancelled if peer
38
+ * reconnects before expiry.
39
+ */
40
+ private readonly graceTimers;
41
+ /** How long to wait after last connection closes before marking peer offline. */
42
+ private readonly graceTimeoutMs;
43
+ /** Callback invoked when grace period expires without reconnection. */
44
+ private onPeerOffline?;
45
+ constructor(graceTimeoutMs?: number);
46
+ /**
47
+ * Register a callback for when a peer's grace period expires
48
+ * (all connections closed and no reconnection within the timeout).
49
+ */
50
+ setOnPeerOffline(callback: (sessionId: string, peerId: string) => void): void;
51
+ /**
52
+ * Add a new WebSocket connection for a peer.
53
+ * Cancels any active grace timer for this peer (Pitfall 4 prevention).
54
+ */
55
+ addConnection(conn: PeerConnection): void;
56
+ /**
57
+ * Remove a specific WebSocket connection for a peer.
58
+ * Clears the connection's heartbeat interval. If the peer has no
59
+ * remaining connections, starts a grace period timer.
60
+ */
61
+ removeConnection(sessionId: string, peerId: string, ws: WSContext): void;
62
+ /**
63
+ * Get all connections for a specific peer in a session.
64
+ * Returns an empty array if the peer has no connections.
65
+ */
66
+ getConnections(sessionId: string, peerId: string): PeerConnection[];
67
+ /**
68
+ * Get all connections in a session (flat array across all peers).
69
+ */
70
+ getAllSessionConnections(sessionId: string): PeerConnection[];
71
+ /**
72
+ * Get the list of peer IDs with active connections in a session.
73
+ */
74
+ getConnectedPeerIds(sessionId: string): string[];
75
+ /**
76
+ * Send data to all connections for a specific peer.
77
+ * Checks readyState === 1 (OPEN) before each send to avoid
78
+ * errors on closing/closed sockets (Pitfall 5 prevention).
79
+ */
80
+ sendToPeer(sessionId: string, peerId: string, data: string): void;
81
+ /**
82
+ * Broadcast data to all connections in a session.
83
+ * Optionally excludes a specific peer (e.g., the message sender).
84
+ */
85
+ broadcastToSession(sessionId: string, data: string, excludePeerId?: string): void;
86
+ /**
87
+ * Check whether a peer has at least one active connection.
88
+ */
89
+ isConnected(sessionId: string, peerId: string): boolean;
90
+ /**
91
+ * Get the total number of connected peers across all sessions.
92
+ * O(sessions) in-memory traversal, no disk I/O.
93
+ */
94
+ getTotalConnectedPeers(): number;
95
+ /**
96
+ * Close all WebSocket connections across all sessions.
97
+ * Clears all heartbeat intervals and grace timers.
98
+ * Used during SIGTERM graceful shutdown.
99
+ */
100
+ closeAll(code: number, reason: string): void;
101
+ /**
102
+ * Start a grace period timer for a peer that has lost all connections.
103
+ * If the timer fires without cancellation (from addConnection),
104
+ * the onPeerOffline callback is invoked.
105
+ */
106
+ private _startGraceTimer;
107
+ }
108
+ //# sourceMappingURL=connection-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection-manager.d.ts","sourceRoot":"","sources":["../../src/services/connection-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,KAAK,SAAS,MAAM,IAAI,CAAC;AAEhC;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,qEAAqE;IACrE,EAAE,EAAE,SAAS,CAAC;IACd,qEAAqE;IACrE,KAAK,EAAE,SAAS,CAAC;IACjB,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,mEAAmE;IACnE,WAAW,EAAE,MAAM,CAAC;IACpB,kEAAkE;IAClE,iBAAiB,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;CACnD;AAED;;;;;;;GAOG;AACH,qBAAa,iBAAiB;IAC5B;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,WAAW,CAGxB;IAEJ;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,WAAW,CAGxB;IAEJ,iFAAiF;IACjF,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IAExC,uEAAuE;IACvE,OAAO,CAAC,aAAa,CAAC,CAA8C;gBAExD,cAAc,GAAE,MAAe;IAI3C;;;OAGG;IACH,gBAAgB,CACd,QAAQ,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,GACpD,IAAI;IAIP;;;OAGG;IACH,aAAa,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI;IA4BzC;;;;OAIG;IACH,gBAAgB,CACd,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,SAAS,GACZ,IAAI;IA4BP;;;OAGG;IACH,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,cAAc,EAAE;IAMnE;;OAEG;IACH,wBAAwB,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,EAAE;IAW7D;;OAEG;IACH,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE;IAMhD;;;;OAIG;IACH,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IASjE;;;OAGG;IACH,kBAAkB,CAChB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,aAAa,CAAC,EAAE,MAAM,GACrB,IAAI;IAcP;;OAEG;IACH,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO;IAOvD;;;OAGG;IACH,sBAAsB,IAAI,MAAM;IAQhC;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAwB5C;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;CAUzB"}
@@ -0,0 +1,216 @@
1
+ /**
2
+ * In-memory WebSocket connection tracker.
3
+ *
4
+ * Manages active connections per session and peer, supports multiple
5
+ * connections per peer, implements grace period timers for offline
6
+ * detection, and checks readyState before sending to avoid errors
7
+ * on closed sockets.
8
+ */
9
+ export class ConnectionManager {
10
+ /**
11
+ * Nested map: sessionId -> peerId -> PeerConnection[]
12
+ * Supports O(1) lookup for a specific peer's connections.
13
+ */
14
+ connections = new Map();
15
+ /**
16
+ * Grace period timers: `${sessionId}:${peerId}` -> timeout handle.
17
+ * Started when last connection for a peer closes; cancelled if peer
18
+ * reconnects before expiry.
19
+ */
20
+ graceTimers = new Map();
21
+ /** How long to wait after last connection closes before marking peer offline. */
22
+ graceTimeoutMs;
23
+ /** Callback invoked when grace period expires without reconnection. */
24
+ onPeerOffline;
25
+ constructor(graceTimeoutMs = 30_000) {
26
+ this.graceTimeoutMs = graceTimeoutMs;
27
+ }
28
+ /**
29
+ * Register a callback for when a peer's grace period expires
30
+ * (all connections closed and no reconnection within the timeout).
31
+ */
32
+ setOnPeerOffline(callback) {
33
+ this.onPeerOffline = callback;
34
+ }
35
+ /**
36
+ * Add a new WebSocket connection for a peer.
37
+ * Cancels any active grace timer for this peer (Pitfall 4 prevention).
38
+ */
39
+ addConnection(conn) {
40
+ const { sessionId, peerId } = conn;
41
+ // Cancel grace timer if reconnecting within grace period
42
+ const timerKey = `${sessionId}:${peerId}`;
43
+ const existingTimer = this.graceTimers.get(timerKey);
44
+ if (existingTimer !== undefined) {
45
+ clearTimeout(existingTimer);
46
+ this.graceTimers.delete(timerKey);
47
+ }
48
+ // Get or create the session map
49
+ let sessionMap = this.connections.get(sessionId);
50
+ if (!sessionMap) {
51
+ sessionMap = new Map();
52
+ this.connections.set(sessionId, sessionMap);
53
+ }
54
+ // Get or create the connections array for this peer
55
+ let peerConns = sessionMap.get(peerId);
56
+ if (!peerConns) {
57
+ peerConns = [];
58
+ sessionMap.set(peerId, peerConns);
59
+ }
60
+ peerConns.push(conn);
61
+ }
62
+ /**
63
+ * Remove a specific WebSocket connection for a peer.
64
+ * Clears the connection's heartbeat interval. If the peer has no
65
+ * remaining connections, starts a grace period timer.
66
+ */
67
+ removeConnection(sessionId, peerId, ws) {
68
+ const sessionMap = this.connections.get(sessionId);
69
+ if (!sessionMap)
70
+ return;
71
+ const peerConns = sessionMap.get(peerId);
72
+ if (!peerConns)
73
+ return;
74
+ // Find and remove the specific connection by ws reference
75
+ const idx = peerConns.findIndex((c) => c.ws === ws);
76
+ if (idx === -1)
77
+ return;
78
+ // Clear the heartbeat interval for this connection
79
+ clearInterval(peerConns[idx].heartbeatInterval);
80
+ peerConns.splice(idx, 1);
81
+ // If no connections remain, start grace period
82
+ if (peerConns.length === 0) {
83
+ sessionMap.delete(peerId);
84
+ // Clean up empty session map
85
+ if (sessionMap.size === 0) {
86
+ this.connections.delete(sessionId);
87
+ }
88
+ this._startGraceTimer(sessionId, peerId);
89
+ }
90
+ }
91
+ /**
92
+ * Get all connections for a specific peer in a session.
93
+ * Returns an empty array if the peer has no connections.
94
+ */
95
+ getConnections(sessionId, peerId) {
96
+ const sessionMap = this.connections.get(sessionId);
97
+ if (!sessionMap)
98
+ return [];
99
+ return sessionMap.get(peerId) ?? [];
100
+ }
101
+ /**
102
+ * Get all connections in a session (flat array across all peers).
103
+ */
104
+ getAllSessionConnections(sessionId) {
105
+ const sessionMap = this.connections.get(sessionId);
106
+ if (!sessionMap)
107
+ return [];
108
+ const result = [];
109
+ for (const conns of sessionMap.values()) {
110
+ result.push(...conns);
111
+ }
112
+ return result;
113
+ }
114
+ /**
115
+ * Get the list of peer IDs with active connections in a session.
116
+ */
117
+ getConnectedPeerIds(sessionId) {
118
+ const sessionMap = this.connections.get(sessionId);
119
+ if (!sessionMap)
120
+ return [];
121
+ return Array.from(sessionMap.keys());
122
+ }
123
+ /**
124
+ * Send data to all connections for a specific peer.
125
+ * Checks readyState === 1 (OPEN) before each send to avoid
126
+ * errors on closing/closed sockets (Pitfall 5 prevention).
127
+ */
128
+ sendToPeer(sessionId, peerId, data) {
129
+ const conns = this.getConnections(sessionId, peerId);
130
+ for (const conn of conns) {
131
+ if (conn.ws.readyState === 1) {
132
+ conn.ws.send(data);
133
+ }
134
+ }
135
+ }
136
+ /**
137
+ * Broadcast data to all connections in a session.
138
+ * Optionally excludes a specific peer (e.g., the message sender).
139
+ */
140
+ broadcastToSession(sessionId, data, excludePeerId) {
141
+ const sessionMap = this.connections.get(sessionId);
142
+ if (!sessionMap)
143
+ return;
144
+ for (const [peerId, conns] of sessionMap) {
145
+ if (peerId === excludePeerId)
146
+ continue;
147
+ for (const conn of conns) {
148
+ if (conn.ws.readyState === 1) {
149
+ conn.ws.send(data);
150
+ }
151
+ }
152
+ }
153
+ }
154
+ /**
155
+ * Check whether a peer has at least one active connection.
156
+ */
157
+ isConnected(sessionId, peerId) {
158
+ const sessionMap = this.connections.get(sessionId);
159
+ if (!sessionMap)
160
+ return false;
161
+ const conns = sessionMap.get(peerId);
162
+ return conns !== undefined && conns.length > 0;
163
+ }
164
+ /**
165
+ * Get the total number of connected peers across all sessions.
166
+ * O(sessions) in-memory traversal, no disk I/O.
167
+ */
168
+ getTotalConnectedPeers() {
169
+ let total = 0;
170
+ for (const sessionMap of this.connections.values()) {
171
+ total += sessionMap.size;
172
+ }
173
+ return total;
174
+ }
175
+ /**
176
+ * Close all WebSocket connections across all sessions.
177
+ * Clears all heartbeat intervals and grace timers.
178
+ * Used during SIGTERM graceful shutdown.
179
+ */
180
+ closeAll(code, reason) {
181
+ // Clear all grace timers first
182
+ for (const timer of this.graceTimers.values()) {
183
+ clearTimeout(timer);
184
+ }
185
+ this.graceTimers.clear();
186
+ // Close all connections
187
+ for (const sessionMap of this.connections.values()) {
188
+ for (const conns of sessionMap.values()) {
189
+ for (const conn of conns) {
190
+ clearInterval(conn.heartbeatInterval);
191
+ try {
192
+ conn.ws.close(code, reason);
193
+ }
194
+ catch {
195
+ // Ignore errors from already-closed sockets during shutdown
196
+ }
197
+ }
198
+ }
199
+ }
200
+ this.connections.clear();
201
+ }
202
+ /**
203
+ * Start a grace period timer for a peer that has lost all connections.
204
+ * If the timer fires without cancellation (from addConnection),
205
+ * the onPeerOffline callback is invoked.
206
+ */
207
+ _startGraceTimer(sessionId, peerId) {
208
+ const timerKey = `${sessionId}:${peerId}`;
209
+ const timer = setTimeout(() => {
210
+ this.graceTimers.delete(timerKey);
211
+ this.onPeerOffline?.(sessionId, peerId);
212
+ }, this.graceTimeoutMs);
213
+ this.graceTimers.set(timerKey, timer);
214
+ }
215
+ }
216
+ //# sourceMappingURL=connection-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection-manager.js","sourceRoot":"","sources":["../../src/services/connection-manager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ import type { MessageRecord } from "@essentialai/cogent";
2
+ import type { SessionStore } from "./session-store.js";
3
+ /**
4
+ * Offline message queuing service with disk persistence and 7-day TTL.
5
+ *
6
+ * When a message is sent to a peer that is not connected via WebSocket,
7
+ * it is enqueued in the session file. On reconnect, all queued messages
8
+ * are flushed immediately (no batching, per user decision).
9
+ *
10
+ * Persistence is via SessionStore.updateSession(), so the queue survives
11
+ * server restarts. A periodic cleanup removes entries older than the
12
+ * configured TTL (default: 7 days).
13
+ */
14
+ export declare class MessageQueueService {
15
+ private readonly sessionStore;
16
+ private readonly queueTtlMs;
17
+ private cleanupHandle;
18
+ constructor(sessionStore: SessionStore, queueTtlMs?: number);
19
+ /**
20
+ * Queue a message for offline delivery to a peer.
21
+ * Creates a QueuedMessage wrapper with the current timestamp as enqueuedAt.
22
+ * Initializes the messageQueue field to {} if not present on the state.
23
+ */
24
+ enqueue(sessionId: string, peerId: string, message: MessageRecord): Promise<void>;
25
+ /**
26
+ * Remove and return all queued messages for a peer.
27
+ * Returns the MessageRecord payloads (not the QueuedMessage wrappers).
28
+ * Returns an empty array if no queue exists for this peer.
29
+ */
30
+ dequeueAll(sessionId: string, peerId: string): Promise<MessageRecord[]>;
31
+ /**
32
+ * Get the number of queued messages for a peer without modifying state.
33
+ */
34
+ getQueueSize(sessionId: string, peerId: string): Promise<number>;
35
+ /**
36
+ * Start periodic cleanup of expired queue entries.
37
+ * Uses setInterval with .unref() so the timer does not prevent
38
+ * Node.js from exiting naturally during graceful shutdown.
39
+ *
40
+ * @param intervalMs How often to run cleanup (default: 6 hours)
41
+ */
42
+ startCleanup(intervalMs?: number): void;
43
+ /**
44
+ * Stop the periodic cleanup timer.
45
+ */
46
+ stopCleanup(): void;
47
+ /**
48
+ * Scan all sessions and remove expired queue entries (older than queueTtlMs).
49
+ * Public method for direct testing without relying on timers
50
+ * (matches PeerCleanup pattern).
51
+ *
52
+ * @returns Total number of expired messages removed across all sessions.
53
+ */
54
+ cleanup(): Promise<number>;
55
+ }
56
+ //# sourceMappingURL=message-queue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message-queue.d.ts","sourceRoot":"","sources":["../../src/services/message-queue.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGvD;;;;;;;;;;GAUG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,aAAa,CAA+C;gBAGlE,YAAY,EAAE,YAAY,EAC1B,UAAU,GAAE,MAAgC;IAM9C;;;;OAIG;IACG,OAAO,CACX,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,IAAI,CAAC;IAsBhB;;;;OAIG;IACG,UAAU,CACd,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,aAAa,EAAE,CAAC;IA0B3B;;OAEG;IACG,YAAY,CAChB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC;IAQlB;;;;;;OAMG;IACH,YAAY,CAAC,UAAU,GAAE,MAAmB,GAAG,IAAI;IAenD;;OAEG;IACH,WAAW,IAAI,IAAI;IAOnB;;;;;;OAMG;IACG,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC;CA6DjC"}
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Offline message queuing service with disk persistence and 7-day TTL.
3
+ *
4
+ * When a message is sent to a peer that is not connected via WebSocket,
5
+ * it is enqueued in the session file. On reconnect, all queued messages
6
+ * are flushed immediately (no batching, per user decision).
7
+ *
8
+ * Persistence is via SessionStore.updateSession(), so the queue survives
9
+ * server restarts. A periodic cleanup removes entries older than the
10
+ * configured TTL (default: 7 days).
11
+ */
12
+ export class MessageQueueService {
13
+ sessionStore;
14
+ queueTtlMs;
15
+ cleanupHandle = null;
16
+ constructor(sessionStore, queueTtlMs = 7 * 24 * 60 * 60 * 1000) {
17
+ this.sessionStore = sessionStore;
18
+ this.queueTtlMs = queueTtlMs;
19
+ }
20
+ /**
21
+ * Queue a message for offline delivery to a peer.
22
+ * Creates a QueuedMessage wrapper with the current timestamp as enqueuedAt.
23
+ * Initializes the messageQueue field to {} if not present on the state.
24
+ */
25
+ async enqueue(sessionId, peerId, message) {
26
+ const entry = {
27
+ messageId: message.id,
28
+ enqueuedAt: new Date().toISOString(),
29
+ message,
30
+ };
31
+ await this.sessionStore.updateSession(sessionId, (state) => {
32
+ // Default messageQueue to {} for legacy session files
33
+ if (!state.messageQueue) {
34
+ state.messageQueue = {};
35
+ }
36
+ if (!state.messageQueue[peerId]) {
37
+ state.messageQueue[peerId] = [];
38
+ }
39
+ state.messageQueue[peerId].push(entry);
40
+ return state;
41
+ });
42
+ }
43
+ /**
44
+ * Remove and return all queued messages for a peer.
45
+ * Returns the MessageRecord payloads (not the QueuedMessage wrappers).
46
+ * Returns an empty array if no queue exists for this peer.
47
+ */
48
+ async dequeueAll(sessionId, peerId) {
49
+ let messages = [];
50
+ await this.sessionStore.updateSession(sessionId, (state) => {
51
+ // Default messageQueue to {} for legacy session files
52
+ if (!state.messageQueue) {
53
+ state.messageQueue = {};
54
+ return state;
55
+ }
56
+ const queue = state.messageQueue[peerId];
57
+ if (!queue || queue.length === 0) {
58
+ return state;
59
+ }
60
+ // Extract message payloads before clearing
61
+ messages = queue.map((entry) => entry.message);
62
+ // Remove the peer's queue entirely
63
+ delete state.messageQueue[peerId];
64
+ return state;
65
+ });
66
+ return messages;
67
+ }
68
+ /**
69
+ * Get the number of queued messages for a peer without modifying state.
70
+ */
71
+ async getQueueSize(sessionId, peerId) {
72
+ const session = await this.sessionStore.getSession(sessionId);
73
+ if (!session)
74
+ return 0;
75
+ const queue = session.messageQueue?.[peerId];
76
+ return queue?.length ?? 0;
77
+ }
78
+ /**
79
+ * Start periodic cleanup of expired queue entries.
80
+ * Uses setInterval with .unref() so the timer does not prevent
81
+ * Node.js from exiting naturally during graceful shutdown.
82
+ *
83
+ * @param intervalMs How often to run cleanup (default: 6 hours)
84
+ */
85
+ startCleanup(intervalMs = 21_600_000) {
86
+ if (this.cleanupHandle !== null) {
87
+ return; // Already running
88
+ }
89
+ this.cleanupHandle = setInterval(() => {
90
+ this.cleanup().catch((err) => {
91
+ console.error("Message queue cleanup error:", err);
92
+ });
93
+ }, intervalMs);
94
+ // Allow Node.js to exit even if the timer is still running
95
+ this.cleanupHandle.unref();
96
+ }
97
+ /**
98
+ * Stop the periodic cleanup timer.
99
+ */
100
+ stopCleanup() {
101
+ if (this.cleanupHandle !== null) {
102
+ clearInterval(this.cleanupHandle);
103
+ this.cleanupHandle = null;
104
+ }
105
+ }
106
+ /**
107
+ * Scan all sessions and remove expired queue entries (older than queueTtlMs).
108
+ * Public method for direct testing without relying on timers
109
+ * (matches PeerCleanup pattern).
110
+ *
111
+ * @returns Total number of expired messages removed across all sessions.
112
+ */
113
+ async cleanup() {
114
+ const sessionIds = await this.sessionStore.listSessions();
115
+ let totalRemoved = 0;
116
+ for (const sessionId of sessionIds) {
117
+ const session = await this.sessionStore.getSession(sessionId);
118
+ if (!session)
119
+ continue;
120
+ // Skip sessions without a message queue
121
+ if (!session.messageQueue)
122
+ continue;
123
+ const now = Date.now();
124
+ let sessionRemoved = 0;
125
+ // Check if any entries are expired across all peer queues
126
+ let hasExpired = false;
127
+ for (const queue of Object.values(session.messageQueue)) {
128
+ for (const entry of queue) {
129
+ if (now - Date.parse(entry.enqueuedAt) > this.queueTtlMs) {
130
+ hasExpired = true;
131
+ break;
132
+ }
133
+ }
134
+ if (hasExpired)
135
+ break;
136
+ }
137
+ // Only update if there are expired entries (avoid unnecessary writes)
138
+ if (!hasExpired)
139
+ continue;
140
+ await this.sessionStore.updateSession(sessionId, (state) => {
141
+ if (!state.messageQueue)
142
+ return state;
143
+ for (const peerId of Object.keys(state.messageQueue)) {
144
+ const queue = state.messageQueue[peerId];
145
+ const before = queue.length;
146
+ state.messageQueue[peerId] = queue.filter((entry) => now - Date.parse(entry.enqueuedAt) <= this.queueTtlMs);
147
+ const after = state.messageQueue[peerId].length;
148
+ sessionRemoved += before - after;
149
+ // Clean up empty arrays
150
+ if (state.messageQueue[peerId].length === 0) {
151
+ delete state.messageQueue[peerId];
152
+ }
153
+ }
154
+ return state;
155
+ });
156
+ totalRemoved += sessionRemoved;
157
+ }
158
+ if (totalRemoved > 0) {
159
+ console.error(`Message queue cleanup: removed ${totalRemoved} expired message(s)`);
160
+ }
161
+ return totalRemoved;
162
+ }
163
+ }
164
+ //# sourceMappingURL=message-queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message-queue.js","sourceRoot":"","sources":["../../src/services/message-queue.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ import type { SessionStore } from "./session-store.js";
2
+ /**
3
+ * Background service that periodically scans all sessions and removes
4
+ * stale peers -- peers whose lastSeenAt timestamp exceeds the configured
5
+ * timeout. Removal adds peer_disconnected events to the session state
6
+ * so poll consumers are notified.
7
+ *
8
+ * Uses setInterval with unref() so the timer does not prevent Node.js
9
+ * from exiting naturally during graceful shutdown.
10
+ */
11
+ export declare class PeerCleanup {
12
+ private readonly sessionStore;
13
+ private readonly staleTimeoutMs;
14
+ private intervalHandle;
15
+ constructor(sessionStore: SessionStore, staleTimeoutMs: number);
16
+ /**
17
+ * Start the periodic cleanup timer.
18
+ *
19
+ * @param intervalMs - How often to check for stale peers (default: 60 seconds)
20
+ */
21
+ start(intervalMs?: number): void;
22
+ /**
23
+ * Stop the periodic cleanup timer.
24
+ */
25
+ stop(): void;
26
+ /**
27
+ * Scan all sessions and remove stale peers.
28
+ *
29
+ * A peer is stale if `Date.now() - Date.parse(peer.lastSeenAt)` exceeds
30
+ * the configured staleTimeoutMs.
31
+ *
32
+ * For each removed peer, a peer_disconnected event is recorded in the
33
+ * session state so poll consumers are notified.
34
+ *
35
+ * @returns Total number of stale peers removed across all sessions.
36
+ */
37
+ cleanup(): Promise<number>;
38
+ }
39
+ //# sourceMappingURL=peer-cleanup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"peer-cleanup.d.ts","sourceRoot":"","sources":["../../src/services/peer-cleanup.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD;;;;;;;;GAQG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,cAAc,CAA+C;gBAEzD,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM;IAK9D;;;;OAIG;IACH,KAAK,CAAC,UAAU,GAAE,MAAe,GAAG,IAAI;IAexC;;OAEG;IACH,IAAI,IAAI,IAAI;IAOZ;;;;;;;;;;OAUG;IACG,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC;CAiDjC"}