@syncar/server 1.0.0-alpha.2 → 1.0.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -148
- package/dist/index.d.ts +329 -2519
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/index.d.ts +160 -0
- package/dist/middleware/index.js +2 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/types-DeE5vqYX.d.ts +630 -0
- package/package.json +5 -1
|
@@ -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.
|
|
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": [
|