@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/LICENSE +363 -0
- package/README.md +890 -0
- package/package.json +61 -0
- package/src/context-pool.js +58 -0
- package/src/http-context.js +761 -0
- package/src/index.js +785 -0
- package/src/ws-context.js +108 -0
package/README.md
ADDED
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
# @swarmmachina/swm-core
|
|
2
|
+
|
|
3
|
+
[](https://opensource.org/licenses/MPL-2.0)
|
|
4
|
+
[](https://nodejs.org/)
|
|
5
|
+
[](#)
|
|
6
|
+
[](#)
|
|
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.
|