@syncar/server 1.0.0-alpha.2 → 1.0.0-alpha.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/README.md +111 -148
- package/dist/index.d.ts +329 -2519
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/index.d.ts +160 -0
- package/dist/middleware/index.js +2 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/types-DeE5vqYX.d.ts +630 -0
- package/package.json +5 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,944 +1,9 @@
|
|
|
1
1
|
import * as node_https from 'node:https';
|
|
2
2
|
import * as node_http from 'node:http';
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import { C as ChannelName, a as ClientId, I as IClientConnection, D as DataMessage, b as IMiddleware, c as ILogger, d as IContext, M as Message, e as IMiddlewareAction, f as IMiddlewareRejectionError } from './types-DeE5vqYX.js';
|
|
4
|
+
export { A as AckMessage, E as ErrorCode, g as ErrorMessage, h as MessageId, i as MessageType, S as SignalMessage, j as SignalType, T as Timestamp } from './types-DeE5vqYX.js';
|
|
5
5
|
import { EventEmitter } from 'node:events';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Message identifier
|
|
9
|
-
*
|
|
10
|
-
* @remarks
|
|
11
|
-
* Unique identifier for a message. Typically a UUID or similar unique string.
|
|
12
|
-
*
|
|
13
|
-
* @example
|
|
14
|
-
* ```ts
|
|
15
|
-
* const messageId: MessageId = "550e8400-e29b-41d4-a716-446655440000"
|
|
16
|
-
* ```
|
|
17
|
-
*/
|
|
18
|
-
type MessageId = string;
|
|
19
|
-
/**
|
|
20
|
-
* Client identifier (e.g., WebSocket connection ID)
|
|
21
|
-
*
|
|
22
|
-
* @remarks
|
|
23
|
-
* Unique identifier for a connected client. This ID is generated when
|
|
24
|
-
* a client connects and is used to address messages to specific clients.
|
|
25
|
-
*
|
|
26
|
-
* @example
|
|
27
|
-
* ```ts
|
|
28
|
-
* const clientId: ClientId = "client_123abc"
|
|
29
|
-
* ```
|
|
30
|
-
*/
|
|
31
|
-
type ClientId = string;
|
|
32
|
-
/**
|
|
33
|
-
* Subscriber identifier (typically client ID)
|
|
34
|
-
*
|
|
35
|
-
* @remarks
|
|
36
|
-
* Identifier for a subscriber to a channel. In most cases, this is the
|
|
37
|
-
* same as the client ID, but can be different for custom subscription models.
|
|
38
|
-
*
|
|
39
|
-
* @example
|
|
40
|
-
* ```ts
|
|
41
|
-
* const subscriberId: SubscriberId = "client_123abc"
|
|
42
|
-
* ```
|
|
43
|
-
*/
|
|
44
|
-
type SubscriberId = string;
|
|
45
|
-
/**
|
|
46
|
-
* Channel name
|
|
47
|
-
*
|
|
48
|
-
* @remarks
|
|
49
|
-
* String identifier for a channel. Channel names starting with `__` are
|
|
50
|
-
* reserved for internal use (e.g., `__broadcast__`).
|
|
51
|
-
*
|
|
52
|
-
* @example
|
|
53
|
-
* ```ts
|
|
54
|
-
* const channelName: ChannelName = "chat"
|
|
55
|
-
* ```
|
|
56
|
-
*/
|
|
57
|
-
type ChannelName = string;
|
|
58
|
-
/**
|
|
59
|
-
* Unix timestamp in milliseconds
|
|
60
|
-
*
|
|
61
|
-
* @remarks
|
|
62
|
-
* Standard timestamp format used throughout the server for tracking
|
|
63
|
-
* connection times, message timestamps, and other time-based events.
|
|
64
|
-
*
|
|
65
|
-
* @example
|
|
66
|
-
* ```ts
|
|
67
|
-
* const timestamp: Timestamp = Date.now()
|
|
68
|
-
* ```
|
|
69
|
-
*/
|
|
70
|
-
type Timestamp = number;
|
|
71
|
-
/**
|
|
72
|
-
* Generic data payload for messages
|
|
73
|
-
*
|
|
74
|
-
* @remarks
|
|
75
|
-
* Type alias for the data payload in messages. Can be any type,
|
|
76
|
-
* with `unknown` as the default for type safety.
|
|
77
|
-
*
|
|
78
|
-
* @template T - The type of the data payload (default: unknown)
|
|
79
|
-
*
|
|
80
|
-
* @example
|
|
81
|
-
* ```ts
|
|
82
|
-
* const payload: DataPayload<string> = "Hello world"
|
|
83
|
-
* const complexPayload: DataPayload<{ text: string; user: string }> = {
|
|
84
|
-
* text: "Hello",
|
|
85
|
-
* user: "Alice"
|
|
86
|
-
* }
|
|
87
|
-
* ```
|
|
88
|
-
*/
|
|
89
|
-
type DataPayload<T = unknown> = T;
|
|
90
|
-
/**
|
|
91
|
-
* Logger interface
|
|
92
|
-
*
|
|
93
|
-
* @remarks
|
|
94
|
-
* Interface for logging operations. Implementations can write to console,
|
|
95
|
-
* files, or external logging services. All methods accept variadic arguments
|
|
96
|
-
* for flexible logging.
|
|
97
|
-
*
|
|
98
|
-
* @example
|
|
99
|
-
* ```ts
|
|
100
|
-
* const logger: ILogger = {
|
|
101
|
-
* debug: (msg, ...args) => console.debug('[DEBUG]', msg, ...args),
|
|
102
|
-
* info: (msg, ...args) => console.info('[INFO]', msg, ...args),
|
|
103
|
-
* warn: (msg, ...args) => console.warn('[WARN]', msg, ...args),
|
|
104
|
-
* error: (msg, ...args) => console.error('[ERROR]', msg, ...args),
|
|
105
|
-
* }
|
|
106
|
-
* ```
|
|
107
|
-
*
|
|
108
|
-
* @example
|
|
109
|
-
* ### Using with pino
|
|
110
|
-
* ```ts
|
|
111
|
-
* import pino from 'pino'
|
|
112
|
-
*
|
|
113
|
-
* const logger: ILogger = pino({
|
|
114
|
-
* level: 'info',
|
|
115
|
-
* transport: {
|
|
116
|
-
* target: 'pino-pretty',
|
|
117
|
-
* },
|
|
118
|
-
* })
|
|
119
|
-
* ```
|
|
120
|
-
*/
|
|
121
|
-
interface ILogger {
|
|
122
|
-
/**
|
|
123
|
-
* Log a debug message
|
|
124
|
-
*
|
|
125
|
-
* @param message - The log message
|
|
126
|
-
* @param args - Additional arguments to log
|
|
127
|
-
*/
|
|
128
|
-
debug(message: string, ...args: unknown[]): void;
|
|
129
|
-
/**
|
|
130
|
-
* Log an info message
|
|
131
|
-
*
|
|
132
|
-
* @param message - The log message
|
|
133
|
-
* @param args - Additional arguments to log
|
|
134
|
-
*/
|
|
135
|
-
info(message: string, ...args: unknown[]): void;
|
|
136
|
-
/**
|
|
137
|
-
* Log a warning message
|
|
138
|
-
*
|
|
139
|
-
* @param message - The log message
|
|
140
|
-
* @param args - Additional arguments to log
|
|
141
|
-
*/
|
|
142
|
-
warn(message: string, ...args: unknown[]): void;
|
|
143
|
-
/**
|
|
144
|
-
* Log an error message
|
|
145
|
-
*
|
|
146
|
-
* @param message - The log message
|
|
147
|
-
* @param args - Additional arguments to log
|
|
148
|
-
*/
|
|
149
|
-
error(message: string, ...args: unknown[]): void;
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* ID Generator function type
|
|
153
|
-
*
|
|
154
|
-
* @remarks
|
|
155
|
-
* Function type for generating unique client IDs from incoming HTTP requests.
|
|
156
|
-
* Can be synchronous or asynchronous, and can throw to reject connections.
|
|
157
|
-
*
|
|
158
|
-
* @param request - The incoming HTTP upgrade request
|
|
159
|
-
* @returns The generated client ID
|
|
160
|
-
*
|
|
161
|
-
* @example
|
|
162
|
-
* ```ts
|
|
163
|
-
* const generateId: IdGenerator = (request) => {
|
|
164
|
-
* const token = request.headers.authorization?.split(' ')[1]
|
|
165
|
-
* return extractUserIdFromToken(token)
|
|
166
|
-
* }
|
|
167
|
-
* ```
|
|
168
|
-
*
|
|
169
|
-
* @example
|
|
170
|
-
* ### Async ID generator
|
|
171
|
-
* ```ts
|
|
172
|
-
* const generateId: IdGenerator = async (request) => {
|
|
173
|
-
* const token = request.headers.authorization?.split(' ')[1]
|
|
174
|
-
* const user = await verifyJwt(token)
|
|
175
|
-
* return user.id
|
|
176
|
-
* }
|
|
177
|
-
* ```
|
|
178
|
-
*/
|
|
179
|
-
type IdGenerator = (request: IncomingMessage) => ClientId | Promise<ClientId>;
|
|
180
|
-
/**
|
|
181
|
-
* Base client connection interface
|
|
182
|
-
*
|
|
183
|
-
* @remarks
|
|
184
|
-
* Represents a connected WebSocket client with metadata about the connection.
|
|
185
|
-
* This interface is used throughout the server for client tracking and management.
|
|
186
|
-
*
|
|
187
|
-
* @property id - Unique client identifier
|
|
188
|
-
* @property connectedAt - Unix timestamp (ms) when the client connected
|
|
189
|
-
* @property lastPingAt - Unix timestamp (ms) of the last ping/pong (optional)
|
|
190
|
-
* @property socket - The raw WebSocket instance
|
|
191
|
-
*
|
|
192
|
-
* @example
|
|
193
|
-
* ```ts
|
|
194
|
-
* const client: IClientConnection = {
|
|
195
|
-
* id: 'client_123abc',
|
|
196
|
-
* connectedAt: 1699123456789,
|
|
197
|
-
* lastPingAt: 1699123459999,
|
|
198
|
-
* socket: wsSocket
|
|
199
|
-
* }
|
|
200
|
-
*
|
|
201
|
-
* console.log(`Client ${client.id} connected at ${new Date(client.connectedAt).toLocaleString()}`)
|
|
202
|
-
* ```
|
|
203
|
-
*/
|
|
204
|
-
interface IClientConnection {
|
|
205
|
-
/** Unique client identifier */
|
|
206
|
-
readonly id: ClientId;
|
|
207
|
-
/** Connected timestamp */
|
|
208
|
-
readonly connectedAt: Timestamp;
|
|
209
|
-
/** Last ping timestamp */
|
|
210
|
-
lastPingAt?: Timestamp;
|
|
211
|
-
/** Raw WebSocket instance */
|
|
212
|
-
readonly socket: WebSocket;
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* Message types for protocol communication
|
|
216
|
-
*
|
|
217
|
-
* @remarks
|
|
218
|
-
* Enum defining all message types supported by the Syncar protocol.
|
|
219
|
-
*
|
|
220
|
-
* @example
|
|
221
|
-
* ```ts
|
|
222
|
-
* if (message.type === MessageType.DATA) {
|
|
223
|
-
* // Handle data message
|
|
224
|
-
* } else if (message.type === MessageType.SIGNAL) {
|
|
225
|
-
* // Handle signal message
|
|
226
|
-
* }
|
|
227
|
-
* ```
|
|
228
|
-
*
|
|
229
|
-
* @see {@link SignalType} for signal message subtypes
|
|
230
|
-
*/
|
|
231
|
-
declare enum MessageType {
|
|
232
|
-
/** Data message - carries application data */
|
|
233
|
-
DATA = "data",
|
|
234
|
-
/** Signal message - control message for subscriptions, pings, etc. */
|
|
235
|
-
SIGNAL = "signal",
|
|
236
|
-
/** Error message - reports errors to clients */
|
|
237
|
-
ERROR = "error",
|
|
238
|
-
/** Acknowledgment message - confirms message receipt */
|
|
239
|
-
ACK = "ack"
|
|
240
|
-
}
|
|
241
|
-
/**
|
|
242
|
-
* Signal types for control messages
|
|
243
|
-
*
|
|
244
|
-
* @remarks
|
|
245
|
-
* Enum defining all signal types used for protocol control operations.
|
|
246
|
-
*
|
|
247
|
-
* @example
|
|
248
|
-
* ```ts
|
|
249
|
-
* if (message.type === MessageType.SIGNAL) {
|
|
250
|
-
* if (message.signal === SignalType.SUBSCRIBE) {
|
|
251
|
-
* // Handle subscription
|
|
252
|
-
* } else if (message.signal === SignalType.PING) {
|
|
253
|
-
* // Respond with pong
|
|
254
|
-
* }
|
|
255
|
-
* }
|
|
256
|
-
* ```
|
|
257
|
-
*/
|
|
258
|
-
declare enum SignalType {
|
|
259
|
-
/** Subscribe to a channel */
|
|
260
|
-
SUBSCRIBE = "subscribe",
|
|
261
|
-
/** Unsubscribe from a channel */
|
|
262
|
-
UNSUBSCRIBE = "unsubscribe",
|
|
263
|
-
/** Ping message for keep-alive */
|
|
264
|
-
PING = "ping",
|
|
265
|
-
/** Pong response to ping */
|
|
266
|
-
PONG = "pong",
|
|
267
|
-
/** Confirmation of successful subscription */
|
|
268
|
-
SUBSCRIBED = "subscribed",
|
|
269
|
-
/** Confirmation of successful unsubscription */
|
|
270
|
-
UNSUBSCRIBED = "unsubscribed"
|
|
271
|
-
}
|
|
272
|
-
/**
|
|
273
|
-
* Standard error codes for messages
|
|
274
|
-
*
|
|
275
|
-
* @remarks
|
|
276
|
-
* Enum defining error codes used in error messages sent to clients.
|
|
277
|
-
*
|
|
278
|
-
* @example
|
|
279
|
-
* ```ts
|
|
280
|
-
* if (message.type === MessageType.ERROR) {
|
|
281
|
-
* const { code, message } = message.data
|
|
282
|
-
* console.error(`Error ${code}: ${message}`)
|
|
283
|
-
* }
|
|
284
|
-
* ```
|
|
285
|
-
*/
|
|
286
|
-
declare enum ErrorCode {
|
|
287
|
-
/** Invalid message type */
|
|
288
|
-
INVALID_MESSAGE_TYPE = "INVALID_MESSAGE_TYPE",
|
|
289
|
-
/** Channel name missing from message */
|
|
290
|
-
MISSING_CHANNEL = "MISSING_CHANNEL",
|
|
291
|
-
/** Unknown signal type */
|
|
292
|
-
UNKNOWN_SIGNAL = "UNKNOWN_SIGNAL",
|
|
293
|
-
/** Channel not found */
|
|
294
|
-
CHANNEL_NOT_FOUND = "CHANNEL_NOT_FOUND",
|
|
295
|
-
/** Reserved channel name (starts with __) */
|
|
296
|
-
RESERVED_CHANNEL_NAME = "RESERVED_CHANNEL_NAME",
|
|
297
|
-
/** Invalid message format */
|
|
298
|
-
INVALID_MESSAGE_FORMAT = "INVALID_MESSAGE_FORMAT",
|
|
299
|
-
/** ID required but not provided */
|
|
300
|
-
ID_REQUIRED = "ID_REQUIRED",
|
|
301
|
-
/** ID already taken */
|
|
302
|
-
ID_TAKEN = "ID_TAKEN",
|
|
303
|
-
/** Invalid ID format */
|
|
304
|
-
INVALID_ID_FORMAT = "INVALID_ID_FORMAT",
|
|
305
|
-
/** Rate limit exceeded */
|
|
306
|
-
RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED"
|
|
307
|
-
}
|
|
308
|
-
/**
|
|
309
|
-
* Base message interface
|
|
310
|
-
*
|
|
311
|
-
* @remarks
|
|
312
|
-
* All messages in the Syncar protocol extend this interface with
|
|
313
|
-
* common fields like ID, timestamp, and optional channel.
|
|
314
|
-
*
|
|
315
|
-
* @property id - Unique message identifier
|
|
316
|
-
* @property timestamp - Unix timestamp (ms) when message was created
|
|
317
|
-
* @property channel - Optional channel name (required for most message types)
|
|
318
|
-
*
|
|
319
|
-
* @example
|
|
320
|
-
* ```ts
|
|
321
|
-
* const baseMessage: BaseMessage = {
|
|
322
|
-
* id: generateId(),
|
|
323
|
-
* timestamp: Date.now(),
|
|
324
|
-
* channel: 'chat'
|
|
325
|
-
* }
|
|
326
|
-
* ```
|
|
327
|
-
*/
|
|
328
|
-
interface BaseMessage {
|
|
329
|
-
/** Unique message identifier */
|
|
330
|
-
id: MessageId;
|
|
331
|
-
/** Message timestamp */
|
|
332
|
-
timestamp: Timestamp;
|
|
333
|
-
/** Channel name */
|
|
334
|
-
channel?: ChannelName;
|
|
335
|
-
}
|
|
336
|
-
/**
|
|
337
|
-
* Data message (typed message with channel)
|
|
338
|
-
*
|
|
339
|
-
* @remarks
|
|
340
|
-
* Carries application data from sender to channel subscribers.
|
|
341
|
-
* This is the primary message type for user-defined communication.
|
|
342
|
-
*
|
|
343
|
-
* @template T - Type of the data payload (default: unknown)
|
|
344
|
-
*
|
|
345
|
-
* @property type - Message type discriminator (always DATA)
|
|
346
|
-
* @property channel - The target channel name
|
|
347
|
-
* @property data - The message data payload
|
|
348
|
-
*
|
|
349
|
-
* @example
|
|
350
|
-
* ```ts
|
|
351
|
-
* const chatMessage: DataMessage<{ text: string; user: string }> = {
|
|
352
|
-
* id: generateId(),
|
|
353
|
-
* timestamp: Date.now(),
|
|
354
|
-
* type: MessageType.DATA,
|
|
355
|
-
* channel: 'chat',
|
|
356
|
-
* data: {
|
|
357
|
-
* text: 'Hello world!',
|
|
358
|
-
* user: 'Alice'
|
|
359
|
-
* }
|
|
360
|
-
* }
|
|
361
|
-
*
|
|
362
|
-
* // Type narrowing
|
|
363
|
-
* if (message.type === MessageType.DATA) {
|
|
364
|
-
* console.log(`${message.data.user}: ${message.data.text}`)
|
|
365
|
-
* }
|
|
366
|
-
* ```
|
|
367
|
-
*/
|
|
368
|
-
interface DataMessage<T = unknown> extends BaseMessage {
|
|
369
|
-
type: MessageType.DATA;
|
|
370
|
-
channel: ChannelName;
|
|
371
|
-
/** Message payload */
|
|
372
|
-
data: T;
|
|
373
|
-
}
|
|
374
|
-
/**
|
|
375
|
-
* Signal message (control message)
|
|
376
|
-
*
|
|
377
|
-
* @remarks
|
|
378
|
-
* Control messages for protocol operations like subscribe/unsubscribe,
|
|
379
|
-
* ping/pong keep-alive, and subscription confirmations.
|
|
380
|
-
*
|
|
381
|
-
* @property type - Message type discriminator (always SIGNAL)
|
|
382
|
-
* @property channel - The target channel name
|
|
383
|
-
* @property signal - The signal type
|
|
384
|
-
* @property data - Optional data payload
|
|
385
|
-
*
|
|
386
|
-
* @example
|
|
387
|
-
* ### Subscribe signal
|
|
388
|
-
* ```ts
|
|
389
|
-
* const subscribeSignal: SignalMessage = {
|
|
390
|
-
* id: generateId(),
|
|
391
|
-
* timestamp: Date.now(),
|
|
392
|
-
* type: MessageType.SIGNAL,
|
|
393
|
-
* channel: 'chat',
|
|
394
|
-
* signal: SignalType.SUBSCRIBE
|
|
395
|
-
* }
|
|
396
|
-
* ```
|
|
397
|
-
*
|
|
398
|
-
* @example
|
|
399
|
-
* ### Ping signal
|
|
400
|
-
* ```ts
|
|
401
|
-
* const pingSignal: SignalMessage = {
|
|
402
|
-
* id: generateId(),
|
|
403
|
-
* timestamp: Date.now(),
|
|
404
|
-
* type: MessageType.SIGNAL,
|
|
405
|
-
* signal: SignalType.PING
|
|
406
|
-
* }
|
|
407
|
-
* ```
|
|
408
|
-
*/
|
|
409
|
-
interface SignalMessage extends BaseMessage {
|
|
410
|
-
type: MessageType.SIGNAL;
|
|
411
|
-
channel: ChannelName;
|
|
412
|
-
/** Signal type */
|
|
413
|
-
signal: SignalType;
|
|
414
|
-
/** Message payload */
|
|
415
|
-
data?: DataPayload;
|
|
416
|
-
}
|
|
417
|
-
/**
|
|
418
|
-
* Error data structure
|
|
419
|
-
*
|
|
420
|
-
* @remarks
|
|
421
|
-
* Contains error information sent to clients when an error occurs.
|
|
422
|
-
*
|
|
423
|
-
* @property message - Human-readable error message
|
|
424
|
-
* @property code - Optional machine-readable error code
|
|
425
|
-
*
|
|
426
|
-
* @example
|
|
427
|
-
* ```ts
|
|
428
|
-
* const errorData: ErrorData = {
|
|
429
|
-
* message: 'Channel not found',
|
|
430
|
-
* code: ErrorCode.CHANNEL_NOT_FOUND
|
|
431
|
-
* }
|
|
432
|
-
* ```
|
|
433
|
-
*/
|
|
434
|
-
interface ErrorData {
|
|
435
|
-
/** Human-readable error message */
|
|
436
|
-
message: string;
|
|
437
|
-
/** Machine-readable error code */
|
|
438
|
-
code?: ErrorCode;
|
|
439
|
-
/** Additional error context */
|
|
440
|
-
[key: string]: unknown;
|
|
441
|
-
}
|
|
442
|
-
/**
|
|
443
|
-
* Error message
|
|
444
|
-
*
|
|
445
|
-
* @remarks
|
|
446
|
-
* Sent to clients when an error occurs during message processing.
|
|
447
|
-
*
|
|
448
|
-
* @property type - Message type discriminator (always ERROR)
|
|
449
|
-
* @property data - Error details including message and optional code
|
|
450
|
-
*
|
|
451
|
-
* @example
|
|
452
|
-
* ```ts
|
|
453
|
-
* const errorMessage: ErrorMessage = {
|
|
454
|
-
* id: generateId(),
|
|
455
|
-
* timestamp: Date.now(),
|
|
456
|
-
* type: MessageType.ERROR,
|
|
457
|
-
* data: {
|
|
458
|
-
* message: 'Channel not found',
|
|
459
|
-
* code: ErrorCode.CHANNEL_NOT_FOUND
|
|
460
|
-
* }
|
|
461
|
-
* }
|
|
462
|
-
* ```
|
|
463
|
-
*/
|
|
464
|
-
interface ErrorMessage extends BaseMessage {
|
|
465
|
-
type: MessageType.ERROR;
|
|
466
|
-
/** Error detail */
|
|
467
|
-
data: ErrorData;
|
|
468
|
-
}
|
|
469
|
-
/**
|
|
470
|
-
* Acknowledgment message
|
|
471
|
-
*
|
|
472
|
-
* @remarks
|
|
473
|
-
* Confirms receipt of a message. Used for reliable messaging patterns.
|
|
474
|
-
*
|
|
475
|
-
* @property type - Message type discriminator (always ACK)
|
|
476
|
-
* @property ackMessageId - ID of the message being acknowledged
|
|
477
|
-
*
|
|
478
|
-
* @example
|
|
479
|
-
* ```ts
|
|
480
|
-
* const ackMessage: AckMessage = {
|
|
481
|
-
* id: generateId(),
|
|
482
|
-
* timestamp: Date.now(),
|
|
483
|
-
* type: MessageType.ACK,
|
|
484
|
-
* ackMessageId: 'original-message-id'
|
|
485
|
-
* }
|
|
486
|
-
* ```
|
|
487
|
-
*/
|
|
488
|
-
interface AckMessage extends BaseMessage {
|
|
489
|
-
type: MessageType.ACK;
|
|
490
|
-
/** Original message being acknowledged */
|
|
491
|
-
ackMessageId: MessageId;
|
|
492
|
-
}
|
|
493
|
-
/**
|
|
494
|
-
* Union type for all supported messages in the protocol.
|
|
495
|
-
*
|
|
496
|
-
* @remarks
|
|
497
|
-
* Discriminated union of all message types. Use type narrowing with
|
|
498
|
-
* the `type` property to access specific message fields.
|
|
499
|
-
*
|
|
500
|
-
* @template T - Type of the data payload for DataMessage (default: unknown)
|
|
501
|
-
*
|
|
502
|
-
* @example
|
|
503
|
-
* ```ts
|
|
504
|
-
* function handleMessage(message: Message) {
|
|
505
|
-
* switch (message.type) {
|
|
506
|
-
* case MessageType.DATA:
|
|
507
|
-
* // message is DataMessage
|
|
508
|
-
* console.log('Data:', message.data)
|
|
509
|
-
* break
|
|
510
|
-
* case MessageType.SIGNAL:
|
|
511
|
-
* // message is SignalMessage
|
|
512
|
-
* console.log('Signal:', message.signal)
|
|
513
|
-
* break
|
|
514
|
-
* case MessageType.ERROR:
|
|
515
|
-
* // message is ErrorMessage
|
|
516
|
-
* console.error('Error:', message.data.message)
|
|
517
|
-
* break
|
|
518
|
-
* case MessageType.ACK:
|
|
519
|
-
* // message is AckMessage
|
|
520
|
-
* console.log('Ack for:', message.ackMessageId)
|
|
521
|
-
* break
|
|
522
|
-
* }
|
|
523
|
-
* }
|
|
524
|
-
* ```
|
|
525
|
-
*
|
|
526
|
-
* @example
|
|
527
|
-
* ### Type narrowing with DataMessage generic
|
|
528
|
-
* ```ts
|
|
529
|
-
* interface ChatMessage {
|
|
530
|
-
* text: string
|
|
531
|
-
* user: string
|
|
532
|
-
* }
|
|
533
|
-
*
|
|
534
|
-
* function isChatMessage(message: Message): message is DataMessage<ChatMessage> {
|
|
535
|
-
* return message.type === MessageType.DATA && message.channel === 'chat'
|
|
536
|
-
* }
|
|
537
|
-
*
|
|
538
|
-
* if (isChatMessage(message)) {
|
|
539
|
-
* console.log(`${message.data.user}: ${message.data.text}`)
|
|
540
|
-
* }
|
|
541
|
-
* ```
|
|
542
|
-
*/
|
|
543
|
-
type Message<T = unknown> = DataMessage<T> | SignalMessage | ErrorMessage | AckMessage;
|
|
544
|
-
/**
|
|
545
|
-
* Middleware action types
|
|
546
|
-
*
|
|
547
|
-
* @remarks
|
|
548
|
-
* Defines all actions that middleware can intercept and process.
|
|
549
|
-
*
|
|
550
|
-
* @example
|
|
551
|
-
* ```ts
|
|
552
|
-
* import type { IMiddlewareAction } from '@syncar/server'
|
|
553
|
-
*
|
|
554
|
-
* function handleAction(action: IMiddlewareAction) {
|
|
555
|
-
* switch (action) {
|
|
556
|
-
* case 'connect':
|
|
557
|
-
* console.log('Client connecting')
|
|
558
|
-
* break
|
|
559
|
-
* case 'disconnect':
|
|
560
|
-
* console.log('Client disconnecting')
|
|
561
|
-
* break
|
|
562
|
-
* case 'message':
|
|
563
|
-
* console.log('Message received')
|
|
564
|
-
* break
|
|
565
|
-
* case 'subscribe':
|
|
566
|
-
* console.log('Channel subscription')
|
|
567
|
-
* break
|
|
568
|
-
* case 'unsubscribe':
|
|
569
|
-
* console.log('Channel unsubscription')
|
|
570
|
-
* break
|
|
571
|
-
* }
|
|
572
|
-
* }
|
|
573
|
-
* ```
|
|
574
|
-
*/
|
|
575
|
-
type IMiddlewareAction = 'connect' | 'disconnect' | 'message' | 'subscribe' | 'unsubscribe';
|
|
576
|
-
/**
|
|
577
|
-
* Next function type for middleware chain continuation
|
|
578
|
-
*
|
|
579
|
-
* @remarks
|
|
580
|
-
* Function passed to middleware to continue execution to the next
|
|
581
|
-
* middleware in the chain.
|
|
582
|
-
*
|
|
583
|
-
* @example
|
|
584
|
-
* ```ts
|
|
585
|
-
* const middleware: Middleware = async (context, next) => {
|
|
586
|
-
* // Pre-processing
|
|
587
|
-
* console.log('Before next')
|
|
588
|
-
*
|
|
589
|
-
* // Continue to next middleware
|
|
590
|
-
* await next()
|
|
591
|
-
*
|
|
592
|
-
* // Post-processing
|
|
593
|
-
* console.log('After next')
|
|
594
|
-
* }
|
|
595
|
-
* ```
|
|
596
|
-
*/
|
|
597
|
-
type Next = () => Promise<void>;
|
|
598
|
-
/**
|
|
599
|
-
* Middleware context interface
|
|
600
|
-
*
|
|
601
|
-
* @remarks
|
|
602
|
-
* Provides middleware functions with access to request information,
|
|
603
|
-
* state management, and control flow methods. Inspired by Hono's context pattern.
|
|
604
|
-
*
|
|
605
|
-
* @template S - Type of the state object (default: Record<string, unknown>)
|
|
606
|
-
*
|
|
607
|
-
* @property req - Request information (client, message, channel, action)
|
|
608
|
-
* @property error - Optional error object
|
|
609
|
-
* @property finalized - Whether the context has been finalized
|
|
610
|
-
* @property res - Optional response data
|
|
611
|
-
* @property var - State object for storing custom data
|
|
612
|
-
* @property get - Get a value from state by key
|
|
613
|
-
* @property set - Set a value in state by key
|
|
614
|
-
* @property reject - Reject the action with a reason (throws)
|
|
615
|
-
*
|
|
616
|
-
* @example
|
|
617
|
-
* ### Basic usage
|
|
618
|
-
* ```ts
|
|
619
|
-
* const middleware: Middleware = async (context, next) => {
|
|
620
|
-
* console.log(`Action: ${context.req.action}`)
|
|
621
|
-
* console.log(`Client: ${context.req.client?.id}`)
|
|
622
|
-
* console.log(`Channel: ${context.req.channel}`)
|
|
623
|
-
*
|
|
624
|
-
* await next()
|
|
625
|
-
* }
|
|
626
|
-
* ```
|
|
627
|
-
*
|
|
628
|
-
* @example
|
|
629
|
-
* ### Using state
|
|
630
|
-
* ```ts
|
|
631
|
-
* interface MyState {
|
|
632
|
-
* user: { id: string; email: string }
|
|
633
|
-
* requestId: string
|
|
634
|
-
* }
|
|
635
|
-
*
|
|
636
|
-
* const middleware: Middleware<MyState> = async (context, next) => {
|
|
637
|
-
* // Set state
|
|
638
|
-
* context.set('requestId', generateId())
|
|
639
|
-
*
|
|
640
|
-
* // Get state
|
|
641
|
-
* const requestId = context.get('requestId')
|
|
642
|
-
*
|
|
643
|
-
* await next()
|
|
644
|
-
* }
|
|
645
|
-
* ```
|
|
646
|
-
*
|
|
647
|
-
* @example
|
|
648
|
-
* ### Rejecting actions
|
|
649
|
-
* ```ts
|
|
650
|
-
* const middleware: Middleware = async (context, next) => {
|
|
651
|
-
* if (context.req.action === 'connect') {
|
|
652
|
-
* // Check connection validity
|
|
653
|
-
* if (!isValidConnection(context.req.client)) {
|
|
654
|
-
* context.reject('Connection not allowed')
|
|
655
|
-
* // Function never returns (throws)
|
|
656
|
-
* }
|
|
657
|
-
* }
|
|
658
|
-
*
|
|
659
|
-
* await next()
|
|
660
|
-
* }
|
|
661
|
-
* ```
|
|
662
|
-
*/
|
|
663
|
-
interface Context<S = Record<string, unknown>> {
|
|
664
|
-
/** Request information */
|
|
665
|
-
readonly req: {
|
|
666
|
-
/** The client connection (if applicable) */
|
|
667
|
-
readonly client?: IClientConnection;
|
|
668
|
-
/** The message being processed (if applicable) */
|
|
669
|
-
readonly message?: Message;
|
|
670
|
-
/** The channel name (if applicable) */
|
|
671
|
-
readonly channel?: ChannelName;
|
|
672
|
-
/** The action being performed */
|
|
673
|
-
readonly action: IMiddlewareAction;
|
|
674
|
-
};
|
|
675
|
-
/** Optional error object */
|
|
676
|
-
error?: Error;
|
|
677
|
-
/** Whether the context has been finalized */
|
|
678
|
-
finalized: boolean;
|
|
679
|
-
/** Optional response data */
|
|
680
|
-
res?: unknown;
|
|
681
|
-
/** State object for storing custom data */
|
|
682
|
-
readonly var: S;
|
|
683
|
-
/** Get a value from state by key */
|
|
684
|
-
get<K extends keyof S>(key: K): S[K];
|
|
685
|
-
/** Set a value in state by key */
|
|
686
|
-
set<K extends keyof S>(key: K, value: S[K]): void;
|
|
687
|
-
/** Reject the action with a reason (throws) */
|
|
688
|
-
reject(reason: string): never;
|
|
689
|
-
}
|
|
690
|
-
/**
|
|
691
|
-
* Middleware function signature
|
|
692
|
-
*
|
|
693
|
-
* @remarks
|
|
694
|
-
* Type definition for middleware functions. Middleware can be sync or async
|
|
695
|
-
* and can return any value (though the return value is typically ignored).
|
|
696
|
-
*
|
|
697
|
-
* @template S - Type of the state object (default: Record<string, unknown>)
|
|
698
|
-
*
|
|
699
|
-
* @param c - The middleware context
|
|
700
|
-
* @param next - Function to continue to the next middleware
|
|
701
|
-
*
|
|
702
|
-
* @example
|
|
703
|
-
* ```ts
|
|
704
|
-
* import type { Middleware } from '@syncar/server'
|
|
705
|
-
*
|
|
706
|
-
* const authMiddleware: Middleware = async (context, next) => {
|
|
707
|
-
* const token = context.req.message?.data?.token
|
|
708
|
-
*
|
|
709
|
-
* if (!token) {
|
|
710
|
-
* context.reject('Authentication required')
|
|
711
|
-
* }
|
|
712
|
-
*
|
|
713
|
-
* const user = await verifyToken(token)
|
|
714
|
-
* context.set('user', user)
|
|
715
|
-
*
|
|
716
|
-
* await next()
|
|
717
|
-
* }
|
|
718
|
-
* ```
|
|
719
|
-
*
|
|
720
|
-
* @see {@link Context} for context interface
|
|
721
|
-
* @see {@link IMiddlewareAction} for available actions
|
|
722
|
-
*/
|
|
723
|
-
type Middleware<S = Record<string, unknown>> = (
|
|
724
|
-
/** The middleware context */
|
|
725
|
-
c: Context<S>,
|
|
726
|
-
/** Function to continue to the next middleware */
|
|
727
|
-
next: Next) => void | Promise<void> | unknown;
|
|
728
|
-
/**
|
|
729
|
-
* Alias for Middleware type
|
|
730
|
-
*
|
|
731
|
-
* @remarks
|
|
732
|
-
* Alias maintained for backward compatibility. Prefer using `Middleware` directly.
|
|
733
|
-
*
|
|
734
|
-
* @template S - Type of the state object (default: Record<string, unknown>)
|
|
735
|
-
*/
|
|
736
|
-
type IMiddleware<S = Record<string, unknown>> = Middleware<S>;
|
|
737
|
-
/**
|
|
738
|
-
* Middleware rejection error interface
|
|
739
|
-
*
|
|
740
|
-
* @remarks
|
|
741
|
-
* Interface for errors thrown when middleware rejects an action using
|
|
742
|
-
* `context.reject()`. This is a standard interface - actual errors should
|
|
743
|
-
* use the `MiddlewareRejectionError` class from the errors module.
|
|
744
|
-
*
|
|
745
|
-
* @property reason - Human-readable reason for rejection
|
|
746
|
-
* @property action - The action that was rejected
|
|
747
|
-
* @property name - Fixed value 'MiddlewareRejectionError' for interface compliance
|
|
748
|
-
*
|
|
749
|
-
* @example
|
|
750
|
-
* ```ts
|
|
751
|
-
* function isRejectionError(error: unknown): error is IMiddlewareRejectionError {
|
|
752
|
-
* return (
|
|
753
|
-
* typeof error === 'object' &&
|
|
754
|
-
* error !== null &&
|
|
755
|
-
* 'name' in error &&
|
|
756
|
-
* error.name === 'MiddlewareRejectionError'
|
|
757
|
-
* )
|
|
758
|
-
* }
|
|
759
|
-
*
|
|
760
|
-
* try {
|
|
761
|
-
* await someOperation()
|
|
762
|
-
* } catch (error) {
|
|
763
|
-
* if (isRejectionError(error)) {
|
|
764
|
-
* console.error(`Action '${error.action}' rejected: ${error.reason}`)
|
|
765
|
-
* }
|
|
766
|
-
* }
|
|
767
|
-
* ```
|
|
768
|
-
*/
|
|
769
|
-
interface IMiddlewareRejectionError {
|
|
770
|
-
/** Human-readable reason for rejection */
|
|
771
|
-
reason: string;
|
|
772
|
-
/** The action that was rejected */
|
|
773
|
-
action: string;
|
|
774
|
-
/** Fixed value 'MiddlewareRejectionError' for interface compliance */
|
|
775
|
-
name: 'MiddlewareRejectionError';
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
/**
|
|
779
|
-
* Client Registry
|
|
780
|
-
* Manages connected clients, their subscriptions, and channel instances.
|
|
781
|
-
*/
|
|
782
|
-
declare class ClientRegistry {
|
|
783
|
-
readonly connections: Map<ClientId, IClientConnection>;
|
|
784
|
-
private readonly subscriptions;
|
|
785
|
-
private readonly channels;
|
|
786
|
-
private readonly channelInstances;
|
|
787
|
-
readonly logger?: ILogger;
|
|
788
|
-
constructor(logger?: ILogger);
|
|
789
|
-
register(connection: IClientConnection): IClientConnection;
|
|
790
|
-
unregister(clientId: ClientId): boolean;
|
|
791
|
-
private clearSubscriptions;
|
|
792
|
-
get(clientId: ClientId): IClientConnection | undefined;
|
|
793
|
-
getAll(): IClientConnection[];
|
|
794
|
-
getCount(): number;
|
|
795
|
-
registerChannel(channel: BaseChannel<unknown>): void;
|
|
796
|
-
getChannel<T = unknown>(name: ChannelName): BaseChannel<T> | undefined;
|
|
797
|
-
removeChannel(name: ChannelName): boolean;
|
|
798
|
-
subscribe(clientId: ClientId, channel: ChannelName): boolean;
|
|
799
|
-
unsubscribe(clientId: ClientId, channel: ChannelName): boolean;
|
|
800
|
-
getSubscribers(channel: ChannelName): IClientConnection[];
|
|
801
|
-
getSubscriberCount(channel: ChannelName): number;
|
|
802
|
-
getChannels(): ChannelName[];
|
|
803
|
-
getTotalSubscriptionCount(): number;
|
|
804
|
-
isSubscribed(clientId: ClientId, channel: ChannelName): boolean;
|
|
805
|
-
getClientChannels(clientId: ClientId): Set<ChannelName>;
|
|
806
|
-
getChannelSubscribers(channel: ChannelName): Set<ClientId>;
|
|
807
|
-
clear(): void;
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
/**
|
|
811
|
-
* Config Module
|
|
812
|
-
* Single source of truth for all constant values and default configuration for the server.
|
|
813
|
-
*
|
|
814
|
-
* @module config
|
|
815
|
-
*/
|
|
816
|
-
/**
|
|
817
|
-
* Broadcast channel name
|
|
818
|
-
* Messages sent to this channel reach ALL connected clients
|
|
819
|
-
* No subscription required for broadcast channel
|
|
820
|
-
*/
|
|
821
|
-
declare const BROADCAST_CHANNEL: "__broadcast__";
|
|
822
|
-
/**
|
|
823
|
-
* WebSocket close codes
|
|
824
|
-
* Based on RFC 6455 and custom codes for application-specific closures
|
|
825
|
-
*/
|
|
826
|
-
declare const CLOSE_CODES: {
|
|
827
|
-
/**
|
|
828
|
-
* Normal closure
|
|
829
|
-
*/
|
|
830
|
-
readonly NORMAL: 1000;
|
|
831
|
-
/**
|
|
832
|
-
* Endpoint is going away
|
|
833
|
-
*/
|
|
834
|
-
readonly GOING_AWAY: 1001;
|
|
835
|
-
/**
|
|
836
|
-
* Protocol error
|
|
837
|
-
*/
|
|
838
|
-
readonly PROTOCOL_ERROR: 1002;
|
|
839
|
-
/**
|
|
840
|
-
* Unsupported data
|
|
841
|
-
*/
|
|
842
|
-
readonly UNSUPPORTED_DATA: 1003;
|
|
843
|
-
/**
|
|
844
|
-
* No status received
|
|
845
|
-
*/
|
|
846
|
-
readonly NO_STATUS: 1005;
|
|
847
|
-
/**
|
|
848
|
-
* Abnormal closure
|
|
849
|
-
*/
|
|
850
|
-
readonly ABNORMAL: 1006;
|
|
851
|
-
/**
|
|
852
|
-
* Invalid frame payload data
|
|
853
|
-
*/
|
|
854
|
-
readonly INVALID_PAYLOAD: 1007;
|
|
855
|
-
/**
|
|
856
|
-
* Policy violation
|
|
857
|
-
*/
|
|
858
|
-
readonly POLICY_VIOLATION: 1008;
|
|
859
|
-
/**
|
|
860
|
-
* Message too big
|
|
861
|
-
*/
|
|
862
|
-
readonly MESSAGE_TOO_BIG: 1009;
|
|
863
|
-
/**
|
|
864
|
-
* Missing extension
|
|
865
|
-
*/
|
|
866
|
-
readonly MISSING_EXTENSION: 1010;
|
|
867
|
-
/**
|
|
868
|
-
* Internal error
|
|
869
|
-
*/
|
|
870
|
-
readonly INTERNAL_ERROR: 1011;
|
|
871
|
-
/**
|
|
872
|
-
* Service restart
|
|
873
|
-
*/
|
|
874
|
-
readonly SERVICE_RESTART: 1012;
|
|
875
|
-
/**
|
|
876
|
-
* Try again later
|
|
877
|
-
*/
|
|
878
|
-
readonly TRY_AGAIN_LATER: 1013;
|
|
879
|
-
/**
|
|
880
|
-
* Connection rejected by middleware
|
|
881
|
-
*/
|
|
882
|
-
readonly REJECTED: 4001;
|
|
883
|
-
/**
|
|
884
|
-
* Rate limit exceeded
|
|
885
|
-
*/
|
|
886
|
-
readonly RATE_LIMITED: 4002;
|
|
887
|
-
/**
|
|
888
|
-
* Channel not found
|
|
889
|
-
*/
|
|
890
|
-
readonly CHANNEL_NOT_FOUND: 4003;
|
|
891
|
-
/**
|
|
892
|
-
* Unauthorized
|
|
893
|
-
*/
|
|
894
|
-
readonly UNAUTHORIZED: 4005;
|
|
895
|
-
};
|
|
896
|
-
/**
|
|
897
|
-
* Application error codes
|
|
898
|
-
* Used in error messages sent to clients
|
|
899
|
-
*/
|
|
900
|
-
declare const ERROR_CODES: {
|
|
901
|
-
/**
|
|
902
|
-
* Action rejected by middleware
|
|
903
|
-
*/
|
|
904
|
-
readonly REJECTED: "REJECTED";
|
|
905
|
-
/**
|
|
906
|
-
* Channel name missing from message
|
|
907
|
-
*/
|
|
908
|
-
readonly MISSING_CHANNEL: "MISSING_CHANNEL";
|
|
909
|
-
/**
|
|
910
|
-
* Subscribe action rejected
|
|
911
|
-
*/
|
|
912
|
-
readonly SUBSCRIBE_REJECTED: "SUBSCRIBE_REJECTED";
|
|
913
|
-
/**
|
|
914
|
-
* Unsubscribe action rejected
|
|
915
|
-
*/
|
|
916
|
-
readonly UNSUBSCRIBE_REJECTED: "UNSUBSCRIBE_REJECTED";
|
|
917
|
-
/**
|
|
918
|
-
* Rate limit exceeded
|
|
919
|
-
*/
|
|
920
|
-
readonly RATE_LIMITED: "RATE_LIMITED";
|
|
921
|
-
/**
|
|
922
|
-
* Authentication failed
|
|
923
|
-
*/
|
|
924
|
-
readonly AUTH_FAILED: "AUTH_FAILED";
|
|
925
|
-
/**
|
|
926
|
-
* Authorization failed
|
|
927
|
-
*/
|
|
928
|
-
readonly NOT_AUTHORIZED: "NOT_AUTHORIZED";
|
|
929
|
-
/**
|
|
930
|
-
* Channel not allowed
|
|
931
|
-
*/
|
|
932
|
-
readonly CHANNEL_NOT_ALLOWED: "CHANNEL_NOT_ALLOWED";
|
|
933
|
-
/**
|
|
934
|
-
* Invalid message format
|
|
935
|
-
*/
|
|
936
|
-
readonly INVALID_MESSAGE: "INVALID_MESSAGE";
|
|
937
|
-
/**
|
|
938
|
-
* Server error
|
|
939
|
-
*/
|
|
940
|
-
readonly SERVER_ERROR: "SERVER_ERROR";
|
|
941
|
-
};
|
|
6
|
+
import { ServerOptions, WebSocketServer } from 'ws';
|
|
942
7
|
|
|
943
8
|
/**
|
|
944
9
|
* Channel state information
|
|
@@ -951,16 +16,6 @@ declare const ERROR_CODES: {
|
|
|
951
16
|
* @property subscriberCount - Current number of subscribers
|
|
952
17
|
* @property createdAt - Unix timestamp (ms) when the channel was created
|
|
953
18
|
* @property lastMessageAt - Unix timestamp (ms) of the last published message
|
|
954
|
-
*
|
|
955
|
-
* @example
|
|
956
|
-
* ```ts
|
|
957
|
-
* const state: IChannelState = {
|
|
958
|
-
* name: 'chat',
|
|
959
|
-
* subscriberCount: 42,
|
|
960
|
-
* createdAt: 1699123456789,
|
|
961
|
-
* lastMessageAt: 1699123459999
|
|
962
|
-
* }
|
|
963
|
-
* ```
|
|
964
19
|
*/
|
|
965
20
|
interface IChannelState {
|
|
966
21
|
/** The channel name */
|
|
@@ -972,678 +27,162 @@ interface IChannelState {
|
|
|
972
27
|
/** Unix timestamp (ms) of the last published message */
|
|
973
28
|
lastMessageAt?: number;
|
|
974
29
|
}
|
|
30
|
+
type ChannelFlow = 'bidirectional' | 'send-only' | 'receive-only';
|
|
975
31
|
/**
|
|
976
|
-
*
|
|
977
|
-
*
|
|
978
|
-
* @remarks
|
|
979
|
-
* Controls which clients receive a published message through
|
|
980
|
-
* inclusion/exclusion filtering.
|
|
981
|
-
*
|
|
982
|
-
* @property to - Optional list of client IDs to receive the message (exclusive mode)
|
|
983
|
-
* @property exclude - Optional list of client IDs to exclude from receiving the message
|
|
984
|
-
*
|
|
985
|
-
* @example
|
|
986
|
-
* ```ts
|
|
987
|
-
* // Send to specific clients only
|
|
988
|
-
* channel.publish('Hello', { to: ['client-1', 'client-2'] })
|
|
989
|
-
*
|
|
990
|
-
* // Send to all except specific clients
|
|
991
|
-
* channel.publish('Hello', { exclude: ['client-123'] })
|
|
992
|
-
*
|
|
993
|
-
* // Both options can be combined (to takes precedence)
|
|
994
|
-
* channel.publish('Hello', { to: ['client-1'], exclude: ['client-2'] })
|
|
995
|
-
* ```
|
|
996
|
-
*/
|
|
997
|
-
interface IPublishOptions {
|
|
998
|
-
/**
|
|
999
|
-
* Optional list of client IDs to receive the message
|
|
1000
|
-
*
|
|
1001
|
-
* @remarks
|
|
1002
|
-
* When provided, only clients in this list will receive the message.
|
|
1003
|
-
* This creates an "exclusive mode" where `exclude` is still applied
|
|
1004
|
-
* as a secondary filter.
|
|
1005
|
-
*/
|
|
1006
|
-
to?: readonly ClientId[];
|
|
1007
|
-
/**
|
|
1008
|
-
* Optional list of client IDs to exclude from receiving the message
|
|
1009
|
-
*
|
|
1010
|
-
* @remarks
|
|
1011
|
-
* Clients in this list will not receive the message, even if they
|
|
1012
|
-
* are subscribed to the channel. Useful for excluding the sender.
|
|
1013
|
-
*/
|
|
1014
|
-
exclude?: readonly ClientId[];
|
|
1015
|
-
}
|
|
1016
|
-
/**
|
|
1017
|
-
* Base message handler signature
|
|
1018
|
-
*
|
|
1019
|
-
* @remarks
|
|
1020
|
-
* Type-safe handler function for processing incoming messages on a channel.
|
|
1021
|
-
* Handlers receive the message data, sending client, and the original message object.
|
|
1022
|
-
*
|
|
1023
|
-
* @template T - Type of data expected in messages
|
|
1024
|
-
*
|
|
1025
|
-
* @example
|
|
1026
|
-
* ```ts
|
|
1027
|
-
* const handler: IMessageHandler<{ text: string }> = (data, client, message) => {
|
|
1028
|
-
* console.log(`${client.id} sent: ${data.text}`)
|
|
1029
|
-
* console.log(`Message ID: ${message.id}`)
|
|
1030
|
-
* }
|
|
1031
|
-
*
|
|
1032
|
-
* channel.onMessage(handler)
|
|
1033
|
-
* ```
|
|
1034
|
-
*/
|
|
1035
|
-
type IMessageHandler<T> = (
|
|
1036
|
-
/** The message data payload */
|
|
1037
|
-
data: T,
|
|
1038
|
-
/** The client connection that sent the message */
|
|
1039
|
-
client: IClientConnection,
|
|
1040
|
-
/** The complete message object with metadata */
|
|
1041
|
-
message: DataMessage<T>) => void | Promise<void>;
|
|
1042
|
-
|
|
1043
|
-
/**
|
|
1044
|
-
* Base Channel implementation
|
|
1045
|
-
*
|
|
1046
|
-
* @remarks
|
|
1047
|
-
* Abstract base class for all channel types. Handles the complexities of
|
|
1048
|
-
* chunked publishing, client filtering, and message delivery. Provides
|
|
1049
|
-
* automatic chunking for large broadcasts to avoid event loop blocking.
|
|
1050
|
-
*
|
|
1051
|
-
* @template T - Type of data published on this channel (default: unknown)
|
|
1052
|
-
* @template N - Type of the channel name (default: ChannelName)
|
|
1053
|
-
*
|
|
1054
|
-
* @example
|
|
1055
|
-
* ```ts
|
|
1056
|
-
* // Extend BaseChannel for custom channel types
|
|
1057
|
-
* class CustomChannel<T> extends BaseChannel<T> {
|
|
1058
|
-
* get subscriberCount() { return 0 }
|
|
1059
|
-
* isEmpty() { return true }
|
|
1060
|
-
* getMiddlewares() { return [] }
|
|
1061
|
-
* protected getTargetClients() { return [] }
|
|
1062
|
-
* }
|
|
1063
|
-
* ```
|
|
1064
|
-
*
|
|
1065
|
-
* @see {@link BroadcastChannel} for channel that sends to all clients
|
|
1066
|
-
* @see {@link MulticastChannel} for channel that sends to subscribers only
|
|
1067
|
-
*/
|
|
1068
|
-
declare abstract class BaseChannel<T = unknown, N extends ChannelName = ChannelName> {
|
|
1069
|
-
/** The channel name */
|
|
1070
|
-
readonly name: N;
|
|
1071
|
-
/** The client registry for connection management */
|
|
1072
|
-
protected readonly registry: ClientRegistry;
|
|
1073
|
-
/** Number of clients to process per chunk for large broadcasts (default: 500) */
|
|
1074
|
-
protected readonly chunkSize: number;
|
|
1075
|
-
/**
|
|
1076
|
-
* Creates a new BaseChannel instance
|
|
1077
|
-
*
|
|
1078
|
-
* @param name - The channel name
|
|
1079
|
-
* @param registry - The client registry for connection management
|
|
1080
|
-
* @param chunkSize - Number of clients to process per chunk for large broadcasts (default: 500)
|
|
1081
|
-
*/
|
|
1082
|
-
constructor(
|
|
1083
|
-
/** The channel name */
|
|
1084
|
-
name: N,
|
|
1085
|
-
/** The client registry for connection management */
|
|
1086
|
-
registry: ClientRegistry,
|
|
1087
|
-
/** Number of clients to process per chunk for large broadcasts (default: 500) */
|
|
1088
|
-
chunkSize?: number);
|
|
1089
|
-
/**
|
|
1090
|
-
* Get the current subscriber count
|
|
1091
|
-
*
|
|
1092
|
-
* @remarks
|
|
1093
|
-
* Abstract method that must be implemented by subclasses.
|
|
1094
|
-
* Returns the number of clients currently receiving messages from this channel.
|
|
1095
|
-
*/
|
|
1096
|
-
abstract get subscriberCount(): number;
|
|
1097
|
-
/**
|
|
1098
|
-
* Check if the channel has no subscribers
|
|
1099
|
-
*
|
|
1100
|
-
* @remarks
|
|
1101
|
-
* Abstract method that must be implemented by subclasses.
|
|
1102
|
-
* Returns `true` if the channel has no subscribers.
|
|
1103
|
-
*/
|
|
1104
|
-
abstract isEmpty(): boolean;
|
|
1105
|
-
/**
|
|
1106
|
-
* Get the middleware for this channel
|
|
1107
|
-
*
|
|
1108
|
-
* @remarks
|
|
1109
|
-
* Abstract method that must be implemented by subclasses.
|
|
1110
|
-
* Returns an array of middleware functions applied to this channel.
|
|
1111
|
-
*/
|
|
1112
|
-
abstract getMiddlewares(): IMiddleware[];
|
|
1113
|
-
/**
|
|
1114
|
-
* Publish data to the channel
|
|
1115
|
-
*
|
|
1116
|
-
* @remarks
|
|
1117
|
-
* Sends the data to all target clients. If the number of clients exceeds
|
|
1118
|
-
* `chunkSize`, messages are sent in chunks using `setImmediate` to avoid
|
|
1119
|
-
* blocking the event loop.
|
|
1120
|
-
*
|
|
1121
|
-
* @param data - The data to publish
|
|
1122
|
-
* @param options - Optional publish options for client filtering
|
|
1123
|
-
*
|
|
1124
|
-
* @example
|
|
1125
|
-
* ```ts
|
|
1126
|
-
* // Publish to all clients
|
|
1127
|
-
* channel.publish('Hello everyone!')
|
|
1128
|
-
*
|
|
1129
|
-
* // Publish excluding specific clients
|
|
1130
|
-
* channel.publish('Hello', { exclude: ['client-123'] })
|
|
1131
|
-
*
|
|
1132
|
-
* // Publish to specific clients only
|
|
1133
|
-
* channel.publish('Private message', { to: ['client-1', 'client-2'] })
|
|
1134
|
-
* ```
|
|
1135
|
-
*/
|
|
1136
|
-
publish(data: T, options?: IPublishOptions): void;
|
|
1137
|
-
/**
|
|
1138
|
-
* Dispatch an incoming client message
|
|
1139
|
-
*
|
|
1140
|
-
* @remarks
|
|
1141
|
-
* Called when a client sends a message to this channel.
|
|
1142
|
-
* Override in subclasses to handle incoming messages.
|
|
1143
|
-
* Default implementation is a no-op (e.g., BroadcastChannel doesn't receive messages).
|
|
1144
|
-
*
|
|
1145
|
-
* @param data - The message data
|
|
1146
|
-
* @param client - The client that sent the message
|
|
1147
|
-
* @param message - The complete message object
|
|
1148
|
-
*/
|
|
1149
|
-
dispatch(_data: T, _client: IClientConnection, _message: DataMessage<T>): Promise<void>;
|
|
1150
|
-
protected abstract getTargetClients(options?: IPublishOptions): ClientId[];
|
|
1151
|
-
protected publishToClients(data: T, clientIds: ClientId[], options?: IPublishOptions): void;
|
|
1152
|
-
protected publishInChunks(data: T, clientIds: ClientId[], options?: IPublishOptions): void;
|
|
1153
|
-
}
|
|
1154
|
-
/**
|
|
1155
|
-
* Broadcast Channel - sends messages to ALL connected clients
|
|
1156
|
-
*
|
|
1157
|
-
* @remarks
|
|
1158
|
-
* A special channel that delivers messages to every connected client,
|
|
1159
|
-
* regardless of subscription. This is useful for server-wide announcements,
|
|
1160
|
-
* system notifications, and global events.
|
|
1161
|
-
*
|
|
1162
|
-
* The broadcast channel is a singleton with the reserved name `__broadcast__`.
|
|
1163
|
-
* It is automatically created when the server starts and can be accessed
|
|
1164
|
-
* via `server.createBroadcast()`.
|
|
1165
|
-
*
|
|
1166
|
-
* @template T - Type of data to be broadcast (default: unknown)
|
|
1167
|
-
*
|
|
1168
|
-
* @example
|
|
1169
|
-
* ### Basic broadcasting
|
|
1170
|
-
* ```ts
|
|
1171
|
-
* const broadcast = server.createBroadcast<string>()
|
|
1172
|
-
* broadcast.publish('Server maintenance in 5 minutes')
|
|
1173
|
-
* ```
|
|
1174
|
-
*
|
|
1175
|
-
* @example
|
|
1176
|
-
* ### Broadcasting objects
|
|
1177
|
-
* ```ts
|
|
1178
|
-
* const alerts = server.createBroadcast<{ type: string; message: string }>()
|
|
1179
|
-
* alerts.publish({ type: 'warning', message: 'High load detected' })
|
|
1180
|
-
* ```
|
|
1181
|
-
*
|
|
1182
|
-
* @example
|
|
1183
|
-
* ### Client filtering
|
|
1184
|
-
* ```ts
|
|
1185
|
-
* // Send to all except specific clients
|
|
1186
|
-
* broadcast.publish('Admin message', { exclude: ['client-123'] })
|
|
1187
|
-
*
|
|
1188
|
-
* // Send to specific clients only
|
|
1189
|
-
* broadcast.publish('Private message', { to: ['client-1', 'client-2'] })
|
|
1190
|
-
* ```
|
|
1191
|
-
*
|
|
1192
|
-
* @example
|
|
1193
|
-
* ### System announcements
|
|
1194
|
-
* ```ts
|
|
1195
|
-
* const broadcast = server.createBroadcast<string>()
|
|
1196
|
-
*
|
|
1197
|
-
* // Send periodic announcements
|
|
1198
|
-
* setInterval(() => {
|
|
1199
|
-
* const time = new Date().toLocaleTimeString()
|
|
1200
|
-
* broadcast.publish(`Server time: ${time}`)
|
|
1201
|
-
* }, 60000)
|
|
1202
|
-
* ```
|
|
1203
|
-
*
|
|
1204
|
-
* @see {@link BROADCAST_CHANNEL} for the reserved channel name constant
|
|
1205
|
-
* @see {@link MulticastChannel} for subscription-based channels
|
|
1206
|
-
*/
|
|
1207
|
-
declare class BroadcastChannel<T = unknown> extends BaseChannel<T, typeof BROADCAST_CHANNEL> {
|
|
1208
|
-
/**
|
|
1209
|
-
* Creates a new BroadcastChannel instance
|
|
1210
|
-
*
|
|
1211
|
-
* @remarks
|
|
1212
|
-
* BroadcastChannel is created automatically by the server and typically
|
|
1213
|
-
* accessed via `server.createBroadcast()` rather than instantiated directly.
|
|
1214
|
-
*
|
|
1215
|
-
* @param registry - The client registry for connection management
|
|
1216
|
-
* @param chunkSize - Number of clients to process per chunk for large broadcasts (default: 500)
|
|
1217
|
-
*/
|
|
1218
|
-
constructor(registry: ClientRegistry, chunkSize?: number);
|
|
1219
|
-
/**
|
|
1220
|
-
* Get all connected clients as target recipients
|
|
1221
|
-
*
|
|
1222
|
-
* @remarks
|
|
1223
|
-
* Broadcast channel targets ALL connected clients. The `options` parameter
|
|
1224
|
-
* is still applied after this method returns for filtering.
|
|
1225
|
-
*
|
|
1226
|
-
* @param _options - Publish options (ignored for broadcast target selection)
|
|
1227
|
-
* @returns Array of all connected client IDs
|
|
1228
|
-
*
|
|
1229
|
-
* @internal
|
|
1230
|
-
*/
|
|
1231
|
-
protected getTargetClients(_options?: IPublishOptions): ClientId[];
|
|
1232
|
-
/**
|
|
1233
|
-
* Get the number of connected clients
|
|
1234
|
-
*
|
|
1235
|
-
* @returns The total number of connected clients
|
|
1236
|
-
*
|
|
1237
|
-
* @example
|
|
1238
|
-
* ```ts
|
|
1239
|
-
* const broadcast = server.createBroadcast<string>()
|
|
1240
|
-
* console.log(`Connected clients: ${broadcast.subscriberCount}`)
|
|
1241
|
-
* ```
|
|
1242
|
-
*/
|
|
1243
|
-
get subscriberCount(): number;
|
|
1244
|
-
/**
|
|
1245
|
-
* Check if there are no connected clients
|
|
1246
|
-
*
|
|
1247
|
-
* @returns `true` if no clients are connected, `false` otherwise
|
|
1248
|
-
*
|
|
1249
|
-
* @example
|
|
1250
|
-
* ```ts
|
|
1251
|
-
* const broadcast = server.createBroadcast<string>()
|
|
1252
|
-
* if (broadcast.isEmpty()) {
|
|
1253
|
-
* console.log('No clients connected')
|
|
1254
|
-
* }
|
|
1255
|
-
* ```
|
|
1256
|
-
*/
|
|
1257
|
-
isEmpty(): boolean;
|
|
1258
|
-
/**
|
|
1259
|
-
* Get the middleware for this channel
|
|
1260
|
-
*
|
|
1261
|
-
* @remarks
|
|
1262
|
-
* Broadcast channel has no middleware by default. Returns an empty array.
|
|
1263
|
-
*
|
|
1264
|
-
* @returns Empty array (broadcast channels don't have middleware)
|
|
1265
|
-
*/
|
|
1266
|
-
getMiddlewares(): IMiddleware[];
|
|
1267
|
-
}
|
|
1268
|
-
/**
|
|
1269
|
-
* Multicast channel options
|
|
1270
|
-
*
|
|
1271
|
-
* @remarks
|
|
1272
|
-
* Configuration options for creating a multicast channel.
|
|
1273
|
-
*
|
|
1274
|
-
* @property chunkSize - Number of clients to process per chunk for large broadcasts
|
|
32
|
+
* Channel creation options
|
|
1275
33
|
*
|
|
1276
34
|
* @example
|
|
1277
35
|
* ```ts
|
|
1278
|
-
* const
|
|
36
|
+
* const options: ChannelOptions = { flow: 'receive-only' }
|
|
1279
37
|
* ```
|
|
1280
38
|
*/
|
|
1281
|
-
interface
|
|
1282
|
-
/**
|
|
1283
|
-
|
|
1284
|
-
*
|
|
1285
|
-
* @remarks
|
|
1286
|
-
* When broadcasting to more than this many subscribers, messages are sent
|
|
1287
|
-
* in chunks to avoid blocking the event loop.
|
|
1288
|
-
*
|
|
1289
|
-
* @default 500
|
|
1290
|
-
*/
|
|
1291
|
-
chunkSize?: number;
|
|
39
|
+
interface ChannelOptions {
|
|
40
|
+
/** Message direction: 'bidirectional', 'send-only', or 'receive-only' */
|
|
41
|
+
flow?: ChannelFlow;
|
|
1292
42
|
}
|
|
1293
43
|
/**
|
|
1294
|
-
*
|
|
1295
|
-
*
|
|
1296
|
-
* @remarks
|
|
1297
|
-
* A topic-based channel that delivers messages only to clients that have
|
|
1298
|
-
* explicitly subscribed. This is the standard channel type for implementing
|
|
1299
|
-
* chat rooms, notifications, and other subscription-based messaging patterns.
|
|
1300
|
-
*
|
|
1301
|
-
* Key features:
|
|
1302
|
-
* - Clients must subscribe to receive messages
|
|
1303
|
-
* - Supports message handlers for intercepting incoming messages
|
|
1304
|
-
* - Auto-relays messages to all subscribers (excluding sender) when no handler is set
|
|
1305
|
-
* - Supports per-channel middleware
|
|
1306
|
-
*
|
|
1307
|
-
* @template T - Type of data published on this channel (default: unknown)
|
|
1308
|
-
*
|
|
1309
|
-
* @example
|
|
1310
|
-
* ### Creating a channel
|
|
1311
|
-
* ```ts
|
|
1312
|
-
* const chat = server.createMulticast<{ text: string; user: string }>('chat')
|
|
1313
|
-
* ```
|
|
1314
|
-
*
|
|
1315
|
-
* @example
|
|
1316
|
-
* ### Publishing messages
|
|
1317
|
-
* ```ts
|
|
1318
|
-
* // Publish to all subscribers
|
|
1319
|
-
* chat.publish({ text: 'Hello everyone!', user: 'Alice' })
|
|
1320
|
-
*
|
|
1321
|
-
* // Publish excluding sender
|
|
1322
|
-
* chat.publish({ text: 'Welcome!', user: 'System' }, { exclude: ['client-123'] })
|
|
1323
|
-
*
|
|
1324
|
-
* // Publish to specific subscribers only
|
|
1325
|
-
* chat.publish({ text: 'Private message', user: 'Bob' }, { to: ['client-1'] })
|
|
1326
|
-
* ```
|
|
1327
|
-
*
|
|
1328
|
-
* @example
|
|
1329
|
-
* ### Handling incoming messages with auto-relay
|
|
1330
|
-
* ```ts
|
|
1331
|
-
* // When no handler is set, messages are auto-relayed to all subscribers (excluding sender)
|
|
1332
|
-
* // This is the default behavior for simple chat rooms
|
|
1333
|
-
* ```
|
|
1334
|
-
*
|
|
1335
|
-
* @example
|
|
1336
|
-
* ### Handling incoming messages with custom handler
|
|
1337
|
-
* ```ts
|
|
1338
|
-
* chat.onMessage((data, client) => {
|
|
1339
|
-
* console.log(`${data.user} (${client.id}): ${data.text}`)
|
|
1340
|
-
*
|
|
1341
|
-
* // Apply custom logic (filtering, transformation, persistence, etc.)
|
|
1342
|
-
* if (isProfane(data.text)) {
|
|
1343
|
-
* return // Don't relay
|
|
1344
|
-
* }
|
|
1345
|
-
*
|
|
1346
|
-
* // Manually publish to subscribers
|
|
1347
|
-
* chat.publish(data, { exclude: [client.id] })
|
|
1348
|
-
* })
|
|
1349
|
-
* ```
|
|
1350
|
-
*
|
|
1351
|
-
* @example
|
|
1352
|
-
* ### Managing subscriptions
|
|
1353
|
-
* ```ts
|
|
1354
|
-
* // Subscribe a client
|
|
1355
|
-
* chat.subscribe('client-123')
|
|
1356
|
-
*
|
|
1357
|
-
* // Unsubscribe a client
|
|
1358
|
-
* chat.unsubscribe('client-123')
|
|
44
|
+
* Base message handler signature
|
|
1359
45
|
*
|
|
1360
|
-
*
|
|
1361
|
-
*
|
|
1362
|
-
*
|
|
1363
|
-
* }
|
|
46
|
+
* @remarks
|
|
47
|
+
* Type-safe handler function for processing incoming messages on a channel.
|
|
48
|
+
* Handlers receive the message data, sending client, and the original message object.
|
|
1364
49
|
*
|
|
1365
|
-
*
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
50
|
+
* @template T - Type of data expected in messages
|
|
51
|
+
*/
|
|
52
|
+
type IMessageHandler<T> = (
|
|
53
|
+
/** The message data payload */
|
|
54
|
+
data: T,
|
|
55
|
+
/** The client connection that sent the message */
|
|
56
|
+
client: IClientConnection,
|
|
57
|
+
/** The complete message object with metadata */
|
|
58
|
+
message: DataMessage<T>) => void | Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Unified Channel implementation
|
|
1369
61
|
*
|
|
1370
62
|
* @example
|
|
1371
|
-
* ### Channel middleware
|
|
1372
63
|
* ```ts
|
|
1373
|
-
* chat.
|
|
1374
|
-
*
|
|
1375
|
-
*
|
|
1376
|
-
* const data = context.req.message?.data as { text: string }
|
|
1377
|
-
* if (data?.text && isProfane(data.text)) {
|
|
1378
|
-
* context.reject('Profanity is not allowed')
|
|
1379
|
-
* }
|
|
1380
|
-
* }
|
|
1381
|
-
* await next()
|
|
64
|
+
* const chat = server.createChannel('chat')
|
|
65
|
+
* chat.onMessage((data, client) => {
|
|
66
|
+
* chat.publish(data, [client.id])
|
|
1382
67
|
* })
|
|
1383
68
|
* ```
|
|
1384
|
-
*
|
|
1385
|
-
* @see {@link BroadcastChannel} for broadcasting to all clients
|
|
1386
69
|
*/
|
|
1387
|
-
declare class
|
|
70
|
+
declare class Channel<T = unknown> {
|
|
71
|
+
/** The channel name */
|
|
72
|
+
readonly name: ChannelName;
|
|
73
|
+
/** The channel flow: 'bidirectional', 'send-only', or 'receive-only' */
|
|
74
|
+
readonly flow: ChannelFlow;
|
|
75
|
+
/** @internal */
|
|
76
|
+
protected readonly registry: ClientRegistry;
|
|
77
|
+
/** @internal */
|
|
78
|
+
protected readonly chunkSize: number;
|
|
1388
79
|
private readonly middlewares;
|
|
1389
80
|
private readonly messageHandlers;
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
* @remarks
|
|
1394
|
-
* MulticastChannel is typically created via `server.createMulticast()`
|
|
1395
|
-
* rather than instantiated directly.
|
|
1396
|
-
*
|
|
1397
|
-
* @param config.name - The channel name (must not start with `__`)
|
|
1398
|
-
* @param config.registry - The client registry for connection management
|
|
1399
|
-
* @param config.options - Optional channel configuration
|
|
1400
|
-
*
|
|
1401
|
-
* @throws {ValidationError} If the channel name is invalid (starts with `__`)
|
|
1402
|
-
*/
|
|
81
|
+
private _lastMessageAt?;
|
|
82
|
+
private _createdAt;
|
|
83
|
+
/** @internal */
|
|
1403
84
|
constructor(config: {
|
|
1404
|
-
/** The channel name (must not start with `__`) */
|
|
1405
85
|
name: ChannelName;
|
|
1406
|
-
/** The client registry for connection management */
|
|
1407
86
|
registry: ClientRegistry;
|
|
1408
|
-
|
|
1409
|
-
|
|
87
|
+
options?: ChannelOptions;
|
|
88
|
+
chunkSize?: number;
|
|
1410
89
|
});
|
|
1411
90
|
/**
|
|
1412
|
-
*
|
|
1413
|
-
*
|
|
1414
|
-
* @
|
|
1415
|
-
* Only clients that have subscribed to this channel will receive messages.
|
|
1416
|
-
* The `options` parameter is still applied after this method returns for filtering.
|
|
1417
|
-
*
|
|
1418
|
-
* @param _options - Publish options (ignored for multicast target selection)
|
|
1419
|
-
* @returns Array of subscribed client IDs
|
|
1420
|
-
*
|
|
1421
|
-
* @internal
|
|
91
|
+
* Subscribe a client to this channel
|
|
92
|
+
* @param subscriber - Client ID to subscribe
|
|
93
|
+
* @returns `true` if subscribed successfully
|
|
1422
94
|
*/
|
|
1423
|
-
|
|
95
|
+
subscribe(subscriber: ClientId): boolean;
|
|
1424
96
|
/**
|
|
1425
|
-
*
|
|
1426
|
-
*
|
|
1427
|
-
* @
|
|
1428
|
-
* Adds a middleware function that runs for all actions on this channel,
|
|
1429
|
-
* after global middleware. Channel middleware is useful for channel-specific
|
|
1430
|
-
* validation, filtering, or enrichment.
|
|
1431
|
-
*
|
|
1432
|
-
* @param middleware - The middleware function to register
|
|
1433
|
-
*
|
|
1434
|
-
* @example
|
|
1435
|
-
* ```ts
|
|
1436
|
-
* chat.use(async (context, next) => {
|
|
1437
|
-
* if (context.req.action === 'message') {
|
|
1438
|
-
* const data = context.req.message?.data as { text: string }
|
|
1439
|
-
* if (data?.text && isProfane(data.text)) {
|
|
1440
|
-
* context.reject('Profanity is not allowed')
|
|
1441
|
-
* }
|
|
1442
|
-
* }
|
|
1443
|
-
* await next()
|
|
1444
|
-
* })
|
|
1445
|
-
* ```
|
|
1446
|
-
*
|
|
1447
|
-
* @see {@link IMiddleware} for middleware interface
|
|
97
|
+
* Unsubscribe a client from this channel
|
|
98
|
+
* @param subscriber - Client ID to unsubscribe
|
|
99
|
+
* @returns `true` if unsubscribed successfully
|
|
1448
100
|
*/
|
|
1449
|
-
|
|
101
|
+
unsubscribe(subscriber: ClientId): boolean;
|
|
1450
102
|
/**
|
|
1451
|
-
*
|
|
1452
|
-
*
|
|
1453
|
-
* @returns Array of middleware functions registered on this channel
|
|
103
|
+
* Check if a client is subscribed to this channel
|
|
104
|
+
* @param subscriber - The client ID to check
|
|
1454
105
|
*/
|
|
1455
|
-
|
|
106
|
+
hasSubscriber(subscriber: ClientId): boolean;
|
|
1456
107
|
/**
|
|
1457
|
-
* Get
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
*
|
|
1462
|
-
* ```ts
|
|
1463
|
-
* console.log(`Chat subscribers: ${chat.subscriberCount}`)
|
|
1464
|
-
* ```
|
|
108
|
+
* Get all subscriber IDs for this channel
|
|
109
|
+
*/
|
|
110
|
+
getSubscribers(): Set<ClientId>;
|
|
111
|
+
/**
|
|
112
|
+
* Get the current count of subscribers
|
|
1465
113
|
*/
|
|
1466
114
|
get subscriberCount(): number;
|
|
1467
115
|
/**
|
|
1468
|
-
*
|
|
1469
|
-
*
|
|
1470
|
-
* @remarks
|
|
1471
|
-
* Registers a handler that intercepts incoming messages to this channel.
|
|
1472
|
-
* When handlers are registered, they receive full control over message
|
|
1473
|
-
* processing - auto-relay is disabled.
|
|
1474
|
-
*
|
|
1475
|
-
* @param handler - The message handler function
|
|
1476
|
-
* @returns Unsubscribe function that removes the handler when called
|
|
1477
|
-
*
|
|
1478
|
-
* @example
|
|
1479
|
-
* ```ts
|
|
1480
|
-
* const unsubscribe = chat.onMessage((data, client) => {
|
|
1481
|
-
* console.log(`${client.id}: ${data.text}`)
|
|
1482
|
-
* // Custom processing logic
|
|
1483
|
-
* })
|
|
1484
|
-
*
|
|
1485
|
-
* // Later, remove the handler
|
|
1486
|
-
* unsubscribe()
|
|
1487
|
-
* ```
|
|
1488
|
-
*
|
|
1489
|
-
* @remarks
|
|
1490
|
-
* Multiple handlers can be registered. They will be executed in the order
|
|
1491
|
-
* they were registered. If any handler throws an error, it will be logged
|
|
1492
|
-
* but other handlers will still execute.
|
|
116
|
+
* Check if the channel has no subscribers
|
|
1493
117
|
*/
|
|
1494
|
-
|
|
118
|
+
isEmpty(): boolean;
|
|
1495
119
|
/**
|
|
1496
|
-
*
|
|
1497
|
-
*
|
|
1498
|
-
* @
|
|
1499
|
-
* Subscribes a client to receive messages from this channel.
|
|
1500
|
-
* This is typically called automatically when a client sends a
|
|
1501
|
-
* subscribe signal, but can also be called manually.
|
|
1502
|
-
*
|
|
1503
|
-
* @param subscriber - The subscriber (client) ID to subscribe
|
|
1504
|
-
* @returns `true` if subscription was successful, `false` if already subscribed
|
|
1505
|
-
*
|
|
1506
|
-
* @example
|
|
1507
|
-
* ```ts
|
|
1508
|
-
* chat.subscribe('client-123')
|
|
1509
|
-
*
|
|
1510
|
-
* if (chat.subscribe('client-456')) {
|
|
1511
|
-
* console.log('Subscribed successfully')
|
|
1512
|
-
* }
|
|
1513
|
-
* ```
|
|
120
|
+
* Publish data to all subscribers
|
|
121
|
+
* @param data - The data to publish
|
|
122
|
+
* @param exclude - Optional array of client IDs to exclude
|
|
1514
123
|
*/
|
|
1515
|
-
|
|
124
|
+
publish(data: T, exclude?: ClientId[]): void;
|
|
1516
125
|
/**
|
|
1517
|
-
*
|
|
1518
|
-
*
|
|
1519
|
-
* @remarks
|
|
1520
|
-
* Removes a client's subscription from this channel.
|
|
1521
|
-
* This is typically called automatically when a client sends an
|
|
1522
|
-
* unsubscribe signal or disconnects, but can also be called manually.
|
|
1523
|
-
*
|
|
1524
|
-
* @param subscriber - The subscriber (client) ID to unsubscribe
|
|
1525
|
-
* @returns `true` if unsubscription was successful, `false` if not subscribed
|
|
1526
|
-
*
|
|
1527
|
-
* @example
|
|
1528
|
-
* ```ts
|
|
1529
|
-
* chat.unsubscribe('client-123')
|
|
126
|
+
* Register a message handler for incoming client data
|
|
1530
127
|
*
|
|
1531
|
-
*
|
|
1532
|
-
* console.log('Unsubscribed successfully')
|
|
1533
|
-
* }
|
|
1534
|
-
* ```
|
|
128
|
+
* @returns Function to remove the handler
|
|
1535
129
|
*/
|
|
1536
|
-
|
|
130
|
+
onMessage(handler: IMessageHandler<T>): () => void;
|
|
1537
131
|
/**
|
|
1538
132
|
* Dispatch an incoming client message
|
|
1539
133
|
*
|
|
1540
|
-
* @remarks
|
|
1541
|
-
* Processes incoming messages from clients. The behavior depends on
|
|
1542
|
-
* whether message handlers are registered:
|
|
1543
|
-
*
|
|
1544
|
-
* - **With handlers**: All registered handlers are executed with full
|
|
1545
|
-
* control over the message. No auto-relay occurs.
|
|
1546
|
-
*
|
|
1547
|
-
* - **Without handlers**: The message is automatically relayed to all
|
|
1548
|
-
* subscribers except the sender.
|
|
1549
|
-
*
|
|
1550
|
-
* @param data - The message data payload
|
|
1551
|
-
* @param client - The client that sent the message
|
|
1552
|
-
* @param message - The complete message object
|
|
1553
|
-
*
|
|
1554
|
-
* @example
|
|
1555
|
-
* ### Auto-relay mode (no handler)
|
|
1556
|
-
* ```ts
|
|
1557
|
-
* // Client sends message → automatically relayed to all subscribers
|
|
1558
|
-
* // No need to call publish()
|
|
1559
|
-
* ```
|
|
1560
|
-
*
|
|
1561
|
-
* @example
|
|
1562
|
-
* ### Intercept mode (with handler)
|
|
1563
|
-
* ```ts
|
|
1564
|
-
* chat.onMessage((data, client) => {
|
|
1565
|
-
* // Full control - implement custom logic
|
|
1566
|
-
* if (isValidMessage(data)) {
|
|
1567
|
-
* chat.publish(data, { exclude: [client.id] })
|
|
1568
|
-
* }
|
|
1569
|
-
* })
|
|
1570
|
-
* ```
|
|
1571
|
-
*
|
|
1572
134
|
* @internal
|
|
1573
135
|
*/
|
|
1574
136
|
dispatch(data: T, client: IClientConnection, message: DataMessage<T>): Promise<void>;
|
|
1575
137
|
/**
|
|
1576
|
-
*
|
|
1577
|
-
*
|
|
1578
|
-
* @param subscriber - The subscriber (client) ID to check
|
|
1579
|
-
* @returns `true` if the client is subscribed, `false` otherwise
|
|
1580
|
-
*
|
|
1581
|
-
* @example
|
|
1582
|
-
* ```ts
|
|
1583
|
-
* if (chat.hasSubscriber('client-123')) {
|
|
1584
|
-
* console.log('Client is subscribed to chat')
|
|
1585
|
-
* }
|
|
1586
|
-
* ```
|
|
138
|
+
* Register channel-specific middleware
|
|
1587
139
|
*/
|
|
1588
|
-
|
|
140
|
+
use(middleware: IMiddleware): void;
|
|
1589
141
|
/**
|
|
1590
|
-
* Get
|
|
1591
|
-
*
|
|
1592
|
-
* @remarks
|
|
1593
|
-
* Returns a copy of the subscribers set to prevent external modification.
|
|
1594
|
-
* The returned set is a snapshot and may not reflect future changes.
|
|
1595
|
-
*
|
|
1596
|
-
* @returns A Set of subscriber IDs
|
|
1597
|
-
*
|
|
1598
|
-
* @example
|
|
1599
|
-
* ```ts
|
|
1600
|
-
* const subscribers = chat.getSubscribers()
|
|
1601
|
-
* console.log(`Subscribers: ${Array.from(subscribers).join(', ')}`)
|
|
1602
|
-
*
|
|
1603
|
-
* // Check if a specific client is subscribed
|
|
1604
|
-
* if (subscribers.has('client-123')) {
|
|
1605
|
-
* console.log('Client 123 is subscribed')
|
|
1606
|
-
* }
|
|
1607
|
-
* ```
|
|
142
|
+
* Get registered middleware for this channel
|
|
1608
143
|
*/
|
|
1609
|
-
|
|
144
|
+
getMiddlewares(): IMiddleware[];
|
|
1610
145
|
/**
|
|
1611
|
-
*
|
|
1612
|
-
*
|
|
1613
|
-
* @returns `true` if no clients are subscribed, `false` otherwise
|
|
1614
|
-
*
|
|
1615
|
-
* @example
|
|
1616
|
-
* ```ts
|
|
1617
|
-
* if (chat.isEmpty()) {
|
|
1618
|
-
* console.log('No one is in the chat')
|
|
1619
|
-
* }
|
|
1620
|
-
* ```
|
|
146
|
+
* Get runtime channel statistics
|
|
1621
147
|
*/
|
|
1622
|
-
|
|
148
|
+
getState(): IChannelState;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Client Registry
|
|
153
|
+
* Manages connected clients, their subscriptions, and channel instances.
|
|
154
|
+
*/
|
|
155
|
+
declare class ClientRegistry {
|
|
156
|
+
readonly connections: Map<ClientId, IClientConnection>;
|
|
157
|
+
private readonly subscriptions;
|
|
158
|
+
private readonly channels;
|
|
159
|
+
private readonly channelInstances;
|
|
160
|
+
readonly logger?: ILogger;
|
|
161
|
+
constructor(logger?: ILogger);
|
|
162
|
+
register(connection: IClientConnection): IClientConnection;
|
|
163
|
+
unregister(clientId: ClientId): boolean;
|
|
164
|
+
private clearSubscriptions;
|
|
165
|
+
get(clientId: ClientId): IClientConnection | undefined;
|
|
166
|
+
getAll(): IClientConnection[];
|
|
167
|
+
getCount(): number;
|
|
168
|
+
registerChannel(channel: Channel<any>): void;
|
|
169
|
+
getChannel<T = unknown>(name: ChannelName): Channel<T> | undefined;
|
|
170
|
+
removeChannel(name: ChannelName): boolean;
|
|
171
|
+
subscribe(clientId: ClientId, channel: ChannelName): boolean;
|
|
172
|
+
unsubscribe(clientId: ClientId, channel: ChannelName): boolean;
|
|
173
|
+
getSubscribers(channel: ChannelName): IClientConnection[];
|
|
174
|
+
getSubscriberCount(channel: ChannelName): number;
|
|
175
|
+
getChannels(): ChannelName[];
|
|
176
|
+
getTotalSubscriptionCount(): number;
|
|
177
|
+
isSubscribed(clientId: ClientId, channel: ChannelName): boolean;
|
|
178
|
+
getClientChannels(clientId: ClientId): Set<ChannelName>;
|
|
179
|
+
getChannelSubscribers(channel: ChannelName): Set<ClientId>;
|
|
180
|
+
clear(): void;
|
|
1623
181
|
}
|
|
1624
182
|
|
|
1625
183
|
type ServerInstance = WebSocketServer;
|
|
1626
184
|
/**
|
|
1627
185
|
* WebSocket Server Transport Configuration
|
|
1628
|
-
*
|
|
1629
|
-
* @remarks
|
|
1630
|
-
* Configuration options for the WebSocket transport layer. Extends the
|
|
1631
|
-
* standard `ws` library options with Syncar-specific settings.
|
|
1632
|
-
*
|
|
1633
|
-
* @example
|
|
1634
|
-
* ```ts
|
|
1635
|
-
* const config: WebSocketServerTransportConfig = {
|
|
1636
|
-
* server: httpServer,
|
|
1637
|
-
* path: '/ws',
|
|
1638
|
-
* enablePing: true,
|
|
1639
|
-
* pingInterval: 30000,
|
|
1640
|
-
* pingTimeout: 5000,
|
|
1641
|
-
* maxPayload: 1048576,
|
|
1642
|
-
* connections: new Map(),
|
|
1643
|
-
* generateId: (request) => extractUserId(request),
|
|
1644
|
-
* logger: console
|
|
1645
|
-
* }
|
|
1646
|
-
* ```
|
|
1647
186
|
*/
|
|
1648
187
|
interface WebSocketServerTransportConfig extends ServerOptions {
|
|
1649
188
|
/**
|
|
@@ -1682,22 +221,6 @@ interface WebSocketServerTransportConfig extends ServerOptions {
|
|
|
1682
221
|
* If not provided, a new map will be created.
|
|
1683
222
|
*/
|
|
1684
223
|
connections?: Map<ClientId, IClientConnection>;
|
|
1685
|
-
/**
|
|
1686
|
-
* Custom ID generator for new connections
|
|
1687
|
-
*
|
|
1688
|
-
* @remarks
|
|
1689
|
-
* Function to generate unique client IDs from incoming HTTP requests.
|
|
1690
|
-
* Useful for implementing custom authentication or ID generation strategies.
|
|
1691
|
-
*
|
|
1692
|
-
* @example
|
|
1693
|
-
* ```ts
|
|
1694
|
-
* generateId: async (request) => {
|
|
1695
|
-
* const token = request.headers.authorization?.split(' ')[1]
|
|
1696
|
-
* return verifyToken(token).then(user => user.id)
|
|
1697
|
-
* }
|
|
1698
|
-
* ```
|
|
1699
|
-
*/
|
|
1700
|
-
generateId?: IdGenerator;
|
|
1701
224
|
/**
|
|
1702
225
|
* Custom WebSocket Server constructor
|
|
1703
226
|
*
|
|
@@ -1706,84 +229,15 @@ interface WebSocketServerTransportConfig extends ServerOptions {
|
|
|
1706
229
|
* Defaults to the standard `ws` WebSocketServer.
|
|
1707
230
|
*/
|
|
1708
231
|
ServerConstructor?: new (config: ServerOptions) => ServerInstance;
|
|
1709
|
-
/**
|
|
1710
|
-
* Logger instance
|
|
1711
|
-
*
|
|
1712
|
-
* @remarks
|
|
1713
|
-
* Optional logger for transport-level logging.
|
|
1714
|
-
*/
|
|
1715
|
-
logger?: ILogger;
|
|
1716
232
|
}
|
|
1717
233
|
/**
|
|
1718
234
|
* WebSocket Server Transport
|
|
1719
235
|
*
|
|
1720
|
-
* @remarks
|
|
1721
|
-
* Handles low-level WebSocket communication using the `ws` library.
|
|
1722
|
-
* Manages connections, message parsing, ping/pong keep-alive, and
|
|
1723
|
-
* emits high-level events for the server to consume.
|
|
1724
|
-
*
|
|
1725
|
-
* This transport:
|
|
1726
|
-
* - Wraps the `ws` WebSocketServer
|
|
1727
|
-
* - Generates unique client IDs
|
|
1728
|
-
* - Handles connection lifecycle (connect, disconnect, error)
|
|
1729
|
-
* - Parses incoming messages as JSON
|
|
1730
|
-
* - Manages ping/pong for connection health
|
|
1731
|
-
* - Emits typed events for server consumption
|
|
1732
|
-
*
|
|
1733
|
-
* @example
|
|
1734
|
-
* ### Basic usage
|
|
1735
|
-
* ```ts
|
|
1736
|
-
* import { WebSocketServerTransport } from '@syncar/server'
|
|
1737
|
-
*
|
|
1738
|
-
* const transport = new WebSocketServerTransport({
|
|
1739
|
-
* server: httpServer,
|
|
1740
|
-
* path: '/ws',
|
|
1741
|
-
* enablePing: true,
|
|
1742
|
-
* pingInterval: 30000,
|
|
1743
|
-
* pingTimeout: 5000
|
|
1744
|
-
* })
|
|
1745
|
-
*
|
|
1746
|
-
* transport.on('connection', (client) => {
|
|
1747
|
-
* console.log(`Client connected: ${client.id}`)
|
|
1748
|
-
* })
|
|
1749
|
-
*
|
|
1750
|
-
* transport.on('message', (clientId, message) => {
|
|
1751
|
-
* console.log(`Message from ${clientId}:`, message)
|
|
1752
|
-
* })
|
|
1753
|
-
*
|
|
1754
|
-
* transport.on('disconnection', (clientId) => {
|
|
1755
|
-
* console.log(`Client disconnected: ${clientId}`)
|
|
1756
|
-
* })
|
|
1757
|
-
* ```
|
|
1758
|
-
*
|
|
1759
|
-
* @example
|
|
1760
|
-
* ### With custom ID generator
|
|
1761
|
-
* ```ts
|
|
1762
|
-
* const transport = new WebSocketServerTransport({
|
|
1763
|
-
* server: httpServer,
|
|
1764
|
-
* generateId: async (request) => {
|
|
1765
|
-
* const token = request.headers.authorization?.split(' ')[1]
|
|
1766
|
-
* const user = await verifyJwt(token)
|
|
1767
|
-
* return user.id
|
|
1768
|
-
* }
|
|
1769
|
-
* })
|
|
1770
|
-
* ```
|
|
1771
|
-
*
|
|
1772
236
|
* @example
|
|
1773
|
-
* ### With shared connections
|
|
1774
237
|
* ```ts
|
|
1775
|
-
* const
|
|
1776
|
-
*
|
|
1777
|
-
* const transport1 = new WebSocketServerTransport({
|
|
1778
|
-
* connections: sharedConnections
|
|
1779
|
-
* })
|
|
1780
|
-
*
|
|
1781
|
-
* const transport2 = new WebSocketServerTransport({
|
|
1782
|
-
* connections: sharedConnections
|
|
1783
|
-
* })
|
|
238
|
+
* const transport = new WebSocketServerTransport({ server: httpServer, path: '/ws' })
|
|
239
|
+
* transport.on('connection', (client) => console.log(client.id))
|
|
1784
240
|
* ```
|
|
1785
|
-
*
|
|
1786
|
-
* @see {@link EventEmitter} for event methods (on, off, emit, etc.)
|
|
1787
241
|
*/
|
|
1788
242
|
declare class WebSocketServerTransport extends EventEmitter {
|
|
1789
243
|
/**
|
|
@@ -1800,8 +254,6 @@ declare class WebSocketServerTransport extends EventEmitter {
|
|
|
1800
254
|
private readonly config;
|
|
1801
255
|
/** @internal */
|
|
1802
256
|
private pingTimer?;
|
|
1803
|
-
/** @internal */
|
|
1804
|
-
private authenticator?;
|
|
1805
257
|
/**
|
|
1806
258
|
* Creates a new WebSocket Server Transport instance
|
|
1807
259
|
*
|
|
@@ -1829,29 +281,6 @@ declare class WebSocketServerTransport extends EventEmitter {
|
|
|
1829
281
|
* @emits error When an error occurs
|
|
1830
282
|
*/
|
|
1831
283
|
constructor(config: WebSocketServerTransportConfig);
|
|
1832
|
-
/**
|
|
1833
|
-
* Set a custom authentication handler
|
|
1834
|
-
*
|
|
1835
|
-
* @remarks
|
|
1836
|
-
* Sets an authenticator function that receives the HTTP upgrade request
|
|
1837
|
-
* and returns a client ID. The authenticator can throw to reject the connection.
|
|
1838
|
-
*
|
|
1839
|
-
* @param authenticator - Function that receives the HTTP upgrade request
|
|
1840
|
-
* and returns a client ID (or throws to reject)
|
|
1841
|
-
*
|
|
1842
|
-
* @example
|
|
1843
|
-
* ```ts
|
|
1844
|
-
* transport.setAuthenticator(async (request) => {
|
|
1845
|
-
* const token = request.headers.authorization?.split(' ')[1]
|
|
1846
|
-
* if (!token) {
|
|
1847
|
-
* throw new Error('No token provided')
|
|
1848
|
-
* }
|
|
1849
|
-
* const user = await verifyJwt(token)
|
|
1850
|
-
* return user.id
|
|
1851
|
-
* })
|
|
1852
|
-
* ```
|
|
1853
|
-
*/
|
|
1854
|
-
setAuthenticator(authenticator: (request: node_http.IncomingMessage) => string | Promise<string>): void;
|
|
1855
284
|
private setupEventHandlers;
|
|
1856
285
|
private handleConnection;
|
|
1857
286
|
private handleMessage;
|
|
@@ -1861,29 +290,6 @@ declare class WebSocketServerTransport extends EventEmitter {
|
|
|
1861
290
|
private checkConnections;
|
|
1862
291
|
}
|
|
1863
292
|
|
|
1864
|
-
/**
|
|
1865
|
-
* Server statistics interface
|
|
1866
|
-
*
|
|
1867
|
-
* @remarks
|
|
1868
|
-
* Provides real-time statistics about the server state including
|
|
1869
|
-
* connected clients, active channels, and total subscriptions.
|
|
1870
|
-
*
|
|
1871
|
-
* @property clientCount - Number of currently connected clients
|
|
1872
|
-
* @property channelCount - Number of active channels
|
|
1873
|
-
* @property subscriptionCount - Total number of channel subscriptions across all channels
|
|
1874
|
-
* @property startedAt - Unix timestamp (ms) when the server was started
|
|
1875
|
-
*
|
|
1876
|
-
* @example
|
|
1877
|
-
* ```ts
|
|
1878
|
-
* const server = createSyncarServer({ port: 3000 })
|
|
1879
|
-
* await server.start()
|
|
1880
|
-
*
|
|
1881
|
-
* const stats = server.getStats()
|
|
1882
|
-
* console.log(`Clients: ${stats.clientCount}`)
|
|
1883
|
-
* console.log(`Channels: ${stats.channelCount}`)
|
|
1884
|
-
* console.log(`Started at: ${new Date(stats.startedAt!).toLocaleString()}`)
|
|
1885
|
-
* ```
|
|
1886
|
-
*/
|
|
1887
293
|
interface IServerStats {
|
|
1888
294
|
/** Number of currently connected clients */
|
|
1889
295
|
clientCount: number;
|
|
@@ -1894,30 +300,8 @@ interface IServerStats {
|
|
|
1894
300
|
/** Unix timestamp (ms) when the server was started */
|
|
1895
301
|
startedAt?: number;
|
|
1896
302
|
}
|
|
1897
|
-
|
|
1898
303
|
/**
|
|
1899
304
|
* Server configuration options
|
|
1900
|
-
*
|
|
1901
|
-
* @remarks
|
|
1902
|
-
* Complete configuration interface for the Syncar server. These options
|
|
1903
|
-
* control the WebSocket transport layer, connection handling, middleware,
|
|
1904
|
-
* and performance tuning parameters.
|
|
1905
|
-
*
|
|
1906
|
-
* @example
|
|
1907
|
-
* ```ts
|
|
1908
|
-
* import { createSyncarServer } from '@syncar/server'
|
|
1909
|
-
*
|
|
1910
|
-
* const server = createSyncarServer({
|
|
1911
|
-
* port: 3000,
|
|
1912
|
-
* host: '0.0.0.0',
|
|
1913
|
-
* path: '/ws',
|
|
1914
|
-
* enablePing: true,
|
|
1915
|
-
* pingInterval: 30000,
|
|
1916
|
-
* pingTimeout: 5000,
|
|
1917
|
-
* broadcastChunkSize: 500,
|
|
1918
|
-
* })
|
|
1919
|
-
* ```
|
|
1920
|
-
*
|
|
1921
305
|
* @see {@link DEFAULT_SERVER_CONFIG} for default values
|
|
1922
306
|
*/
|
|
1923
307
|
interface IServerOptions {
|
|
@@ -1928,23 +312,7 @@ interface IServerOptions {
|
|
|
1928
312
|
* If provided, the WebSocket server will attach to this existing server.
|
|
1929
313
|
* If not provided, a new HTTP server will be created automatically.
|
|
1930
314
|
*/
|
|
1931
|
-
server?: node_http.Server | node_https.Server;
|
|
1932
|
-
/**
|
|
1933
|
-
* Custom connection ID generator
|
|
1934
|
-
*
|
|
1935
|
-
* @remarks
|
|
1936
|
-
* Function to generate unique client IDs from incoming HTTP requests.
|
|
1937
|
-
* Useful for implementing custom authentication or ID generation strategies.
|
|
1938
|
-
*
|
|
1939
|
-
* @example
|
|
1940
|
-
* ```ts
|
|
1941
|
-
* generateId: (request) => {
|
|
1942
|
-
* const token = request.headers.authorization?.split(' ')[1]
|
|
1943
|
-
* return extractUserIdFromToken(token)
|
|
1944
|
-
* }
|
|
1945
|
-
* ```
|
|
1946
|
-
*/
|
|
1947
|
-
generateId?: IdGenerator;
|
|
315
|
+
server?: node_http.Server | node_https.Server;
|
|
1948
316
|
/**
|
|
1949
317
|
* Custom logger instance
|
|
1950
318
|
*
|
|
@@ -2027,9 +395,9 @@ interface IServerOptions {
|
|
|
2027
395
|
* @example
|
|
2028
396
|
* ```ts
|
|
2029
397
|
* middleware: [
|
|
2030
|
-
*
|
|
2031
|
-
*
|
|
2032
|
-
*
|
|
398
|
+
* authenticate({ verifyToken }),
|
|
399
|
+
* logger(),
|
|
400
|
+
* rateLimit({ maxRequests: 100 })
|
|
2033
401
|
* ]
|
|
2034
402
|
* ```
|
|
2035
403
|
*/
|
|
@@ -2042,37 +410,19 @@ interface IServerOptions {
|
|
|
2042
410
|
* in chunks to avoid blocking the event loop. Lower values reduce latency
|
|
2043
411
|
* per chunk but increase total broadcast time.
|
|
2044
412
|
*/
|
|
2045
|
-
|
|
413
|
+
chunkSize: number;
|
|
2046
414
|
}
|
|
2047
415
|
/**
|
|
2048
416
|
* Syncar Server - Real-time WebSocket server with pub/sub channels
|
|
2049
417
|
*
|
|
2050
|
-
* @remarks
|
|
2051
|
-
* The main server class providing WebSocket communication with broadcast
|
|
2052
|
-
* and multicast channels, middleware support, and connection management.
|
|
2053
|
-
*
|
|
2054
418
|
* @example
|
|
2055
419
|
* ```ts
|
|
2056
|
-
* import { createSyncarServer } from '@syncar/server'
|
|
2057
|
-
*
|
|
2058
420
|
* const server = createSyncarServer({ port: 3000 })
|
|
2059
421
|
* await server.start()
|
|
2060
422
|
*
|
|
2061
|
-
*
|
|
2062
|
-
*
|
|
2063
|
-
* const chat = server.createMulticast<{ text: string }>('chat')
|
|
2064
|
-
*
|
|
2065
|
-
* // Listen for events
|
|
2066
|
-
* server.on('connection', (client) => {
|
|
2067
|
-
* console.log(`Client connected: ${client.id}`)
|
|
2068
|
-
* })
|
|
2069
|
-
*
|
|
2070
|
-
* // Publish messages
|
|
2071
|
-
* broadcast.publish('Hello everyone!')
|
|
2072
|
-
* chat.publish({ text: 'Welcome!' })
|
|
423
|
+
* const chat = server.createChannel('chat')
|
|
424
|
+
* chat.publish('Hello!')
|
|
2073
425
|
* ```
|
|
2074
|
-
*
|
|
2075
|
-
* @see {@link createSyncarServer} for factory function
|
|
2076
426
|
*/
|
|
2077
427
|
declare class SyncarServer {
|
|
2078
428
|
private readonly config;
|
|
@@ -2083,25 +433,24 @@ declare class SyncarServer {
|
|
|
2083
433
|
private connectionHandler;
|
|
2084
434
|
private messageHandler;
|
|
2085
435
|
private signalHandler;
|
|
2086
|
-
private broadcastChannel;
|
|
2087
436
|
constructor(config: IServerOptions);
|
|
2088
437
|
/**
|
|
2089
438
|
* Start the server and begin accepting connections
|
|
2090
439
|
*
|
|
2091
440
|
* @remarks
|
|
2092
441
|
* Initializes the WebSocket transport layer, sets up event handlers,
|
|
2093
|
-
*
|
|
442
|
+
* and prepares the server for connections.
|
|
2094
443
|
*
|
|
2095
444
|
* @throws {StateError} If the server is already started
|
|
2096
445
|
*
|
|
2097
446
|
* @example
|
|
2098
447
|
* ```ts
|
|
2099
448
|
* const server = createSyncarServer({ port: 3000 })
|
|
2100
|
-
*
|
|
449
|
+
* server.start()
|
|
2101
450
|
* console.log('Server is running')
|
|
2102
451
|
* ```
|
|
2103
452
|
*/
|
|
2104
|
-
start():
|
|
453
|
+
start(): void;
|
|
2105
454
|
/**
|
|
2106
455
|
* Stop the server and close all connections
|
|
2107
456
|
*
|
|
@@ -2112,88 +461,34 @@ declare class SyncarServer {
|
|
|
2112
461
|
*
|
|
2113
462
|
* @example
|
|
2114
463
|
* ```ts
|
|
2115
|
-
*
|
|
464
|
+
* server.stop()
|
|
2116
465
|
* console.log('Server stopped')
|
|
2117
466
|
* ```
|
|
2118
467
|
*/
|
|
2119
|
-
stop():
|
|
2120
|
-
/**
|
|
2121
|
-
* Get or create the broadcast channel
|
|
2122
|
-
*
|
|
2123
|
-
* @remarks
|
|
2124
|
-
* Returns the singleton broadcast channel that sends messages to ALL
|
|
2125
|
-
* connected clients. No subscription is required - all clients receive
|
|
2126
|
-
* broadcast messages automatically.
|
|
2127
|
-
*
|
|
2128
|
-
* @template T - Type of data to be broadcast (default: unknown)
|
|
2129
|
-
* @returns The broadcast channel instance
|
|
2130
|
-
*
|
|
2131
|
-
* @throws {StateError} If the server hasn't been started yet
|
|
2132
|
-
*
|
|
2133
|
-
* @example
|
|
2134
|
-
* ```ts
|
|
2135
|
-
* // Broadcast a string to all clients
|
|
2136
|
-
* const broadcast = server.createBroadcast<string>()
|
|
2137
|
-
* broadcast.publish('Server maintenance in 5 minutes')
|
|
2138
|
-
*
|
|
2139
|
-
* // Broadcast an object
|
|
2140
|
-
* const alerts = server.createBroadcast<{ type: string; message: string }>()
|
|
2141
|
-
* alerts.publish({ type: 'warning', message: 'High load detected' })
|
|
2142
|
-
*
|
|
2143
|
-
* // Exclude specific clients
|
|
2144
|
-
* broadcast.publish('Admin message', { exclude: ['client-123'] })
|
|
2145
|
-
*
|
|
2146
|
-
* // Send to specific clients only
|
|
2147
|
-
* broadcast.publish('Private message', { to: ['client-1', 'client-2'] })
|
|
2148
|
-
* ```
|
|
2149
|
-
*
|
|
2150
|
-
* @see {@link BroadcastChannel} for channel API
|
|
2151
|
-
*/
|
|
2152
|
-
createBroadcast<T = unknown>(): BroadcastChannel<T>;
|
|
468
|
+
stop(): void;
|
|
2153
469
|
/**
|
|
2154
|
-
* Create or retrieve a
|
|
470
|
+
* Create or retrieve a channel
|
|
2155
471
|
*
|
|
2156
472
|
* @remarks
|
|
2157
|
-
*
|
|
2158
|
-
* Clients must explicitly subscribe to receive messages. If a channel with
|
|
2159
|
-
* the given name already exists, it will be returned instead of creating a new one.
|
|
473
|
+
* Unified channel creation.
|
|
2160
474
|
*
|
|
2161
475
|
* @template T - Type of data to be published on this channel (default: unknown)
|
|
2162
|
-
* @param name - Unique channel name
|
|
2163
|
-
* @
|
|
476
|
+
* @param name - Unique channel name
|
|
477
|
+
* @param options - Channel configuration options
|
|
478
|
+
* @returns The channel instance
|
|
2164
479
|
*
|
|
2165
480
|
* @throws {StateError} If the server hasn't been started yet
|
|
2166
|
-
* @throws {ValidationError} If the channel name is invalid (starts with `__`)
|
|
2167
481
|
*
|
|
2168
482
|
* @example
|
|
483
|
+
* ### Default: bidirectional (chat room)
|
|
2169
484
|
* ```ts
|
|
2170
|
-
*
|
|
2171
|
-
* const chat = server.createMulticast<{ text: string; user: string }>('chat')
|
|
2172
|
-
*
|
|
2173
|
-
* // Handle incoming messages
|
|
485
|
+
* const chat = server.createChannel('chat')
|
|
2174
486
|
* chat.onMessage((data, client) => {
|
|
2175
|
-
*
|
|
2176
|
-
* // Relay to all subscribers except sender
|
|
2177
|
-
* chat.publish(data, { exclude: [client.id] })
|
|
487
|
+
* chat.publish(data, [client.id]) // Relay to others, excluding sender
|
|
2178
488
|
* })
|
|
2179
|
-
*
|
|
2180
|
-
* // Publish to all subscribers
|
|
2181
|
-
* chat.publish({ text: 'Hello!', user: 'System' })
|
|
2182
|
-
*
|
|
2183
|
-
* // Check channel existence
|
|
2184
|
-
* if (server.hasChannel('chat')) {
|
|
2185
|
-
* const existingChat = server.createMulticast('chat')
|
|
2186
|
-
* }
|
|
2187
|
-
*
|
|
2188
|
-
* // Get all channel names
|
|
2189
|
-
* const channels = server.getChannels()
|
|
2190
|
-
* // ['chat', 'notifications', 'presence']
|
|
2191
489
|
* ```
|
|
2192
|
-
*
|
|
2193
|
-
* @see {@link MulticastChannel} for channel API
|
|
2194
|
-
* @see {@link BROADCAST_CHANNEL} for reserved channel name
|
|
2195
490
|
*/
|
|
2196
|
-
|
|
491
|
+
createChannel<T = unknown>(name: ChannelName, options?: ChannelOptions): Channel<T>;
|
|
2197
492
|
/**
|
|
2198
493
|
* Check if a channel exists
|
|
2199
494
|
*
|
|
@@ -2203,525 +498,100 @@ declare class SyncarServer {
|
|
|
2203
498
|
* @example
|
|
2204
499
|
* ```ts
|
|
2205
500
|
* if (!server.hasChannel('chat')) {
|
|
2206
|
-
* const chat = server.
|
|
501
|
+
* const chat = server.createChannel('chat')
|
|
2207
502
|
* }
|
|
2208
503
|
* ```
|
|
2209
504
|
*/
|
|
2210
505
|
hasChannel(name: ChannelName): boolean;
|
|
2211
506
|
/**
|
|
2212
|
-
*
|
|
2213
|
-
*
|
|
2214
|
-
* @returns Array of channel names currently registered on the server
|
|
2215
|
-
*
|
|
2216
|
-
* @example
|
|
2217
|
-
* ```ts
|
|
2218
|
-
* const channels = server.getChannels()
|
|
2219
|
-
* console.log('Active channels:', channels)
|
|
2220
|
-
* // ['chat', 'notifications', 'presence']
|
|
2221
|
-
* ```
|
|
2222
|
-
*/
|
|
2223
|
-
getChannels(): ChannelName[];
|
|
2224
|
-
/**
|
|
2225
|
-
* Register a global middleware
|
|
2226
|
-
*
|
|
2227
|
-
* @remarks
|
|
2228
|
-
* Adds a middleware function to the global middleware chain.
|
|
2229
|
-
* Global middleware runs before channel-specific middleware for all actions.
|
|
2230
|
-
*
|
|
2231
|
-
* @param middleware - The middleware function to register
|
|
2232
|
-
*
|
|
2233
|
-
* @example
|
|
2234
|
-
* ```ts
|
|
2235
|
-
* import { createAuthMiddleware } from '@syncar/server'
|
|
2236
|
-
*
|
|
2237
|
-
* server.use(createAuthMiddleware({
|
|
2238
|
-
* verifyToken: async (token) => jwt.verify(token, SECRET)
|
|
2239
|
-
* }))
|
|
2240
|
-
* ```
|
|
2241
|
-
*
|
|
2242
|
-
* @see {@link IMiddleware} for middleware interface
|
|
2243
|
-
*/
|
|
2244
|
-
use(middleware: IMiddleware): void;
|
|
2245
|
-
/**
|
|
2246
|
-
* Set a custom authentication handler for connection validation
|
|
507
|
+
* Broadcast data to all connected clients
|
|
2247
508
|
*
|
|
2248
509
|
* @remarks
|
|
2249
|
-
*
|
|
2250
|
-
*
|
|
2251
|
-
*
|
|
2252
|
-
* WebSocket handshake.
|
|
510
|
+
* A convenience method to send data to every client currently connected
|
|
511
|
+
* to the server. This uses the `publishInChunks` utility to efficiently
|
|
512
|
+
* broadcast messages without blocking the event loop.
|
|
2253
513
|
*
|
|
2254
|
-
* @param
|
|
2255
|
-
* and returns a ClientId (or throws to reject the connection)
|
|
514
|
+
* @param data - The data to broadcast to all clients
|
|
2256
515
|
*
|
|
2257
516
|
* @example
|
|
2258
517
|
* ```ts
|
|
2259
|
-
* server.
|
|
2260
|
-
*
|
|
2261
|
-
*
|
|
2262
|
-
* throw new Error('No token provided')
|
|
2263
|
-
* }
|
|
2264
|
-
* const user = await verifyJwt(token)
|
|
2265
|
-
* return user.id
|
|
518
|
+
* server.broadcast({
|
|
519
|
+
* type: 'announcement',
|
|
520
|
+
* content: 'Server update in progress'
|
|
2266
521
|
* })
|
|
2267
522
|
* ```
|
|
2268
523
|
*/
|
|
2269
|
-
|
|
2270
|
-
/**
|
|
2271
|
-
* Get server statistics
|
|
2272
|
-
*
|
|
2273
|
-
* @returns Server statistics including client count, channel count,
|
|
2274
|
-
* subscription count, and start time
|
|
2275
|
-
*
|
|
2276
|
-
* @example
|
|
2277
|
-
* ```ts
|
|
2278
|
-
* const stats = server.getStats()
|
|
2279
|
-
* console.log(`Clients: ${stats.clientCount}`)
|
|
2280
|
-
* console.log(`Channels: ${stats.channelCount}`)
|
|
2281
|
-
* console.log(`Subscriptions: ${stats.subscriptionCount}`)
|
|
2282
|
-
* console.log(`Started: ${new Date(stats.startedAt!).toLocaleString()}`)
|
|
2283
|
-
* ```
|
|
2284
|
-
*
|
|
2285
|
-
* @see {@link IServerStats} for statistics structure
|
|
2286
|
-
*/
|
|
2287
|
-
getStats(): IServerStats;
|
|
2288
|
-
/**
|
|
2289
|
-
* Get the server configuration (read-only)
|
|
2290
|
-
*
|
|
2291
|
-
* @returns Readonly copy of the server configuration options
|
|
2292
|
-
*
|
|
2293
|
-
* @example
|
|
2294
|
-
* ```ts
|
|
2295
|
-
* const config = server.getConfig()
|
|
2296
|
-
* console.log(`Port: ${config.port}`)
|
|
2297
|
-
* console.log(`Host: ${config.host}`)
|
|
2298
|
-
* console.log(`Path: ${config.path}`)
|
|
2299
|
-
* ```
|
|
2300
|
-
*/
|
|
2301
|
-
getConfig(): Readonly<IServerOptions>;
|
|
2302
|
-
/**
|
|
2303
|
-
* Get the client registry
|
|
2304
|
-
*
|
|
2305
|
-
* @returns The client registry instance used by this server
|
|
2306
|
-
*
|
|
2307
|
-
* @remarks
|
|
2308
|
-
* The registry manages client connections and channel subscriptions.
|
|
2309
|
-
* Direct access allows for advanced operations like manual client
|
|
2310
|
-
* lookup or subscription management.
|
|
2311
|
-
*
|
|
2312
|
-
* @example
|
|
2313
|
-
* ```ts
|
|
2314
|
-
* const registry = server.getRegistry()
|
|
2315
|
-
* const client = registry.get('client-123')
|
|
2316
|
-
* if (client) {
|
|
2317
|
-
* console.log(`Client connected at: ${new Date(client.connectedAt).toLocaleString()}`)
|
|
2318
|
-
* }
|
|
2319
|
-
* ```
|
|
2320
|
-
*
|
|
2321
|
-
* @see {@link ClientRegistry} for registry API
|
|
2322
|
-
*/
|
|
2323
|
-
getRegistry(): ClientRegistry;
|
|
2324
|
-
private setupTransportHandlers;
|
|
2325
|
-
}
|
|
2326
|
-
/**
|
|
2327
|
-
* Create a Syncar server with automatic WebSocket transport setup
|
|
2328
|
-
*
|
|
2329
|
-
* @remarks
|
|
2330
|
-
* Factory function that creates a configured SyncarServer instance.
|
|
2331
|
-
* Automatically sets up the WebSocket transport layer if not provided,
|
|
2332
|
-
* merges user configuration with defaults, and creates the client registry.
|
|
2333
|
-
*
|
|
2334
|
-
* @param config - Optional partial server configuration. All properties are optional
|
|
2335
|
-
* and will be merged with {@link DEFAULT_SERVER_CONFIG}.
|
|
2336
|
-
*
|
|
2337
|
-
* @returns Configured Syncar server instance ready to be started
|
|
2338
|
-
*
|
|
2339
|
-
* @example
|
|
2340
|
-
* ### Basic usage
|
|
2341
|
-
* ```ts
|
|
2342
|
-
* import { createSyncarServer } from '@syncar/server'
|
|
2343
|
-
*
|
|
2344
|
-
* const server = createSyncarServer({ port: 3000 })
|
|
2345
|
-
* await server.start()
|
|
2346
|
-
* ```
|
|
2347
|
-
*
|
|
2348
|
-
* @example
|
|
2349
|
-
* ### With custom configuration
|
|
2350
|
-
* ```ts
|
|
2351
|
-
* const server = createSyncarServer({
|
|
2352
|
-
* port: 8080,
|
|
2353
|
-
* host: 'localhost',
|
|
2354
|
-
* path: '/ws',
|
|
2355
|
-
* enablePing: true,
|
|
2356
|
-
* pingInterval: 30000,
|
|
2357
|
-
* pingTimeout: 5000,
|
|
2358
|
-
* broadcastChunkSize: 1000,
|
|
2359
|
-
* })
|
|
2360
|
-
* await server.start()
|
|
2361
|
-
* ```
|
|
2362
|
-
*
|
|
2363
|
-
* @example
|
|
2364
|
-
* ### With existing HTTP server
|
|
2365
|
-
* ```ts
|
|
2366
|
-
* import { createServer } from 'node:http'
|
|
2367
|
-
* import { createSyncarServer } from '@syncar/server'
|
|
2368
|
-
*
|
|
2369
|
-
* const httpServer = createServer((req, res) => {
|
|
2370
|
-
* res.writeHead(200)
|
|
2371
|
-
* res.end('OK')
|
|
2372
|
-
* })
|
|
2373
|
-
*
|
|
2374
|
-
* const server = createSyncarServer({
|
|
2375
|
-
* server: httpServer,
|
|
2376
|
-
* path: '/ws',
|
|
2377
|
-
* })
|
|
2378
|
-
*
|
|
2379
|
-
* await server.start()
|
|
2380
|
-
* ```
|
|
2381
|
-
*
|
|
2382
|
-
* @example
|
|
2383
|
-
* ### With Express
|
|
2384
|
-
* ```ts
|
|
2385
|
-
* import express from 'express'
|
|
2386
|
-
* import { createSyncarServer } from '@syncar/server'
|
|
2387
|
-
*
|
|
2388
|
-
* const app = express()
|
|
2389
|
-
* const httpServer = app.listen(3000)
|
|
2390
|
-
*
|
|
2391
|
-
* const server = createSyncarServer({
|
|
2392
|
-
* server: httpServer,
|
|
2393
|
-
* path: '/ws',
|
|
2394
|
-
* })
|
|
2395
|
-
*
|
|
2396
|
-
* await server.start()
|
|
2397
|
-
* ```
|
|
2398
|
-
*
|
|
2399
|
-
* @example
|
|
2400
|
-
* ### With custom logger
|
|
2401
|
-
* ```ts
|
|
2402
|
-
* import { createSyncarServer } from '@syncar/server'
|
|
2403
|
-
*
|
|
2404
|
-
* const server = createSyncarServer({
|
|
2405
|
-
* port: 3000,
|
|
2406
|
-
* logger: {
|
|
2407
|
-
* debug: (msg, ...args) => console.debug('[DEBUG]', msg, ...args),
|
|
2408
|
-
* info: (msg, ...args) => console.info('[INFO]', msg, ...args),
|
|
2409
|
-
* warn: (msg, ...args) => console.warn('[WARN]', msg, ...args),
|
|
2410
|
-
* error: (msg, ...args) => console.error('[ERROR]', msg, ...args),
|
|
2411
|
-
* },
|
|
2412
|
-
* })
|
|
2413
|
-
* ```
|
|
2414
|
-
*
|
|
2415
|
-
* @example
|
|
2416
|
-
* ### With middleware
|
|
2417
|
-
* ```ts
|
|
2418
|
-
* import { createSyncarServer, createLoggingMiddleware } from '@syncar/server'
|
|
2419
|
-
*
|
|
2420
|
-
* const server = createSyncarServer({
|
|
2421
|
-
* port: 3000,
|
|
2422
|
-
* middleware: [
|
|
2423
|
-
* createLoggingMiddleware(),
|
|
2424
|
-
* ],
|
|
2425
|
-
* })
|
|
2426
|
-
*
|
|
2427
|
-
* await server.start()
|
|
2428
|
-
* ```
|
|
2429
|
-
*
|
|
2430
|
-
* @see {@link SyncarServer} for server class API
|
|
2431
|
-
* @see {@link DEFAULT_SERVER_CONFIG} for default configuration values
|
|
2432
|
-
*/
|
|
2433
|
-
declare function createSyncarServer(config?: Partial<IServerOptions>): SyncarServer;
|
|
2434
|
-
|
|
2435
|
-
/**
|
|
2436
|
-
* Middleware Factories
|
|
2437
|
-
* Factory functions for creating common middleware implementations.
|
|
2438
|
-
*
|
|
2439
|
-
* @module middleware/factories
|
|
2440
|
-
*/
|
|
2441
|
-
|
|
2442
|
-
/**
|
|
2443
|
-
* Auth middleware options
|
|
2444
|
-
*
|
|
2445
|
-
* @example
|
|
2446
|
-
* ```ts
|
|
2447
|
-
* const options: AuthMiddlewareOptions = {
|
|
2448
|
-
* verifyToken: async (token) => {
|
|
2449
|
-
* const user = await verifyJwt(token)
|
|
2450
|
-
* return { id: user.id, email: user.email }
|
|
2451
|
-
* },
|
|
2452
|
-
* getToken: (context) => {
|
|
2453
|
-
* // Extract token from message or connection
|
|
2454
|
-
* return context.message?.data?.token
|
|
2455
|
-
* },
|
|
2456
|
-
* attachProperty: 'user'
|
|
2457
|
-
* }
|
|
2458
|
-
* ```
|
|
2459
|
-
*/
|
|
2460
|
-
interface AuthMiddlewareOptions {
|
|
2461
|
-
/**
|
|
2462
|
-
* Verify and decode a token
|
|
2463
|
-
* Returns the user data to attach to the client
|
|
2464
|
-
*
|
|
2465
|
-
* @param token - The token to verify
|
|
2466
|
-
* @returns User data to attach
|
|
2467
|
-
* @throws Error if token is invalid
|
|
2468
|
-
*/
|
|
2469
|
-
verifyToken: (token: string) => Promise<unknown> | unknown;
|
|
2470
|
-
/**
|
|
2471
|
-
* Extract token from the middleware context
|
|
2472
|
-
*
|
|
2473
|
-
* @param c - The middleware context
|
|
2474
|
-
* @returns The token string or undefined if not found
|
|
2475
|
-
*/
|
|
2476
|
-
getToken?: (c: Context) => string | undefined;
|
|
2477
|
-
/**
|
|
2478
|
-
* Property name to attach verified user data
|
|
2479
|
-
* @default 'user'
|
|
2480
|
-
*/
|
|
2481
|
-
attachProperty?: string;
|
|
2482
|
-
/**
|
|
2483
|
-
* Actions to require authentication
|
|
2484
|
-
* @default All actions require auth
|
|
2485
|
-
*/
|
|
2486
|
-
actions?: IMiddlewareAction[];
|
|
2487
|
-
}
|
|
2488
|
-
/**
|
|
2489
|
-
* Create an authentication middleware
|
|
2490
|
-
*
|
|
2491
|
-
* This middleware verifies tokens and attaches user data to clients.
|
|
2492
|
-
* Rejects connections that fail authentication.
|
|
2493
|
-
*
|
|
2494
|
-
* @param options - Authentication options
|
|
2495
|
-
* @returns Middleware function
|
|
2496
|
-
*
|
|
2497
|
-
* @example
|
|
2498
|
-
* ```ts
|
|
2499
|
-
* import { createAuthMiddleware } from '@syncar/server/middleware'
|
|
2500
|
-
*
|
|
2501
|
-
* const authMiddleware = createAuthMiddleware({
|
|
2502
|
-
* verifyToken: async (token) => {
|
|
2503
|
-
* const user = await jwt.verify(token, SECRET)
|
|
2504
|
-
* return { id: user.sub, email: user.email }
|
|
2505
|
-
* },
|
|
2506
|
-
* getToken: (context) => context.message?.data?.token,
|
|
2507
|
-
* attachProperty: 'user'
|
|
2508
|
-
* })
|
|
2509
|
-
*
|
|
2510
|
-
* server.use(authMiddleware)
|
|
2511
|
-
* ```
|
|
2512
|
-
*/
|
|
2513
|
-
declare function createAuthMiddleware(options: AuthMiddlewareOptions): IMiddleware;
|
|
2514
|
-
/**
|
|
2515
|
-
* Logging middleware options
|
|
2516
|
-
*
|
|
2517
|
-
* @example
|
|
2518
|
-
* ```ts
|
|
2519
|
-
* const options: LoggingMiddlewareOptions = {
|
|
2520
|
-
* logger: console,
|
|
2521
|
-
* logLevel: 'info',
|
|
2522
|
-
* includeMessageData: false
|
|
2523
|
-
* }
|
|
2524
|
-
* ```
|
|
2525
|
-
*/
|
|
2526
|
-
interface LoggingMiddlewareOptions {
|
|
2527
|
-
/**
|
|
2528
|
-
* Logger instance to use
|
|
2529
|
-
* @default console
|
|
2530
|
-
*/
|
|
2531
|
-
logger?: Pick<Console, 'log' | 'info' | 'warn' | 'error'>;
|
|
2532
|
-
/**
|
|
2533
|
-
* Log level
|
|
2534
|
-
* @default 'info'
|
|
2535
|
-
*/
|
|
2536
|
-
logLevel?: 'log' | 'info' | 'warn' | 'error';
|
|
2537
|
-
/**
|
|
2538
|
-
* Whether to include message data in logs
|
|
2539
|
-
* @default false
|
|
2540
|
-
*/
|
|
2541
|
-
includeMessageData?: boolean;
|
|
2542
|
-
/**
|
|
2543
|
-
* Custom format function for log output
|
|
2544
|
-
*
|
|
2545
|
-
* @param context - The middleware context
|
|
2546
|
-
* @returns Formatted log string
|
|
2547
|
-
*/
|
|
2548
|
-
format?: (context: {
|
|
2549
|
-
action: string;
|
|
2550
|
-
clientId?: string;
|
|
2551
|
-
channel?: string;
|
|
2552
|
-
message?: unknown;
|
|
2553
|
-
duration?: number;
|
|
2554
|
-
}) => string;
|
|
2555
|
-
/**
|
|
2556
|
-
* Actions to log
|
|
2557
|
-
* @default All actions are logged
|
|
2558
|
-
*/
|
|
2559
|
-
actions?: IMiddlewareAction[];
|
|
2560
|
-
}
|
|
2561
|
-
/**
|
|
2562
|
-
* Create a logging middleware
|
|
2563
|
-
*
|
|
2564
|
-
* Logs all middleware actions with client and action information.
|
|
2565
|
-
*
|
|
2566
|
-
* @param options - Logging options
|
|
2567
|
-
* @returns Middleware function
|
|
2568
|
-
*
|
|
2569
|
-
* @example
|
|
2570
|
-
* ```ts
|
|
2571
|
-
* import { createLoggingMiddleware } from '@syncar/server/middleware'
|
|
2572
|
-
*
|
|
2573
|
-
* const loggingMiddleware = createLoggingMiddleware({
|
|
2574
|
-
* logger: console,
|
|
2575
|
-
* logLevel: 'info',
|
|
2576
|
-
* includeMessageData: false
|
|
2577
|
-
* })
|
|
2578
|
-
*
|
|
2579
|
-
* server.use(loggingMiddleware)
|
|
2580
|
-
* ```
|
|
2581
|
-
*/
|
|
2582
|
-
declare function createLoggingMiddleware(options?: LoggingMiddlewareOptions): IMiddleware;
|
|
2583
|
-
/**
|
|
2584
|
-
* Rate limit middleware options
|
|
2585
|
-
*
|
|
2586
|
-
* @example
|
|
2587
|
-
* ```ts
|
|
2588
|
-
* const options: RateLimitMiddlewareOptions = {
|
|
2589
|
-
* maxRequests: 100,
|
|
2590
|
-
* windowMs: 60000,
|
|
2591
|
-
* getMessageId: (context) => context.client?.id ?? ''
|
|
2592
|
-
* }
|
|
2593
|
-
* ```
|
|
2594
|
-
*/
|
|
2595
|
-
interface RateLimitMiddlewareOptions {
|
|
2596
|
-
/**
|
|
2597
|
-
* Maximum number of requests allowed per window
|
|
2598
|
-
* @default 100
|
|
2599
|
-
*/
|
|
2600
|
-
maxRequests?: number;
|
|
2601
|
-
/**
|
|
2602
|
-
* Time window in milliseconds
|
|
2603
|
-
* @default 60000 (1 minute)
|
|
2604
|
-
*/
|
|
2605
|
-
windowMs?: number;
|
|
2606
|
-
/**
|
|
2607
|
-
* Extract a unique identifier for rate limiting
|
|
2608
|
-
* Defaults to client ID
|
|
2609
|
-
*
|
|
2610
|
-
* @param c - The middleware context
|
|
2611
|
-
* @returns Unique identifier for rate limiting
|
|
2612
|
-
*/
|
|
2613
|
-
getMessageId?: (c: Context) => string;
|
|
2614
|
-
/**
|
|
2615
|
-
* Actions to rate limit
|
|
2616
|
-
* @default 'message' only
|
|
2617
|
-
*/
|
|
2618
|
-
actions?: IMiddlewareAction[];
|
|
2619
|
-
}
|
|
2620
|
-
/**
|
|
2621
|
-
* Create a rate limiting middleware
|
|
2622
|
-
*
|
|
2623
|
-
* Limits the rate of requests per client within a time window.
|
|
2624
|
-
*
|
|
2625
|
-
* @param options - Rate limit options
|
|
2626
|
-
* @returns Middleware function
|
|
2627
|
-
*
|
|
2628
|
-
* @example
|
|
2629
|
-
* ```ts
|
|
2630
|
-
* import { createRateLimitMiddleware } from '@syncar/server/middleware'
|
|
2631
|
-
*
|
|
2632
|
-
* const rateLimitMiddleware = createRateLimitMiddleware({
|
|
2633
|
-
* maxRequests: 100,
|
|
2634
|
-
* windowMs: 60000
|
|
2635
|
-
* })
|
|
2636
|
-
*
|
|
2637
|
-
* server.use(rateLimitMiddleware)
|
|
2638
|
-
* ```
|
|
2639
|
-
*/
|
|
2640
|
-
declare function createRateLimitMiddleware(options?: RateLimitMiddlewareOptions): IMiddleware;
|
|
2641
|
-
/**
|
|
2642
|
-
* Channel whitelist middleware options
|
|
2643
|
-
*
|
|
2644
|
-
* @example
|
|
2645
|
-
* ```ts
|
|
2646
|
-
* const options: ChannelWhitelistMiddlewareOptions = {
|
|
2647
|
-
* allowedChannels: ['chat', 'notifications'],
|
|
2648
|
-
* isDynamic: false
|
|
2649
|
-
* }
|
|
2650
|
-
* ```
|
|
2651
|
-
*/
|
|
2652
|
-
interface ChannelWhitelistMiddlewareOptions {
|
|
524
|
+
broadcast<T = unknown>(data: T): void;
|
|
2653
525
|
/**
|
|
2654
|
-
*
|
|
2655
|
-
*
|
|
526
|
+
* Get all active channel names
|
|
527
|
+
*
|
|
528
|
+
* @returns Array of channel names currently registered on the server
|
|
529
|
+
*
|
|
530
|
+
* @example
|
|
531
|
+
* ```ts
|
|
532
|
+
* const channels = server.getChannels()
|
|
533
|
+
* console.log('Active channels:', channels)
|
|
534
|
+
* // ['chat', 'notifications', 'presence']
|
|
535
|
+
* ```
|
|
2656
536
|
*/
|
|
2657
|
-
|
|
537
|
+
getChannels(): ChannelName[];
|
|
2658
538
|
/**
|
|
2659
|
-
*
|
|
2660
|
-
*
|
|
539
|
+
* Register a global middleware
|
|
540
|
+
*
|
|
541
|
+
* @remarks
|
|
542
|
+
* Adds a middleware function to the global middleware chain.
|
|
543
|
+
* Global middleware runs before channel-specific middleware for all actions.
|
|
2661
544
|
*
|
|
2662
|
-
* @param
|
|
2663
|
-
* @param client - The client attempting to access the channel
|
|
2664
|
-
* @returns true if channel is allowed
|
|
545
|
+
* @param middleware - The middleware function to register
|
|
2665
546
|
*
|
|
2666
547
|
* @example
|
|
2667
548
|
* ```ts
|
|
2668
|
-
*
|
|
2669
|
-
*
|
|
2670
|
-
*
|
|
2671
|
-
*
|
|
549
|
+
* import { authenticate } from '@syncar/server'
|
|
550
|
+
*
|
|
551
|
+
* server.use(authenticate({
|
|
552
|
+
* verifyToken: async (token) => jwt.verify(token, SECRET)
|
|
553
|
+
* }))
|
|
2672
554
|
* ```
|
|
555
|
+
*
|
|
556
|
+
* @see {@link IMiddleware} for middleware interface
|
|
557
|
+
*/
|
|
558
|
+
use(middleware: IMiddleware): void;
|
|
559
|
+
/**
|
|
560
|
+
* Get server statistics
|
|
561
|
+
*
|
|
562
|
+
* @returns Server statistics including client count, channel count,
|
|
563
|
+
* subscription count, and start time
|
|
564
|
+
* @see {@link IServerStats} for statistics structure
|
|
2673
565
|
*/
|
|
2674
|
-
|
|
566
|
+
getStats(): IServerStats;
|
|
2675
567
|
/**
|
|
2676
|
-
*
|
|
2677
|
-
*
|
|
568
|
+
* Get the server configuration (read-only)
|
|
569
|
+
*
|
|
570
|
+
* @returns Readonly copy of the server configuration options
|
|
2678
571
|
*/
|
|
2679
|
-
|
|
572
|
+
getConfig(): Readonly<IServerOptions>;
|
|
573
|
+
private setupTransportHandlers;
|
|
2680
574
|
}
|
|
2681
575
|
/**
|
|
2682
|
-
* Create a
|
|
2683
|
-
*
|
|
2684
|
-
* Restricts which channels clients can subscribe to.
|
|
576
|
+
* Create a Syncar server with automatic WebSocket transport setup
|
|
2685
577
|
*
|
|
2686
|
-
* @param
|
|
2687
|
-
* @returns
|
|
578
|
+
* @param config - Optional partial server configuration
|
|
579
|
+
* @returns Configured Syncar server instance
|
|
2688
580
|
*
|
|
2689
581
|
* @example
|
|
2690
582
|
* ```ts
|
|
2691
|
-
*
|
|
2692
|
-
*
|
|
2693
|
-
* const whitelistMiddleware = createChannelWhitelistMiddleware({
|
|
2694
|
-
* allowedChannels: ['chat', 'notifications']
|
|
2695
|
-
* })
|
|
2696
|
-
*
|
|
2697
|
-
* server.use(whitelistMiddleware)
|
|
583
|
+
* const server = createSyncarServer({ port: 3000 })
|
|
584
|
+
* server.start()
|
|
2698
585
|
* ```
|
|
2699
586
|
*/
|
|
2700
|
-
declare function
|
|
587
|
+
declare function createSyncarServer(config?: Partial<IServerOptions>): SyncarServer;
|
|
2701
588
|
|
|
2702
589
|
/**
|
|
2703
590
|
* Context Data Options
|
|
2704
591
|
*
|
|
2705
|
-
* @remarks
|
|
2706
|
-
* Options for creating a new middleware context with pre-populated state.
|
|
2707
|
-
*
|
|
2708
|
-
* @template S - Type of the state object (default: Record<string, unknown>)
|
|
2709
|
-
*
|
|
2710
|
-
* @property action - The middleware action being performed
|
|
2711
|
-
* @property client - Optional client connection
|
|
2712
|
-
* @property message - Optional message being processed
|
|
2713
|
-
* @property channel - Optional channel name
|
|
2714
|
-
* @property initialState - Optional initial state values
|
|
2715
|
-
*
|
|
2716
592
|
* @example
|
|
2717
593
|
* ```ts
|
|
2718
|
-
* const options: ContextOptions
|
|
2719
|
-
* action: 'message',
|
|
2720
|
-
* client: connection,
|
|
2721
|
-
* message: dataMessage,
|
|
2722
|
-
* channel: 'chat',
|
|
2723
|
-
* initialState: { user: { id: '123', name: 'Alice' } }
|
|
2724
|
-
* }
|
|
594
|
+
* const options: ContextOptions = { action: 'message', channel: 'chat' }
|
|
2725
595
|
* ```
|
|
2726
596
|
*/
|
|
2727
597
|
interface ContextOptions<S = Record<string, unknown>> {
|
|
@@ -2737,105 +607,32 @@ interface ContextOptions<S = Record<string, unknown>> {
|
|
|
2737
607
|
initialState?: S;
|
|
2738
608
|
}
|
|
2739
609
|
/**
|
|
2740
|
-
* Create
|
|
2741
|
-
*
|
|
2742
|
-
* @remarks
|
|
2743
|
-
* Creates a lightweight middleware context using closures to ensure properties
|
|
2744
|
-
* can be destructured safely. The context provides access to request information,
|
|
2745
|
-
* state management, and control flow methods.
|
|
2746
|
-
*
|
|
2747
|
-
* @template S - Type of the state object (default: Record<string, unknown>)
|
|
2748
|
-
*
|
|
2749
|
-
* @param options - Context initialization options
|
|
2750
|
-
* @returns A new Context object
|
|
2751
|
-
*
|
|
2752
|
-
* @example
|
|
2753
|
-
* ### Basic usage
|
|
2754
|
-
* ```ts
|
|
2755
|
-
* const context = createContext({
|
|
2756
|
-
* action: 'message',
|
|
2757
|
-
* client: connection,
|
|
2758
|
-
* message: dataMessage,
|
|
2759
|
-
* channel: 'chat'
|
|
2760
|
-
* })
|
|
2761
|
-
* ```
|
|
610
|
+
* Create a new Onion-style middleware context
|
|
2762
611
|
*
|
|
2763
612
|
* @example
|
|
2764
|
-
* ### With initial state
|
|
2765
613
|
* ```ts
|
|
2766
|
-
*
|
|
2767
|
-
* user: { id: string; name: string }
|
|
2768
|
-
* requestId: string
|
|
2769
|
-
* }
|
|
2770
|
-
*
|
|
2771
|
-
* const context = createContext<AppState>({
|
|
2772
|
-
* action: 'message',
|
|
2773
|
-
* initialState: {
|
|
2774
|
-
* user: { id: '123', name: 'Alice' },
|
|
2775
|
-
* requestId: generateId()
|
|
2776
|
-
* }
|
|
2777
|
-
* })
|
|
2778
|
-
*
|
|
2779
|
-
* // Access state
|
|
2780
|
-
* const user = context.get('user')
|
|
2781
|
-
* const requestId = context.get('requestId')
|
|
614
|
+
* const context = createContext({ action: 'message', channel: 'chat' })
|
|
2782
615
|
* ```
|
|
2783
|
-
*
|
|
2784
|
-
* @see {@link Context} for the context interface
|
|
2785
616
|
*/
|
|
2786
|
-
declare function createContext<S = Record<string, unknown>>(options: ContextOptions<S>):
|
|
617
|
+
declare function createContext<S = Record<string, unknown>>(options: ContextOptions<S>): IContext<S>;
|
|
2787
618
|
/**
|
|
2788
619
|
* Context Manager - manages and executes middleware functions
|
|
2789
620
|
*
|
|
2790
|
-
* @remarks
|
|
2791
|
-
* The ContextManager is responsible for registering middleware functions
|
|
2792
|
-
* and executing them in the correct order for various actions (connect,
|
|
2793
|
-
* disconnect, message, subscribe, unsubscribe).
|
|
2794
|
-
*
|
|
2795
|
-
* Key features:
|
|
2796
|
-
* - Global middleware registration
|
|
2797
|
-
* - Per-action middleware execution
|
|
2798
|
-
* - Channel-specific middleware support
|
|
2799
|
-
* - Automatic error wrapping and reporting
|
|
2800
|
-
*
|
|
2801
621
|
* @example
|
|
2802
622
|
* ```ts
|
|
2803
|
-
* import { ContextManager } from '@syncar/server'
|
|
2804
|
-
*
|
|
2805
623
|
* const manager = new ContextManager()
|
|
2806
|
-
*
|
|
2807
|
-
*
|
|
2808
|
-
* manager.use(async (context, next) => {
|
|
2809
|
-
* console.log(`Action: ${context.req.action}`)
|
|
624
|
+
* manager.use(async (ctx, next) => {
|
|
625
|
+
* console.log(ctx.req.action)
|
|
2810
626
|
* await next()
|
|
2811
627
|
* })
|
|
2812
|
-
*
|
|
2813
|
-
* // Execute middleware for an action
|
|
2814
|
-
* const context = await manager.executeConnection(client, 'connect')
|
|
2815
628
|
* ```
|
|
2816
|
-
*
|
|
2817
|
-
* @see {@link createSyncarServer} for server-level context management
|
|
2818
629
|
*/
|
|
2819
630
|
declare class ContextManager {
|
|
2820
631
|
/** Registered middleware functions */
|
|
2821
632
|
protected readonly middlewares: IMiddleware[];
|
|
2822
633
|
/**
|
|
2823
|
-
* Register a middleware function
|
|
2824
|
-
*
|
|
2825
|
-
* @remarks
|
|
2826
|
-
* Adds a middleware function to the global middleware chain.
|
|
2827
|
-
* Middleware is executed in the order it is registered.
|
|
2828
|
-
*
|
|
2829
|
-
* @param middleware - The middleware function to register
|
|
2830
|
-
*
|
|
2831
|
-
* @example
|
|
2832
|
-
* ```ts
|
|
2833
|
-
* manager.use(async (context, next) => {
|
|
2834
|
-
* console.log('Before')
|
|
2835
|
-
* await next()
|
|
2836
|
-
* console.log('After')
|
|
2837
|
-
* })
|
|
2838
|
-
* ```
|
|
634
|
+
* Register a global middleware function
|
|
635
|
+
* @param middleware - The middleware function
|
|
2839
636
|
*/
|
|
2840
637
|
use(middleware: IMiddleware): void;
|
|
2841
638
|
/**
|
|
@@ -2929,7 +726,7 @@ declare class ContextManager {
|
|
|
2929
726
|
*
|
|
2930
727
|
* @internal
|
|
2931
728
|
*/
|
|
2932
|
-
executeConnection(client: IClientConnection, action: 'connect' | 'disconnect'): Promise<
|
|
729
|
+
executeConnection(client: IClientConnection, action: 'connect' | 'disconnect'): Promise<IContext>;
|
|
2933
730
|
/**
|
|
2934
731
|
* Execute middleware for message actions
|
|
2935
732
|
*
|
|
@@ -2948,7 +745,7 @@ declare class ContextManager {
|
|
|
2948
745
|
*
|
|
2949
746
|
* @internal
|
|
2950
747
|
*/
|
|
2951
|
-
executeMessage(client: IClientConnection, message: Message): Promise<
|
|
748
|
+
executeMessage(client: IClientConnection, message: Message): Promise<IContext>;
|
|
2952
749
|
/**
|
|
2953
750
|
* Execute middleware for subscribe actions
|
|
2954
751
|
*
|
|
@@ -2971,7 +768,7 @@ declare class ContextManager {
|
|
|
2971
768
|
*
|
|
2972
769
|
* @internal
|
|
2973
770
|
*/
|
|
2974
|
-
executeSubscribe(client: IClientConnection, channel: ChannelName, finalHandler?: () => Promise<void>): Promise<
|
|
771
|
+
executeSubscribe(client: IClientConnection, channel: ChannelName, finalHandler?: () => Promise<void>): Promise<IContext>;
|
|
2975
772
|
/**
|
|
2976
773
|
* Execute middleware for unsubscribe actions
|
|
2977
774
|
*
|
|
@@ -2994,7 +791,7 @@ declare class ContextManager {
|
|
|
2994
791
|
*
|
|
2995
792
|
* @internal
|
|
2996
793
|
*/
|
|
2997
|
-
executeUnsubscribe(client: IClientConnection, channel: ChannelName, finalHandler?: () => Promise<void>): Promise<
|
|
794
|
+
executeUnsubscribe(client: IClientConnection, channel: ChannelName, finalHandler?: () => Promise<void>): Promise<IContext>;
|
|
2998
795
|
/**
|
|
2999
796
|
* Execute middleware pipeline
|
|
3000
797
|
*
|
|
@@ -3017,7 +814,7 @@ declare class ContextManager {
|
|
|
3017
814
|
*
|
|
3018
815
|
* @internal
|
|
3019
816
|
*/
|
|
3020
|
-
execute(context:
|
|
817
|
+
execute(context: IContext, middlewares?: IMiddleware[], finalHandler?: () => Promise<void>): Promise<IContext>;
|
|
3021
818
|
/**
|
|
3022
819
|
* Create a connection context
|
|
3023
820
|
*
|
|
@@ -3035,7 +832,7 @@ declare class ContextManager {
|
|
|
3035
832
|
*
|
|
3036
833
|
* @internal
|
|
3037
834
|
*/
|
|
3038
|
-
createConnectionContext(client: IClientConnection, action: 'connect' | 'disconnect'):
|
|
835
|
+
createConnectionContext(client: IClientConnection, action: 'connect' | 'disconnect'): IContext;
|
|
3039
836
|
/**
|
|
3040
837
|
* Create a message context
|
|
3041
838
|
*
|
|
@@ -3053,7 +850,7 @@ declare class ContextManager {
|
|
|
3053
850
|
*
|
|
3054
851
|
* @internal
|
|
3055
852
|
*/
|
|
3056
|
-
createMessageContext(client: IClientConnection, message: Message):
|
|
853
|
+
createMessageContext(client: IClientConnection, message: Message): IContext;
|
|
3057
854
|
/**
|
|
3058
855
|
* Create a subscribe context
|
|
3059
856
|
*
|
|
@@ -3071,7 +868,7 @@ declare class ContextManager {
|
|
|
3071
868
|
*
|
|
3072
869
|
* @internal
|
|
3073
870
|
*/
|
|
3074
|
-
createSubscribeContext(client: IClientConnection, channel: ChannelName):
|
|
871
|
+
createSubscribeContext(client: IClientConnection, channel: ChannelName): IContext;
|
|
3075
872
|
/**
|
|
3076
873
|
* Create an unsubscribe context
|
|
3077
874
|
*
|
|
@@ -3089,7 +886,7 @@ declare class ContextManager {
|
|
|
3089
886
|
*
|
|
3090
887
|
* @internal
|
|
3091
888
|
*/
|
|
3092
|
-
createUnsubscribeContext(client: IClientConnection, channel: ChannelName):
|
|
889
|
+
createUnsubscribeContext(client: IClientConnection, channel: ChannelName): IContext;
|
|
3093
890
|
/**
|
|
3094
891
|
* Get the number of registered middleware
|
|
3095
892
|
*
|
|
@@ -3118,93 +915,14 @@ declare class ContextManager {
|
|
|
3118
915
|
|
|
3119
916
|
/**
|
|
3120
917
|
* Errors Module
|
|
3121
|
-
*
|
|
3122
|
-
* @description
|
|
3123
|
-
* Custom error classes for the Syncar server. All errors extend from
|
|
3124
|
-
* {@link SyncarError} and include error codes for programmatic handling.
|
|
3125
|
-
*
|
|
3126
|
-
* @remarks
|
|
3127
|
-
* The error hierarchy:
|
|
3128
|
-
*
|
|
3129
|
-
* - {@link SyncarError} - Base error class
|
|
3130
|
-
* - {@link ConfigError} - Server configuration issues
|
|
3131
|
-
* - {@link TransportError} - WebSocket transport issues
|
|
3132
|
-
* - {@link ChannelError} - Channel operation failures
|
|
3133
|
-
* - {@link ClientError} - Client operation failures
|
|
3134
|
-
* - {@link MessageError} - Message processing failures
|
|
3135
|
-
* - {@link ValidationError} - Input validation failures
|
|
3136
|
-
* - {@link StateError} - Invalid state operations
|
|
3137
|
-
* - {@link MiddlewareRejectionError} - Explicit middleware rejections
|
|
3138
|
-
* - {@link MiddlewareExecutionError} - Unexpected middleware errors
|
|
3139
|
-
*
|
|
3140
|
-
* @example
|
|
3141
|
-
* ### Throwing errors
|
|
3142
|
-
* ```ts
|
|
3143
|
-
* import { StateError, ValidationError } from '@syncar/server'
|
|
3144
|
-
*
|
|
3145
|
-
* function createChannel(name: string) {
|
|
3146
|
-
* if (!name) {
|
|
3147
|
-
* throw new ValidationError('Channel name is required')
|
|
3148
|
-
* }
|
|
3149
|
-
* if (!server.started) {
|
|
3150
|
-
* throw new StateError('Server must be started first')
|
|
3151
|
-
* }
|
|
3152
|
-
* }
|
|
3153
|
-
* ```
|
|
3154
|
-
*
|
|
3155
|
-
* @example
|
|
3156
|
-
* ### Catching errors
|
|
3157
|
-
* ```ts
|
|
3158
|
-
* import {
|
|
3159
|
-
* SyncarError,
|
|
3160
|
-
* MiddlewareRejectionError,
|
|
3161
|
-
* StateError
|
|
3162
|
-
* } from '@syncar/server'
|
|
3163
|
-
*
|
|
3164
|
-
* try {
|
|
3165
|
-
* await server.start()
|
|
3166
|
-
* } catch (error) {
|
|
3167
|
-
* if (error instanceof StateError) {
|
|
3168
|
-
* console.error('Invalid state:', error.message)
|
|
3169
|
-
* } else if (error instanceof MiddlewareRejectionError) {
|
|
3170
|
-
* console.error(`Action rejected: ${error.reason}`)
|
|
3171
|
-
* } else if (error instanceof SyncarError) {
|
|
3172
|
-
* console.error(`[${error.code}] ${error.message}`)
|
|
3173
|
-
* }
|
|
3174
|
-
* }
|
|
3175
|
-
* ```
|
|
3176
|
-
*
|
|
3177
|
-
* @module errors
|
|
918
|
+
* @description Custom error classes for the Syncar server.
|
|
3178
919
|
*/
|
|
3179
920
|
|
|
3180
921
|
/**
|
|
3181
922
|
* Base Syncar error class
|
|
3182
|
-
*
|
|
3183
|
-
* @remarks
|
|
3184
|
-
* All custom errors in the Syncar server extend this class. Provides
|
|
3185
|
-
* consistent error handling with error codes, context, and serialization.
|
|
3186
|
-
*
|
|
3187
|
-
* @property code - Error code for programmatic handling
|
|
3188
|
-
* @property context - Optional additional error context
|
|
3189
|
-
*
|
|
3190
|
-
* @example
|
|
3191
|
-
* ```ts
|
|
3192
|
-
* throw new SyncarError('Something went wrong', 'CUSTOM_ERROR', { userId: '123' })
|
|
3193
|
-
* ```
|
|
3194
|
-
*
|
|
3195
923
|
* @example
|
|
3196
|
-
* ### Error handling
|
|
3197
924
|
* ```ts
|
|
3198
|
-
*
|
|
3199
|
-
* // ...
|
|
3200
|
-
* } catch (error) {
|
|
3201
|
-
* if (error instanceof SyncarError) {
|
|
3202
|
-
* console.log(error.code) // 'CUSTOM_ERROR'
|
|
3203
|
-
* console.log(error.message) // 'Something went wrong'
|
|
3204
|
-
* console.log(error.context) // { userId: '123' }
|
|
3205
|
-
* console.log(error.toJSON()) // Serialized error
|
|
3206
|
-
* }
|
|
3207
|
-
* }
|
|
925
|
+
* throw new SyncarError('Something went wrong', 'INTERNAL_ERROR')
|
|
3208
926
|
* ```
|
|
3209
927
|
*/
|
|
3210
928
|
declare class SyncarError extends Error {
|
|
@@ -3389,44 +1107,9 @@ declare class StateError extends SyncarError {
|
|
|
3389
1107
|
}
|
|
3390
1108
|
/**
|
|
3391
1109
|
* Middleware rejection error
|
|
3392
|
-
*
|
|
3393
|
-
* @remarks
|
|
3394
|
-
* Thrown when middleware explicitly rejects an action using the
|
|
3395
|
-
* `context.reject()` function. This is an expected error type that
|
|
3396
|
-
* indicates intentional rejection rather than a failure.
|
|
3397
|
-
*
|
|
3398
|
-
* @property reason - Human-readable reason for the rejection
|
|
3399
|
-
* @property action - The action that was rejected
|
|
3400
|
-
* @property code - Optional error code for programmatic handling
|
|
3401
|
-
* @property context - Additional context about the rejection
|
|
3402
|
-
*
|
|
3403
|
-
* @example
|
|
3404
|
-
* ### Throwing from middleware
|
|
3405
|
-
* ```ts
|
|
3406
|
-
* const middleware: Middleware = async (context, next) => {
|
|
3407
|
-
* if (!context.req.client) {
|
|
3408
|
-
* context.reject('Client is required')
|
|
3409
|
-
* // Function never returns (throws MiddlewareRejectionError)
|
|
3410
|
-
* }
|
|
3411
|
-
* await next()
|
|
3412
|
-
* }
|
|
3413
|
-
* ```
|
|
3414
|
-
*
|
|
3415
1110
|
* @example
|
|
3416
|
-
* ### Catching rejections
|
|
3417
1111
|
* ```ts
|
|
3418
|
-
*
|
|
3419
|
-
* await manager.executeConnection(client, 'connect')
|
|
3420
|
-
* } catch (error) {
|
|
3421
|
-
* if (error instanceof MiddlewareRejectionError) {
|
|
3422
|
-
* console.log(`Action '${error.action}' rejected: ${error.reason}`)
|
|
3423
|
-
* // Send error to client
|
|
3424
|
-
* client.socket.send(JSON.stringify({
|
|
3425
|
-
* type: 'error',
|
|
3426
|
-
* data: { message: error.reason, code: error.code }
|
|
3427
|
-
* }))
|
|
3428
|
-
* }
|
|
3429
|
-
* }
|
|
1112
|
+
* if (!context.req.client) context.reject('Client is required')
|
|
3430
1113
|
* ```
|
|
3431
1114
|
*/
|
|
3432
1115
|
declare class MiddlewareRejectionError extends Error implements IMiddlewareRejectionError {
|
|
@@ -3599,4 +1282,131 @@ declare class MiddlewareExecutionError extends Error {
|
|
|
3599
1282
|
toString(): string;
|
|
3600
1283
|
}
|
|
3601
1284
|
|
|
3602
|
-
|
|
1285
|
+
/**
|
|
1286
|
+
* Config Module
|
|
1287
|
+
* Single source of truth for all constant values and default configuration for the server.
|
|
1288
|
+
*
|
|
1289
|
+
* @module config
|
|
1290
|
+
*/
|
|
1291
|
+
/**
|
|
1292
|
+
* WebSocket close codes
|
|
1293
|
+
* Based on RFC 6455 and custom codes for application-specific closures
|
|
1294
|
+
*/
|
|
1295
|
+
declare const CLOSE_CODES: {
|
|
1296
|
+
/**
|
|
1297
|
+
* Normal closure
|
|
1298
|
+
*/
|
|
1299
|
+
readonly NORMAL: 1000;
|
|
1300
|
+
/**
|
|
1301
|
+
* Endpoint is going away
|
|
1302
|
+
*/
|
|
1303
|
+
readonly GOING_AWAY: 1001;
|
|
1304
|
+
/**
|
|
1305
|
+
* Protocol error
|
|
1306
|
+
*/
|
|
1307
|
+
readonly PROTOCOL_ERROR: 1002;
|
|
1308
|
+
/**
|
|
1309
|
+
* Unsupported data
|
|
1310
|
+
*/
|
|
1311
|
+
readonly UNSUPPORTED_DATA: 1003;
|
|
1312
|
+
/**
|
|
1313
|
+
* No status received
|
|
1314
|
+
*/
|
|
1315
|
+
readonly NO_STATUS: 1005;
|
|
1316
|
+
/**
|
|
1317
|
+
* Abnormal closure
|
|
1318
|
+
*/
|
|
1319
|
+
readonly ABNORMAL: 1006;
|
|
1320
|
+
/**
|
|
1321
|
+
* Invalid frame payload data
|
|
1322
|
+
*/
|
|
1323
|
+
readonly INVALID_PAYLOAD: 1007;
|
|
1324
|
+
/**
|
|
1325
|
+
* Policy violation
|
|
1326
|
+
*/
|
|
1327
|
+
readonly POLICY_VIOLATION: 1008;
|
|
1328
|
+
/**
|
|
1329
|
+
* Message too big
|
|
1330
|
+
*/
|
|
1331
|
+
readonly MESSAGE_TOO_BIG: 1009;
|
|
1332
|
+
/**
|
|
1333
|
+
* Missing extension
|
|
1334
|
+
*/
|
|
1335
|
+
readonly MISSING_EXTENSION: 1010;
|
|
1336
|
+
/**
|
|
1337
|
+
* Internal error
|
|
1338
|
+
*/
|
|
1339
|
+
readonly INTERNAL_ERROR: 1011;
|
|
1340
|
+
/**
|
|
1341
|
+
* Service restart
|
|
1342
|
+
*/
|
|
1343
|
+
readonly SERVICE_RESTART: 1012;
|
|
1344
|
+
/**
|
|
1345
|
+
* Try again later
|
|
1346
|
+
*/
|
|
1347
|
+
readonly TRY_AGAIN_LATER: 1013;
|
|
1348
|
+
/**
|
|
1349
|
+
* Connection rejected by middleware
|
|
1350
|
+
*/
|
|
1351
|
+
readonly REJECTED: 4001;
|
|
1352
|
+
/**
|
|
1353
|
+
* Rate limit exceeded
|
|
1354
|
+
*/
|
|
1355
|
+
readonly RATE_LIMITED: 4002;
|
|
1356
|
+
/**
|
|
1357
|
+
* Channel not found
|
|
1358
|
+
*/
|
|
1359
|
+
readonly CHANNEL_NOT_FOUND: 4003;
|
|
1360
|
+
/**
|
|
1361
|
+
* Unauthorized
|
|
1362
|
+
*/
|
|
1363
|
+
readonly UNAUTHORIZED: 4005;
|
|
1364
|
+
};
|
|
1365
|
+
/**
|
|
1366
|
+
* Application error codes
|
|
1367
|
+
* Used in error messages sent to clients
|
|
1368
|
+
*/
|
|
1369
|
+
declare const ERROR_CODES: {
|
|
1370
|
+
/**
|
|
1371
|
+
* Action rejected by middleware
|
|
1372
|
+
*/
|
|
1373
|
+
readonly REJECTED: "REJECTED";
|
|
1374
|
+
/**
|
|
1375
|
+
* Channel name missing from message
|
|
1376
|
+
*/
|
|
1377
|
+
readonly MISSING_CHANNEL: "MISSING_CHANNEL";
|
|
1378
|
+
/**
|
|
1379
|
+
* Subscribe action rejected
|
|
1380
|
+
*/
|
|
1381
|
+
readonly SUBSCRIBE_REJECTED: "SUBSCRIBE_REJECTED";
|
|
1382
|
+
/**
|
|
1383
|
+
* Unsubscribe action rejected
|
|
1384
|
+
*/
|
|
1385
|
+
readonly UNSUBSCRIBE_REJECTED: "UNSUBSCRIBE_REJECTED";
|
|
1386
|
+
/**
|
|
1387
|
+
* Rate limit exceeded
|
|
1388
|
+
*/
|
|
1389
|
+
readonly RATE_LIMITED: "RATE_LIMITED";
|
|
1390
|
+
/**
|
|
1391
|
+
* Authentication failed
|
|
1392
|
+
*/
|
|
1393
|
+
readonly AUTH_FAILED: "AUTH_FAILED";
|
|
1394
|
+
/**
|
|
1395
|
+
* Authorization failed
|
|
1396
|
+
*/
|
|
1397
|
+
readonly NOT_AUTHORIZED: "NOT_AUTHORIZED";
|
|
1398
|
+
/**
|
|
1399
|
+
* Channel not allowed
|
|
1400
|
+
*/
|
|
1401
|
+
readonly CHANNEL_NOT_ALLOWED: "CHANNEL_NOT_ALLOWED";
|
|
1402
|
+
/**
|
|
1403
|
+
* Invalid message format
|
|
1404
|
+
*/
|
|
1405
|
+
readonly INVALID_MESSAGE: "INVALID_MESSAGE";
|
|
1406
|
+
/**
|
|
1407
|
+
* Server error
|
|
1408
|
+
*/
|
|
1409
|
+
readonly SERVER_ERROR: "SERVER_ERROR";
|
|
1410
|
+
};
|
|
1411
|
+
|
|
1412
|
+
export { CLOSE_CODES, Channel, ChannelError, type ChannelFlow, ChannelName, type ChannelOptions, ClientError, ClientId, ConfigError, ContextManager, DataMessage, ERROR_CODES, type IChannelState, IContext, type IMessageHandler, IMiddleware, type IServerOptions, type IServerStats, Message, MessageError, MiddlewareExecutionError, MiddlewareRejectionError, StateError, SyncarServer as Syncar, SyncarError, SyncarServer, TransportError, ValidationError, WebSocketServerTransport, createContext, createSyncarServer };
|