@syncar/server 1.0.0-alpha.2 → 1.0.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,630 @@
1
+ import WebSocket from 'ws';
2
+
3
+ /**
4
+ * Message identifier
5
+ */
6
+ type MessageId = string;
7
+ /**
8
+ * Client identifier (e.g., WebSocket connection ID)
9
+ */
10
+ type ClientId = string;
11
+ /**
12
+ * Channel name
13
+ */
14
+ type ChannelName = string;
15
+ /**
16
+ * Unix timestamp in milliseconds
17
+ */
18
+ type Timestamp = number;
19
+ /**
20
+ * Generic data payload for messages
21
+ */
22
+ type DataPayload<T = unknown> = T;
23
+ /**
24
+ * Logger interface
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * const logger: ILogger = {
29
+ * debug: (msg, ...args) => console.debug(msg, ...args),
30
+ * info: (msg, ...args) => console.info(msg, ...args),
31
+ * warn: (msg, ...args) => console.warn(msg, ...args),
32
+ * error: (msg, ...args) => console.error(msg, ...args),
33
+ * }
34
+ * ```
35
+ */
36
+ interface ILogger {
37
+ /**
38
+ * Log a debug message
39
+ *
40
+ * @param message - The log message
41
+ * @param args - Additional arguments to log
42
+ */
43
+ debug(message: string, ...args: unknown[]): void;
44
+ /**
45
+ * Log an info message
46
+ *
47
+ * @param message - The log message
48
+ * @param args - Additional arguments to log
49
+ */
50
+ info(message: string, ...args: unknown[]): void;
51
+ /**
52
+ * Log a warning message
53
+ *
54
+ * @param message - The log message
55
+ * @param args - Additional arguments to log
56
+ */
57
+ warn(message: string, ...args: unknown[]): void;
58
+ /**
59
+ * Log an error message
60
+ *
61
+ * @param message - The log message
62
+ * @param args - Additional arguments to log
63
+ */
64
+ error(message: string, ...args: unknown[]): void;
65
+ }
66
+ /**
67
+ * Base client connection interface
68
+ *
69
+ * @remarks
70
+ * Represents a connected WebSocket client with metadata about the connection.
71
+ * This interface is used throughout the server for client tracking and management.
72
+ *
73
+ * @property id - Unique client identifier
74
+ * @property connectedAt - Unix timestamp (ms) when the client connected
75
+ * @property lastPingAt - Unix timestamp (ms) of the last ping/pong (optional)
76
+ * @property socket - The raw WebSocket instance
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * const client: IClientConnection = {
81
+ * id: 'client_123abc',
82
+ * connectedAt: 1699123456789,
83
+ * lastPingAt: 1699123459999,
84
+ * socket: wsSocket
85
+ * }
86
+ *
87
+ * console.log(`Client ${client.id} connected at ${new Date(client.connectedAt).toLocaleString()}`)
88
+ * ```
89
+ */
90
+ interface IClientConnection {
91
+ /** Unique client identifier */
92
+ readonly id: ClientId;
93
+ /** Connected timestamp */
94
+ readonly connectedAt: Timestamp;
95
+ /** Last ping timestamp */
96
+ lastPingAt?: Timestamp;
97
+ /** Raw WebSocket instance */
98
+ readonly socket: WebSocket;
99
+ }
100
+ /**
101
+ * Message types for protocol communication
102
+ *
103
+ * @remarks
104
+ * Enum defining all message types supported by the Syncar protocol.
105
+ *
106
+ * @see {@link SignalType} for signal message subtypes
107
+ */
108
+ declare enum MessageType {
109
+ /** Data message - carries application data */
110
+ DATA = "data",
111
+ /** Signal message - control message for subscriptions, pings, etc. */
112
+ SIGNAL = "signal",
113
+ /** Error message - reports errors to clients */
114
+ ERROR = "error",
115
+ /** Acknowledgment message - confirms message receipt */
116
+ ACK = "ack"
117
+ }
118
+ /**
119
+ * Signal types for control messages
120
+ *
121
+ * @remarks
122
+ * Enum defining all signal types used for protocol control operations.
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * if (message.type === MessageType.SIGNAL) {
127
+ * if (message.signal === SignalType.SUBSCRIBE) {
128
+ * // Handle subscription
129
+ * } else if (message.signal === SignalType.PING) {
130
+ * // Respond with pong
131
+ * }
132
+ * }
133
+ * ```
134
+ */
135
+ declare enum SignalType {
136
+ /** Subscribe to a channel */
137
+ SUBSCRIBE = "subscribe",
138
+ /** Unsubscribe from a channel */
139
+ UNSUBSCRIBE = "unsubscribe",
140
+ /** Ping message for keep-alive */
141
+ PING = "ping",
142
+ /** Pong response to ping */
143
+ PONG = "pong",
144
+ /** Confirmation of successful subscription */
145
+ SUBSCRIBED = "subscribed",
146
+ /** Confirmation of successful unsubscription */
147
+ UNSUBSCRIBED = "unsubscribed"
148
+ }
149
+ /**
150
+ * Standard error codes for messages
151
+ *
152
+ * @remarks
153
+ * Enum defining error codes used in error messages sent to clients.
154
+ */
155
+ declare enum ErrorCode {
156
+ /** Invalid message type */
157
+ INVALID_MESSAGE_TYPE = "INVALID_MESSAGE_TYPE",
158
+ /** Channel name missing from message */
159
+ MISSING_CHANNEL = "MISSING_CHANNEL",
160
+ /** Unknown signal type */
161
+ UNKNOWN_SIGNAL = "UNKNOWN_SIGNAL",
162
+ /** Channel not found */
163
+ CHANNEL_NOT_FOUND = "CHANNEL_NOT_FOUND",
164
+ /** Reserved channel name (starts with __) */
165
+ RESERVED_CHANNEL_NAME = "RESERVED_CHANNEL_NAME",
166
+ /** Invalid message format */
167
+ INVALID_MESSAGE_FORMAT = "INVALID_MESSAGE_FORMAT",
168
+ /** Rate limit exceeded */
169
+ RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED"
170
+ }
171
+ /**
172
+ * Base message interface
173
+ *
174
+ * @remarks
175
+ * All messages in the Syncar protocol extend this interface with
176
+ * common fields like ID, timestamp, and optional channel.
177
+ *
178
+ * @property id - Unique message identifier
179
+ * @property timestamp - Unix timestamp (ms) when message was created
180
+ * @property channel - Optional channel name (required for most message types)
181
+ *
182
+ * @example
183
+ * ```ts
184
+ * const baseMessage: BaseMessage = {
185
+ * id: generateId(),
186
+ * timestamp: Date.now(),
187
+ * channel: 'chat'
188
+ * }
189
+ * ```
190
+ */
191
+ interface BaseMessage {
192
+ /** Unique message identifier */
193
+ id: MessageId;
194
+ /** Message timestamp */
195
+ timestamp: Timestamp;
196
+ /** Channel name */
197
+ channel?: ChannelName;
198
+ }
199
+ /**
200
+ * Data message (typed message with channel)
201
+ *
202
+ * @remarks
203
+ * Carries application data from sender to channel subscribers.
204
+ * This is the primary message type for user-defined communication.
205
+ *
206
+ * @template T - Type of the data payload (default: unknown)
207
+ *
208
+ * @property type - Message type discriminator (always DATA)
209
+ * @property channel - The target channel name
210
+ * @property data - The message data payload
211
+ *
212
+ * @example
213
+ * ```ts
214
+ * const chatMessage: DataMessage<{ text: string; user: string }> = {
215
+ * id: generateId(),
216
+ * timestamp: Date.now(),
217
+ * type: MessageType.DATA,
218
+ * channel: 'chat',
219
+ * data: {
220
+ * text: 'Hello world!',
221
+ * user: 'Alice'
222
+ * }
223
+ * }
224
+ *
225
+ * // Type narrowing
226
+ * if (message.type === MessageType.DATA) {
227
+ * console.log(`${message.data.user}: ${message.data.text}`)
228
+ * }
229
+ * ```
230
+ */
231
+ interface DataMessage<T = unknown> extends BaseMessage {
232
+ type: MessageType.DATA;
233
+ channel: ChannelName;
234
+ /** Message payload */
235
+ data: T;
236
+ }
237
+ /**
238
+ * Signal message (control message)
239
+ *
240
+ * @remarks
241
+ * Control messages for protocol operations like subscribe/unsubscribe,
242
+ * ping/pong keep-alive, and subscription confirmations.
243
+ *
244
+ * @property type - Message type discriminator (always SIGNAL)
245
+ * @property channel - The target channel name
246
+ * @property signal - The signal type
247
+ * @property data - Optional data payload
248
+ *
249
+ * @example
250
+ * ### Subscribe signal
251
+ * ```ts
252
+ * const subscribeSignal: SignalMessage = {
253
+ * id: generateId(),
254
+ * timestamp: Date.now(),
255
+ * type: MessageType.SIGNAL,
256
+ * channel: 'chat',
257
+ * signal: SignalType.SUBSCRIBE
258
+ * }
259
+ * ```
260
+ *
261
+ * @example
262
+ * ### Ping signal
263
+ * ```ts
264
+ * const pingSignal: SignalMessage = {
265
+ * id: generateId(),
266
+ * timestamp: Date.now(),
267
+ * type: MessageType.SIGNAL,
268
+ * signal: SignalType.PING
269
+ * }
270
+ * ```
271
+ */
272
+ interface SignalMessage extends BaseMessage {
273
+ type: MessageType.SIGNAL;
274
+ channel: ChannelName;
275
+ /** Signal type */
276
+ signal: SignalType;
277
+ /** Message payload */
278
+ data?: DataPayload;
279
+ }
280
+ /**
281
+ * Error data structure
282
+ *
283
+ * @remarks
284
+ * Contains error information sent to clients when an error occurs.
285
+ *
286
+ * @property message - Human-readable error message
287
+ * @property code - Optional machine-readable error code
288
+ *
289
+ * @example
290
+ * ```ts
291
+ * const errorData: ErrorData = {
292
+ * message: 'Channel not found',
293
+ * code: ErrorCode.CHANNEL_NOT_FOUND
294
+ * }
295
+ * ```
296
+ */
297
+ interface ErrorData {
298
+ /** Human-readable error message */
299
+ message: string;
300
+ /** Machine-readable error code */
301
+ code?: ErrorCode;
302
+ /** Additional error context */
303
+ [key: string]: unknown;
304
+ }
305
+ /**
306
+ * Error message
307
+ *
308
+ * @remarks
309
+ * Sent to clients when an error occurs during message processing.
310
+ *
311
+ * @property type - Message type discriminator (always ERROR)
312
+ * @property data - Error details including message and optional code
313
+ *
314
+ * @example
315
+ * ```ts
316
+ * const errorMessage: ErrorMessage = {
317
+ * id: generateId(),
318
+ * timestamp: Date.now(),
319
+ * type: MessageType.ERROR,
320
+ * data: {
321
+ * message: 'Channel not found',
322
+ * code: ErrorCode.CHANNEL_NOT_FOUND
323
+ * }
324
+ * }
325
+ * ```
326
+ */
327
+ interface ErrorMessage extends BaseMessage {
328
+ type: MessageType.ERROR;
329
+ /** Error detail */
330
+ data: ErrorData;
331
+ }
332
+ /**
333
+ * Acknowledgment message
334
+ *
335
+ * @remarks
336
+ * Confirms receipt of a message. Used for reliable messaging patterns.
337
+ *
338
+ * @property type - Message type discriminator (always ACK)
339
+ * @property ackMessageId - ID of the message being acknowledged
340
+ *
341
+ * @example
342
+ * ```ts
343
+ * const ackMessage: AckMessage = {
344
+ * id: generateId(),
345
+ * timestamp: Date.now(),
346
+ * type: MessageType.ACK,
347
+ * ackMessageId: 'original-message-id'
348
+ * }
349
+ * ```
350
+ */
351
+ interface AckMessage extends BaseMessage {
352
+ type: MessageType.ACK;
353
+ /** Original message being acknowledged */
354
+ ackMessageId: MessageId;
355
+ }
356
+ /**
357
+ * Union type for all supported messages in the protocol.
358
+ *
359
+ * @remarks
360
+ * Discriminated union of all message types. Use type narrowing with
361
+ * the `type` property to access specific message fields.
362
+ *
363
+ * @template T - Type of the data payload for DataMessage (default: unknown)
364
+ *
365
+ * @example
366
+ * ```ts
367
+ * function handleMessage(message: Message) {
368
+ * switch (message.type) {
369
+ * case MessageType.DATA:
370
+ * // message is DataMessage
371
+ * console.log('Data:', message.data)
372
+ * break
373
+ * case MessageType.SIGNAL:
374
+ * // message is SignalMessage
375
+ * console.log('Signal:', message.signal)
376
+ * break
377
+ * case MessageType.ERROR:
378
+ * // message is ErrorMessage
379
+ * console.error('Error:', message.data.message)
380
+ * break
381
+ * case MessageType.ACK:
382
+ * // message is AckMessage
383
+ * console.log('Ack for:', message.ackMessageId)
384
+ * break
385
+ * }
386
+ * }
387
+ * ```
388
+ *
389
+ * @example
390
+ * ### Type narrowing with DataMessage generic
391
+ * ```ts
392
+ * interface ChatMessage {
393
+ * text: string
394
+ * user: string
395
+ * }
396
+ *
397
+ * function isChatMessage(message: Message): message is DataMessage<ChatMessage> {
398
+ * return message.type === MessageType.DATA && message.channel === 'chat'
399
+ * }
400
+ *
401
+ * if (isChatMessage(message)) {
402
+ * console.log(`${message.data.user}: ${message.data.text}`)
403
+ * }
404
+ * ```
405
+ */
406
+ type Message<T = unknown> = DataMessage<T> | SignalMessage | ErrorMessage | AckMessage;
407
+ /**
408
+ * Middleware action types
409
+ *
410
+ * @remarks
411
+ * Defines all actions that middleware can intercept and process.
412
+ *
413
+ * @example
414
+ * ```ts
415
+ * import type { IMiddlewareAction } from '@syncar/server'
416
+ *
417
+ * function handleAction(action: IMiddlewareAction) {
418
+ * switch (action) {
419
+ * case 'connect':
420
+ * console.log('Client connecting')
421
+ * break
422
+ * case 'disconnect':
423
+ * console.log('Client disconnecting')
424
+ * break
425
+ * case 'message':
426
+ * console.log('Message received')
427
+ * break
428
+ * case 'subscribe':
429
+ * console.log('Channel subscription')
430
+ * break
431
+ * case 'unsubscribe':
432
+ * console.log('Channel unsubscription')
433
+ * break
434
+ * }
435
+ * }
436
+ * ```
437
+ */
438
+ type IMiddlewareAction = 'connect' | 'disconnect' | 'message' | 'subscribe' | 'unsubscribe';
439
+ /**
440
+ * Next function type for middleware chain continuation
441
+ *
442
+ * @remarks
443
+ * Function passed to middleware to continue execution to the next
444
+ * middleware in the chain.
445
+ *
446
+ * @example
447
+ * ```ts
448
+ * const middleware: Middleware = async (context, next) => {
449
+ * // Pre-processing
450
+ * console.log('Before next')
451
+ *
452
+ * // Continue to next middleware
453
+ * await next()
454
+ *
455
+ * // Post-processing
456
+ * console.log('After next')
457
+ * }
458
+ * ```
459
+ */
460
+ type Next = () => Promise<void>;
461
+ /**
462
+ * Middleware context interface
463
+ *
464
+ * @remarks
465
+ * Provides middleware functions with access to request information,
466
+ * state management, and control flow methods. Inspired by Hono's context pattern.
467
+ *
468
+ * @template S - Type of the state object (default: Record<string, unknown>)
469
+ *
470
+ * @property req - Request information (client, message, channel, action)
471
+ * @property error - Optional error object
472
+ * @property finalized - Whether the context has been finalized
473
+ * @property res - Optional response data
474
+ * @property var - State object for storing custom data
475
+ * @property get - Get a value from state by key
476
+ * @property set - Set a value in state by key
477
+ * @property reject - Reject the action with a reason (throws)
478
+ *
479
+ * @example
480
+ * ### Basic usage
481
+ * ```ts
482
+ * const middleware: Middleware = async (context, next) => {
483
+ * console.log(`Action: ${context.req.action}`)
484
+ * console.log(`Client: ${context.req.client?.id}`)
485
+ * console.log(`Channel: ${context.req.channel}`)
486
+ *
487
+ * await next()
488
+ * }
489
+ * ```
490
+ *
491
+ * @example
492
+ * ### Using state
493
+ * ```ts
494
+ * interface MyState {
495
+ * user: { id: string; email: string }
496
+ * requestId: string
497
+ * }
498
+ *
499
+ * const middleware: Middleware<MyState> = async (context, next) => {
500
+ * // Set state
501
+ * context.set('requestId', generateId())
502
+ *
503
+ * // Get state
504
+ * const requestId = context.get('requestId')
505
+ *
506
+ * await next()
507
+ * }
508
+ * ```
509
+ *
510
+ * @example
511
+ * ### Rejecting actions
512
+ * ```ts
513
+ * const middleware: Middleware = async (context, next) => {
514
+ * if (context.req.action === 'connect') {
515
+ * // Check connection validity
516
+ * if (!isValidConnection(context.req.client)) {
517
+ * context.reject('Connection not allowed')
518
+ * // Function never returns (throws)
519
+ * }
520
+ * }
521
+ *
522
+ * await next()
523
+ * }
524
+ * ```
525
+ */
526
+ interface IContext<S = Record<string, unknown>> {
527
+ /** Request information */
528
+ readonly req: {
529
+ /** The client connection (if applicable) */
530
+ readonly client?: IClientConnection;
531
+ /** The message being processed (if applicable) */
532
+ readonly message?: Message;
533
+ /** The channel name (if applicable) */
534
+ readonly channel?: ChannelName;
535
+ /** The action being performed */
536
+ readonly action: IMiddlewareAction;
537
+ };
538
+ /** Optional error object */
539
+ error?: Error;
540
+ /** Whether the context has been finalized */
541
+ finalized: boolean;
542
+ /** Optional response data */
543
+ res?: unknown;
544
+ /** Get a value from state by key */
545
+ get<K extends keyof S>(key: K): S[K];
546
+ /** Set a value in state by key */
547
+ set<K extends keyof S>(key: K, value: S[K]): void;
548
+ /** Reject the action with a reason (throws) */
549
+ reject(reason: string): never;
550
+ }
551
+ /**
552
+ * Middleware function signature
553
+ *
554
+ * @remarks
555
+ * Type definition for middleware functions. Middleware can be sync or async
556
+ * and can return any value (though the return value is typically ignored).
557
+ *
558
+ * @template S - Type of the state object (default: Record<string, unknown>)
559
+ *
560
+ * @param c - The middleware context
561
+ * @param next - Function to continue to the next middleware
562
+ *
563
+ * @example
564
+ * ```ts
565
+ * import type { Middleware } from '@syncar/server'
566
+ *
567
+ * const authMiddleware: Middleware = async (context, next) => {
568
+ * const token = context.req.message?.data?.token
569
+ *
570
+ * if (!token) {
571
+ * context.reject('Authentication required')
572
+ * }
573
+ *
574
+ * const user = await verifyToken(token)
575
+ * context.set('user', user)
576
+ *
577
+ * await next()
578
+ * }
579
+ * ```
580
+ *
581
+ * @see {@link Context} for context interface
582
+ * @see {@link IMiddlewareAction} for available actions
583
+ */
584
+ type IMiddleware<S = Record<string, unknown>> = (
585
+ /** The middleware context */
586
+ c: IContext<S>,
587
+ /** Function to continue to the next middleware */
588
+ next: Next) => void | Promise<void> | unknown;
589
+ /**
590
+ * Middleware rejection error interface
591
+ *
592
+ * @remarks
593
+ * Interface for errors thrown when middleware rejects an action using
594
+ * `context.reject()`. This is a standard interface - actual errors should
595
+ * use the `MiddlewareRejectionError` class from the errors module.
596
+ *
597
+ * @property reason - Human-readable reason for rejection
598
+ * @property action - The action that was rejected
599
+ * @property name - Fixed value 'MiddlewareRejectionError' for interface compliance
600
+ *
601
+ * @example
602
+ * ```ts
603
+ * function isRejectionError(error: unknown): error is IMiddlewareRejectionError {
604
+ * return (
605
+ * typeof error === 'object' &&
606
+ * error !== null &&
607
+ * 'name' in error &&
608
+ * error.name === 'MiddlewareRejectionError'
609
+ * )
610
+ * }
611
+ *
612
+ * try {
613
+ * await someOperation()
614
+ * } catch (error) {
615
+ * if (isRejectionError(error)) {
616
+ * console.error(`Action '${error.action}' rejected: ${error.reason}`)
617
+ * }
618
+ * }
619
+ * ```
620
+ */
621
+ interface IMiddlewareRejectionError {
622
+ /** Human-readable reason for rejection */
623
+ reason: string;
624
+ /** The action that was rejected */
625
+ action: string;
626
+ /** Fixed value 'MiddlewareRejectionError' for interface compliance */
627
+ name: 'MiddlewareRejectionError';
628
+ }
629
+
630
+ export { type AckMessage as A, type ChannelName as C, type DataMessage as D, ErrorCode as E, type IClientConnection as I, type Message as M, type SignalMessage as S, type Timestamp as T, type ClientId as a, type IMiddleware as b, type ILogger as c, type IContext as d, type IMiddlewareAction as e, type IMiddlewareRejectionError as f, type ErrorMessage as g, type MessageId as h, MessageType as i, SignalType as j };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncar/server",
3
- "version": "1.0.0-alpha.2",
3
+ "version": "1.0.0-alpha.3",
4
4
  "description": "Node.js server for Syncar real-time synchronization",
5
5
  "author": "M16BAPPI - [m16bappi@gmail.com]",
6
6
  "type": "module",
@@ -11,6 +11,10 @@
11
11
  ".": {
12
12
  "types": "./dist/index.d.ts",
13
13
  "import": "./dist/index.js"
14
+ },
15
+ "./middleware": {
16
+ "types": "./dist/middleware/index.d.ts",
17
+ "import": "./dist/middleware/index.js"
14
18
  }
15
19
  },
16
20
  "files": [