@swarmmachina/swm-core 1.1.5 → 1.2.0
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 +87 -6
- package/package.json +3 -1
- package/src/http-context.js +140 -20
- package/src/res-streamer.js +1 -1
package/README.md
CHANGED
|
@@ -291,7 +291,7 @@ The `ctx` object passed to the router function:
|
|
|
291
291
|
|
|
292
292
|
Get request lowercased method.
|
|
293
293
|
|
|
294
|
-
```
|
|
294
|
+
```javascript
|
|
295
295
|
const method = ctx.method()
|
|
296
296
|
```
|
|
297
297
|
|
|
@@ -408,7 +408,7 @@ ctx.status(201).send({ created: true })
|
|
|
408
408
|
|
|
409
409
|
##### `ctx.setHeader(key, value)`
|
|
410
410
|
|
|
411
|
-
Set a response header.
|
|
411
|
+
Set or replace a staged response header. Header names are case-insensitive. Repeated `setHeader()` calls replace previously staged values for the same header. Null or undefined values are silently ignored.
|
|
412
412
|
|
|
413
413
|
```javascript
|
|
414
414
|
ctx.setHeader('x-header-any', 'string-value').status(201).send({ created: true })
|
|
@@ -416,6 +416,37 @@ ctx.setHeader('x-header-any', 'string-value').status(201).send({ created: true }
|
|
|
416
416
|
|
|
417
417
|
**Returns:** `HttpContext`
|
|
418
418
|
|
|
419
|
+
##### `ctx.appendHeader(key, value)`
|
|
420
|
+
|
|
421
|
+
Append another staged response header line without replacing existing values. Useful for repeated headers such as `Set-Cookie`. Null or undefined values are silently ignored.
|
|
422
|
+
|
|
423
|
+
```javascript
|
|
424
|
+
ctx.appendHeader('set-cookie', 'access=...; Path=/; HttpOnly')
|
|
425
|
+
ctx.appendHeader('set-cookie', 'refresh=...; Path=/refresh; HttpOnly')
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
**Returns:** `HttpContext`
|
|
429
|
+
|
|
430
|
+
##### `ctx.setHeaders(headers)`
|
|
431
|
+
|
|
432
|
+
Set multiple response headers at once. Equivalent to calling `setHeader()` for each key. Header values may be strings or arrays of strings.
|
|
433
|
+
|
|
434
|
+
```javascript
|
|
435
|
+
ctx.setHeaders({
|
|
436
|
+
'x-request-id': '123',
|
|
437
|
+
'cache-control': 'no-cache',
|
|
438
|
+
'set-cookie': ['a=1; Path=/', 'b=2; Path=/refresh']
|
|
439
|
+
})
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
##### `ctx.flushHeaders([headers])`
|
|
443
|
+
|
|
444
|
+
Flush all staged headers (and optionally stage additional ones) to the underlying response. Called automatically by `reply()` and `startStreaming()` — only needed for advanced use cases.
|
|
445
|
+
|
|
446
|
+
```javascript
|
|
447
|
+
ctx.flushHeaders({ 'x-extra': 'value' })
|
|
448
|
+
```
|
|
449
|
+
|
|
419
450
|
##### `ctx.send(data)`
|
|
420
451
|
|
|
421
452
|
Send response with automatic content-type detection.
|
|
@@ -429,12 +460,59 @@ ctx.send(null) // 204 No Content
|
|
|
429
460
|
|
|
430
461
|
**Supported types:** Object, String, Buffer, null, undefined
|
|
431
462
|
|
|
463
|
+
##### `ctx.sendJson(data, [status])`
|
|
464
|
+
|
|
465
|
+
Send a JSON response with explicit status code. Defaults to `200`.
|
|
466
|
+
|
|
467
|
+
```javascript
|
|
468
|
+
ctx.sendJson({ users: [] })
|
|
469
|
+
ctx.sendJson({ error: 'Not found' }, 404)
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
##### `ctx.sendText(text, [status])`
|
|
473
|
+
|
|
474
|
+
Send a plain text response with explicit status code. Defaults to `200`.
|
|
475
|
+
|
|
476
|
+
```javascript
|
|
477
|
+
ctx.sendText('OK')
|
|
478
|
+
ctx.sendText('Created', 201)
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
##### `ctx.sendBuffer(buffer, [status])`
|
|
482
|
+
|
|
483
|
+
Send a binary response with explicit status code. Defaults to `200`.
|
|
484
|
+
|
|
485
|
+
```javascript
|
|
486
|
+
ctx.sendBuffer(Buffer.from('data'))
|
|
487
|
+
ctx.sendBuffer(imageBuffer, 201)
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
##### `ctx.sendError(error)`
|
|
491
|
+
|
|
492
|
+
Send an error response. If `error.status` is a finite number, uses that as the HTTP status with `error.message` as the body. Otherwise responds with `500 Internal Server Error`.
|
|
493
|
+
|
|
494
|
+
```javascript
|
|
495
|
+
ctx.sendError(new Error('Something broke'))
|
|
496
|
+
|
|
497
|
+
// With custom status
|
|
498
|
+
const err = new Error('Not found')
|
|
499
|
+
err.status = 404
|
|
500
|
+
ctx.sendError(err)
|
|
501
|
+
```
|
|
502
|
+
|
|
432
503
|
##### `ctx.reply(status, headers, body)`
|
|
433
504
|
|
|
434
|
-
Send response with full control over status, headers, and body.
|
|
505
|
+
Send response with full control over status, headers, and body. Header values may be strings or arrays of strings. Array values are written as separate header lines.
|
|
435
506
|
|
|
436
507
|
```javascript
|
|
437
|
-
ctx.reply(
|
|
508
|
+
ctx.reply(
|
|
509
|
+
200,
|
|
510
|
+
{
|
|
511
|
+
'content-type': 'application/json',
|
|
512
|
+
'set-cookie': ['a=1; Path=/', 'b=2; Path=/refresh']
|
|
513
|
+
},
|
|
514
|
+
'{"ok":true}'
|
|
515
|
+
)
|
|
438
516
|
```
|
|
439
517
|
|
|
440
518
|
##### `ctx.stream(readable, [status], [headers])`
|
|
@@ -452,10 +530,13 @@ await ctx.stream(stream, 200, { 'content-type': 'video/mp4' })
|
|
|
452
530
|
|
|
453
531
|
##### `ctx.startStreaming([status], [headers])`
|
|
454
532
|
|
|
455
|
-
Start streaming response manually (for advanced use cases).
|
|
533
|
+
Start streaming response manually (for advanced use cases). Header values may be strings or arrays of strings.
|
|
456
534
|
|
|
457
535
|
```javascript
|
|
458
|
-
ctx.startStreaming(200, {
|
|
536
|
+
ctx.startStreaming(200, {
|
|
537
|
+
'content-type': 'text/plain',
|
|
538
|
+
'set-cookie': ['a=1; Path=/', 'b=2; Path=/refresh']
|
|
539
|
+
})
|
|
459
540
|
```
|
|
460
541
|
|
|
461
542
|
##### `ctx.write(chunk)`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmmachina/swm-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Zero-dependency high-performance HTTP/WebSocket server built on uWebSockets.js",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"http",
|
|
@@ -44,6 +44,8 @@
|
|
|
44
44
|
"test:e2e:ws": "node --test tests/e2e/ws/*.test.js",
|
|
45
45
|
"bench": "node ./benchmark/bench.js --test base --runs 1 --warmup 10 --sample-ms 250 --v8prof true",
|
|
46
46
|
"bench:core": "node ./benchmark/bench.js --test base --runs 1 --warmup 10 --sample-ms 250 --v8prof true --fw core",
|
|
47
|
+
"bench:headers": "node ./benchmark/bench.js --test headers --runs 1 --warmup 10 --sample-ms 250 --v8prof true",
|
|
48
|
+
"bench:headers:core": "node ./benchmark/bench.js --test headers --runs 1 --warmup 10 --sample-ms 250 --v8prof true --fw core",
|
|
47
49
|
"prepublishOnly": "npm run fix && npm test",
|
|
48
50
|
"release": "npm run check && npm test && npm publish"
|
|
49
51
|
},
|
package/src/http-context.js
CHANGED
|
@@ -428,12 +428,61 @@ export default class HttpContext {
|
|
|
428
428
|
return this
|
|
429
429
|
}
|
|
430
430
|
|
|
431
|
-
|
|
431
|
+
if (typeof key !== 'string') {
|
|
432
|
+
throw new TypeError('Header name must be a string')
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (value === undefined || value === null) {
|
|
436
|
+
return this
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const headerKey = key.toLowerCase()
|
|
440
|
+
|
|
441
|
+
this.#pendingHeaders.set(headerKey, [key, `${value}`])
|
|
442
|
+
return this
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* @param {string} key
|
|
447
|
+
* @param {string} value
|
|
448
|
+
* @returns {HttpContext}
|
|
449
|
+
*/
|
|
450
|
+
appendHeader(key, value) {
|
|
451
|
+
if (this.replied || this.aborted) {
|
|
452
|
+
return this
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (typeof key !== 'string') {
|
|
456
|
+
throw new TypeError('Header name must be a string')
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (value === undefined || value === null) {
|
|
460
|
+
return this
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const headerKey = key.toLowerCase()
|
|
464
|
+
const headerValue = `${value}`
|
|
465
|
+
const pendingHeader = this.#pendingHeaders.get(headerKey)
|
|
466
|
+
|
|
467
|
+
if (!pendingHeader) {
|
|
468
|
+
this.#pendingHeaders.set(headerKey, [key, headerValue])
|
|
469
|
+
return this
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
pendingHeader[0] = key
|
|
473
|
+
const cur = pendingHeader[1]
|
|
474
|
+
|
|
475
|
+
if (typeof cur === 'string') {
|
|
476
|
+
pendingHeader[1] = [cur, headerValue]
|
|
477
|
+
} else {
|
|
478
|
+
cur[cur.length] = headerValue
|
|
479
|
+
}
|
|
480
|
+
|
|
432
481
|
return this
|
|
433
482
|
}
|
|
434
483
|
|
|
435
484
|
/**
|
|
436
|
-
* @param {Record<string, string> | null | undefined} headers
|
|
485
|
+
* @param {Record<string, string | string[]> | null | undefined} headers
|
|
437
486
|
*/
|
|
438
487
|
setHeaders(headers) {
|
|
439
488
|
if (this.replied || this.aborted) {
|
|
@@ -444,7 +493,7 @@ export default class HttpContext {
|
|
|
444
493
|
}
|
|
445
494
|
|
|
446
495
|
/**
|
|
447
|
-
* @param {Record<string, string> | null | undefined} headers
|
|
496
|
+
* @param {Record<string, string | string[]> | null | undefined} headers
|
|
448
497
|
*/
|
|
449
498
|
flushHeaders(headers = null) {
|
|
450
499
|
this.#flushPendingHeaders(headers)
|
|
@@ -452,21 +501,83 @@ export default class HttpContext {
|
|
|
452
501
|
|
|
453
502
|
/**
|
|
454
503
|
* @param {string} key
|
|
455
|
-
* @param {string} value
|
|
504
|
+
* @param {string | string[] | null | undefined} value
|
|
505
|
+
* @param {boolean} append
|
|
456
506
|
*/
|
|
457
|
-
#
|
|
507
|
+
#stagePendingHeader(key, value, append) {
|
|
458
508
|
if (value === undefined || value === null) {
|
|
459
509
|
return
|
|
460
510
|
}
|
|
461
511
|
|
|
462
|
-
const
|
|
463
|
-
|
|
512
|
+
const headerKey = key.toLowerCase()
|
|
513
|
+
|
|
514
|
+
if (!Array.isArray(value)) {
|
|
515
|
+
const headerValue = `${value}`
|
|
516
|
+
|
|
517
|
+
if (!append) {
|
|
518
|
+
this.#pendingHeaders.set(headerKey, [key, headerValue])
|
|
519
|
+
return
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const pendingHeader = this.#pendingHeaders.get(headerKey)
|
|
523
|
+
|
|
524
|
+
if (!pendingHeader) {
|
|
525
|
+
this.#pendingHeaders.set(headerKey, [key, headerValue])
|
|
526
|
+
return
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
pendingHeader[0] = key
|
|
530
|
+
const cur = pendingHeader[1]
|
|
531
|
+
|
|
532
|
+
if (typeof cur === 'string') {
|
|
533
|
+
pendingHeader[1] = [cur, headerValue]
|
|
534
|
+
} else {
|
|
535
|
+
cur[cur.length] = headerValue
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
this.#stagePendingHeaderArray(key, headerKey, value, append)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* @param {string} key
|
|
546
|
+
* @param {string} headerKey
|
|
547
|
+
* @param {string[]} value
|
|
548
|
+
* @param {boolean} append
|
|
549
|
+
*/
|
|
550
|
+
#stagePendingHeaderArray(key, headerKey, value, append) {
|
|
551
|
+
let pendingHeader = append ? this.#pendingHeaders.get(headerKey) : null
|
|
552
|
+
|
|
553
|
+
for (let i = 0, len = value.length; i < len; i++) {
|
|
554
|
+
const entry = value[i]
|
|
555
|
+
|
|
556
|
+
if (entry === undefined || entry === null) {
|
|
557
|
+
continue
|
|
558
|
+
}
|
|
464
559
|
|
|
465
|
-
|
|
560
|
+
const headerValue = `${entry}`
|
|
561
|
+
|
|
562
|
+
if (!pendingHeader) {
|
|
563
|
+
pendingHeader = [key, headerValue]
|
|
564
|
+
this.#pendingHeaders.set(headerKey, pendingHeader)
|
|
565
|
+
continue
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
pendingHeader[0] = key
|
|
569
|
+
const cur = pendingHeader[1]
|
|
570
|
+
|
|
571
|
+
if (typeof cur === 'string') {
|
|
572
|
+
pendingHeader[1] = [cur, headerValue]
|
|
573
|
+
} else {
|
|
574
|
+
cur[cur.length] = headerValue
|
|
575
|
+
}
|
|
576
|
+
}
|
|
466
577
|
}
|
|
467
578
|
|
|
468
579
|
/**
|
|
469
|
-
* @param {Record<string, string> | null | undefined} headers
|
|
580
|
+
* @param {Record<string, string | string[]> | null | undefined} headers
|
|
470
581
|
*/
|
|
471
582
|
#stageHeaders(headers) {
|
|
472
583
|
if (!headers) {
|
|
@@ -474,29 +585,27 @@ export default class HttpContext {
|
|
|
474
585
|
}
|
|
475
586
|
|
|
476
587
|
if (headers === TEXT_PLAIN_HEADER) {
|
|
477
|
-
this.#
|
|
588
|
+
this.#pendingHeaders.set('content-type', ['content-type', 'text/plain; charset=utf-8'])
|
|
478
589
|
return
|
|
479
590
|
}
|
|
480
591
|
|
|
481
592
|
if (headers === JSON_HEADER) {
|
|
482
|
-
this.#
|
|
593
|
+
this.#pendingHeaders.set('content-type', ['content-type', 'application/json; charset=utf-8'])
|
|
483
594
|
return
|
|
484
595
|
}
|
|
485
596
|
|
|
486
597
|
if (headers === OCTET_STREAM_HEADER) {
|
|
487
|
-
this.#
|
|
598
|
+
this.#pendingHeaders.set('content-type', ['content-type', 'application/octet-stream'])
|
|
488
599
|
return
|
|
489
600
|
}
|
|
490
601
|
|
|
491
602
|
for (const key in headers) {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
this.#stageHeader(key, value)
|
|
603
|
+
this.#stagePendingHeader(key, headers[key], false)
|
|
495
604
|
}
|
|
496
605
|
}
|
|
497
606
|
|
|
498
607
|
/**
|
|
499
|
-
* @param {Record<string, string> | null | undefined} headers
|
|
608
|
+
* @param {Record<string, string | string[]> | null | undefined} headers
|
|
500
609
|
*/
|
|
501
610
|
#flushPendingHeaders(headers = null) {
|
|
502
611
|
if (!this.res) {
|
|
@@ -508,8 +617,19 @@ export default class HttpContext {
|
|
|
508
617
|
this.#stageHeaders(headers)
|
|
509
618
|
}
|
|
510
619
|
|
|
511
|
-
for (const [,
|
|
512
|
-
|
|
620
|
+
for (const [, pendingHeader] of this.#pendingHeaders) {
|
|
621
|
+
const headerValue = pendingHeader[1]
|
|
622
|
+
|
|
623
|
+
if (typeof headerValue === 'string') {
|
|
624
|
+
this.res.writeHeader(pendingHeader[0], headerValue)
|
|
625
|
+
continue
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const name = pendingHeader[0]
|
|
629
|
+
|
|
630
|
+
for (let i = 0, len = headerValue.length; i < len; i++) {
|
|
631
|
+
this.res.writeHeader(name, headerValue[i])
|
|
632
|
+
}
|
|
513
633
|
}
|
|
514
634
|
|
|
515
635
|
this.#pendingHeaders.clear()
|
|
@@ -582,7 +702,7 @@ export default class HttpContext {
|
|
|
582
702
|
|
|
583
703
|
/**
|
|
584
704
|
* @param {number} status
|
|
585
|
-
* @param {Record<string,string>} headers
|
|
705
|
+
* @param {Record<string, string | string[]>} headers
|
|
586
706
|
* @param {string|ArrayBuffer|Uint8Array|Buffer|null|undefined} body
|
|
587
707
|
*/
|
|
588
708
|
reply(status = 200, headers = null, body = null) {
|
|
@@ -610,7 +730,7 @@ export default class HttpContext {
|
|
|
610
730
|
|
|
611
731
|
/**
|
|
612
732
|
* @param {number} status
|
|
613
|
-
* @param {Record<string,string>} headers
|
|
733
|
+
* @param {Record<string, string | string[]>} headers
|
|
614
734
|
* @returns {HttpContext}
|
|
615
735
|
*/
|
|
616
736
|
startStreaming(status = 200, headers = null) {
|
package/src/res-streamer.js
CHANGED
|
@@ -82,7 +82,7 @@ export default class ResStreamer {
|
|
|
82
82
|
|
|
83
83
|
/**
|
|
84
84
|
* @param {number|string} status
|
|
85
|
-
* @param {Record<string,string>|null} headers
|
|
85
|
+
* @param {Record<string, string | string[]>|null} headers
|
|
86
86
|
* @returns {ResStreamer}
|
|
87
87
|
*/
|
|
88
88
|
begin(status = 200, headers = null) {
|