@syncar/server 1.0.0-alpha.1
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 +140 -0
- package/dist/index.d.ts +3602 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3602 @@
|
|
|
1
|
+
import * as node_https from 'node:https';
|
|
2
|
+
import * as node_http from 'node:http';
|
|
3
|
+
import { IncomingMessage } from 'node:http';
|
|
4
|
+
import WebSocket, { ServerOptions, WebSocketServer } from 'ws';
|
|
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 Synca 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 Synca 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 '@synca/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 '@synca/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
|
+
};
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* Channel state information
|
|
945
|
+
*
|
|
946
|
+
* @remarks
|
|
947
|
+
* Provides runtime information about a channel including its name,
|
|
948
|
+
* subscriber count, creation time, and last message timestamp.
|
|
949
|
+
*
|
|
950
|
+
* @property name - The channel name
|
|
951
|
+
* @property subscriberCount - Current number of subscribers
|
|
952
|
+
* @property createdAt - Unix timestamp (ms) when the channel was created
|
|
953
|
+
* @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
|
+
*/
|
|
965
|
+
interface IChannelState {
|
|
966
|
+
/** The channel name */
|
|
967
|
+
name: string;
|
|
968
|
+
/** Current number of subscribers */
|
|
969
|
+
subscriberCount: number;
|
|
970
|
+
/** Unix timestamp (ms) when the channel was created */
|
|
971
|
+
createdAt: number;
|
|
972
|
+
/** Unix timestamp (ms) of the last published message */
|
|
973
|
+
lastMessageAt?: number;
|
|
974
|
+
}
|
|
975
|
+
/**
|
|
976
|
+
* Publish options for channel messages
|
|
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
|
|
1275
|
+
*
|
|
1276
|
+
* @example
|
|
1277
|
+
* ```ts
|
|
1278
|
+
* const chat = server.createMulticast('chat', { chunkSize: 1000 })
|
|
1279
|
+
* ```
|
|
1280
|
+
*/
|
|
1281
|
+
interface MulticastChannelOptions {
|
|
1282
|
+
/**
|
|
1283
|
+
* Number of clients to process per chunk for large broadcasts
|
|
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;
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* Multicast Channel - sends messages to subscribed clients only
|
|
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')
|
|
1359
|
+
*
|
|
1360
|
+
* // Check if subscribed
|
|
1361
|
+
* if (chat.hasSubscriber('client-123')) {
|
|
1362
|
+
* console.log('Client is subscribed')
|
|
1363
|
+
* }
|
|
1364
|
+
*
|
|
1365
|
+
* // Get all subscribers
|
|
1366
|
+
* const subscribers = chat.getSubscribers()
|
|
1367
|
+
* console.log(`Subscribers: ${Array.from(subscribers).join(', ')}`)
|
|
1368
|
+
* ```
|
|
1369
|
+
*
|
|
1370
|
+
* @example
|
|
1371
|
+
* ### Channel middleware
|
|
1372
|
+
* ```ts
|
|
1373
|
+
* chat.use(async (context, next) => {
|
|
1374
|
+
* if (context.req.action === 'message') {
|
|
1375
|
+
* // Filter profanity
|
|
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()
|
|
1382
|
+
* })
|
|
1383
|
+
* ```
|
|
1384
|
+
*
|
|
1385
|
+
* @see {@link BroadcastChannel} for broadcasting to all clients
|
|
1386
|
+
*/
|
|
1387
|
+
declare class MulticastChannel<T = unknown> extends BaseChannel<T> {
|
|
1388
|
+
private readonly middlewares;
|
|
1389
|
+
private readonly messageHandlers;
|
|
1390
|
+
/**
|
|
1391
|
+
* Creates a new MulticastChannel instance
|
|
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
|
+
*/
|
|
1403
|
+
constructor(config: {
|
|
1404
|
+
/** The channel name (must not start with `__`) */
|
|
1405
|
+
name: ChannelName;
|
|
1406
|
+
/** The client registry for connection management */
|
|
1407
|
+
registry: ClientRegistry;
|
|
1408
|
+
/** Optional channel configuration */
|
|
1409
|
+
options?: MulticastChannelOptions;
|
|
1410
|
+
});
|
|
1411
|
+
/**
|
|
1412
|
+
* Get all subscribed clients as target recipients
|
|
1413
|
+
*
|
|
1414
|
+
* @remarks
|
|
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
|
|
1422
|
+
*/
|
|
1423
|
+
protected getTargetClients(_options?: IPublishOptions): ClientId[];
|
|
1424
|
+
/**
|
|
1425
|
+
* Register a channel-specific middleware
|
|
1426
|
+
*
|
|
1427
|
+
* @remarks
|
|
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
|
|
1448
|
+
*/
|
|
1449
|
+
use(middleware: IMiddleware): void;
|
|
1450
|
+
/**
|
|
1451
|
+
* Get the middleware for this channel
|
|
1452
|
+
*
|
|
1453
|
+
* @returns Array of middleware functions registered on this channel
|
|
1454
|
+
*/
|
|
1455
|
+
getMiddlewares(): IMiddleware[];
|
|
1456
|
+
/**
|
|
1457
|
+
* Get the number of subscribers
|
|
1458
|
+
*
|
|
1459
|
+
* @returns The number of clients subscribed to this channel
|
|
1460
|
+
*
|
|
1461
|
+
* @example
|
|
1462
|
+
* ```ts
|
|
1463
|
+
* console.log(`Chat subscribers: ${chat.subscriberCount}`)
|
|
1464
|
+
* ```
|
|
1465
|
+
*/
|
|
1466
|
+
get subscriberCount(): number;
|
|
1467
|
+
/**
|
|
1468
|
+
* Register a message handler for incoming messages
|
|
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.
|
|
1493
|
+
*/
|
|
1494
|
+
onMessage(handler: IMessageHandler<T>): () => void;
|
|
1495
|
+
/**
|
|
1496
|
+
* Subscribe a client to this channel
|
|
1497
|
+
*
|
|
1498
|
+
* @remarks
|
|
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
|
+
* ```
|
|
1514
|
+
*/
|
|
1515
|
+
subscribe(subscriber: SubscriberId): boolean;
|
|
1516
|
+
/**
|
|
1517
|
+
* Unsubscribe a client from this channel
|
|
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')
|
|
1530
|
+
*
|
|
1531
|
+
* if (chat.unsubscribe('client-456')) {
|
|
1532
|
+
* console.log('Unsubscribed successfully')
|
|
1533
|
+
* }
|
|
1534
|
+
* ```
|
|
1535
|
+
*/
|
|
1536
|
+
unsubscribe(subscriber: SubscriberId): boolean;
|
|
1537
|
+
/**
|
|
1538
|
+
* Dispatch an incoming client message
|
|
1539
|
+
*
|
|
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
|
+
* @internal
|
|
1573
|
+
*/
|
|
1574
|
+
dispatch(data: T, client: IClientConnection, message: DataMessage<T>): Promise<void>;
|
|
1575
|
+
/**
|
|
1576
|
+
* Check if a client is subscribed to this channel
|
|
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
|
+
* ```
|
|
1587
|
+
*/
|
|
1588
|
+
hasSubscriber(subscriber: SubscriberId): boolean;
|
|
1589
|
+
/**
|
|
1590
|
+
* Get all subscribers to this channel
|
|
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
|
+
* ```
|
|
1608
|
+
*/
|
|
1609
|
+
getSubscribers(): Set<SubscriberId>;
|
|
1610
|
+
/**
|
|
1611
|
+
* Check if the channel has no subscribers
|
|
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
|
+
* ```
|
|
1621
|
+
*/
|
|
1622
|
+
isEmpty(): boolean;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
type ServerInstance = WebSocketServer;
|
|
1626
|
+
/**
|
|
1627
|
+
* WebSocket Server Transport Configuration
|
|
1628
|
+
*
|
|
1629
|
+
* @remarks
|
|
1630
|
+
* Configuration options for the WebSocket transport layer. Extends the
|
|
1631
|
+
* standard `ws` library options with Synca-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
|
+
*/
|
|
1648
|
+
interface WebSocketServerTransportConfig extends ServerOptions {
|
|
1649
|
+
/**
|
|
1650
|
+
* Enable client ping/pong
|
|
1651
|
+
*
|
|
1652
|
+
* @remarks
|
|
1653
|
+
* When enabled, the server sends periodic ping frames to detect
|
|
1654
|
+
* dead connections and maintain keep-alive.
|
|
1655
|
+
*
|
|
1656
|
+
* @default true
|
|
1657
|
+
*/
|
|
1658
|
+
enablePing?: boolean;
|
|
1659
|
+
/**
|
|
1660
|
+
* Ping interval in milliseconds
|
|
1661
|
+
*
|
|
1662
|
+
* @remarks
|
|
1663
|
+
* Time between ping frames when `enablePing` is true.
|
|
1664
|
+
*
|
|
1665
|
+
* @default 30000 (30 seconds)
|
|
1666
|
+
*/
|
|
1667
|
+
pingInterval?: number;
|
|
1668
|
+
/**
|
|
1669
|
+
* Ping timeout in milliseconds
|
|
1670
|
+
*
|
|
1671
|
+
* @remarks
|
|
1672
|
+
* Time to wait for pong response before closing connection.
|
|
1673
|
+
*
|
|
1674
|
+
* @default 5000 (5 seconds)
|
|
1675
|
+
*/
|
|
1676
|
+
pingTimeout?: number;
|
|
1677
|
+
/**
|
|
1678
|
+
* Shared connection map
|
|
1679
|
+
*
|
|
1680
|
+
* @remarks
|
|
1681
|
+
* Optional map for sharing connections across multiple server instances.
|
|
1682
|
+
* If not provided, a new map will be created.
|
|
1683
|
+
*/
|
|
1684
|
+
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
|
+
/**
|
|
1702
|
+
* Custom WebSocket Server constructor
|
|
1703
|
+
*
|
|
1704
|
+
* @remarks
|
|
1705
|
+
* Allows using a custom WebSocket server implementation.
|
|
1706
|
+
* Defaults to the standard `ws` WebSocketServer.
|
|
1707
|
+
*/
|
|
1708
|
+
ServerConstructor?: new (config: ServerOptions) => ServerInstance;
|
|
1709
|
+
/**
|
|
1710
|
+
* Logger instance
|
|
1711
|
+
*
|
|
1712
|
+
* @remarks
|
|
1713
|
+
* Optional logger for transport-level logging.
|
|
1714
|
+
*/
|
|
1715
|
+
logger?: ILogger;
|
|
1716
|
+
}
|
|
1717
|
+
/**
|
|
1718
|
+
* WebSocket Server Transport
|
|
1719
|
+
*
|
|
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 '@synca/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
|
+
* @example
|
|
1773
|
+
* ### With shared connections
|
|
1774
|
+
* ```ts
|
|
1775
|
+
* const sharedConnections = new Map()
|
|
1776
|
+
*
|
|
1777
|
+
* const transport1 = new WebSocketServerTransport({
|
|
1778
|
+
* connections: sharedConnections
|
|
1779
|
+
* })
|
|
1780
|
+
*
|
|
1781
|
+
* const transport2 = new WebSocketServerTransport({
|
|
1782
|
+
* connections: sharedConnections
|
|
1783
|
+
* })
|
|
1784
|
+
* ```
|
|
1785
|
+
*
|
|
1786
|
+
* @see {@link EventEmitter} for event methods (on, off, emit, etc.)
|
|
1787
|
+
*/
|
|
1788
|
+
declare class WebSocketServerTransport extends EventEmitter {
|
|
1789
|
+
/**
|
|
1790
|
+
* Map of connected clients by ID
|
|
1791
|
+
*
|
|
1792
|
+
* @remarks
|
|
1793
|
+
* Public map of all active connections. Can be used to look up clients
|
|
1794
|
+
* by ID or iterate over all connections.
|
|
1795
|
+
*/
|
|
1796
|
+
readonly connections: Map<ClientId, IClientConnection>;
|
|
1797
|
+
/** @internal */
|
|
1798
|
+
private readonly wsServer;
|
|
1799
|
+
/** @internal */
|
|
1800
|
+
private readonly config;
|
|
1801
|
+
/** @internal */
|
|
1802
|
+
private pingTimer?;
|
|
1803
|
+
/** @internal */
|
|
1804
|
+
private authenticator?;
|
|
1805
|
+
/**
|
|
1806
|
+
* Creates a new WebSocket Server Transport instance
|
|
1807
|
+
*
|
|
1808
|
+
* @remarks
|
|
1809
|
+
* Initializes the WebSocket transport layer with the provided configuration.
|
|
1810
|
+
* Sets up the underlying WebSocketServer, configures ping/pong, and
|
|
1811
|
+
* establishes event handlers.
|
|
1812
|
+
*
|
|
1813
|
+
* @param config - Transport configuration options
|
|
1814
|
+
*
|
|
1815
|
+
* @example
|
|
1816
|
+
* ```ts
|
|
1817
|
+
* const transport = new WebSocketServerTransport({
|
|
1818
|
+
* server: httpServer,
|
|
1819
|
+
* path: '/ws',
|
|
1820
|
+
* enablePing: true,
|
|
1821
|
+
* pingInterval: 30000,
|
|
1822
|
+
* pingTimeout: 5000
|
|
1823
|
+
* })
|
|
1824
|
+
* ```
|
|
1825
|
+
*
|
|
1826
|
+
* @emits connection When a new client connects
|
|
1827
|
+
* @emits disconnection When a client disconnects
|
|
1828
|
+
* @emits message When a message is received from a client
|
|
1829
|
+
* @emits error When an error occurs
|
|
1830
|
+
*/
|
|
1831
|
+
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
|
+
private setupEventHandlers;
|
|
1856
|
+
private handleConnection;
|
|
1857
|
+
private handleMessage;
|
|
1858
|
+
private handleDisconnection;
|
|
1859
|
+
private setupPingPong;
|
|
1860
|
+
private startPingTimer;
|
|
1861
|
+
private checkConnections;
|
|
1862
|
+
}
|
|
1863
|
+
|
|
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 = createSyncaServer({ 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
|
+
interface IServerStats {
|
|
1888
|
+
/** Number of currently connected clients */
|
|
1889
|
+
clientCount: number;
|
|
1890
|
+
/** Number of active channels */
|
|
1891
|
+
channelCount: number;
|
|
1892
|
+
/** Total number of channel subscriptions across all channels */
|
|
1893
|
+
subscriptionCount: number;
|
|
1894
|
+
/** Unix timestamp (ms) when the server was started */
|
|
1895
|
+
startedAt?: number;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
/**
|
|
1899
|
+
* Server configuration options
|
|
1900
|
+
*
|
|
1901
|
+
* @remarks
|
|
1902
|
+
* Complete configuration interface for the Synca server. These options
|
|
1903
|
+
* control the WebSocket transport layer, connection handling, middleware,
|
|
1904
|
+
* and performance tuning parameters.
|
|
1905
|
+
*
|
|
1906
|
+
* @example
|
|
1907
|
+
* ```ts
|
|
1908
|
+
* import { createSyncaServer } from '@synca/server'
|
|
1909
|
+
*
|
|
1910
|
+
* const server = createSyncaServer({
|
|
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
|
+
* @see {@link DEFAULT_SERVER_CONFIG} for default values
|
|
1922
|
+
*/
|
|
1923
|
+
interface IServerOptions {
|
|
1924
|
+
/**
|
|
1925
|
+
* HTTP or HTTPS server instance
|
|
1926
|
+
*
|
|
1927
|
+
* @remarks
|
|
1928
|
+
* If provided, the WebSocket server will attach to this existing server.
|
|
1929
|
+
* If not provided, a new HTTP server will be created automatically.
|
|
1930
|
+
*/
|
|
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;
|
|
1948
|
+
/**
|
|
1949
|
+
* Custom logger instance
|
|
1950
|
+
*
|
|
1951
|
+
* @remarks
|
|
1952
|
+
* Logger conforming to the {@link ILogger} interface. Used for
|
|
1953
|
+
* debugging, error reporting, and operational monitoring.
|
|
1954
|
+
*/
|
|
1955
|
+
logger: ILogger;
|
|
1956
|
+
/**
|
|
1957
|
+
* Port to listen on (default: 3000)
|
|
1958
|
+
*
|
|
1959
|
+
* @remarks
|
|
1960
|
+
* Only used when creating a new HTTP server. Ignored if `server`
|
|
1961
|
+
* option is provided.
|
|
1962
|
+
*/
|
|
1963
|
+
port: number;
|
|
1964
|
+
/**
|
|
1965
|
+
* Host to bind to (default: '0.0.0.0')
|
|
1966
|
+
*
|
|
1967
|
+
* @remarks
|
|
1968
|
+
* Determines which network interface the server listens on.
|
|
1969
|
+
* Use 'localhost' for local-only access or '0.0.0.0' for all interfaces.
|
|
1970
|
+
*/
|
|
1971
|
+
host: string;
|
|
1972
|
+
/**
|
|
1973
|
+
* WebSocket path (default: '/synca')
|
|
1974
|
+
*
|
|
1975
|
+
* @remarks
|
|
1976
|
+
* The URL path for WebSocket connections. Clients must connect to
|
|
1977
|
+
* `ws://host:port/path` to establish a connection.
|
|
1978
|
+
*/
|
|
1979
|
+
path: string;
|
|
1980
|
+
/**
|
|
1981
|
+
* Transport implementation
|
|
1982
|
+
*
|
|
1983
|
+
* @remarks
|
|
1984
|
+
* Custom WebSocket transport layer. Defaults to {@link WebSocketServerTransport}
|
|
1985
|
+
* if not provided. Allows for custom transport implementations.
|
|
1986
|
+
*/
|
|
1987
|
+
transport: WebSocketServerTransport;
|
|
1988
|
+
/**
|
|
1989
|
+
* Enable automatic ping/pong (default: true)
|
|
1990
|
+
*
|
|
1991
|
+
* @remarks
|
|
1992
|
+
* When enabled, the server sends periodic ping frames to detect
|
|
1993
|
+
* dead connections and maintain keep-alive.
|
|
1994
|
+
*/
|
|
1995
|
+
enablePing: boolean;
|
|
1996
|
+
/**
|
|
1997
|
+
* Ping interval in ms (default: 30000)
|
|
1998
|
+
*
|
|
1999
|
+
* @remarks
|
|
2000
|
+
* Time between ping frames when `enablePing` is true.
|
|
2001
|
+
* Lower values detect dead connections faster but increase bandwidth.
|
|
2002
|
+
*/
|
|
2003
|
+
pingInterval: number;
|
|
2004
|
+
/**
|
|
2005
|
+
* Ping timeout in ms (default: 5000)
|
|
2006
|
+
*
|
|
2007
|
+
* @remarks
|
|
2008
|
+
* Time to wait for pong response before closing connection.
|
|
2009
|
+
* Should be significantly less than `pingInterval`.
|
|
2010
|
+
*/
|
|
2011
|
+
pingTimeout: number;
|
|
2012
|
+
/**
|
|
2013
|
+
* Client registry instance
|
|
2014
|
+
*
|
|
2015
|
+
* @remarks
|
|
2016
|
+
* Shared registry for tracking clients and subscriptions.
|
|
2017
|
+
* Allows multiple server instances to share state.
|
|
2018
|
+
*/
|
|
2019
|
+
registry: ClientRegistry;
|
|
2020
|
+
/**
|
|
2021
|
+
* Global middleware chain
|
|
2022
|
+
*
|
|
2023
|
+
* @remarks
|
|
2024
|
+
* Middleware functions applied to all actions before channel-specific middleware.
|
|
2025
|
+
* Executed in the order they are defined.
|
|
2026
|
+
*
|
|
2027
|
+
* @example
|
|
2028
|
+
* ```ts
|
|
2029
|
+
* middleware: [
|
|
2030
|
+
* createAuthMiddleware({ verifyToken }),
|
|
2031
|
+
* createLoggingMiddleware(),
|
|
2032
|
+
* createRateLimitMiddleware({ maxRequests: 100 })
|
|
2033
|
+
* ]
|
|
2034
|
+
* ```
|
|
2035
|
+
*/
|
|
2036
|
+
middleware: IMiddleware[];
|
|
2037
|
+
/**
|
|
2038
|
+
* Chunk size for large broadcasts (default: 500)
|
|
2039
|
+
*
|
|
2040
|
+
* @remarks
|
|
2041
|
+
* When broadcasting to more than this many clients, messages are sent
|
|
2042
|
+
* in chunks to avoid blocking the event loop. Lower values reduce latency
|
|
2043
|
+
* per chunk but increase total broadcast time.
|
|
2044
|
+
*/
|
|
2045
|
+
broadcastChunkSize: number;
|
|
2046
|
+
}
|
|
2047
|
+
/**
|
|
2048
|
+
* Synca Server - Real-time WebSocket server with pub/sub channels
|
|
2049
|
+
*
|
|
2050
|
+
* @remarks
|
|
2051
|
+
* The main server class providing WebSocket communication with broadcast
|
|
2052
|
+
* and multicast channels, middleware support, and connection management.
|
|
2053
|
+
*
|
|
2054
|
+
* @example
|
|
2055
|
+
* ```ts
|
|
2056
|
+
* import { createSyncaServer } from '@synca/server'
|
|
2057
|
+
*
|
|
2058
|
+
* const server = createSyncaServer({ port: 3000 })
|
|
2059
|
+
* await server.start()
|
|
2060
|
+
*
|
|
2061
|
+
* // Create channels
|
|
2062
|
+
* const broadcast = server.createBroadcast<string>()
|
|
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!' })
|
|
2073
|
+
* ```
|
|
2074
|
+
*
|
|
2075
|
+
* @see {@link createSyncaServer} for factory function
|
|
2076
|
+
*/
|
|
2077
|
+
declare class SyncaServer {
|
|
2078
|
+
private readonly config;
|
|
2079
|
+
private transport;
|
|
2080
|
+
readonly registry: ClientRegistry;
|
|
2081
|
+
private readonly context;
|
|
2082
|
+
private readonly status;
|
|
2083
|
+
private connectionHandler;
|
|
2084
|
+
private messageHandler;
|
|
2085
|
+
private signalHandler;
|
|
2086
|
+
private broadcastChannel;
|
|
2087
|
+
constructor(config: IServerOptions);
|
|
2088
|
+
/**
|
|
2089
|
+
* Start the server and begin accepting connections
|
|
2090
|
+
*
|
|
2091
|
+
* @remarks
|
|
2092
|
+
* Initializes the WebSocket transport layer, sets up event handlers,
|
|
2093
|
+
* creates the broadcast channel, and prepares the server for connections.
|
|
2094
|
+
*
|
|
2095
|
+
* @throws {StateError} If the server is already started
|
|
2096
|
+
*
|
|
2097
|
+
* @example
|
|
2098
|
+
* ```ts
|
|
2099
|
+
* const server = createSyncaServer({ port: 3000 })
|
|
2100
|
+
* await server.start()
|
|
2101
|
+
* console.log('Server is running')
|
|
2102
|
+
* ```
|
|
2103
|
+
*/
|
|
2104
|
+
start(): Promise<void>;
|
|
2105
|
+
/**
|
|
2106
|
+
* Stop the server and close all connections
|
|
2107
|
+
*
|
|
2108
|
+
* @remarks
|
|
2109
|
+
* Gracefully shuts down the server by clearing all handlers,
|
|
2110
|
+
* removing all channels, and allowing the transport layer to close.
|
|
2111
|
+
* Existing connections will be terminated.
|
|
2112
|
+
*
|
|
2113
|
+
* @example
|
|
2114
|
+
* ```ts
|
|
2115
|
+
* await server.stop()
|
|
2116
|
+
* console.log('Server stopped')
|
|
2117
|
+
* ```
|
|
2118
|
+
*/
|
|
2119
|
+
stop(): Promise<void>;
|
|
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>;
|
|
2153
|
+
/**
|
|
2154
|
+
* Create or retrieve a multicast channel
|
|
2155
|
+
*
|
|
2156
|
+
* @remarks
|
|
2157
|
+
* Creates a named channel that delivers messages only to subscribed clients.
|
|
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.
|
|
2160
|
+
*
|
|
2161
|
+
* @template T - Type of data to be published on this channel (default: unknown)
|
|
2162
|
+
* @param name - Unique channel name (must not start with `__` which is reserved)
|
|
2163
|
+
* @returns The multicast channel instance
|
|
2164
|
+
*
|
|
2165
|
+
* @throws {StateError} If the server hasn't been started yet
|
|
2166
|
+
* @throws {ValidationError} If the channel name is invalid (starts with `__`)
|
|
2167
|
+
*
|
|
2168
|
+
* @example
|
|
2169
|
+
* ```ts
|
|
2170
|
+
* // Create a chat channel
|
|
2171
|
+
* const chat = server.createMulticast<{ text: string; user: string }>('chat')
|
|
2172
|
+
*
|
|
2173
|
+
* // Handle incoming messages
|
|
2174
|
+
* chat.onMessage((data, client) => {
|
|
2175
|
+
* console.log(`${client.id}: ${data.text}`)
|
|
2176
|
+
* // Relay to all subscribers except sender
|
|
2177
|
+
* chat.publish(data, { exclude: [client.id] })
|
|
2178
|
+
* })
|
|
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
|
+
* ```
|
|
2192
|
+
*
|
|
2193
|
+
* @see {@link MulticastChannel} for channel API
|
|
2194
|
+
* @see {@link BROADCAST_CHANNEL} for reserved channel name
|
|
2195
|
+
*/
|
|
2196
|
+
createMulticast<T = unknown>(name: ChannelName): MulticastChannel<T>;
|
|
2197
|
+
/**
|
|
2198
|
+
* Check if a channel exists
|
|
2199
|
+
*
|
|
2200
|
+
* @param name - The channel name to check
|
|
2201
|
+
* @returns `true` if a channel with this name exists, `false` otherwise
|
|
2202
|
+
*
|
|
2203
|
+
* @example
|
|
2204
|
+
* ```ts
|
|
2205
|
+
* if (!server.hasChannel('chat')) {
|
|
2206
|
+
* const chat = server.createMulticast('chat')
|
|
2207
|
+
* }
|
|
2208
|
+
* ```
|
|
2209
|
+
*/
|
|
2210
|
+
hasChannel(name: ChannelName): boolean;
|
|
2211
|
+
/**
|
|
2212
|
+
* Get all active channel names
|
|
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 '@synca/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
|
|
2247
|
+
*
|
|
2248
|
+
* @remarks
|
|
2249
|
+
* Sets an authenticator function that receives the HTTP upgrade request
|
|
2250
|
+
* and returns a client ID. The authenticator can throw to reject the connection.
|
|
2251
|
+
* This is useful for implementing token-based authentication during the
|
|
2252
|
+
* WebSocket handshake.
|
|
2253
|
+
*
|
|
2254
|
+
* @param authenticator - A function that receives the HTTP upgrade request
|
|
2255
|
+
* and returns a ClientId (or throws to reject the connection)
|
|
2256
|
+
*
|
|
2257
|
+
* @example
|
|
2258
|
+
* ```ts
|
|
2259
|
+
* server.authenticate(async (request) => {
|
|
2260
|
+
* const token = request.headers.authorization?.split(' ')[1]
|
|
2261
|
+
* if (!token) {
|
|
2262
|
+
* throw new Error('No token provided')
|
|
2263
|
+
* }
|
|
2264
|
+
* const user = await verifyJwt(token)
|
|
2265
|
+
* return user.id
|
|
2266
|
+
* })
|
|
2267
|
+
* ```
|
|
2268
|
+
*/
|
|
2269
|
+
authenticate(authenticator: (request: node_http.IncomingMessage) => string | Promise<string>): void;
|
|
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 Synca server with automatic WebSocket transport setup
|
|
2328
|
+
*
|
|
2329
|
+
* @remarks
|
|
2330
|
+
* Factory function that creates a configured SyncaServer 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 Synca server instance ready to be started
|
|
2338
|
+
*
|
|
2339
|
+
* @example
|
|
2340
|
+
* ### Basic usage
|
|
2341
|
+
* ```ts
|
|
2342
|
+
* import { createSyncaServer } from '@synca/server'
|
|
2343
|
+
*
|
|
2344
|
+
* const server = createSyncaServer({ port: 3000 })
|
|
2345
|
+
* await server.start()
|
|
2346
|
+
* ```
|
|
2347
|
+
*
|
|
2348
|
+
* @example
|
|
2349
|
+
* ### With custom configuration
|
|
2350
|
+
* ```ts
|
|
2351
|
+
* const server = createSyncaServer({
|
|
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 { createSyncaServer } from '@synca/server'
|
|
2368
|
+
*
|
|
2369
|
+
* const httpServer = createServer((req, res) => {
|
|
2370
|
+
* res.writeHead(200)
|
|
2371
|
+
* res.end('OK')
|
|
2372
|
+
* })
|
|
2373
|
+
*
|
|
2374
|
+
* const server = createSyncaServer({
|
|
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 { createSyncaServer } from '@synca/server'
|
|
2387
|
+
*
|
|
2388
|
+
* const app = express()
|
|
2389
|
+
* const httpServer = app.listen(3000)
|
|
2390
|
+
*
|
|
2391
|
+
* const server = createSyncaServer({
|
|
2392
|
+
* server: httpServer,
|
|
2393
|
+
* path: '/ws',
|
|
2394
|
+
* })
|
|
2395
|
+
*
|
|
2396
|
+
* await server.start()
|
|
2397
|
+
* ```
|
|
2398
|
+
*
|
|
2399
|
+
* @example
|
|
2400
|
+
* ### With custom logger
|
|
2401
|
+
* ```ts
|
|
2402
|
+
* import { createSyncaServer } from '@synca/server'
|
|
2403
|
+
*
|
|
2404
|
+
* const server = createSyncaServer({
|
|
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 { createSyncaServer, createLoggingMiddleware } from '@synca/server'
|
|
2419
|
+
*
|
|
2420
|
+
* const server = createSyncaServer({
|
|
2421
|
+
* port: 3000,
|
|
2422
|
+
* middleware: [
|
|
2423
|
+
* createLoggingMiddleware(),
|
|
2424
|
+
* ],
|
|
2425
|
+
* })
|
|
2426
|
+
*
|
|
2427
|
+
* await server.start()
|
|
2428
|
+
* ```
|
|
2429
|
+
*
|
|
2430
|
+
* @see {@link SyncaServer} for server class API
|
|
2431
|
+
* @see {@link DEFAULT_SERVER_CONFIG} for default configuration values
|
|
2432
|
+
*/
|
|
2433
|
+
declare function createSyncaServer(config?: Partial<IServerOptions>): SyncaServer;
|
|
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 '@synca/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 '@synca/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 '@synca/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 {
|
|
2653
|
+
/**
|
|
2654
|
+
* List of allowed channels
|
|
2655
|
+
* If isDynamic is true, this is used as a fallback
|
|
2656
|
+
*/
|
|
2657
|
+
allowedChannels?: ChannelName[];
|
|
2658
|
+
/**
|
|
2659
|
+
* Dynamic check function for channel access
|
|
2660
|
+
* If provided, this takes precedence over allowedChannels
|
|
2661
|
+
*
|
|
2662
|
+
* @param channel - The channel name to check
|
|
2663
|
+
* @param client - The client attempting to access the channel
|
|
2664
|
+
* @returns true if channel is allowed
|
|
2665
|
+
*
|
|
2666
|
+
* @example
|
|
2667
|
+
* ```ts
|
|
2668
|
+
* const isDynamic: (channel, client) => {
|
|
2669
|
+
* // Check if user has permission for this channel
|
|
2670
|
+
* return user.permissions.includes(channel)
|
|
2671
|
+
* }
|
|
2672
|
+
* ```
|
|
2673
|
+
*/
|
|
2674
|
+
isDynamic?: (channel: ChannelName, client?: IClientConnection) => boolean;
|
|
2675
|
+
/**
|
|
2676
|
+
* Whether to also check unsubscribe actions
|
|
2677
|
+
* @default false (only restrict subscribe)
|
|
2678
|
+
*/
|
|
2679
|
+
restrictUnsubscribe?: boolean;
|
|
2680
|
+
}
|
|
2681
|
+
/**
|
|
2682
|
+
* Create a channel whitelist middleware
|
|
2683
|
+
*
|
|
2684
|
+
* Restricts which channels clients can subscribe to.
|
|
2685
|
+
*
|
|
2686
|
+
* @param options - Channel whitelist options
|
|
2687
|
+
* @returns Middleware function
|
|
2688
|
+
*
|
|
2689
|
+
* @example
|
|
2690
|
+
* ```ts
|
|
2691
|
+
* import { createChannelWhitelistMiddleware } from '@synca/server/middleware'
|
|
2692
|
+
*
|
|
2693
|
+
* const whitelistMiddleware = createChannelWhitelistMiddleware({
|
|
2694
|
+
* allowedChannels: ['chat', 'notifications']
|
|
2695
|
+
* })
|
|
2696
|
+
*
|
|
2697
|
+
* server.use(whitelistMiddleware)
|
|
2698
|
+
* ```
|
|
2699
|
+
*/
|
|
2700
|
+
declare function createChannelWhitelistMiddleware(options?: ChannelWhitelistMiddlewareOptions): IMiddleware;
|
|
2701
|
+
|
|
2702
|
+
/**
|
|
2703
|
+
* Context Data Options
|
|
2704
|
+
*
|
|
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
|
+
* @example
|
|
2717
|
+
* ```ts
|
|
2718
|
+
* const options: ContextOptions<{ user: UserInfo }> = {
|
|
2719
|
+
* action: 'message',
|
|
2720
|
+
* client: connection,
|
|
2721
|
+
* message: dataMessage,
|
|
2722
|
+
* channel: 'chat',
|
|
2723
|
+
* initialState: { user: { id: '123', name: 'Alice' } }
|
|
2724
|
+
* }
|
|
2725
|
+
* ```
|
|
2726
|
+
*/
|
|
2727
|
+
interface ContextOptions<S = Record<string, unknown>> {
|
|
2728
|
+
/** The middleware action being performed */
|
|
2729
|
+
action: IMiddlewareAction;
|
|
2730
|
+
/** Optional client connection */
|
|
2731
|
+
client?: IClientConnection;
|
|
2732
|
+
/** Optional message being processed */
|
|
2733
|
+
message?: Message;
|
|
2734
|
+
/** Optional channel name */
|
|
2735
|
+
channel?: ChannelName;
|
|
2736
|
+
/** Optional initial state values */
|
|
2737
|
+
initialState?: S;
|
|
2738
|
+
}
|
|
2739
|
+
/**
|
|
2740
|
+
* Create a new Hono-style middleware context
|
|
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
|
+
* ```
|
|
2762
|
+
*
|
|
2763
|
+
* @example
|
|
2764
|
+
* ### With initial state
|
|
2765
|
+
* ```ts
|
|
2766
|
+
* interface AppState {
|
|
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')
|
|
2782
|
+
* ```
|
|
2783
|
+
*
|
|
2784
|
+
* @see {@link Context} for the context interface
|
|
2785
|
+
*/
|
|
2786
|
+
declare function createContext<S = Record<string, unknown>>(options: ContextOptions<S>): Context<S>;
|
|
2787
|
+
/**
|
|
2788
|
+
* Context Manager - manages and executes middleware functions
|
|
2789
|
+
*
|
|
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
|
+
* @example
|
|
2802
|
+
* ```ts
|
|
2803
|
+
* import { ContextManager } from '@synca/server'
|
|
2804
|
+
*
|
|
2805
|
+
* const manager = new ContextManager()
|
|
2806
|
+
*
|
|
2807
|
+
* // Register middleware
|
|
2808
|
+
* manager.use(async (context, next) => {
|
|
2809
|
+
* console.log(`Action: ${context.req.action}`)
|
|
2810
|
+
* await next()
|
|
2811
|
+
* })
|
|
2812
|
+
*
|
|
2813
|
+
* // Execute middleware for an action
|
|
2814
|
+
* const context = await manager.executeConnection(client, 'connect')
|
|
2815
|
+
* ```
|
|
2816
|
+
*
|
|
2817
|
+
* @see {@link createSyncaServer} for server-level context management
|
|
2818
|
+
*/
|
|
2819
|
+
declare class ContextManager {
|
|
2820
|
+
/** Registered middleware functions */
|
|
2821
|
+
protected readonly middlewares: IMiddleware[];
|
|
2822
|
+
/**
|
|
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
|
+
* ```
|
|
2839
|
+
*/
|
|
2840
|
+
use(middleware: IMiddleware): void;
|
|
2841
|
+
/**
|
|
2842
|
+
* Remove a middleware function
|
|
2843
|
+
*
|
|
2844
|
+
* @remarks
|
|
2845
|
+
* Removes a previously registered middleware function from the chain.
|
|
2846
|
+
*
|
|
2847
|
+
* @param middleware - The middleware function to remove
|
|
2848
|
+
* @returns `true` if the middleware was found and removed, `false` otherwise
|
|
2849
|
+
*
|
|
2850
|
+
* @example
|
|
2851
|
+
* ```ts
|
|
2852
|
+
* const middleware = async (context, next) => { /* ... *\/ }
|
|
2853
|
+
* manager.use(middleware)
|
|
2854
|
+
*
|
|
2855
|
+
* // Later, remove it
|
|
2856
|
+
* if (manager.remove(middleware)) {
|
|
2857
|
+
* console.log('Middleware removed')
|
|
2858
|
+
* }
|
|
2859
|
+
* ```
|
|
2860
|
+
*/
|
|
2861
|
+
remove(middleware: IMiddleware): boolean;
|
|
2862
|
+
/**
|
|
2863
|
+
* Clear all middleware
|
|
2864
|
+
*
|
|
2865
|
+
* @remarks
|
|
2866
|
+
* Removes all registered middleware functions from the chain.
|
|
2867
|
+
*
|
|
2868
|
+
* @example
|
|
2869
|
+
* ```ts
|
|
2870
|
+
* manager.clear()
|
|
2871
|
+
* console.log('All middleware cleared')
|
|
2872
|
+
* ```
|
|
2873
|
+
*/
|
|
2874
|
+
clear(): void;
|
|
2875
|
+
/**
|
|
2876
|
+
* Get all registered middleware
|
|
2877
|
+
*
|
|
2878
|
+
* @remarks
|
|
2879
|
+
* Returns a shallow copy of the middleware array to prevent
|
|
2880
|
+
* external modification.
|
|
2881
|
+
*
|
|
2882
|
+
* @returns Array of middleware functions
|
|
2883
|
+
*
|
|
2884
|
+
* @example
|
|
2885
|
+
* ```ts
|
|
2886
|
+
* const allMiddleware = manager.getMiddlewares()
|
|
2887
|
+
* console.log(`Registered middleware: ${allMiddleware.length}`)
|
|
2888
|
+
* ```
|
|
2889
|
+
*/
|
|
2890
|
+
getMiddlewares(): IMiddleware[];
|
|
2891
|
+
/**
|
|
2892
|
+
* Get the complete middleware pipeline
|
|
2893
|
+
*
|
|
2894
|
+
* @remarks
|
|
2895
|
+
* Returns the combined middleware pipeline including global middleware
|
|
2896
|
+
* and any channel-specific middleware from the provided channel instance.
|
|
2897
|
+
*
|
|
2898
|
+
* @param channelInstance - Optional channel instance with middleware
|
|
2899
|
+
* @returns Combined array of middleware functions
|
|
2900
|
+
*
|
|
2901
|
+
* @example
|
|
2902
|
+
* ```ts
|
|
2903
|
+
* const chat = server.createMulticast('chat')
|
|
2904
|
+
* const pipeline = manager.getPipeline(chat)
|
|
2905
|
+
* // Returns global middleware + chat channel middleware
|
|
2906
|
+
* ```
|
|
2907
|
+
*
|
|
2908
|
+
* @internal
|
|
2909
|
+
*/
|
|
2910
|
+
getPipeline(channelInstance?: {
|
|
2911
|
+
getMiddlewares?: () => IMiddleware[];
|
|
2912
|
+
}): IMiddleware[];
|
|
2913
|
+
/**
|
|
2914
|
+
* Execute middleware for connection actions
|
|
2915
|
+
*
|
|
2916
|
+
* @remarks
|
|
2917
|
+
* Creates a connection context and executes the middleware pipeline
|
|
2918
|
+
* for connect or disconnect actions.
|
|
2919
|
+
*
|
|
2920
|
+
* @param client - The client connection
|
|
2921
|
+
* @param action - The action ('connect' or 'disconnect')
|
|
2922
|
+
* @returns The executed context
|
|
2923
|
+
*
|
|
2924
|
+
* @example
|
|
2925
|
+
* ```ts
|
|
2926
|
+
* await manager.executeConnection(client, 'connect')
|
|
2927
|
+
* await manager.executeConnection(client, 'disconnect')
|
|
2928
|
+
* ```
|
|
2929
|
+
*
|
|
2930
|
+
* @internal
|
|
2931
|
+
*/
|
|
2932
|
+
executeConnection(client: IClientConnection, action: 'connect' | 'disconnect'): Promise<Context>;
|
|
2933
|
+
/**
|
|
2934
|
+
* Execute middleware for message actions
|
|
2935
|
+
*
|
|
2936
|
+
* @remarks
|
|
2937
|
+
* Creates a message context and executes the middleware pipeline
|
|
2938
|
+
* for incoming client messages.
|
|
2939
|
+
*
|
|
2940
|
+
* @param client - The client connection
|
|
2941
|
+
* @param message - The message being processed
|
|
2942
|
+
* @returns The executed context
|
|
2943
|
+
*
|
|
2944
|
+
* @example
|
|
2945
|
+
* ```ts
|
|
2946
|
+
* await manager.executeMessage(client, dataMessage)
|
|
2947
|
+
* ```
|
|
2948
|
+
*
|
|
2949
|
+
* @internal
|
|
2950
|
+
*/
|
|
2951
|
+
executeMessage(client: IClientConnection, message: Message): Promise<Context>;
|
|
2952
|
+
/**
|
|
2953
|
+
* Execute middleware for subscribe actions
|
|
2954
|
+
*
|
|
2955
|
+
* @remarks
|
|
2956
|
+
* Creates a subscribe context and executes the middleware pipeline
|
|
2957
|
+
* for channel subscription requests.
|
|
2958
|
+
*
|
|
2959
|
+
* @param client - The client connection
|
|
2960
|
+
* @param channel - The channel name
|
|
2961
|
+
* @param finalHandler - Optional final handler to execute after middleware
|
|
2962
|
+
* @returns The executed context
|
|
2963
|
+
*
|
|
2964
|
+
* @example
|
|
2965
|
+
* ```ts
|
|
2966
|
+
* await manager.executeSubscribe(client, 'chat', async () => {
|
|
2967
|
+
* // Final handler - perform the actual subscription
|
|
2968
|
+
* channel.subscribe(client.id)
|
|
2969
|
+
* })
|
|
2970
|
+
* ```
|
|
2971
|
+
*
|
|
2972
|
+
* @internal
|
|
2973
|
+
*/
|
|
2974
|
+
executeSubscribe(client: IClientConnection, channel: ChannelName, finalHandler?: () => Promise<void>): Promise<Context>;
|
|
2975
|
+
/**
|
|
2976
|
+
* Execute middleware for unsubscribe actions
|
|
2977
|
+
*
|
|
2978
|
+
* @remarks
|
|
2979
|
+
* Creates an unsubscribe context and executes the middleware pipeline
|
|
2980
|
+
* for channel unsubscription requests.
|
|
2981
|
+
*
|
|
2982
|
+
* @param client - The client connection
|
|
2983
|
+
* @param channel - The channel name
|
|
2984
|
+
* @param finalHandler - Optional final handler to execute after middleware
|
|
2985
|
+
* @returns The executed context
|
|
2986
|
+
*
|
|
2987
|
+
* @example
|
|
2988
|
+
* ```ts
|
|
2989
|
+
* await manager.executeUnsubscribe(client, 'chat', async () => {
|
|
2990
|
+
* // Final handler - perform the actual unsubscription
|
|
2991
|
+
* channel.unsubscribe(client.id)
|
|
2992
|
+
* })
|
|
2993
|
+
* ```
|
|
2994
|
+
*
|
|
2995
|
+
* @internal
|
|
2996
|
+
*/
|
|
2997
|
+
executeUnsubscribe(client: IClientConnection, channel: ChannelName, finalHandler?: () => Promise<void>): Promise<Context>;
|
|
2998
|
+
/**
|
|
2999
|
+
* Execute middleware pipeline
|
|
3000
|
+
*
|
|
3001
|
+
* @remarks
|
|
3002
|
+
* Executes the provided middleware functions in order, wrapping
|
|
3003
|
+
* each to capture and report errors appropriately.
|
|
3004
|
+
*
|
|
3005
|
+
* @param context - The middleware context
|
|
3006
|
+
* @param middlewares - The middleware functions to execute (defaults to registered middleware)
|
|
3007
|
+
* @param finalHandler - Optional final handler to execute after all middleware
|
|
3008
|
+
* @returns The executed context
|
|
3009
|
+
*
|
|
3010
|
+
* @throws {MiddlewareExecutionError} If a middleware function throws an unexpected error
|
|
3011
|
+
*
|
|
3012
|
+
* @example
|
|
3013
|
+
* ```ts
|
|
3014
|
+
* const context = createContext({ action: 'message' })
|
|
3015
|
+
* await manager.execute(context)
|
|
3016
|
+
* ```
|
|
3017
|
+
*
|
|
3018
|
+
* @internal
|
|
3019
|
+
*/
|
|
3020
|
+
execute(context: Context, middlewares?: IMiddleware[], finalHandler?: () => Promise<void>): Promise<Context>;
|
|
3021
|
+
/**
|
|
3022
|
+
* Create a connection context
|
|
3023
|
+
*
|
|
3024
|
+
* @remarks
|
|
3025
|
+
* Creates a middleware context for connection or disconnect actions.
|
|
3026
|
+
*
|
|
3027
|
+
* @param client - The client connection
|
|
3028
|
+
* @param action - The action ('connect' or 'disconnect')
|
|
3029
|
+
* @returns A new connection context
|
|
3030
|
+
*
|
|
3031
|
+
* @example
|
|
3032
|
+
* ```ts
|
|
3033
|
+
* const context = manager.createConnectionContext(client, 'connect')
|
|
3034
|
+
* ```
|
|
3035
|
+
*
|
|
3036
|
+
* @internal
|
|
3037
|
+
*/
|
|
3038
|
+
createConnectionContext(client: IClientConnection, action: 'connect' | 'disconnect'): Context;
|
|
3039
|
+
/**
|
|
3040
|
+
* Create a message context
|
|
3041
|
+
*
|
|
3042
|
+
* @remarks
|
|
3043
|
+
* Creates a middleware context for message processing.
|
|
3044
|
+
*
|
|
3045
|
+
* @param client - The client connection
|
|
3046
|
+
* @param message - The message being processed
|
|
3047
|
+
* @returns A new message context
|
|
3048
|
+
*
|
|
3049
|
+
* @example
|
|
3050
|
+
* ```ts
|
|
3051
|
+
* const context = manager.createMessageContext(client, dataMessage)
|
|
3052
|
+
* ```
|
|
3053
|
+
*
|
|
3054
|
+
* @internal
|
|
3055
|
+
*/
|
|
3056
|
+
createMessageContext(client: IClientConnection, message: Message): Context;
|
|
3057
|
+
/**
|
|
3058
|
+
* Create a subscribe context
|
|
3059
|
+
*
|
|
3060
|
+
* @remarks
|
|
3061
|
+
* Creates a middleware context for channel subscription.
|
|
3062
|
+
*
|
|
3063
|
+
* @param client - The client connection
|
|
3064
|
+
* @param channel - The channel name
|
|
3065
|
+
* @returns A new subscribe context
|
|
3066
|
+
*
|
|
3067
|
+
* @example
|
|
3068
|
+
* ```ts
|
|
3069
|
+
* const context = manager.createSubscribeContext(client, 'chat')
|
|
3070
|
+
* ```
|
|
3071
|
+
*
|
|
3072
|
+
* @internal
|
|
3073
|
+
*/
|
|
3074
|
+
createSubscribeContext(client: IClientConnection, channel: ChannelName): Context;
|
|
3075
|
+
/**
|
|
3076
|
+
* Create an unsubscribe context
|
|
3077
|
+
*
|
|
3078
|
+
* @remarks
|
|
3079
|
+
* Creates a middleware context for channel unsubscription.
|
|
3080
|
+
*
|
|
3081
|
+
* @param client - The client connection
|
|
3082
|
+
* @param channel - The channel name
|
|
3083
|
+
* @returns A new unsubscribe context
|
|
3084
|
+
*
|
|
3085
|
+
* @example
|
|
3086
|
+
* ```ts
|
|
3087
|
+
* const context = manager.createUnsubscribeContext(client, 'chat')
|
|
3088
|
+
* ```
|
|
3089
|
+
*
|
|
3090
|
+
* @internal
|
|
3091
|
+
*/
|
|
3092
|
+
createUnsubscribeContext(client: IClientConnection, channel: ChannelName): Context;
|
|
3093
|
+
/**
|
|
3094
|
+
* Get the number of registered middleware
|
|
3095
|
+
*
|
|
3096
|
+
* @returns The count of middleware functions
|
|
3097
|
+
*
|
|
3098
|
+
* @example
|
|
3099
|
+
* ```ts
|
|
3100
|
+
* console.log(`Middleware count: ${manager.getCount()}`)
|
|
3101
|
+
* ```
|
|
3102
|
+
*/
|
|
3103
|
+
getCount(): number;
|
|
3104
|
+
/**
|
|
3105
|
+
* Check if any middleware is registered
|
|
3106
|
+
*
|
|
3107
|
+
* @returns `true` if at least one middleware is registered, `false` otherwise
|
|
3108
|
+
*
|
|
3109
|
+
* @example
|
|
3110
|
+
* ```ts
|
|
3111
|
+
* if (manager.hasMiddleware()) {
|
|
3112
|
+
* console.log('Middleware is configured')
|
|
3113
|
+
* }
|
|
3114
|
+
* ```
|
|
3115
|
+
*/
|
|
3116
|
+
hasMiddleware(): boolean;
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
/**
|
|
3120
|
+
* Errors Module
|
|
3121
|
+
*
|
|
3122
|
+
* @description
|
|
3123
|
+
* Custom error classes for the Synca server. All errors extend from
|
|
3124
|
+
* {@link SyncaError} and include error codes for programmatic handling.
|
|
3125
|
+
*
|
|
3126
|
+
* @remarks
|
|
3127
|
+
* The error hierarchy:
|
|
3128
|
+
*
|
|
3129
|
+
* - {@link SyncaError} - 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 '@synca/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
|
+
* SyncaError,
|
|
3160
|
+
* MiddlewareRejectionError,
|
|
3161
|
+
* StateError
|
|
3162
|
+
* } from '@synca/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 SyncaError) {
|
|
3172
|
+
* console.error(`[${error.code}] ${error.message}`)
|
|
3173
|
+
* }
|
|
3174
|
+
* }
|
|
3175
|
+
* ```
|
|
3176
|
+
*
|
|
3177
|
+
* @module errors
|
|
3178
|
+
*/
|
|
3179
|
+
|
|
3180
|
+
/**
|
|
3181
|
+
* Base Synca error class
|
|
3182
|
+
*
|
|
3183
|
+
* @remarks
|
|
3184
|
+
* All custom errors in the Synca 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 SyncaError('Something went wrong', 'CUSTOM_ERROR', { userId: '123' })
|
|
3193
|
+
* ```
|
|
3194
|
+
*
|
|
3195
|
+
* @example
|
|
3196
|
+
* ### Error handling
|
|
3197
|
+
* ```ts
|
|
3198
|
+
* try {
|
|
3199
|
+
* // ...
|
|
3200
|
+
* } catch (error) {
|
|
3201
|
+
* if (error instanceof SyncaError) {
|
|
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
|
+
* }
|
|
3208
|
+
* ```
|
|
3209
|
+
*/
|
|
3210
|
+
declare class SyncaError extends Error {
|
|
3211
|
+
/**
|
|
3212
|
+
* Error code for programmatic error handling
|
|
3213
|
+
*
|
|
3214
|
+
* @remarks
|
|
3215
|
+
* Machine-readable error code that can be used for conditional
|
|
3216
|
+
* error handling and error response generation.
|
|
3217
|
+
*/
|
|
3218
|
+
readonly code: string;
|
|
3219
|
+
/**
|
|
3220
|
+
* Additional error context (optional)
|
|
3221
|
+
*
|
|
3222
|
+
* @remarks
|
|
3223
|
+
* Arbitrary data attached to the error for debugging or logging.
|
|
3224
|
+
* Common uses include user IDs, request IDs, or validation details.
|
|
3225
|
+
*/
|
|
3226
|
+
readonly context?: Record<string, unknown>;
|
|
3227
|
+
/**
|
|
3228
|
+
* Creates a new SyncaError
|
|
3229
|
+
*
|
|
3230
|
+
* @param message - Human-readable error message
|
|
3231
|
+
* @param code - Error code for programmatic handling (default: 'SYNNEL_ERROR')
|
|
3232
|
+
* @param context - Optional additional error context
|
|
3233
|
+
*/
|
|
3234
|
+
constructor(message: string, code?: string, context?: Record<string, unknown>);
|
|
3235
|
+
/**
|
|
3236
|
+
* Convert error to JSON for logging/serialization
|
|
3237
|
+
*
|
|
3238
|
+
* @returns JSON representation of the error
|
|
3239
|
+
*
|
|
3240
|
+
* @example
|
|
3241
|
+
* ```ts
|
|
3242
|
+
* const error = new SyncaError('Failed', 'FAIL', { id: 123 })
|
|
3243
|
+
* console.log(JSON.stringify(error.toJSON(), null, 2))
|
|
3244
|
+
* // {
|
|
3245
|
+
* // "name": "SyncaError",
|
|
3246
|
+
* // "message": "Failed",
|
|
3247
|
+
* // "code": "FAIL",
|
|
3248
|
+
* // "context": { "id": 123 },
|
|
3249
|
+
* // "stack": "..."
|
|
3250
|
+
* // }
|
|
3251
|
+
* ```
|
|
3252
|
+
*/
|
|
3253
|
+
toJSON(): {
|
|
3254
|
+
name: string;
|
|
3255
|
+
message: string;
|
|
3256
|
+
code: string;
|
|
3257
|
+
context?: Record<string, unknown>;
|
|
3258
|
+
stack?: string;
|
|
3259
|
+
};
|
|
3260
|
+
/**
|
|
3261
|
+
* Get a summary of the error for logging
|
|
3262
|
+
*
|
|
3263
|
+
* @returns Formatted error summary string
|
|
3264
|
+
*
|
|
3265
|
+
* @example
|
|
3266
|
+
* ```ts
|
|
3267
|
+
* const error = new SyncaError('Failed', 'FAIL')
|
|
3268
|
+
* console.log(error.toString())
|
|
3269
|
+
* // "[SyncaError:FAIL] Failed"
|
|
3270
|
+
* ```
|
|
3271
|
+
*/
|
|
3272
|
+
toString(): string;
|
|
3273
|
+
}
|
|
3274
|
+
/**
|
|
3275
|
+
* Configuration error
|
|
3276
|
+
*
|
|
3277
|
+
* @remarks
|
|
3278
|
+
* Thrown when server configuration is invalid or missing required values.
|
|
3279
|
+
*
|
|
3280
|
+
* @example
|
|
3281
|
+
* ```ts
|
|
3282
|
+
* if (!config.port) {
|
|
3283
|
+
* throw new ConfigError('Port is required', { config })
|
|
3284
|
+
* }
|
|
3285
|
+
* ```
|
|
3286
|
+
*/
|
|
3287
|
+
declare class ConfigError extends SyncaError {
|
|
3288
|
+
constructor(message: string, context?: Record<string, unknown>);
|
|
3289
|
+
}
|
|
3290
|
+
/**
|
|
3291
|
+
* Transport error
|
|
3292
|
+
*
|
|
3293
|
+
* @remarks
|
|
3294
|
+
* Thrown when the transport layer fails (WebSocket connection issues, etc.).
|
|
3295
|
+
*
|
|
3296
|
+
* @example
|
|
3297
|
+
* ```ts
|
|
3298
|
+
* if (!wsServer) {
|
|
3299
|
+
* throw new TransportError('WebSocket server not initialized')
|
|
3300
|
+
* }
|
|
3301
|
+
* ```
|
|
3302
|
+
*/
|
|
3303
|
+
declare class TransportError extends SyncaError {
|
|
3304
|
+
constructor(message: string, context?: Record<string, unknown>);
|
|
3305
|
+
}
|
|
3306
|
+
/**
|
|
3307
|
+
* Channel error
|
|
3308
|
+
*
|
|
3309
|
+
* @remarks
|
|
3310
|
+
* Thrown when channel operations fail (invalid channel name, etc.).
|
|
3311
|
+
*
|
|
3312
|
+
* @example
|
|
3313
|
+
* ```ts
|
|
3314
|
+
* if (channelName.startsWith('__')) {
|
|
3315
|
+
* throw new ChannelError('Reserved channel name', { channelName })
|
|
3316
|
+
* }
|
|
3317
|
+
* ```
|
|
3318
|
+
*/
|
|
3319
|
+
declare class ChannelError extends SyncaError {
|
|
3320
|
+
constructor(message: string, context?: Record<string, unknown>);
|
|
3321
|
+
}
|
|
3322
|
+
/**
|
|
3323
|
+
* Client error
|
|
3324
|
+
*
|
|
3325
|
+
* @remarks
|
|
3326
|
+
* Thrown when client operations fail (client not found, etc.).
|
|
3327
|
+
*
|
|
3328
|
+
* @example
|
|
3329
|
+
* ```ts
|
|
3330
|
+
* if (!registry.has(clientId)) {
|
|
3331
|
+
* throw new ClientError('Client not found', { clientId })
|
|
3332
|
+
* }
|
|
3333
|
+
* ```
|
|
3334
|
+
*/
|
|
3335
|
+
declare class ClientError extends SyncaError {
|
|
3336
|
+
constructor(message: string, context?: Record<string, unknown>);
|
|
3337
|
+
}
|
|
3338
|
+
/**
|
|
3339
|
+
* Message error
|
|
3340
|
+
*
|
|
3341
|
+
* @remarks
|
|
3342
|
+
* Thrown when message processing fails (invalid format, etc.).
|
|
3343
|
+
*
|
|
3344
|
+
* @example
|
|
3345
|
+
* ```ts
|
|
3346
|
+
* if (!message.type) {
|
|
3347
|
+
* throw new MessageError('Invalid message format', { message })
|
|
3348
|
+
* }
|
|
3349
|
+
* ```
|
|
3350
|
+
*/
|
|
3351
|
+
declare class MessageError extends SyncaError {
|
|
3352
|
+
constructor(message: string, context?: Record<string, unknown>);
|
|
3353
|
+
}
|
|
3354
|
+
/**
|
|
3355
|
+
* Validation error
|
|
3356
|
+
*
|
|
3357
|
+
* @remarks
|
|
3358
|
+
* Thrown when input validation fails.
|
|
3359
|
+
*
|
|
3360
|
+
* @example
|
|
3361
|
+
* ```ts
|
|
3362
|
+
* if (!isValidChannelName(name)) {
|
|
3363
|
+
* throw new ValidationError('Invalid channel name', { name })
|
|
3364
|
+
* }
|
|
3365
|
+
* ```
|
|
3366
|
+
*/
|
|
3367
|
+
declare class ValidationError extends SyncaError {
|
|
3368
|
+
constructor(message: string, context?: Record<string, unknown>);
|
|
3369
|
+
}
|
|
3370
|
+
/**
|
|
3371
|
+
* State error
|
|
3372
|
+
*
|
|
3373
|
+
* @remarks
|
|
3374
|
+
* Thrown when an operation is invalid for the current state.
|
|
3375
|
+
*
|
|
3376
|
+
* @example
|
|
3377
|
+
* ```ts
|
|
3378
|
+
* if (server.started) {
|
|
3379
|
+
* throw new StateError('Server is already started')
|
|
3380
|
+
* }
|
|
3381
|
+
*
|
|
3382
|
+
* if (!server.started) {
|
|
3383
|
+
* throw new StateError('Server must be started first')
|
|
3384
|
+
* }
|
|
3385
|
+
* ```
|
|
3386
|
+
*/
|
|
3387
|
+
declare class StateError extends SyncaError {
|
|
3388
|
+
constructor(message: string, context?: Record<string, unknown>);
|
|
3389
|
+
}
|
|
3390
|
+
/**
|
|
3391
|
+
* 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
|
+
* @example
|
|
3416
|
+
* ### Catching rejections
|
|
3417
|
+
* ```ts
|
|
3418
|
+
* try {
|
|
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
|
+
* }
|
|
3430
|
+
* ```
|
|
3431
|
+
*/
|
|
3432
|
+
declare class MiddlewareRejectionError extends Error implements IMiddlewareRejectionError {
|
|
3433
|
+
/**
|
|
3434
|
+
* The reason for rejection
|
|
3435
|
+
*
|
|
3436
|
+
* @remarks
|
|
3437
|
+
* Human-readable explanation of why the action was rejected.
|
|
3438
|
+
*/
|
|
3439
|
+
readonly reason: string;
|
|
3440
|
+
/**
|
|
3441
|
+
* The action that was rejected
|
|
3442
|
+
*
|
|
3443
|
+
* @remarks
|
|
3444
|
+
* One of: 'connect', 'disconnect', 'message', 'subscribe', 'unsubscribe'
|
|
3445
|
+
*/
|
|
3446
|
+
readonly action: string;
|
|
3447
|
+
/**
|
|
3448
|
+
* Error name (fixed value for interface compliance)
|
|
3449
|
+
*/
|
|
3450
|
+
readonly name = "MiddlewareRejectionError";
|
|
3451
|
+
/**
|
|
3452
|
+
* Optional error code for programmatic handling
|
|
3453
|
+
*
|
|
3454
|
+
* @remarks
|
|
3455
|
+
* Can be used for mapping to client-facing error codes.
|
|
3456
|
+
*/
|
|
3457
|
+
readonly code?: string;
|
|
3458
|
+
/**
|
|
3459
|
+
* Additional context about the rejection
|
|
3460
|
+
*
|
|
3461
|
+
* @remarks
|
|
3462
|
+
* Arbitrary data for debugging or logging.
|
|
3463
|
+
*/
|
|
3464
|
+
readonly context?: Record<string, unknown>;
|
|
3465
|
+
/**
|
|
3466
|
+
* Creates a new MiddlewareRejectionError
|
|
3467
|
+
*
|
|
3468
|
+
* @param reason - Human-readable reason for the rejection
|
|
3469
|
+
* @param action - The action that was rejected
|
|
3470
|
+
* @param code - Optional error code for programmatic handling
|
|
3471
|
+
* @param context - Additional context about the rejection
|
|
3472
|
+
*/
|
|
3473
|
+
constructor(reason: string, action: IMiddlewareAction | string, code?: string, context?: Record<string, unknown>);
|
|
3474
|
+
/**
|
|
3475
|
+
* Convert error to JSON for logging/serialization
|
|
3476
|
+
*
|
|
3477
|
+
* @returns JSON representation of the rejection error
|
|
3478
|
+
*
|
|
3479
|
+
* @example
|
|
3480
|
+
* ```ts
|
|
3481
|
+
* const error = new MiddlewareRejectionError('Not allowed', 'subscribe', 'FORBIDDEN')
|
|
3482
|
+
* console.log(JSON.stringify(error.toJSON(), null, 2))
|
|
3483
|
+
* // {
|
|
3484
|
+
* // "name": "MiddlewareRejectionError",
|
|
3485
|
+
* // "reason": "Not allowed",
|
|
3486
|
+
* // "action": "subscribe",
|
|
3487
|
+
* // "code": "FORBIDDEN",
|
|
3488
|
+
* // "message": "Action 'subscribe' rejected: Not allowed",
|
|
3489
|
+
* // "stack": "..."
|
|
3490
|
+
* // }
|
|
3491
|
+
* ```
|
|
3492
|
+
*/
|
|
3493
|
+
toJSON(): {
|
|
3494
|
+
name: string;
|
|
3495
|
+
reason: string;
|
|
3496
|
+
action: string;
|
|
3497
|
+
code?: string;
|
|
3498
|
+
context?: Record<string, unknown>;
|
|
3499
|
+
message: string;
|
|
3500
|
+
stack?: string;
|
|
3501
|
+
};
|
|
3502
|
+
/**
|
|
3503
|
+
* Get a summary of the rejection for logging
|
|
3504
|
+
*
|
|
3505
|
+
* @returns Formatted error summary string
|
|
3506
|
+
*
|
|
3507
|
+
* @example
|
|
3508
|
+
* ```ts
|
|
3509
|
+
* const error = new MiddlewareRejectionError('Not allowed', 'subscribe')
|
|
3510
|
+
* console.log(error.toString())
|
|
3511
|
+
* // "[MiddlewareRejectionError:subscribe] Not allowed"
|
|
3512
|
+
* ```
|
|
3513
|
+
*/
|
|
3514
|
+
toString(): string;
|
|
3515
|
+
}
|
|
3516
|
+
/**
|
|
3517
|
+
* Middleware execution error
|
|
3518
|
+
*
|
|
3519
|
+
* @remarks
|
|
3520
|
+
* Thrown when a middleware function throws an unexpected error
|
|
3521
|
+
* (not using `context.reject()`). This indicates a bug or failure
|
|
3522
|
+
* in the middleware rather than an intentional rejection.
|
|
3523
|
+
*
|
|
3524
|
+
* @property action - The action being processed when the error occurred
|
|
3525
|
+
* @property middleware - The name/index of the middleware that failed
|
|
3526
|
+
* @property cause - The original error thrown by the middleware
|
|
3527
|
+
*
|
|
3528
|
+
* @example
|
|
3529
|
+
* ### Error scenario
|
|
3530
|
+
* ```ts
|
|
3531
|
+
* const buggyMiddleware: Middleware = async (context, next) => {
|
|
3532
|
+
* // This throws an unexpected error
|
|
3533
|
+
* JSON.parse(context.req.message as string)
|
|
3534
|
+
* await next()
|
|
3535
|
+
* }
|
|
3536
|
+
* // Results in MiddlewareExecutionError
|
|
3537
|
+
* ```
|
|
3538
|
+
*
|
|
3539
|
+
* @example
|
|
3540
|
+
* ### Catching execution errors
|
|
3541
|
+
* ```ts
|
|
3542
|
+
* try {
|
|
3543
|
+
* await manager.execute(context)
|
|
3544
|
+
* } catch (error) {
|
|
3545
|
+
* if (error instanceof MiddlewareExecutionError) {
|
|
3546
|
+
* console.error(`${error.middleware} failed during ${error.action}:`)
|
|
3547
|
+
* console.error(error.cause)
|
|
3548
|
+
* }
|
|
3549
|
+
* }
|
|
3550
|
+
* ```
|
|
3551
|
+
*/
|
|
3552
|
+
declare class MiddlewareExecutionError extends Error {
|
|
3553
|
+
/**
|
|
3554
|
+
* The action being processed when the error occurred
|
|
3555
|
+
*/
|
|
3556
|
+
readonly action: string;
|
|
3557
|
+
/**
|
|
3558
|
+
* The name/index of the middleware that failed
|
|
3559
|
+
*/
|
|
3560
|
+
readonly middleware: string;
|
|
3561
|
+
/**
|
|
3562
|
+
* The original error thrown by the middleware
|
|
3563
|
+
*/
|
|
3564
|
+
readonly cause: Error;
|
|
3565
|
+
/**
|
|
3566
|
+
* Creates a new MiddlewareExecutionError
|
|
3567
|
+
*
|
|
3568
|
+
* @param action - The action being processed
|
|
3569
|
+
* @param middleware - The name/index of the middleware
|
|
3570
|
+
* @param cause - The original error
|
|
3571
|
+
*/
|
|
3572
|
+
constructor(action: string, middleware: string, cause: Error);
|
|
3573
|
+
/**
|
|
3574
|
+
* Get the original error cause
|
|
3575
|
+
*
|
|
3576
|
+
* @returns The original error thrown by the middleware
|
|
3577
|
+
*
|
|
3578
|
+
* @example
|
|
3579
|
+
* ```ts
|
|
3580
|
+
* if (error instanceof MiddlewareExecutionError) {
|
|
3581
|
+
* const originalError = error.getCause()
|
|
3582
|
+
* console.error('Original error:', originalError.message)
|
|
3583
|
+
* }
|
|
3584
|
+
* ```
|
|
3585
|
+
*/
|
|
3586
|
+
getCause(): Error;
|
|
3587
|
+
/**
|
|
3588
|
+
* Get a summary of the error for logging
|
|
3589
|
+
*
|
|
3590
|
+
* @returns Formatted error summary string
|
|
3591
|
+
*
|
|
3592
|
+
* @example
|
|
3593
|
+
* ```ts
|
|
3594
|
+
* const error = new MiddlewareExecutionError('message', 'auth', originalError)
|
|
3595
|
+
* console.log(error.toString())
|
|
3596
|
+
* // "[MiddlewareExecutionError] auth failed during message: Invalid token"
|
|
3597
|
+
* ```
|
|
3598
|
+
*/
|
|
3599
|
+
toString(): string;
|
|
3600
|
+
}
|
|
3601
|
+
|
|
3602
|
+
export { type AckMessage, BROADCAST_CHANNEL, BroadcastChannel, CLOSE_CODES, ChannelError, type ChannelName, ClientError, type ClientId, ConfigError, type Context, ContextManager, type DataMessage, ERROR_CODES, ErrorCode, type ErrorMessage, type IChannelState, type IClientConnection, type IMessageHandler, type IPublishOptions, type IServerOptions, type IServerStats, type Message, MessageError, type MessageId, MessageType, type Middleware, MiddlewareExecutionError, MiddlewareRejectionError, MulticastChannel, type SignalMessage, SignalType, StateError, type SubscriberId, SyncaServer as Synca, SyncaError, SyncaServer, type Timestamp, TransportError, ValidationError, WebSocketServerTransport, createAuthMiddleware, createChannelWhitelistMiddleware, createContext, createLoggingMiddleware, createRateLimitMiddleware, createSyncaServer };
|