tina4-nodejs 3.13.92 → 3.13.95

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 (193) hide show
  1. package/CLAUDE.md +170 -28
  2. package/README.md +2 -2
  3. package/package.json +13 -9
  4. package/packages/cli/dist/bin.js +33126 -30055
  5. package/packages/cli/src/commands/metrics.ts +17 -11
  6. package/packages/cli/src/commands/serve.ts +10 -9
  7. package/packages/core/dist/index.js +33062 -29908
  8. package/packages/core/src/ai.ts +7 -1
  9. package/packages/core/src/auth.ts +191 -39
  10. package/packages/core/src/background.ts +19 -19
  11. package/packages/core/src/cache.ts +492 -49
  12. package/packages/core/src/devAdmin.ts +79 -32
  13. package/packages/core/src/devMailbox.ts +20 -44
  14. package/packages/core/src/dispatchPipeline.ts +285 -0
  15. package/packages/core/src/dotenv.ts +185 -40
  16. package/packages/core/src/index.ts +7 -6
  17. package/packages/core/src/logger.ts +257 -36
  18. package/packages/core/src/mcp.ts +1 -1
  19. package/packages/core/src/messenger.ts +81 -13
  20. package/packages/core/src/metrics.ts +199 -961
  21. package/packages/core/src/middleware.ts +390 -123
  22. package/packages/core/src/queue.ts +188 -32
  23. package/packages/core/src/queueBackends/kafkaBackend.ts +109 -13
  24. package/packages/core/src/queueBackends/liteBackend.ts +13 -0
  25. package/packages/core/src/queueBackends/mongoBackend.ts +101 -9
  26. package/packages/core/src/queueBackends/rabbitmqBackend.ts +22 -4
  27. package/packages/core/src/rateLimiter.ts +10 -5
  28. package/packages/core/src/request.ts +6 -9
  29. package/packages/core/src/response.ts +46 -1
  30. package/packages/core/src/router.ts +29 -4
  31. package/packages/core/src/server.ts +751 -414
  32. package/packages/core/src/session.ts +244 -27
  33. package/packages/core/src/sessionHandlers/childError.ts +72 -0
  34. package/packages/core/src/sessionHandlers/databaseHandler.ts +338 -48
  35. package/packages/core/src/sessionHandlers/memcachedHandler.ts +181 -0
  36. package/packages/core/src/sessionHandlers/mongoClient.ts +293 -202
  37. package/packages/core/src/sessionHandlers/mongoHandler.ts +88 -8
  38. package/packages/core/src/sessionHandlers/respClient.ts +16 -143
  39. package/packages/core/src/sessionHandlers/sqlClient.ts +290 -0
  40. package/packages/core/src/sessionHandlers/syncBridge.ts +190 -0
  41. package/packages/core/src/sessionHandlers/syncSocket.ts +236 -0
  42. package/packages/core/src/testClient.ts +18 -5
  43. package/packages/core/src/trustedProxy.ts +249 -0
  44. package/packages/core/src/types.ts +29 -5
  45. package/packages/core/src/websocket.ts +66 -0
  46. package/packages/frond/dist/index.js +74 -31
  47. package/packages/frond/src/engine.ts +99 -33
  48. package/packages/orm/dist/index.js +26554 -23400
  49. package/packages/orm/src/adapters/firebird.ts +183 -56
  50. package/packages/orm/src/adapters/mongodb.ts +25 -4
  51. package/packages/orm/src/adapters/mssql.ts +114 -29
  52. package/packages/orm/src/adapters/mysql.ts +103 -40
  53. package/packages/orm/src/adapters/odbc.ts +44 -21
  54. package/packages/orm/src/adapters/postgres.ts +118 -26
  55. package/packages/orm/src/adapters/sqlDialect.ts +120 -0
  56. package/packages/orm/src/adapters/sqlite.ts +64 -25
  57. package/packages/orm/src/baseModel.ts +135 -40
  58. package/packages/orm/src/cachedDatabase.ts +43 -19
  59. package/packages/orm/src/connectTimeout.ts +265 -0
  60. package/packages/orm/src/database.ts +338 -198
  61. package/packages/orm/src/databaseResult.ts +65 -13
  62. package/packages/orm/src/databaseUrl.ts +484 -0
  63. package/packages/orm/src/docstore.ts +386 -145
  64. package/packages/orm/src/index.ts +13 -3
  65. package/packages/orm/src/migration.ts +18 -3
  66. package/packages/orm/src/queryBuilder.ts +38 -4
  67. package/packages/orm/src/sqlTranslator.ts +310 -4
  68. package/packages/orm/src/types.ts +15 -4
  69. package/types/cli/src/bin.d.ts +92 -0
  70. package/types/cli/src/commands/build.d.ts +2 -0
  71. package/types/cli/src/commands/generate.d.ts +47 -0
  72. package/types/cli/src/commands/init.d.ts +1 -0
  73. package/types/cli/src/commands/metrics.d.ts +6 -0
  74. package/types/cli/src/commands/migrate.d.ts +1 -0
  75. package/types/cli/src/commands/migrateCreate.d.ts +1 -0
  76. package/types/cli/src/commands/migrateRollback.d.ts +1 -0
  77. package/types/cli/src/commands/migrateStatus.d.ts +1 -0
  78. package/types/cli/src/commands/queue.d.ts +20 -0
  79. package/types/cli/src/commands/routes.d.ts +1 -0
  80. package/types/cli/src/commands/seed.d.ts +1 -0
  81. package/types/cli/src/commands/serve.d.ts +6 -0
  82. package/types/cli/src/commands/test.d.ts +1 -0
  83. package/types/core/src/ai.d.ts +64 -0
  84. package/types/core/src/api.d.ts +262 -0
  85. package/types/core/src/auth.d.ts +177 -0
  86. package/types/core/src/authGate.d.ts +20 -0
  87. package/types/core/src/background.d.ts +34 -0
  88. package/types/core/src/cache.d.ts +163 -0
  89. package/types/core/src/constants.d.ts +38 -0
  90. package/types/core/src/container.d.ts +44 -0
  91. package/types/core/src/context/chunker.d.ts +31 -0
  92. package/types/core/src/context/index.d.ts +93 -0
  93. package/types/core/src/devAdmin.d.ts +179 -0
  94. package/types/core/src/devMailbox.d.ts +54 -0
  95. package/types/core/src/dispatchPipeline.d.ts +117 -0
  96. package/types/core/src/docs.d.ts +141 -0
  97. package/types/core/src/docsAutoDiscovery.d.ts +6 -0
  98. package/types/core/src/dotenv.d.ts +87 -0
  99. package/types/core/src/env.d.ts +28 -0
  100. package/types/core/src/errorOverlay.d.ts +36 -0
  101. package/types/core/src/events.d.ts +75 -0
  102. package/types/core/src/fakeData.d.ts +55 -0
  103. package/types/core/src/feedback.d.ts +90 -0
  104. package/types/core/src/graphql.d.ts +207 -0
  105. package/types/core/src/health.d.ts +22 -0
  106. package/types/core/src/htmlElement.d.ts +75 -0
  107. package/types/core/src/i18n.d.ts +37 -0
  108. package/types/core/src/index.d.ts +92 -0
  109. package/types/core/src/job.d.ts +39 -0
  110. package/types/core/src/logger.d.ts +200 -0
  111. package/types/core/src/mcp.d.ts +248 -0
  112. package/types/core/src/messenger.d.ts +191 -0
  113. package/types/core/src/metrics.d.ts +41 -0
  114. package/types/core/src/middleware.d.ts +330 -0
  115. package/types/core/src/mqtt.d.ts +257 -0
  116. package/types/core/src/mqttMessage.d.ts +67 -0
  117. package/types/core/src/plan.d.ts +96 -0
  118. package/types/core/src/projectIndex.d.ts +56 -0
  119. package/types/core/src/queue.d.ts +268 -0
  120. package/types/core/src/queueBackends/kafkaBackend.d.ts +117 -0
  121. package/types/core/src/queueBackends/liteBackend.d.ts +128 -0
  122. package/types/core/src/queueBackends/mongoBackend.d.ts +119 -0
  123. package/types/core/src/queueBackends/rabbitmqBackend.d.ts +55 -0
  124. package/types/core/src/rateLimiter.d.ts +49 -0
  125. package/types/core/src/request.d.ts +25 -0
  126. package/types/core/src/response.d.ts +28 -0
  127. package/types/core/src/routeDiscovery.d.ts +12 -0
  128. package/types/core/src/router.d.ts +366 -0
  129. package/types/core/src/scss.d.ts +19 -0
  130. package/types/core/src/server.d.ts +146 -0
  131. package/types/core/src/service.d.ts +115 -0
  132. package/types/core/src/session.d.ts +341 -0
  133. package/types/core/src/sessionHandlers/childError.d.ts +34 -0
  134. package/types/core/src/sessionHandlers/databaseHandler.d.ts +97 -0
  135. package/types/core/src/sessionHandlers/memcachedHandler.d.ts +60 -0
  136. package/types/core/src/sessionHandlers/mongoClient.d.ts +35 -0
  137. package/types/core/src/sessionHandlers/mongoHandler.d.ts +109 -0
  138. package/types/core/src/sessionHandlers/respClient.d.ts +22 -0
  139. package/types/core/src/sessionHandlers/sqlClient.d.ts +39 -0
  140. package/types/core/src/sessionHandlers/syncBridge.d.ts +91 -0
  141. package/types/core/src/sessionHandlers/syncSocket.d.ts +49 -0
  142. package/types/core/src/sessionHandlers/valkeyHandler.d.ts +65 -0
  143. package/types/core/src/static.d.ts +2 -0
  144. package/types/core/src/test.d.ts +94 -0
  145. package/types/core/src/testClient.d.ts +36 -0
  146. package/types/core/src/testing.d.ts +58 -0
  147. package/types/core/src/trustedProxy.d.ts +44 -0
  148. package/types/core/src/types.d.ts +242 -0
  149. package/types/core/src/validator.d.ts +52 -0
  150. package/types/core/src/websocket.d.ts +402 -0
  151. package/types/core/src/websocketBackplane.d.ts +166 -0
  152. package/types/core/src/websocketConnection.d.ts +54 -0
  153. package/types/core/src/wsdl.d.ts +101 -0
  154. package/types/frond/src/engine.d.ts +263 -0
  155. package/types/frond/src/index.d.ts +2 -0
  156. package/types/orm/src/adapters/firebird.d.ts +183 -0
  157. package/types/orm/src/adapters/mongodb.d.ts +81 -0
  158. package/types/orm/src/adapters/mssql.d.ts +77 -0
  159. package/types/orm/src/adapters/mysql.d.ts +67 -0
  160. package/types/orm/src/adapters/odbc.d.ts +94 -0
  161. package/types/orm/src/adapters/postgres.d.ts +86 -0
  162. package/types/orm/src/adapters/sqlDialect.d.ts +71 -0
  163. package/types/orm/src/adapters/sqlite.d.ts +68 -0
  164. package/types/orm/src/autoCrud.d.ts +73 -0
  165. package/types/orm/src/baseModel.d.ts +427 -0
  166. package/types/orm/src/cachedDatabase.d.ts +190 -0
  167. package/types/orm/src/connectTimeout.d.ts +100 -0
  168. package/types/orm/src/database.d.ts +655 -0
  169. package/types/orm/src/databaseResult.d.ts +109 -0
  170. package/types/orm/src/databaseUrl.d.ts +125 -0
  171. package/types/orm/src/docstore.d.ts +241 -0
  172. package/types/orm/src/fakeData.d.ts +22 -0
  173. package/types/orm/src/index.d.ts +43 -0
  174. package/types/orm/src/migration.d.ts +275 -0
  175. package/types/orm/src/model.d.ts +7 -0
  176. package/types/orm/src/query.d.ts +14 -0
  177. package/types/orm/src/queryBuilder.d.ts +193 -0
  178. package/types/orm/src/realtime/index.d.ts +7 -0
  179. package/types/orm/src/realtime/models/attachment.d.ts +43 -0
  180. package/types/orm/src/realtime/models/channel.d.ts +32 -0
  181. package/types/orm/src/realtime/models/channelMember.d.ts +32 -0
  182. package/types/orm/src/realtime/models/message.d.ts +36 -0
  183. package/types/orm/src/realtime/models/workspace.d.ts +26 -0
  184. package/types/orm/src/realtime/realtime.d.ts +24 -0
  185. package/types/orm/src/realtime/storage.d.ts +61 -0
  186. package/types/orm/src/seeder.d.ts +118 -0
  187. package/types/orm/src/sqlTranslator.d.ts +258 -0
  188. package/types/orm/src/types.d.ts +148 -0
  189. package/types/orm/src/validation.d.ts +6 -0
  190. package/types/swagger/src/generator.d.ts +46 -0
  191. package/types/swagger/src/index.d.ts +2 -0
  192. package/types/swagger/src/ui.d.ts +11 -0
  193. package/packages/core/src/sessionHandlers/redisHandler.ts +0 -206
@@ -0,0 +1,402 @@
1
+ /**
2
+ * Tina4 WebSocket — Zero-dependency RFC 6455 implementation.
3
+ *
4
+ * Native WebSocket server using Node.js built-in `http` module.
5
+ *
6
+ * import { WebSocketServer } from "@tina4/core";
7
+ *
8
+ * const wss = new WebSocketServer({ port: 8080 });
9
+ * wss.on("open", (client) => {
10
+ * console.log("Connected:", client.id);
11
+ * });
12
+ * wss.on("message", (client, message) => {
13
+ * wss.broadcast(message);
14
+ * });
15
+ * await wss.start();
16
+ *
17
+ * Supported:
18
+ * - HTTP Upgrade handshake (RFC 6455 Sec-WebSocket-Accept)
19
+ * - Frame protocol: text, binary, close, ping, pong
20
+ * - Masking / unmasking (client->server)
21
+ * - Extended payload lengths (7-bit, 16-bit, 64-bit)
22
+ * - Connection manager with broadcast
23
+ */
24
+ import { IncomingMessage } from "node:http";
25
+ import type { Socket } from "node:net";
26
+ import type { WebSocketConnection } from "./websocketConnection.js";
27
+ export declare const OP_CONTINUATION = 0;
28
+ export declare const OP_TEXT = 1;
29
+ export declare const OP_BINARY = 2;
30
+ export declare const OP_CLOSE = 8;
31
+ export declare const OP_PING = 9;
32
+ export declare const OP_PONG = 10;
33
+ export declare const CLOSE_NORMAL = 1000;
34
+ export declare const CLOSE_GOING_AWAY = 1001;
35
+ export declare const CLOSE_PROTOCOL_ERROR = 1002;
36
+ export declare const CLOSE_POLICY_VIOLATION = 1008;
37
+ export interface WebSocketClient {
38
+ id: string;
39
+ socket: Socket;
40
+ ip: string;
41
+ connectedAt: number;
42
+ closed: boolean;
43
+ /** The URL path this client connected on (e.g. "/chat", "/notifications"). */
44
+ path: string;
45
+ /**
46
+ * Epoch ms of the last inbound frame. Updated on every frame; the idle
47
+ * reaper closes connections silent longer than `TINA4_WS_IDLE_TIMEOUT`
48
+ * (opt-in). Optional so externally-injected/legacy client objects still fit.
49
+ */
50
+ lastActivity?: number;
51
+ /**
52
+ * Verified JWT payload when this client connected on a secured WS route, or
53
+ * `null` on a public route. Mirrors Python's `connection.auth`. Optional so
54
+ * externally-injected/legacy client objects still fit.
55
+ */
56
+ auth?: Record<string, unknown> | null;
57
+ }
58
+ /**
59
+ * Compute Sec-WebSocket-Accept from Sec-WebSocket-Key per RFC 6455.
60
+ */
61
+ /**
62
+ * Build an RFC 6455 close frame carrying a status code and optional reason.
63
+ *
64
+ * The code is a big-endian uint16 in the first two payload bytes (s5.5.1), so
65
+ * a peer that only reads the code still gets a valid one.
66
+ */
67
+ export declare function buildCloseFrame(code: number, reason?: string): Buffer;
68
+ export declare function computeAcceptKey(key: string): string;
69
+ /**
70
+ * Return true if the upgrade request's `Origin` is permitted.
71
+ *
72
+ * Controlled by `TINA4_WS_ALLOWED_ORIGINS` (comma-separated list of exact
73
+ * origins, e.g. `https://app.example.com,https://admin.example.com`).
74
+ *
75
+ * Empty/unset = allow ALL origins (current behaviour, non-breaking). When set,
76
+ * only requests whose `Origin` header exactly matches a listed value are
77
+ * allowed; a missing `Origin` header is rejected once the allow-list is active.
78
+ * Header lookup is case-insensitive on the key (Node lowercases header keys,
79
+ * but the helper also checks an exact `Origin` so it works with raw maps too).
80
+ */
81
+ export declare function originAllowed(headers: Record<string, string | string[] | undefined>): boolean;
82
+ /**
83
+ * Extract a bearer token from a WebSocket upgrade handshake.
84
+ *
85
+ * Order (mirrors Python's `ws_token`): the `Authorization: Bearer` header (set
86
+ * by server/CLI/mobile clients), then the `Sec-WebSocket-Protocol` subprotocol
87
+ * in the form `"bearer, <token>"` (the only way a *browser* can pass a token,
88
+ * since `new WebSocket()` cannot set headers), then a `?token=` query param.
89
+ * Returns the token string or `null`.
90
+ *
91
+ * @param headers - Upgrade-request headers (case-insensitive lookup).
92
+ * @param queryString - Raw query string (without the leading `?`), e.g. `token=abc`.
93
+ * @param subprotocol - The offered `Sec-WebSocket-Protocol` value, if already parsed out.
94
+ */
95
+ export declare function wsToken(headers: Record<string, string | string[] | undefined>, queryString?: string, subprotocol?: string): string | null;
96
+ /**
97
+ * Per-route WebSocket authentication, checked on the upgrade.
98
+ *
99
+ * A route is secured when `authRequired` is truthy (set by `.secure()` /
100
+ * `{ secured: true }` on the WS route, or a `_secured` flag on the handler).
101
+ * Public routes (the default) always pass. A secured route needs a valid JWT
102
+ * via the Authorization header, the `bearer` subprotocol, or `?token=`.
103
+ *
104
+ * Returns `[payload, ok]` — the verified token payload (or `null`) and whether
105
+ * the upgrade may proceed. Mirrors Python's `ws_authorized`.
106
+ */
107
+ export declare function wsAuthorized(route: {
108
+ authRequired?: boolean;
109
+ } | null | undefined, headers: Record<string, string | string[] | undefined>, queryString?: string, subprotocol?: string): [Record<string, unknown> | null, boolean];
110
+ /**
111
+ * Whether the client offered the `bearer` subprotocol — if so, the server MUST
112
+ * echo `bearer` back as the accepted `Sec-WebSocket-Protocol` in the handshake
113
+ * (RFC 6455 §1.3; browsers send the token as the second subprotocol token).
114
+ */
115
+ export declare function offeredBearerSubprotocol(subprotocol: string): boolean;
116
+ /**
117
+ * Parse HTTP headers from raw upgrade request data.
118
+ */
119
+ export declare function parseUpgradeHeaders(raw: string): Record<string, string>;
120
+ /**
121
+ * Build a WebSocket frame (server->client, never masked).
122
+ */
123
+ export declare function buildFrame(opcode: number, payload: Buffer, fin?: boolean): Buffer;
124
+ /**
125
+ * Parse a WebSocket frame from a buffer.
126
+ * Returns { fin, opcode, payload, bytesConsumed } or null if not enough data.
127
+ */
128
+ export declare function parseFrame(data: Buffer): {
129
+ fin: boolean;
130
+ opcode: number;
131
+ payload: Buffer;
132
+ bytesConsumed: number;
133
+ } | null;
134
+ export declare class WebSocketServer {
135
+ private port;
136
+ private server;
137
+ private clients;
138
+ private handlers;
139
+ /** rooms[roomName] = Set of clientIds */
140
+ private rooms;
141
+ /** clientRooms[clientId] = Set of roomNames */
142
+ private clientRooms;
143
+ /** Route-style handlers registered via route(), keyed by path */
144
+ private _routeHandlers;
145
+ /**
146
+ * Backplane (multi-instance scaling). Lazily wired on the first broadcast
147
+ * via {@link ensureBackplane}. Each instance owns a stable id so it can
148
+ * ignore its own echoes coming back over the shared pub/sub channel.
149
+ */
150
+ private backplane;
151
+ /** Set once ensureBackplane() has fired so we only attempt the wiring once. */
152
+ private backplaneStarted;
153
+ /** Idle-connection reaper timer (opt-in via TINA4_WS_IDLE_TIMEOUT). */
154
+ private reaperTimer;
155
+ constructor(options?: {
156
+ port?: number;
157
+ });
158
+ /** Test/diagnostic helper: this instance's stable backplane id. */
159
+ get instanceId(): string;
160
+ /**
161
+ * Register an event handler.
162
+ */
163
+ on(event: string, handler: Function): WebSocketServer;
164
+ /**
165
+ * Register a WebSocket handler for a path (decorator style, matches Python).
166
+ *
167
+ * The handler receives a WebSocketConnection and sets up callbacks via
168
+ * `conn.onMessage(handler)` and `conn.onClose(handler)`.
169
+ *
170
+ * Internally this creates an adapter that converts from the decorator style
171
+ * to the Router's `(conn, event, data)` style and registers it via
172
+ * `Router.websocket()`.
173
+ */
174
+ route(path: string, handler: (conn: WebSocketConnection) => void | Promise<void>, options?: {
175
+ secured?: boolean;
176
+ }): void;
177
+ /**
178
+ * Broadcast a message to all connected clients.
179
+ *
180
+ * When `path` is provided, only clients connected on that specific path
181
+ * receive the message (matching PHP's WebSocket::broadcast behaviour).
182
+ * When `path` is omitted/undefined, all clients receive the message
183
+ * (backward compatible).
184
+ */
185
+ broadcast(message: string | Buffer, excludeIds?: string[], path?: string): void;
186
+ /**
187
+ * Send a message to a specific client by ID.
188
+ *
189
+ * Local-only: a connection lives on exactly one instance, so there is
190
+ * nothing to fan out over the backplane.
191
+ */
192
+ sendTo(clientId: string, message: string | Buffer): void;
193
+ /**
194
+ * Start the WebSocket server.
195
+ */
196
+ start(): Promise<void>;
197
+ /**
198
+ * Stop the server and disconnect all clients.
199
+ */
200
+ stop(): void;
201
+ /**
202
+ * Get all connected clients.
203
+ */
204
+ getClients(): Map<string, WebSocketClient>;
205
+ /**
206
+ * Close a specific client connection with an optional code and reason.
207
+ */
208
+ close(clientId: string, code?: number, reason?: string): void;
209
+ /**
210
+ * Add a client to a named room.
211
+ */
212
+ joinRoom(clientId: string, roomName: string): void;
213
+ /**
214
+ * Remove a client from a named room.
215
+ */
216
+ leaveRoom(clientId: string, roomName: string): void;
217
+ /**
218
+ * Return the list of client IDs in a room.
219
+ */
220
+ getRoomConnections(roomName: string): string[];
221
+ /**
222
+ * Return the number of clients in a room.
223
+ */
224
+ roomCount(roomName: string): number;
225
+ /**
226
+ * Return the names of all rooms a client has joined.
227
+ */
228
+ getClientRooms(clientId: string): string[];
229
+ /**
230
+ * Broadcast a message to all clients in a room.
231
+ *
232
+ * Resilient to dead clients (a failed send prunes the client, never aborts
233
+ * the loop) and fans out to sibling instances over the backplane when
234
+ * configured — a room can span instances, so each one delivers to its own
235
+ * members.
236
+ */
237
+ broadcastToRoom(roomName: string, message: string | Buffer, excludeIds?: string[]): void;
238
+ private emit;
239
+ /** Lazily wire the backplane on the first broadcast. Idempotent. */
240
+ private ensureBackplane;
241
+ /**
242
+ * Deliver a remote-originated envelope to LOCAL connections only. NEVER
243
+ * re-publishes (that would loop the message around the cluster). Dispatches
244
+ * by `kind`: room / path / all.
245
+ */
246
+ private relayLocal;
247
+ /**
248
+ * Send to ONE client without letting a single dead/slow client abort a
249
+ * broadcast loop. A write failure (or an already-closed client) is logged
250
+ * and the client is pruned. Returns true if the frame was handed to the
251
+ * socket.
252
+ *
253
+ * Slow-client backpressure: `socket.write()` returns false when the kernel
254
+ * send buffer is full. We don't unboundedly buffer — a client whose backlog
255
+ * has blown past TINA4_WS_MAX_BACKLOG bytes is hopelessly behind, so we drop
256
+ * and close it rather than let it grow the process heap without bound.
257
+ */
258
+ private safeSend;
259
+ /** Remove a client from the manager, rooms, and close its socket. */
260
+ private pruneClient;
261
+ /**
262
+ * Close connections whose last inbound frame is older than `timeoutSeconds`.
263
+ * Returns the number reaped. `timeoutSeconds <= 0` is a no-op (the reaper is
264
+ * opt-in via TINA4_WS_IDLE_TIMEOUT).
265
+ */
266
+ reapIdle(timeoutSeconds: number): number;
267
+ /**
268
+ * Spin up the idle-connection reaper when TINA4_WS_IDLE_TIMEOUT is a
269
+ * positive number of seconds. Opt-in and non-breaking — unset/0 means no
270
+ * timer is created at all (current behaviour). Called from start().
271
+ */
272
+ private startIdleReaper;
273
+ private handleUpgrade;
274
+ private processBuffer;
275
+ private removeClientFromAllRooms;
276
+ }
277
+ /** A live route connection in the integrated server. */
278
+ interface RouteConnState {
279
+ conn: WebSocketConnection;
280
+ socket: Socket;
281
+ path: string;
282
+ rooms: Set<string>;
283
+ closed: boolean;
284
+ /** Optional dev-admin tracker id so the connection shows in the websockets list. */
285
+ trackerId?: string;
286
+ }
287
+ /**
288
+ * Process-wide manager for user WebSocket route connections served by the
289
+ * integrated server. Path-scoped broadcast + rooms mirror the standalone
290
+ * WebSocketServer semantics so a route handler behaves the same either way.
291
+ */
292
+ declare class WsRouteManager {
293
+ private connections;
294
+ private rooms;
295
+ private onAdd?;
296
+ private onRemove?;
297
+ /** Wire dev-admin tracking callbacks (WsTracker.add / WsTracker.remove). */
298
+ setTracker(onAdd: (remoteAddress: string, path: string) => string, onRemove: (id: string) => void): void;
299
+ /** Currently-open route connections (test/diagnostic helper). */
300
+ get size(): number;
301
+ add(state: RouteConnState): void;
302
+ remove(id: string): void;
303
+ /**
304
+ * Close every open route connection with an RFC 6455 status code.
305
+ *
306
+ * Used by graceful shutdown with 1001 "going away", which is the code RFC
307
+ * 6455 s7.4.1 defines for exactly this case ("a server going down"). A
308
+ * client that is told 1001 can reconnect on a schedule; a socket that just
309
+ * vanishes looks like a network fault and produces an error instead.
310
+ *
311
+ * Best-effort per connection: a dead socket is skipped, never aborting the
312
+ * rest. Returns how many were signalled.
313
+ */
314
+ closeAll(code?: number, reason?: string): number;
315
+ /** Send a text frame to one connection (best-effort). */
316
+ sendTo(id: string, message: string): void;
317
+ /** Broadcast a text frame to every connection on the same path. */
318
+ broadcastPath(path: string, message: string): void;
319
+ joinRoom(id: string, room: string): void;
320
+ leaveRoom(id: string, room: string): void;
321
+ /**
322
+ * The live connections currently in a room (open only). Returns the
323
+ * WebSocketConnection objects so callers can read each `.auth` (presence
324
+ * rosters). Mirrors Python's mgr.get_room_connections / PHP getRoomConnections.
325
+ */
326
+ getRoomConnections(room: string): WebSocketConnection[];
327
+ /**
328
+ * Broadcast a text frame to every connection in a room, optionally skipping
329
+ * one connection id (self-exclusion). A dead/slow socket is skipped, never
330
+ * aborting the rest.
331
+ */
332
+ broadcastToRoom(room: string, message: string, excludeId?: string): void;
333
+ }
334
+ /** Process-wide manager for integrated-server user WS route connections. */
335
+ export declare const wsRouteManager: WsRouteManager;
336
+ /**
337
+ * Serve a user WebSocket route on the integrated server's `upgrade` event.
338
+ *
339
+ * This is the second half of per-route WS auth in Node: it's the entry point
340
+ * that wires a user-registered WS route (the WS route table) into the integrated
341
+ * server so the route actually gets a live open/message/close lifecycle on a
342
+ * real connection — parity with Python/PHP/Ruby — AND enforces per-route auth.
343
+ *
344
+ * Order mirrors Python's `_handle_dev_websocket` / `_handle_asgi_websocket`:
345
+ * 1. require a Sec-WebSocket-Key (else 400);
346
+ * 2. origin allow-list (else 403) — TINA4_WS_ALLOWED_ORIGINS, unset = allow all;
347
+ * 3. per-route auth (else 401) — public by default, secured needs a valid JWT;
348
+ * 4. accept the handshake, echoing `bearer` when offered;
349
+ * 5. build the connection (conn.auth = payload), fire "open", then pump
350
+ * frames into "message" / "close".
351
+ *
352
+ * Returns true if a matching route was found and handled (accepted OR rejected),
353
+ * false if no WS route matched this path (caller falls through to its 404).
354
+ */
355
+ export declare function serveWebSocketRoute(req: IncomingMessage, socket: Socket, head: Buffer): boolean;
356
+ /**
357
+ * Connection manager for the dev-reload channel (`/__dev_reload`).
358
+ *
359
+ * Mirrors Python's `_ws_manager` scoped to `/__dev_reload`: it accepts the
360
+ * RFC 6455 handshake on the *main* dev server's HTTP `upgrade` event, holds the
361
+ * raw sockets open, and lets `POST /__dev/api/reload` push an instant reload to
362
+ * every connected browser via {@link broadcast}. The framework never reads from
363
+ * the client — the open socket is the whole point. This restores the documented
364
+ * WebSocket-primary DevReload design (the dev toolbar and dev-admin dashboard
365
+ * both connect here). Registered only when `TINA4_DEBUG` is on, and never on the
366
+ * stable AI port.
367
+ */
368
+ declare class DevReloadWsManager {
369
+ private clients;
370
+ /** Optional hooks (add/remove) so the dev-admin connection list stays in sync. */
371
+ private onAdd?;
372
+ private onRemove?;
373
+ /** Wire dev-admin tracking callbacks (WsTracker.add / WsTracker.remove). */
374
+ setTracker(onAdd: (remoteAddress: string, path: string) => string, onRemove: (id: string) => void): void;
375
+ /** Number of currently-open dev-reload sockets (test/diagnostic helper). */
376
+ get size(): number;
377
+ /**
378
+ * Close every open dev-reload socket with an RFC 6455 status code (1001
379
+ * "going away" on shutdown). The browser client reconnects on a schedule
380
+ * when it is told the server went away, rather than reporting an error.
381
+ * Best-effort per socket. Returns how many were signalled.
382
+ */
383
+ closeAll(code?: number, reason?: string): number;
384
+ /**
385
+ * Accept a WebSocket upgrade on `/__dev_reload` and hold the socket open.
386
+ *
387
+ * Completes the RFC 6455 handshake, registers the connection, and drains
388
+ * inbound frames — responding to pings and cleaning up on close — without
389
+ * ever interpreting client data. Returns true if the handshake was accepted.
390
+ */
391
+ handleUpgrade(req: IncomingMessage, socket: Socket, head: Buffer): boolean;
392
+ /**
393
+ * Broadcast a text frame to every connected dev-reload client.
394
+ *
395
+ * Best-effort: a dead socket is dropped silently. Never throws — the caller
396
+ * (`POST /__dev/api/reload`) must not 500 because a browser tab went away.
397
+ */
398
+ broadcast(message: string): void;
399
+ }
400
+ /** Process-wide dev-reload manager (one channel: `/__dev_reload`). */
401
+ export declare const devReloadWs: DevReloadWsManager;
402
+ export {};
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Base interface for scaling WebSocket broadcast across instances.
3
+ *
4
+ * Implementations relay messages over a shared bus so every server instance
5
+ * receives every broadcast, not just the originator.
6
+ */
7
+ export interface WebSocketBackplane {
8
+ /** Publish a message to all instances listening on `channel`. */
9
+ publish(channel: string, message: string): Promise<void>;
10
+ /** Subscribe to `channel`. `callback` is invoked with each incoming message. */
11
+ subscribe(channel: string, callback: (message: string) => void): Promise<void>;
12
+ /** Stop listening on `channel`. */
13
+ unsubscribe(channel: string): Promise<void>;
14
+ /** Tear down connections. */
15
+ close(): Promise<void>;
16
+ }
17
+ /**
18
+ * Redis pub/sub backplane.
19
+ *
20
+ * Requires the `redis` package (`npm install redis`). The import is deferred
21
+ * so the rest of Tina4 works fine without it installed — an error is thrown
22
+ * only when this class is actually instantiated.
23
+ */
24
+ export declare class RedisBackplane implements WebSocketBackplane {
25
+ private publisher;
26
+ private subscriber;
27
+ private url;
28
+ private ready;
29
+ constructor(url?: string);
30
+ publish(channel: string, message: string): Promise<void>;
31
+ subscribe(channel: string, callback: (message: string) => void): Promise<void>;
32
+ unsubscribe(channel: string): Promise<void>;
33
+ close(): Promise<void>;
34
+ }
35
+ /**
36
+ * NATS pub/sub backplane.
37
+ *
38
+ * Requires the `nats` package (`npm install nats`). The import is deferred
39
+ * so the rest of Tina4 works fine without it installed — an error is thrown
40
+ * only when this class is actually instantiated.
41
+ *
42
+ * NATS is async-native. The subscription listener runs via the NATS client's
43
+ * built-in async iteration.
44
+ */
45
+ export declare class NATSBackplane implements WebSocketBackplane {
46
+ private nc;
47
+ private url;
48
+ private subs;
49
+ private ready;
50
+ constructor(url?: string);
51
+ publish(channel: string, message: string): Promise<void>;
52
+ subscribe(channel: string, callback: (message: string) => void): Promise<void>;
53
+ unsubscribe(channel: string): Promise<void>;
54
+ close(): Promise<void>;
55
+ }
56
+ /**
57
+ * Factory that reads TINA4_WS_BACKPLANE and returns the appropriate
58
+ * backplane instance, or `null` if no backplane is configured.
59
+ *
60
+ * This keeps backplane usage entirely optional — callers simply check
61
+ * `if (backplane)` before publishing.
62
+ */
63
+ export declare function createBackplane(url?: string): WebSocketBackplane | null;
64
+ /**
65
+ * The shared pub/sub channel name. Identical across all four Tina4 frameworks
66
+ * (cross-framework constant parity) so a Python, PHP, Ruby and Node instance
67
+ * can all relay each other's broadcasts over the same bus.
68
+ */
69
+ export declare const WS_BACKPLANE_CHANNEL = "tina4:ws";
70
+ /** What `kind` a broadcast envelope carries — mirrors the master design. */
71
+ export type WsEnvelopeKind = "all" | "path" | "room";
72
+ /**
73
+ * The JSON envelope published to the backplane channel. The wire shape is
74
+ * identical across all four frameworks. JSON can't carry bytes, so a string
75
+ * message rides under `text` and a binary message rides under `b64`
76
+ * (base64 of the bytes).
77
+ */
78
+ export interface WsEnvelope {
79
+ /** Stable per-process instance id of the publisher (for the origin guard). */
80
+ src: string;
81
+ /** Delivery kind: every local conn / a path / a room. */
82
+ kind: WsEnvelopeKind;
83
+ /** Optional connection id to skip on delivery. */
84
+ exclude?: string | null;
85
+ /** Room name (only when kind === "room"). */
86
+ room?: string | null;
87
+ /** Path (only when kind === "path"). */
88
+ path?: string | null;
89
+ /** Text payload (str messages). */
90
+ text?: string;
91
+ /** Base64 payload (binary messages). */
92
+ b64?: string;
93
+ }
94
+ /**
95
+ * Wires a {@link WebSocketBackplane} into a local connection manager so a
96
+ * broadcast on one server instance reaches the local connections of every
97
+ * sibling instance.
98
+ *
99
+ * Node is single-threaded async, so — unlike Python's bg-thread →
100
+ * `run_coroutine_threadsafe` bridge — the subscribe callback can relay
101
+ * directly on the event loop. We still apply the two invariants that keep a
102
+ * cluster correct:
103
+ *
104
+ * 1. **Origin guard** — drop any envelope whose `src` is *this* instance's
105
+ * id. We already delivered it locally on broadcast; relaying it again
106
+ * would double-send.
107
+ * 2. **No re-publish** — the relay path only delivers to LOCAL connections;
108
+ * it never publishes, so a message can't loop around the cluster.
109
+ *
110
+ * The manager is generic over the local-delivery callback (`relay`) so it can
111
+ * sit beside the WebSocketServer without importing it (no module cycle).
112
+ */
113
+ export declare class WsBackplaneManager {
114
+ /** Stable per-process id so we can ignore our own echoes. */
115
+ readonly instanceId: string;
116
+ readonly channel: string;
117
+ private backplane;
118
+ private started;
119
+ /** Local-delivery callback, installed by the owner (WebSocketServer). */
120
+ private relay;
121
+ constructor(channel?: string);
122
+ /** True once a backplane is actually attached (a network bus is configured). */
123
+ get active(): boolean;
124
+ /**
125
+ * Lazily wire the configured backplane and subscribe. Idempotent and
126
+ * best-effort — a failure here logs and leaves the manager in local-only
127
+ * mode; it must NEVER crash a broadcast. The `relay` callback is invoked
128
+ * (on this same event loop) for every *remote* envelope that survives the
129
+ * origin guard.
130
+ */
131
+ ensure(relay: (env: WsEnvelope) => void, log?: WsBackplaneLogger): Promise<void>;
132
+ /**
133
+ * Handle a raw envelope arriving on the channel. Applies the origin guard
134
+ * then hands a *remote* envelope to the local relay. Never throws (a
135
+ * malformed envelope is dropped silently).
136
+ */
137
+ onMessage(raw: string): void;
138
+ /**
139
+ * Publish a broadcast to the shared channel for sibling instances. No-op
140
+ * when no backplane is configured. Best-effort — a publish failure logs and
141
+ * is swallowed so the local broadcast that already happened is never undone
142
+ * by a flaky message bus.
143
+ */
144
+ publish(kind: WsEnvelopeKind, message: string | Buffer, opts?: {
145
+ room?: string | null;
146
+ path?: string | null;
147
+ exclude?: string | null;
148
+ }, log?: WsBackplaneLogger): void;
149
+ /**
150
+ * Reconstruct the original str/Buffer message from an envelope. JSON can't
151
+ * carry bytes, so `text` → string and `b64` → Buffer.
152
+ */
153
+ static decodeMessage(env: WsEnvelope): string | Buffer | null;
154
+ }
155
+ /** Minimal logger shape so the manager doesn't import the logger module. */
156
+ export interface WsBackplaneLogger {
157
+ info(message: string): void;
158
+ warn(message: string): void;
159
+ error(message: string): void;
160
+ }
161
+ /** Build the cross-framework envelope. Exported for tests. */
162
+ export declare function buildEnvelope(src: string, kind: WsEnvelopeKind, message: string | Buffer, opts?: {
163
+ room?: string | null;
164
+ path?: string | null;
165
+ exclude?: string | null;
166
+ }): WsEnvelope;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Represents a WebSocket connection with send/broadcast/close capabilities.
3
+ * Used by Router.websocket() route handlers.
4
+ */
5
+ export interface WebSocketConnection {
6
+ /** Unique connection identifier */
7
+ id: string;
8
+ /** The WebSocket route path this connection is on */
9
+ path: string;
10
+ /** Client IP address */
11
+ ip: string;
12
+ /** HTTP headers from the upgrade request */
13
+ headers: Record<string, string>;
14
+ /** Route parameters extracted from `{param}` segments in the path */
15
+ params: Record<string, string>;
16
+ /**
17
+ * Verified JWT payload on a `@secured` / `.secure()` WebSocket route, or
18
+ * `null` on a public route (the default). Set on the upgrade after the token
19
+ * is validated — mirrors Python's `connection.auth`.
20
+ */
21
+ auth: Record<string, unknown> | null;
22
+ /** Send a message to this connection only */
23
+ send(message: string): void;
24
+ /** Serialize an object to JSON and send it to this connection. Parity with Python/PHP. */
25
+ sendJson(data: unknown): void;
26
+ /** Broadcast a message to all connections on the same path (path-scoped) */
27
+ broadcast(message: string): void;
28
+ /**
29
+ * Broadcast a message to every connection in a room. Pass `excludeSelf: true`
30
+ * to skip this connection (the common signalling/chat relay case). Mirrors
31
+ * Python `connection.broadcast_to_room(room, msg, exclude_self=...)`.
32
+ */
33
+ broadcastToRoom(roomName: string, message: string, excludeSelf?: boolean): void;
34
+ /**
35
+ * The live connections currently in a room (for presence rosters — read each
36
+ * returned connection's `.auth`). Mirrors the manager room enumeration used
37
+ * by the Python/PHP/Ruby realtime rosters.
38
+ */
39
+ getRoomConnections(roomName: string): WebSocketConnection[];
40
+ /** Join a room */
41
+ joinRoom(roomName: string): void;
42
+ /** Leave a room */
43
+ leaveRoom(roomName: string): void;
44
+ /** Close this connection */
45
+ close(): void;
46
+ /** Internal message callback, set via onMessage(). */
47
+ _onMessage: ((data: string) => void | Promise<void>) | null;
48
+ /** Internal close callback, set via onClose(). */
49
+ _onClose: (() => void | Promise<void>) | null;
50
+ /** Register a message handler (decorator style, matches Python). */
51
+ onMessage(handler: (data: string) => void | Promise<void>): void;
52
+ /** Register a close handler (decorator style, matches Python). */
53
+ onClose(handler: () => void | Promise<void>): void;
54
+ }