@zero-server/realtime 0.9.1 → 0.9.3

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/lib/ws/room.js ADDED
@@ -0,0 +1,223 @@
1
+ /**
2
+ * @module ws/room
3
+ * @description WebSocket room/channel manager.
4
+ * Provides broadcast, room-based messaging, and connection
5
+ * registry for WebSocket connections.
6
+ */
7
+
8
+ /**
9
+ * Manages a pool of WebSocket connections with room-based grouping.
10
+ *
11
+ * @example
12
+ * const pool = new WebSocketPool();
13
+ * app.ws('/chat', (ws, req) => {
14
+ * pool.add(ws);
15
+ * pool.join(ws, 'general');
16
+ * ws.on('message', msg => pool.toRoom('general', msg));
17
+ * ws.on('close', () => pool.remove(ws));
18
+ * });
19
+ */
20
+ class WebSocketPool
21
+ {
22
+ /** @constructor */
23
+ constructor()
24
+ {
25
+ /** @type {Set<import('./connection')>} All active connections. */
26
+ this._connections = new Set();
27
+ /** @type {Map<string, Set<import('./connection')>>} Room → connection sets. */
28
+ this._rooms = new Map();
29
+ }
30
+
31
+ /**
32
+ * Add a connection to the pool.
33
+ * @param {import('./connection')} ws - WebSocket connection.
34
+ * @returns {WebSocketPool} this
35
+ */
36
+ add(ws)
37
+ {
38
+ this._connections.add(ws);
39
+
40
+ // Auto-remove on close
41
+ ws.once('close', () => this.remove(ws));
42
+
43
+ return this;
44
+ }
45
+
46
+ /**
47
+ * Remove a connection from the pool and all rooms.
48
+ * @param {import('./connection')} ws - WebSocket connection.
49
+ * @returns {WebSocketPool} this
50
+ */
51
+ remove(ws)
52
+ {
53
+ this._connections.delete(ws);
54
+ for (const [room, members] of this._rooms)
55
+ {
56
+ members.delete(ws);
57
+ if (members.size === 0) this._rooms.delete(room);
58
+ }
59
+ return this;
60
+ }
61
+
62
+ /**
63
+ * Join a connection to a room.
64
+ * @param {import('./connection')} ws - WebSocket connection.
65
+ * @param {string} room - Room name.
66
+ * @returns {WebSocketPool} this
67
+ */
68
+ join(ws, room)
69
+ {
70
+ if (!this._rooms.has(room)) this._rooms.set(room, new Set());
71
+ this._rooms.get(room).add(ws);
72
+ return this;
73
+ }
74
+
75
+ /**
76
+ * Remove a connection from a room.
77
+ * @param {import('./connection')} ws - WebSocket connection.
78
+ * @param {string} room - Room name.
79
+ * @returns {WebSocketPool} this
80
+ */
81
+ leave(ws, room)
82
+ {
83
+ const members = this._rooms.get(room);
84
+ if (members)
85
+ {
86
+ members.delete(ws);
87
+ if (members.size === 0) this._rooms.delete(room);
88
+ }
89
+ return this;
90
+ }
91
+
92
+ /**
93
+ * Get all rooms a connection belongs to.
94
+ * @param {import('./connection')} ws - WebSocket connection.
95
+ * @returns {string[]} Room names the connection belongs to.
96
+ */
97
+ roomsOf(ws)
98
+ {
99
+ const result = [];
100
+ for (const [room, members] of this._rooms)
101
+ {
102
+ if (members.has(ws)) result.push(room);
103
+ }
104
+ return result;
105
+ }
106
+
107
+ /**
108
+ * Broadcast a message to ALL connected clients.
109
+ * @param {string|Buffer} data - Payload.
110
+ * @param {import('./connection')} [exclude] - Optional connection to exclude (e.g. the sender).
111
+ */
112
+ broadcast(data, exclude)
113
+ {
114
+ for (const ws of this._connections)
115
+ {
116
+ if (ws !== exclude && ws.readyState === 1) ws.send(data);
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Broadcast a JSON message to ALL connected clients.
122
+ * @param {*} obj - Value to serialise.
123
+ * @param {import('./connection')} [exclude] - Connection(s) to exclude.
124
+ */
125
+ broadcastJSON(obj, exclude)
126
+ {
127
+ const msg = JSON.stringify(obj);
128
+ this.broadcast(msg, exclude);
129
+ }
130
+
131
+ /**
132
+ * Send a message to all connections in a specific room.
133
+ * @param {string} room - Room name.
134
+ * @param {string|Buffer} data - Payload.
135
+ * @param {import('./connection')} [exclude] - Connection(s) to exclude.
136
+ */
137
+ toRoom(room, data, exclude)
138
+ {
139
+ const members = this._rooms.get(room);
140
+ if (!members) return;
141
+ for (const ws of members)
142
+ {
143
+ if (ws !== exclude && ws.readyState === 1) ws.send(data);
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Send a JSON message to all connections in a specific room.
149
+ * @param {string} room - Room name.
150
+ * @param {*} obj - Data object to send.
151
+ * @param {import('./connection')} [exclude] - Connection(s) to exclude.
152
+ */
153
+ toRoomJSON(room, obj, exclude)
154
+ {
155
+ this.toRoom(room, JSON.stringify(obj), exclude);
156
+ }
157
+
158
+ /**
159
+ * Get all connections in a room.
160
+ * @param {string} room - Room name.
161
+ * @returns {import('./connection')[]} Connections in the room (empty array if the room does not exist).
162
+ */
163
+ in(room)
164
+ {
165
+ const members = this._rooms.get(room);
166
+ return members ? Array.from(members) : [];
167
+ }
168
+
169
+ /**
170
+ * Total number of active connections.
171
+ * @type {number}
172
+ */
173
+ get size()
174
+ {
175
+ return this._connections.size;
176
+ }
177
+
178
+ /**
179
+ * Number of connections in a specific room.
180
+ * @param {string} room - Room name.
181
+ * @returns {number} Number of connections in the room.
182
+ */
183
+ roomSize(room)
184
+ {
185
+ const members = this._rooms.get(room);
186
+ return members ? members.size : 0;
187
+ }
188
+
189
+ /**
190
+ * List all active room names.
191
+ * @returns {string[]} Array of room names.
192
+ */
193
+ get rooms()
194
+ {
195
+ return Array.from(this._rooms.keys());
196
+ }
197
+
198
+ /**
199
+ * Get all active connections.
200
+ * @returns {import('./connection')[]} Array of connections in the pool.
201
+ */
202
+ get clients()
203
+ {
204
+ return Array.from(this._connections);
205
+ }
206
+
207
+ /**
208
+ * Close all connections gracefully.
209
+ * @param {number} [code=1001] - Close code.
210
+ * @param {string} [reason] - Close reason.
211
+ */
212
+ closeAll(code = 1001, reason = 'Server shutdown')
213
+ {
214
+ for (const ws of this._connections)
215
+ {
216
+ ws.close(code, reason);
217
+ }
218
+ this._connections.clear();
219
+ this._rooms.clear();
220
+ }
221
+ }
222
+
223
+ module.exports = WebSocketPool;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zero-server/realtime",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "WebSocket connection + room manager and SSE stream controller.",
5
5
  "keywords": [
6
6
  "zero-server",
@@ -20,6 +20,8 @@
20
20
  "./package.json": "./package.json"
21
21
  },
22
22
  "files": [
23
+ "lib",
24
+ "types",
23
25
  "index.js",
24
26
  "index.d.ts",
25
27
  "README.md",
@@ -42,7 +44,12 @@
42
44
  "access": "public"
43
45
  },
44
46
  "sideEffects": false,
45
- "dependencies": {
46
- "@zero-server/sdk": "0.9.1"
47
+ "peerDependencies": {
48
+ "@zero-server/sdk": ">=0.9.3"
49
+ },
50
+ "peerDependenciesMeta": {
51
+ "@zero-server/sdk": {
52
+ "optional": true
53
+ }
47
54
  }
48
55
  }
package/types/app.d.ts ADDED
@@ -0,0 +1,223 @@
1
+ /// <reference types="node" />
2
+
3
+ import { Server as HttpServer } from 'http';
4
+ import { Server as HttpsServer, ServerOptions as TlsOptions } from 'https';
5
+ import { Http2Server, Http2SecureServer } from 'http2';
6
+ import { Request } from './request';
7
+ import { Response } from './response';
8
+ import { RouterInstance, RouteChain, RouteInfo, RouteOptions, RouteHandler } from './router';
9
+ import { MiddlewareFunction, ErrorHandlerFunction, NextFunction } from './middleware';
10
+ import { WebSocketHandler, WebSocketOptions, WebSocketPool } from './websocket';
11
+ import { SSEStream } from './sse';
12
+ import { LifecycleState } from './lifecycle';
13
+ import { MetricsRegistry, HealthCheckResult } from './observe';
14
+ import { ProtoSchema, GrpcServiceOptions, GrpcInterceptor, GrpcHandler } from './grpc';
15
+
16
+ export interface ListenOptions {
17
+ /** Create an HTTP/2 server. Combined with TLS options for h2 over TLS, or h2c (cleartext) otherwise. */
18
+ http2?: boolean;
19
+ /** Allow HTTP/1.1 fallback on HTTP/2 TLS servers (ALPN negotiation). Default: true. */
20
+ allowHTTP1?: boolean;
21
+ }
22
+
23
+ export interface App {
24
+ /** Internal router instance. */
25
+ router: RouterInstance;
26
+ /** Middleware stack. */
27
+ middlewares: MiddlewareFunction[];
28
+ /** Application-level locals, merged into every request/response locals. */
29
+ locals: Record<string, any>;
30
+
31
+ /**
32
+ * Register middleware or mount a sub-router.
33
+ */
34
+ use(fn: MiddlewareFunction): App;
35
+ use(path: string, fn: MiddlewareFunction): App;
36
+ use(path: string, router: RouterInstance): App;
37
+
38
+ /**
39
+ * Register a global error handler.
40
+ */
41
+ onError(fn: ErrorHandlerFunction): void;
42
+
43
+ /**
44
+ * Core request handler for use with `http.createServer()`.
45
+ */
46
+ handler(req: import('http').IncomingMessage, res: import('http').ServerResponse): void;
47
+
48
+ /**
49
+ * Start listening for connections.
50
+ * Pass `{ http2: true }` to create an HTTP/2 server (h2c cleartext or TLS with ALPN).
51
+ */
52
+ listen(port?: number, cb?: () => void): HttpServer;
53
+ listen(port: number, opts: TlsOptions, cb?: () => void): HttpsServer;
54
+ listen(port: number, opts: ListenOptions & { http2: true } & TlsOptions, cb?: () => void): Http2SecureServer;
55
+ listen(port: number, opts: ListenOptions & { http2: true }, cb?: () => void): Http2Server;
56
+ listen(port: number, opts: ListenOptions, cb?: () => void): HttpServer | HttpsServer | Http2Server | Http2SecureServer;
57
+
58
+ /**
59
+ * Gracefully close the server.
60
+ */
61
+ close(cb?: (err?: Error) => void): void;
62
+
63
+ /**
64
+ * Perform a full graceful shutdown.
65
+ */
66
+ shutdown(opts?: { timeout?: number }): Promise<void>;
67
+
68
+ /**
69
+ * Register a lifecycle event listener.
70
+ */
71
+ on(event: 'beforeShutdown' | 'shutdown', fn: () => void | Promise<void>): App;
72
+
73
+ /**
74
+ * Remove a lifecycle event listener.
75
+ */
76
+ off(event: 'beforeShutdown' | 'shutdown', fn: () => void | Promise<void>): App;
77
+
78
+ /**
79
+ * Register a WebSocket pool for graceful shutdown.
80
+ */
81
+ registerPool(pool: WebSocketPool): App;
82
+
83
+ /**
84
+ * Unregister a WebSocket pool from lifecycle management.
85
+ */
86
+ unregisterPool(pool: WebSocketPool): App;
87
+
88
+ /**
89
+ * Track an SSE stream for graceful shutdown.
90
+ */
91
+ trackSSE(stream: SSEStream): App;
92
+
93
+ /**
94
+ * Register an ORM Database for graceful shutdown.
95
+ */
96
+ registerDatabase(db: { close(): Promise<void> }): App;
97
+
98
+ /**
99
+ * Unregister an ORM Database from lifecycle management.
100
+ */
101
+ unregisterDatabase(db: { close(): Promise<void> }): App;
102
+
103
+ /**
104
+ * Configure the shutdown timeout in milliseconds.
105
+ */
106
+ shutdownTimeout(ms: number): App;
107
+
108
+ /**
109
+ * Current lifecycle state.
110
+ */
111
+ readonly lifecycleState: LifecycleState;
112
+
113
+ /**
114
+ * Register a liveness health check endpoint.
115
+ */
116
+ health(path?: string, checks?: Record<string, () => HealthCheckResult | boolean | Promise<HealthCheckResult | boolean>>): App;
117
+ health(checks?: Record<string, () => HealthCheckResult | boolean | Promise<HealthCheckResult | boolean>>): App;
118
+
119
+ /**
120
+ * Register a readiness health check endpoint.
121
+ */
122
+ ready(path?: string, checks?: Record<string, () => HealthCheckResult | boolean | Promise<HealthCheckResult | boolean>>): App;
123
+ ready(checks?: Record<string, () => HealthCheckResult | boolean | Promise<HealthCheckResult | boolean>>): App;
124
+
125
+ /**
126
+ * Register a custom health check.
127
+ */
128
+ addHealthCheck(name: string, fn: () => HealthCheckResult | boolean | Promise<HealthCheckResult | boolean>): App;
129
+
130
+ /**
131
+ * Get the application metrics registry.
132
+ */
133
+ metrics(): MetricsRegistry;
134
+
135
+ /**
136
+ * Mount a Prometheus metrics endpoint.
137
+ */
138
+ metricsEndpoint(path?: string, opts?: { registry?: MetricsRegistry }): App;
139
+
140
+ /**
141
+ * Register a WebSocket upgrade handler.
142
+ */
143
+ ws(path: string, handler: WebSocketHandler): void;
144
+ ws(path: string, opts: WebSocketOptions, handler: WebSocketHandler): void;
145
+
146
+ /**
147
+ * Register a gRPC service with handlers.
148
+ */
149
+ grpc(schema: ProtoSchema, serviceName: string, handlers: Record<string, GrpcHandler>, opts?: GrpcServiceOptions): App;
150
+
151
+ /**
152
+ * Add a global gRPC interceptor.
153
+ */
154
+ grpcInterceptor(fn: GrpcInterceptor): App;
155
+
156
+ /**
157
+ * Return a flat list of all registered routes.
158
+ */
159
+ routes(): RouteInfo[];
160
+
161
+ /**
162
+ * Register a route with a specific HTTP method.
163
+ */
164
+ route(method: string, path: string, ...handlers: (RouteOptions | RouteHandler)[]): App;
165
+
166
+ /**
167
+ * Get a setting value (1 arg) or set a setting value (2 args).
168
+ */
169
+ set(key: string): any;
170
+ set(key: string, val: any): App;
171
+
172
+ /**
173
+ * Get a setting value, or register a GET route.
174
+ * With 1 string arg: returns the setting value.
175
+ * With path + handlers: registers a GET route.
176
+ */
177
+ get(key: string): any;
178
+ get(path: string, ...handlers: (RouteOptions | RouteHandler)[]): App;
179
+
180
+ /**
181
+ * Enable a boolean setting (set to `true`).
182
+ */
183
+ enable(key: string): App;
184
+
185
+ /**
186
+ * Disable a boolean setting (set to `false`).
187
+ */
188
+ disable(key: string): App;
189
+
190
+ /**
191
+ * Check if a setting is truthy.
192
+ */
193
+ enabled(key: string): boolean;
194
+
195
+ /**
196
+ * Check if a setting is falsy.
197
+ */
198
+ disabled(key: string): boolean;
199
+
200
+ /**
201
+ * Register a parameter pre-processing handler.
202
+ */
203
+ param(name: string, fn: (req: Request, res: Response, next: NextFunction, value: string) => void): App;
204
+
205
+ /**
206
+ * Create a route group under a prefix with shared middleware.
207
+ */
208
+ group(prefix: string, ...args: [...MiddlewareFunction[], (router: RouterInstance) => void]): App;
209
+
210
+ /**
211
+ * Create a chainable route builder for the given path.
212
+ */
213
+ chain(path: string): RouteChain;
214
+
215
+ // HTTP method shortcuts
216
+ post(path: string, ...handlers: (RouteOptions | RouteHandler)[]): App;
217
+ put(path: string, ...handlers: (RouteOptions | RouteHandler)[]): App;
218
+ delete(path: string, ...handlers: (RouteOptions | RouteHandler)[]): App;
219
+ patch(path: string, ...handlers: (RouteOptions | RouteHandler)[]): App;
220
+ options(path: string, ...handlers: (RouteOptions | RouteHandler)[]): App;
221
+ head(path: string, ...handlers: (RouteOptions | RouteHandler)[]): App;
222
+ all(path: string, ...handlers: (RouteOptions | RouteHandler)[]): App;
223
+ }