@swarmmachina/swm-core 1.0.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 ADDED
@@ -0,0 +1,890 @@
1
+ # @swarmmachina/swm-core
2
+
3
+ [![License: MPL 2.0](https://img.shields.io/badge/License-MPL%202.0-brightgreen.svg)](https://opensource.org/licenses/MPL-2.0)
4
+ [![Node.js Version](https://img.shields.io/badge/node-%3E%3D22.0.0-brightgreen)](https://nodejs.org/)
5
+ [![dependencies](https://img.shields.io/badge/dependencies-1-brightgreen.svg)](#)
6
+ [![stability](https://img.shields.io/badge/stability-experimental-yellow.svg)](#)
7
+
8
+ A zero-dependency, high-performance HTTP/WebSocket server built
9
+ on [uWebSockets.js](https://github.com/uNetworking/uWebSockets.js).
10
+
11
+ ## Features
12
+
13
+ - **Zero dependencies** - Only uses uWebSockets.js for maximum performance
14
+ - **HTTP + WebSocket** - Both protocols in a single server instance
15
+ - **High performance** - Built on the fastest WebSocket server available
16
+ - **Context pooling** - Minimizes garbage collection overhead
17
+ - **Graceful shutdown** - Cleanly closes active connections
18
+ - **Streaming support** - Efficient handling of large payloads
19
+ - **Auto Content-Type detection** - Automatically sets headers based on response type
20
+ - **Modern ES modules** - Native ESM support (Node.js 22+)
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ # Install the package
26
+ npm install @swarmmachina/swm-core
27
+ ```
28
+
29
+ ## Quick Start
30
+
31
+ ### Basic HTTP Server
32
+
33
+ ```javascript
34
+ import Server from '@swarmmachina/swm-core'
35
+
36
+ const server = new Server({
37
+ port: 3000,
38
+ router: (ctx) => {
39
+ return { message: 'Hello World' }
40
+ }
41
+ })
42
+
43
+ await server.listen()
44
+ console.log('Server listening on port 3000')
45
+ ```
46
+
47
+ ### HTTP Server with Routing (Traditional API)
48
+
49
+ ```javascript
50
+ import Server from '@swarmmachina/swm-core'
51
+
52
+ const server = new Server({
53
+ port: 3000,
54
+ router: async (ctx) => {
55
+ // Simple routing
56
+ if (ctx.url() === '/' && ctx.method() === 'get') {
57
+ return { message: 'Welcome to the API' }
58
+ }
59
+
60
+ if (ctx.url() === '/users' && ctx.method() === 'get') {
61
+ return { users: await getUsers() }
62
+ }
63
+
64
+ if (ctx.url() === '/users' && ctx.method() === 'post') {
65
+ const data = await ctx.json()
66
+ return await createUser(data)
67
+ }
68
+
69
+ // 404 Not Found
70
+ ctx.status(404)
71
+ return { error: 'Not found' }
72
+ },
73
+ onHttpError: (ctx, error) => {
74
+ console.error('HTTP Error:', error)
75
+ }
76
+ })
77
+
78
+ await server.listen()
79
+ ```
80
+
81
+ ### HTTP Server with Native Routing (New API)
82
+
83
+ For better performance and cleaner code, you can use native uWebSockets.js routing:
84
+
85
+ ```javascript
86
+ import Server from '@swarmmachina/swm-core'
87
+
88
+ const server = new Server({
89
+ port: 3000,
90
+ routes: [
91
+ {
92
+ method: 'get',
93
+ path: '/',
94
+ handler: () => ({ message: 'Welcome to the API' })
95
+ },
96
+ {
97
+ method: 'get',
98
+ path: '/users',
99
+ handler: async () => ({ users: await getUsers() })
100
+ },
101
+ {
102
+ method: 'get',
103
+ path: '/users/:id',
104
+ handler: (ctx) => {
105
+ const id = ctx.param('id') // or ctx.param(0)
106
+ return getUserById(id)
107
+ }
108
+ },
109
+ {
110
+ method: 'post',
111
+ path: '/users',
112
+ handler: async (ctx) => {
113
+ const data = await ctx.json()
114
+ return await createUser(data)
115
+ }
116
+ },
117
+ {
118
+ method: 'put',
119
+ path: '/users/:id',
120
+ handler: async (ctx) => {
121
+ const id = ctx.param('id')
122
+ const data = await ctx.json()
123
+ return await updateUser(id, data)
124
+ }
125
+ },
126
+ {
127
+ method: 'delete',
128
+ path: '/users/:id',
129
+ handler: (ctx) => {
130
+ const id = ctx.param('id')
131
+ return deleteUser(id)
132
+ }
133
+ }
134
+ ],
135
+ onHttpError: (ctx, error) => {
136
+ console.error('HTTP Error:', error)
137
+ }
138
+ })
139
+
140
+ await server.listen()
141
+ ```
142
+
143
+ **Benefits of Native Routing:**
144
+
145
+ - **Better Performance** - Routes are registered at C++ level for faster matching
146
+ - **URL Parameters** - Built-in support for `:param` syntax
147
+ - **Cleaner Code** - Declarative route definitions
148
+ - **Method-specific** - Automatic HTTP method routing
149
+
150
+ ### WebSocket Server
151
+
152
+ ```javascript
153
+ import Server from '@swarmmachina/swm-core'
154
+
155
+ const server = new Server({
156
+ port: 3000,
157
+ router: (ctx) => {
158
+ return { message: 'HTTP endpoint' }
159
+ },
160
+ ws: {
161
+ enabled: true,
162
+ wsIdleTimeoutSec: 30,
163
+ onUpgrade: (meta) => ({
164
+ isAllowed: true,
165
+ userData: { ip: meta.ip() }
166
+ }),
167
+ onOpen: (ctx) => {
168
+ console.log('Client connected:', ctx.data.ip)
169
+ ctx.send('Welcome!')
170
+ },
171
+ onMessage: (ctx, message, isBinary) => {
172
+ const text = Buffer.from(message).toString()
173
+ console.log('Received:', text)
174
+ ctx.send(`Echo: ${text}`)
175
+ },
176
+ onClose: (ctx, code, message) => {
177
+ console.log('Client disconnected:', ctx.data.ip)
178
+ },
179
+ onError: (ctx, error) => {
180
+ console.error('WebSocket error:', error)
181
+ }
182
+ }
183
+ })
184
+
185
+ await server.listen()
186
+ ```
187
+
188
+ ## API Documentation
189
+
190
+ ### Server Constructor
191
+
192
+ ```javascript
193
+ new Server(options)
194
+ ```
195
+
196
+ **Options:**
197
+
198
+ | Option | Type | Default | Description |
199
+ | ------------- | ---------- | -------------- | ------------------------------------------------------- |
200
+ | `router` | `Function` | _one required_ | Route handler function `(ctx) => any` (traditional API) |
201
+ | `routes` | `Array` | _one required_ | Array of route definitions (native routing API) |
202
+ | `onHttpError` | `Function` | `() => {}` | Error handler `(ctx, error) => void` |
203
+ | `port` | `Number` | `6000` | Server port (1-65535) |
204
+ | `maxBodySize` | `Number` | `1` | Max request body size in MB (1-64) |
205
+ | `ws` | `Object` | `null` | WebSocket configuration (see below) |
206
+
207
+ **Note:** You must provide either `router` or `routes`, but not both.
208
+
209
+ **Route Definition (for `routes` array):**
210
+
211
+ | Property | Type | Description |
212
+ | --------- | ---------- | ---------------------------------------------------------------------------------------------- |
213
+ | `method` | `String` | HTTP method: `'get'`, `'post'`, `'put'`, `'delete'`, `'patch'`, `'options'`, `'head'`, `'any'` |
214
+ | `path` | `String` | URL path pattern (supports `:param` syntax) |
215
+ | `handler` | `Function` | Handler function `(ctx) => any \| Promise<any>` |
216
+
217
+ **WebSocket Options (`ws` object):**
218
+
219
+ | Option | Type | Default | Description |
220
+ | ------------------ | ---------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
221
+ | `enabled` | `Boolean` | `false` | Enable WebSocket support. If not set and at least one ws handler is provided, WS will be enabled automatically. |
222
+ | `wsIdleTimeoutSec` | `Number` | `15` | Idle timeout in seconds (min: 5) |
223
+ | `onOpen` | `Function` | `(ctx) => {}` | Called when client connects |
224
+ | `onMessage` | `Function` | `(ctx, message, isBinary) => {}` | Called when message received |
225
+ | `onClose` | `Function` | `(ctx, code, message) => {}` | Called when client disconnects |
226
+ | `onDrain` | `Function` | `(ctx) => {}` | Called when socket is writable again |
227
+ | `onError` | `Function` | `(ctx, error) => {}` | Called on WebSocket error |
228
+ | `onUpgrade` | `Function` | `(meta) => ({isAllowed: true, userData?: object})` | Validate WebSocket upgrade. Receives `meta` object with: `url()`, `ip()`, `getHeader(name)`, `getQuery(key)`, `getParameter(indexOrName)`, `aborted` boolean. Return `userData` to make it available via `ctx.data` |
229
+ | `onSubscription` | `Function` | `(ctx, topic, newCount, oldCount) => {}` | Called on topic subscription change |
230
+
231
+ ### Server Methods
232
+
233
+ #### `server.listen()`
234
+
235
+ Start the server and begin accepting connections.
236
+
237
+ ```javascript
238
+ await server.listen()
239
+ ```
240
+
241
+ #### `server.shutdown([timeout])`
242
+
243
+ Gracefully shutdown the server. Waits for active connections to finish.
244
+
245
+ ```javascript
246
+ server.shutdown(10000) // 10 second timeout
247
+ ```
248
+
249
+ #### `server.close()`
250
+
251
+ Forcefully close the server immediately.
252
+
253
+ ```javascript
254
+ server.close()
255
+ ```
256
+
257
+ #### `server.publish(topic, message, [isBinary])`
258
+
259
+ Publish message to all WebSocket clients subscribed to a topic.
260
+
261
+ ```javascript
262
+ server.publish('news', 'Breaking news!', false)
263
+ ```
264
+
265
+ **Returns:** `boolean` - Success status
266
+
267
+ #### `server.getSubscribersCount(topic)`
268
+
269
+ Get number of subscribers for a topic.
270
+
271
+ ```javascript
272
+ const count = server.getSubscribersCount('news')
273
+ ```
274
+
275
+ **Returns:** `number` - Subscriber count
276
+
277
+ ### HttpContext API
278
+
279
+ The `ctx` object passed to the router function:
280
+
281
+ #### Properties
282
+
283
+ | Property | Type | Description |
284
+ | ------------- | --------- | ------------------------------ |
285
+ | `ctx.replied` | `Boolean` | Whether response has been sent |
286
+ | `ctx.aborted` | `Boolean` | Whether request was aborted |
287
+
288
+ #### Methods
289
+
290
+ ##### `ctx.method()`
291
+
292
+ Get request lowercased method.
293
+
294
+ ```javascriptx
295
+ const method = ctx.method()
296
+ ```
297
+
298
+ **Returns:** `string`
299
+
300
+ ##### `ctx.url()`
301
+
302
+ Get request url.
303
+
304
+ ```javascript
305
+ const url = ctx.url()
306
+ ```
307
+
308
+ **Returns:** `string`
309
+
310
+ ##### `ctx.ip()`
311
+
312
+ Get client IP address.
313
+
314
+ ```javascript
315
+ const ip = ctx.ip()
316
+ ```
317
+
318
+ **Returns:** `string`
319
+
320
+ ##### `ctx.query(name)`
321
+
322
+ Get query parameter value.
323
+
324
+ ```javascript
325
+ const page = ctx.query('page') // ?page=1
326
+ ```
327
+
328
+ **Returns:** `string`
329
+
330
+ ##### `ctx.param(indexOrName)`
331
+
332
+ Get URL parameter by index or name (for pattern matching in native routing).
333
+
334
+ ```javascript
335
+ // By index
336
+ const id = ctx.param(0) // First parameter
337
+
338
+ // By name (native routing only)
339
+ const id = ctx.param('id') // /users/:id
340
+
341
+ // Multiple parameters
342
+ const userId = ctx.param('userId') // /users/:userId/posts/:postId
343
+ const postId = ctx.param('postId')
344
+ ```
345
+
346
+ **Returns:** `string`
347
+
348
+ ##### `ctx.header(name)`
349
+
350
+ Get request header value.
351
+
352
+ ```javascript
353
+ const auth = ctx.header('authorization')
354
+ ```
355
+
356
+ **Returns:** `string`
357
+
358
+ ##### `ctx.body([maxSize])`
359
+
360
+ Read request body as Buffer.
361
+
362
+ ```javascript
363
+ const buffer = await ctx.body()
364
+ const buffer = await ctx.body(5 * 1024 * 1024) // 5MB limit
365
+ ```
366
+
367
+ **Returns:** `Promise<Buffer>`
368
+
369
+ ##### `ctx.json([maxSize])`
370
+
371
+ Parse request body as JSON.
372
+
373
+ ```javascript
374
+ const data = await ctx.json()
375
+ ```
376
+
377
+ **Returns:** `Promise<any>`
378
+
379
+ ##### `ctx.text([maxSize])`
380
+
381
+ Read request body as text.
382
+
383
+ ```javascript
384
+ const text = await ctx.text()
385
+ ```
386
+
387
+ **Returns:** `Promise<string>`
388
+
389
+ ##### `ctx.status(code)`
390
+
391
+ Set response status code. Returns context for chaining.
392
+
393
+ ```javascript
394
+ ctx.status(201).send({ created: true })
395
+ ```
396
+
397
+ **Returns:** `HttpContext`
398
+
399
+ ##### `ctx.setHeader(key, value)`
400
+
401
+ Set a response header. Returns context for chaining.
402
+
403
+ ```javascript
404
+ ctx.setHeader('x-header-any', 'string-value').status(201).send({ created: true })
405
+ ```
406
+
407
+ **Returns:** `HttpContext`
408
+
409
+ ##### `ctx.send(data)`
410
+
411
+ Send response with automatic content-type detection.
412
+
413
+ ```javascript
414
+ ctx.send({ message: 'OK' }) // application/json
415
+ ctx.send('Hello') // text/plain
416
+ ctx.send(Buffer.from('data')) // application/octet-stream
417
+ ctx.send(null) // 204 No Content
418
+ ```
419
+
420
+ **Supported types:** Object, String, Buffer, null, undefined
421
+
422
+ ##### `ctx.reply(status, headers, body)`
423
+
424
+ Send response with full control over status, headers, and body.
425
+
426
+ ```javascript
427
+ ctx.reply(200, { 'content-type': 'application/json' }, '{"ok":true}')
428
+ ```
429
+
430
+ ##### `ctx.stream(readable, [status], [headers])`
431
+
432
+ Stream a readable stream to the response.
433
+
434
+ ```javascript
435
+ import fs from 'fs'
436
+
437
+ const stream = fs.createReadStream('./large-file.mp4')
438
+ await ctx.stream(stream, 200, { 'content-type': 'video/mp4' })
439
+ ```
440
+
441
+ **Returns:** `Promise<void>`
442
+
443
+ ##### `ctx.startStreaming([status], [headers])`
444
+
445
+ Start streaming response manually (for advanced use cases).
446
+
447
+ ```javascript
448
+ ctx.startStreaming(200, { 'content-type': 'text/plain' })
449
+ ```
450
+
451
+ ##### `ctx.write(chunk)`
452
+
453
+ Write chunk to streaming response.
454
+
455
+ ```javascript
456
+ const ok = ctx.write('chunk of data')
457
+ if (!ok) {
458
+ // backpressure, pause writing
459
+ }
460
+ ```
461
+
462
+ **Returns:** `boolean` - `false` if backpressure detected
463
+
464
+ ##### `ctx.end([chunk])`
465
+
466
+ End streaming response.
467
+
468
+ ```javascript
469
+ ctx.end('final chunk')
470
+ ```
471
+
472
+ ##### `ctx.onWritable(callback)`
473
+
474
+ Register callback to be called when the response stream becomes writable again (for backpressure handling). The callback
475
+ receives the current write offset.
476
+
477
+ ```javascript
478
+ ctx.onWritable((offset) => {
479
+ // Socket is writable again, can resume writing
480
+ // offset is the current write offset
481
+ })
482
+ ```
483
+
484
+ **Returns:** `void`
485
+
486
+ ##### `ctx.tryEnd(chunk)`
487
+
488
+ Try to end the streaming response with a final chunk. Calculates `totalSize = getWriteOffset() + chunkLen` and calls
489
+ `res.tryEnd(chunk, totalSize)`.
490
+
491
+ ```javascript
492
+ const [ok, done] = ctx.tryEnd('final chunk')
493
+ if (done) {
494
+ // Response is complete
495
+ }
496
+ ```
497
+
498
+ **Returns:** `[boolean, boolean]` - `[ok, done]` where `ok` indicates success and `done` indicates completion
499
+
500
+ ##### `ctx.getWriteOffset()`
501
+
502
+ Get the current write offset (useful for `tryEnd` and backpressure handling).
503
+
504
+ ```javascript
505
+ const offset = ctx.getWriteOffset()
506
+ ```
507
+
508
+ **Returns:** `number` - Current write offset
509
+
510
+ ### WSContext API
511
+
512
+ The `ctx` object passed to WebSocket handlers:
513
+
514
+ #### Properties
515
+
516
+ | Property | Type | Description |
517
+ | ---------- | ----------- | ---------------------------------------------------------- |
518
+ | `ctx.data` | `Object` | User data from `onUpgrade` return value (`userData` field) |
519
+ | `ctx.ws` | `WebSocket` | Raw uWS WebSocket object |
520
+
521
+ #### Methods
522
+
523
+ ##### `ctx.send(data, [isBinary])`
524
+
525
+ Send message to this client.
526
+
527
+ ```javascript
528
+ ctx.send('Hello client!')
529
+ ctx.send(Buffer.from([1, 2, 3]), true) // binary
530
+ ```
531
+
532
+ **Returns:** `number` - Send status
533
+
534
+ ##### `ctx.end([code], [reason])`
535
+
536
+ Close this WebSocket connection.
537
+
538
+ ```javascript
539
+ ctx.end(1000, 'Goodbye')
540
+ ```
541
+
542
+ ##### `ctx.subscribe(topic)`
543
+
544
+ Subscribe this client to a topic.
545
+
546
+ ```javascript
547
+ ctx.subscribe('news')
548
+ ```
549
+
550
+ **Returns:** `boolean` - Success status
551
+
552
+ ##### `ctx.unsubscribe(topic)`
553
+
554
+ Unsubscribe this client from a topic.
555
+
556
+ ```javascript
557
+ ctx.unsubscribe('news')
558
+ ```
559
+
560
+ **Returns:** `boolean` - Success status
561
+
562
+ ##### `ctx.publish(topic, message, [isBinary])`
563
+
564
+ Publish message to all subscribers of a topic.
565
+
566
+ ```javascript
567
+ ctx.publish('news', 'Breaking news!')
568
+ ```
569
+
570
+ **Returns:** `boolean` - Success status
571
+
572
+ ## Examples
573
+
574
+ ### REST API with Error Handling
575
+
576
+ ```javascript
577
+ import Server from '@swarmmachina/swm-core'
578
+
579
+ const users = new Map()
580
+
581
+ const server = new Server({
582
+ port: 3000,
583
+ router: async (ctx) => {
584
+ try {
585
+ // GET /users
586
+ if (ctx.url() === '/users' && ctx.method() === 'get') {
587
+ return Array.from(users.values())
588
+ }
589
+
590
+ // GET /users/:id
591
+ if (ctx.url().startsWith('/users/') && ctx.method() === 'get') {
592
+ const id = ctx.url().split('/')[2]
593
+ const user = users.get(id)
594
+
595
+ if (!user) {
596
+ return ctx.status(404).send({ error: 'User not found' })
597
+ }
598
+
599
+ return user
600
+ }
601
+
602
+ // POST /users
603
+ if (ctx.url() === '/users' && ctx.method() === 'post') {
604
+ const data = await ctx.json()
605
+
606
+ if (!data.name || !data.email) {
607
+ return ctx.status(400).send({ error: 'Missing required fields' })
608
+ }
609
+
610
+ const user = { id: Date.now().toString(), ...data }
611
+ users.set(user.id, user)
612
+
613
+ return ctx.status(201).send(user)
614
+ }
615
+
616
+ // 404
617
+ return ctx.status(404).send({ error: 'Not found' })
618
+ } catch (error) {
619
+ console.error('Route error:', error)
620
+ return ctx.status(500).send({ error: 'Internal server error' })
621
+ }
622
+ },
623
+ onHttpError: (ctx, error) => {
624
+ console.error(`HTTP Error [${ctx.method()} ${ctx.url()}]:`, error)
625
+ }
626
+ })
627
+
628
+ await server.listen()
629
+ console.log('REST API running on http://localhost:3000')
630
+ ```
631
+
632
+ ### File Upload
633
+
634
+ ```javascript
635
+ import Server from '@swarmmachina/swm-core'
636
+ import fs from 'fs/promises'
637
+
638
+ const server = new Server({
639
+ port: 3000,
640
+ maxBodySize: 10, // 10 MB
641
+ router: async (ctx) => {
642
+ if (ctx.url() === '/upload' && ctx.method() === 'post') {
643
+ const filename = ctx.query('filename') || 'upload.bin'
644
+ const body = await ctx.body()
645
+
646
+ await fs.writeFile(`./uploads/${filename}`, body)
647
+
648
+ return ctx.status(201).send({
649
+ success: true,
650
+ filename,
651
+ size: body.length
652
+ })
653
+ }
654
+
655
+ return ctx.status(404).send({ error: 'Not found' })
656
+ }
657
+ })
658
+
659
+ await server.listen()
660
+ ```
661
+
662
+ ### File Streaming
663
+
664
+ ```javascript
665
+ import Server from '@swarmmachina/swm-core'
666
+ import fs from 'fs'
667
+
668
+ const server = new Server({
669
+ port: 3000,
670
+ router: async (ctx) => {
671
+ if (ctx.url() === '/download' && ctx.method() === 'get') {
672
+ const filename = ctx.query('file')
673
+
674
+ if (!filename) {
675
+ return ctx.status(400).send({ error: 'Missing file parameter' })
676
+ }
677
+
678
+ const stream = fs.createReadStream(`./files/${filename}`)
679
+
680
+ await ctx.stream(stream, 200, {
681
+ 'content-type': 'application/octet-stream',
682
+ 'content-disposition': `attachment; filename="${filename}"`
683
+ })
684
+
685
+ return
686
+ }
687
+
688
+ return ctx.status(404).send({ error: 'Not found' })
689
+ }
690
+ })
691
+
692
+ await server.listen()
693
+ ```
694
+
695
+ ### WebSocket Chat Room
696
+
697
+ ```javascript
698
+ import Server from '@swarmmachina/swm-core'
699
+
700
+ const server = new Server({
701
+ port: 3000,
702
+ router: (ctx) => {
703
+ return { message: 'WebSocket chat server' }
704
+ },
705
+ ws: {
706
+ enabled: true,
707
+ onUpgrade: (meta) => ({
708
+ isAllowed: true,
709
+ userData: { username: meta.getQuery('username') || 'Anonymous' }
710
+ }),
711
+ onOpen: (ctx) => {
712
+ console.log('User joined:', ctx.data.username)
713
+ ctx.subscribe('chat')
714
+ ctx.publish(
715
+ 'chat',
716
+ JSON.stringify({
717
+ type: 'join',
718
+ user: ctx.data.username
719
+ })
720
+ )
721
+ },
722
+ onMessage: (ctx, message, isBinary) => {
723
+ const text = Buffer.from(message).toString()
724
+
725
+ // Broadcast to all clients in the chat room
726
+ ctx.publish(
727
+ 'chat',
728
+ JSON.stringify({
729
+ type: 'message',
730
+ user: ctx.data.username,
731
+ text
732
+ })
733
+ )
734
+ },
735
+ onClose: (ctx, code, message) => {
736
+ console.log('User left:', ctx.data.username)
737
+ ctx.publish(
738
+ 'chat',
739
+ JSON.stringify({
740
+ type: 'leave',
741
+ user: ctx.data.username
742
+ })
743
+ )
744
+ }
745
+ }
746
+ })
747
+
748
+ await server.listen()
749
+ console.log('Chat server running on ws://localhost:3000')
750
+ ```
751
+
752
+ ### WebSocket with Authentication
753
+
754
+ ```javascript
755
+ import Server from '@swarmmachina/swm-core'
756
+
757
+ const server = new Server({
758
+ port: 3000,
759
+ router: (ctx) => ({ ok: true }),
760
+ ws: {
761
+ enabled: true,
762
+ onUpgrade: async (meta) => {
763
+ // Validate token from query or header
764
+ const token = meta.getQuery('token') || meta.getHeader('authorization')
765
+
766
+ if (!token) {
767
+ return { isAllowed: false }
768
+ }
769
+
770
+ try {
771
+ const user = await validateToken(token)
772
+
773
+ return {
774
+ isAllowed: true,
775
+ userData: { userId: user.id, username: user.name }
776
+ }
777
+ } catch (error) {
778
+ return { isAllowed: false }
779
+ }
780
+ },
781
+ onOpen: (ctx) => {
782
+ console.log('Authenticated user:', ctx.data.username)
783
+ ctx.send(`Welcome, ${ctx.data.username}!`)
784
+ },
785
+ onMessage: (ctx, message, isBinary) => {
786
+ const text = Buffer.from(message).toString()
787
+ console.log(`[${ctx.data.username}]:`, text)
788
+ }
789
+ }
790
+ })
791
+
792
+ await server.listen()
793
+ ```
794
+
795
+ ## Advanced Usage
796
+
797
+ ### Graceful Shutdown
798
+
799
+ ```javascript
800
+ const server = new Server({
801
+ /* ... */
802
+ })
803
+ await server.listen()
804
+
805
+ // Handle shutdown signals
806
+ process.on('SIGTERM', () => {
807
+ console.log('SIGTERM received, shutting down gracefully...')
808
+ server.shutdown(10000) // 10 second timeout
809
+ })
810
+
811
+ process.on('SIGINT', () => {
812
+ console.log('SIGINT received, shutting down gracefully...')
813
+ server.shutdown(10000)
814
+ })
815
+ ```
816
+
817
+ ### Custom Response Headers
818
+
819
+ ```javascript
820
+ const server = new Server({
821
+ router: (ctx) => {
822
+ // Set custom headers
823
+ ctx.setHeader('custom-header', 'value')
824
+ return ctx.reply(
825
+ 200,
826
+ {
827
+ 'content-type': 'application/json',
828
+ 'x-custom-header': 'value',
829
+ 'cache-control': 'no-cache'
830
+ },
831
+ JSON.stringify({ ok: true })
832
+ )
833
+ }
834
+ })
835
+ ```
836
+
837
+ ### Backpressure Handling
838
+
839
+ ```javascript
840
+ const server = new Server({
841
+ router: async (ctx) => {
842
+ if (ctx.url() === '/stream') {
843
+ ctx.startStreaming(200, { 'content-type': 'text/plain' })
844
+
845
+ for (let i = 0; i < 1000; i++) {
846
+ const ok = ctx.write(`Chunk ${i}\n`)
847
+
848
+ if (!ok) {
849
+ // Handle backpressure
850
+ await new Promise((resolve) => {
851
+ ctx.onWritable((offset) => {
852
+ resolve(offset)
853
+ })
854
+ })
855
+ }
856
+ }
857
+
858
+ ctx.end()
859
+ }
860
+ }
861
+ })
862
+ ```
863
+
864
+ ## Testing
865
+
866
+ ```bash
867
+ # Run tests
868
+ npm test
869
+
870
+ # Run tests with coverage
871
+ npm test:coverage
872
+ ```
873
+
874
+ ## Contributing
875
+
876
+ Contributions are welcome! Please feel free to submit a Pull Request.
877
+
878
+ 1. Fork the repository
879
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
880
+ 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
881
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
882
+ 5. Open a Pull Request
883
+
884
+ ## License
885
+
886
+ Licensed under the MPL-2.0 License.
887
+
888
+ Copyright © 2025 SwarmMachina Team
889
+
890
+ See [LICENSE](LICENSE) file for details.