@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.
@@ -0,0 +1,761 @@
1
+ export const TEXT_PLAIN_HEADER = Object.freeze({ 'content-type': 'text/plain; charset=utf-8' })
2
+ export const JSON_HEADER = Object.freeze({ 'content-type': 'application/json; charset=utf-8' })
3
+ export const OCTET_STREAM_HEADER = Object.freeze({ 'content-type': 'application/octet-stream' })
4
+
5
+ export const STATUS_TEXT = Object.freeze({
6
+ 100: '100 Continue',
7
+ 101: '101 Switching Protocols',
8
+ 102: '102 Processing',
9
+
10
+ 200: '200 OK',
11
+ 201: '201 Created',
12
+ 202: '202 Accepted',
13
+ 203: '203 Non-Authoritative Information',
14
+ 204: '204 No Content',
15
+ 205: '205 Reset Content',
16
+ 206: '206 Partial Content',
17
+
18
+ 300: '300 Multiple Choices',
19
+ 301: '301 Moved Permanently',
20
+ 302: '302 Found',
21
+ 303: '303 See Other',
22
+ 304: '304 Not Modified',
23
+ 307: '307 Temporary Redirect',
24
+ 308: '308 Permanent Redirect',
25
+
26
+ 400: '400 Bad Request',
27
+ 401: '401 Unauthorized',
28
+ 403: '403 Forbidden',
29
+ 404: '404 Not Found',
30
+ 405: '405 Method Not Allowed',
31
+ 406: '406 Not Acceptable',
32
+ 408: '408 Request Timeout',
33
+ 409: '409 Conflict',
34
+ 410: '410 Gone',
35
+ 413: '413 Payload Too Large',
36
+ 414: '414 URI Too Long',
37
+ 415: '415 Unsupported Media Type',
38
+ 418: "418 I'm a teapot",
39
+ 422: '422 Unprocessable Entity',
40
+ 429: '429 Too Many Requests',
41
+
42
+ 500: '500 Internal Server Error',
43
+ 501: '501 Not Implemented',
44
+ 502: '502 Bad Gateway',
45
+ 503: '503 Service Unavailable',
46
+ 504: '504 Gateway Timeout'
47
+ })
48
+
49
+ const CACHED_ERRORS = Object.freeze({
50
+ bodyTooLarge: new Error('Request body too large'),
51
+ aborted: new Error('Request aborted'),
52
+ sizeMismatch: new Error('Request body size mismatch')
53
+ })
54
+
55
+ const NOOP = () => {}
56
+
57
+ export default class HttpContext {
58
+ #ip = ''
59
+ #method = ''
60
+ #url = ''
61
+ #body = null
62
+ #bodyError = null
63
+ #bodyPromise = null
64
+ #finalize = null
65
+ #statusOverride = null
66
+ #contentLength = undefined
67
+ #maxSize = 1024 * 1024 * 16
68
+
69
+ /**
70
+ * @param {ContextPool} pool
71
+ */
72
+ constructor(pool) {
73
+ this.pool = pool
74
+ this.res = null
75
+ this.req = null
76
+ this.replied = false
77
+ this.aborted = false
78
+ this.streaming = false
79
+ this.streamingStarted = false
80
+ this.onWritableCallback = null
81
+ }
82
+
83
+ /**
84
+ * @param {import('uwebsockets.js').HttpResponse} res
85
+ * @param {import('uwebsockets.js').HttpRequest} req
86
+ * @param {Function} [finalize]
87
+ * @param {number} [maxSize]
88
+ * @returns {HttpContext}
89
+ */
90
+ reset(res, req, finalize = null, maxSize = 1024 * 1024 * 16) {
91
+ this.res = res
92
+ this.req = req
93
+
94
+ this.replied = false
95
+ this.aborted = false
96
+ this.streaming = false
97
+ this.streamingStarted = false
98
+ this.onWritableCallback = null
99
+
100
+ this.#body = null
101
+ this.#bodyError = null
102
+ this.#bodyPromise = null
103
+ this.#statusOverride = null
104
+ this.#contentLength = undefined
105
+ this.#maxSize = maxSize
106
+
107
+ this.#ip = ''
108
+ this.#url = ''
109
+ this.#method = ''
110
+
111
+ this.#finalize = finalize
112
+
113
+ return this
114
+ }
115
+
116
+ /**
117
+ */
118
+ clear() {
119
+ this.res = null
120
+ this.req = null
121
+ this.replied = false
122
+ this.aborted = false
123
+ this.streaming = false
124
+ this.streamingStarted = false
125
+ this.onWritableCallback = null
126
+
127
+ this.#body = null
128
+ this.#bodyError = null
129
+ this.#bodyPromise = null
130
+ this.#finalize = null
131
+ this.#statusOverride = null
132
+ this.#contentLength = undefined
133
+ this.#ip = ''
134
+ this.#url = ''
135
+ this.#method = ''
136
+ }
137
+
138
+ /**
139
+ */
140
+ release() {
141
+ if (this.pool) {
142
+ this.pool.release(this)
143
+ }
144
+ }
145
+
146
+ ip() {
147
+ if (!this.res) {
148
+ return ''
149
+ }
150
+
151
+ if (this.#ip) {
152
+ return this.#ip
153
+ }
154
+
155
+ const ipBuffer = this.res.getProxiedRemoteAddressAsText?.() || this.res.getRemoteAddressAsText?.()
156
+
157
+ this.#ip = ipBuffer ? Buffer.from(ipBuffer).toString('utf8') : ''
158
+
159
+ return this.#ip
160
+ }
161
+
162
+ method() {
163
+ if (!this.req) {
164
+ return ''
165
+ }
166
+
167
+ if (this.#method) {
168
+ return this.#method
169
+ }
170
+
171
+ this.#method = this.req.getMethod()
172
+ return this.#method
173
+ }
174
+
175
+ url() {
176
+ if (!this.req) {
177
+ return ''
178
+ }
179
+
180
+ if (this.#url) {
181
+ return this.#url
182
+ }
183
+
184
+ this.#url = this.req.getUrl()
185
+ return this.#url
186
+ }
187
+
188
+ /**
189
+ * @param {string} name
190
+ * @returns {string|undefined}
191
+ */
192
+ query(name) {
193
+ return this.req.getQuery(name)
194
+ }
195
+
196
+ /**
197
+ * @param {number|string} i
198
+ * @returns {string|undefined}
199
+ */
200
+ param(i) {
201
+ return this.req.getParameter(i)
202
+ }
203
+
204
+ /**
205
+ * @param {string} name
206
+ * @returns {string}
207
+ */
208
+ header(name) {
209
+ return this.req.getHeader(name)
210
+ }
211
+
212
+ contentLength() {
213
+ if (this.#contentLength !== undefined) {
214
+ return this.#contentLength
215
+ }
216
+
217
+ const clh = this.header('content-length')
218
+
219
+ if (clh === undefined || clh == null || clh === '') {
220
+ this.#contentLength = null
221
+ return this.#contentLength
222
+ }
223
+
224
+ const n = Number(clh)
225
+
226
+ if (!Number.isInteger(n) || n < 0) {
227
+ this.#contentLength = null
228
+ return this.#contentLength
229
+ }
230
+
231
+ this.#contentLength = n
232
+ return this.#contentLength
233
+ }
234
+
235
+ /**
236
+ * @param {number} code
237
+ * @returns {HttpContext}
238
+ */
239
+ status(code) {
240
+ this.#statusOverride = code
241
+ return this
242
+ }
243
+
244
+ /**
245
+ * @param {number} [maxSize]
246
+ * @returns {Promise<Buffer>}
247
+ */
248
+ body(maxSize) {
249
+ if (this.#body !== null) {
250
+ return Promise.resolve(this.#body)
251
+ }
252
+
253
+ if (this.#bodyError !== null) {
254
+ return Promise.reject(this.#bodyError)
255
+ }
256
+
257
+ if (this.#bodyPromise !== null) {
258
+ return this.#bodyPromise
259
+ }
260
+
261
+ const limit = maxSize ?? this.#maxSize
262
+ const contentLength = this.contentLength()
263
+
264
+ if (this.aborted) {
265
+ this.#bodyError = CACHED_ERRORS.aborted
266
+ return Promise.reject(this.#bodyError)
267
+ }
268
+
269
+ if (contentLength !== null && contentLength > limit) {
270
+ this.#bodyError = CACHED_ERRORS.bodyTooLarge
271
+ return Promise.reject(this.#bodyError)
272
+ }
273
+
274
+ if (contentLength === 0) {
275
+ const buf = Buffer.alloc(0)
276
+
277
+ this.#body = buf
278
+ this.res.onData(NOOP)
279
+ return Promise.resolve(buf)
280
+ }
281
+
282
+ this.#bodyPromise = new Promise((resolve, reject) => {
283
+ let done = false
284
+
285
+ const success = (buf) => {
286
+ if (done) {
287
+ return
288
+ }
289
+
290
+ done = true
291
+ this.#body = buf
292
+ this.#bodyPromise = null
293
+ resolve(buf)
294
+ }
295
+
296
+ const fail = (err) => {
297
+ if (done) {
298
+ return
299
+ }
300
+
301
+ done = true
302
+ this.#bodyError = err
303
+ this.#bodyPromise = null
304
+ reject(err)
305
+ }
306
+
307
+ if (contentLength !== null) {
308
+ this.#parseKnownLength(contentLength, success, fail)
309
+ } else {
310
+ this.#parseUnknownLength(limit, success, fail)
311
+ }
312
+ })
313
+
314
+ return this.#bodyPromise
315
+ }
316
+
317
+ #parseKnownLength(contentLength, resolve, reject) {
318
+ const dst = Buffer.allocUnsafe(contentLength)
319
+
320
+ let offset = 0
321
+ let done = false
322
+
323
+ this.res.onData((ab, isLast) => {
324
+ if (done) {
325
+ return
326
+ }
327
+
328
+ if (this.aborted) {
329
+ if (done) {
330
+ return
331
+ }
332
+
333
+ done = true
334
+ return reject(CACHED_ERRORS.aborted)
335
+ }
336
+
337
+ const u8 = new Uint8Array(ab)
338
+ const chunkSize = u8.byteLength
339
+ const next = offset + chunkSize
340
+
341
+ if (next > contentLength) {
342
+ if (done) {
343
+ return
344
+ }
345
+
346
+ done = true
347
+ return reject(CACHED_ERRORS.sizeMismatch)
348
+ }
349
+
350
+ dst.set(u8, offset)
351
+ offset = next
352
+
353
+ if (isLast || offset === contentLength) {
354
+ if (offset !== contentLength) {
355
+ if (done) {
356
+ return
357
+ }
358
+
359
+ done = true
360
+ return reject(CACHED_ERRORS.sizeMismatch)
361
+ }
362
+
363
+ if (done) {
364
+ return
365
+ }
366
+
367
+ done = true
368
+ return resolve(dst)
369
+ }
370
+ })
371
+ }
372
+
373
+ #parseUnknownLength(limit, resolve, reject) {
374
+ const chunks = []
375
+ let totalSize = 0
376
+ let done = false
377
+
378
+ this.res.onData((ab, isLast) => {
379
+ if (done) {
380
+ return
381
+ }
382
+ if (this.aborted) {
383
+ done = true
384
+ return reject(CACHED_ERRORS.aborted)
385
+ }
386
+
387
+ const buf = Buffer.from(ab) // <- копия
388
+ const nextSize = totalSize + buf.length
389
+
390
+ if (nextSize > limit) {
391
+ done = true
392
+ return reject(CACHED_ERRORS.bodyTooLarge)
393
+ }
394
+
395
+ chunks.push(buf)
396
+ totalSize = nextSize
397
+
398
+ if (isLast) {
399
+ done = true
400
+ return resolve(chunks.length === 1 ? chunks[0] : Buffer.concat(chunks, totalSize))
401
+ }
402
+ })
403
+ }
404
+
405
+ /**
406
+ * @param {number} [maxSize]
407
+ * @returns {Promise<Buffer>}
408
+ */
409
+ buffer(maxSize) {
410
+ return this.body(maxSize)
411
+ }
412
+
413
+ /**
414
+ * @param {number} [maxSize]
415
+ * @returns {Promise<any>}
416
+ */
417
+ async json(maxSize) {
418
+ const buf = await this.body(maxSize)
419
+
420
+ if (buf.length === 0) {
421
+ return null
422
+ }
423
+
424
+ try {
425
+ return JSON.parse(buf.toString('utf8'))
426
+ } catch (err) {
427
+ throw new Error('Invalid JSON: ' + err.message)
428
+ }
429
+ }
430
+
431
+ /**
432
+ * @param {number} [maxSize]
433
+ * @returns {Promise<string>}
434
+ */
435
+ async text(maxSize) {
436
+ const buf = await this.body(maxSize)
437
+
438
+ return buf.toString('utf8')
439
+ }
440
+
441
+ /**
442
+ * @param {string|number} status
443
+ * @returns {any}
444
+ */
445
+ getStatus(status) {
446
+ const finalStatus = this.#statusOverride !== null ? this.#statusOverride : status
447
+
448
+ return STATUS_TEXT[finalStatus ?? 500]
449
+ }
450
+
451
+ /**
452
+ * @param {string} key
453
+ * @param {string} value
454
+ * @returns {HttpContext}
455
+ */
456
+ setHeader(key, value) {
457
+ this.res.writeHeader(key, value)
458
+ return this
459
+ }
460
+
461
+ /**
462
+ * @param {Record<string, string> | null | undefined} headers
463
+ */
464
+ setHeaders(headers) {
465
+ if (!headers) {
466
+ return
467
+ }
468
+
469
+ if (headers === TEXT_PLAIN_HEADER) {
470
+ this.res.writeHeader('content-type', 'text/plain; charset=utf-8')
471
+ return
472
+ }
473
+
474
+ if (headers === JSON_HEADER) {
475
+ this.res.writeHeader('content-type', 'application/json; charset=utf-8')
476
+ return
477
+ }
478
+
479
+ if (headers === OCTET_STREAM_HEADER) {
480
+ this.res.writeHeader('content-type', 'application/octet-stream')
481
+ return
482
+ }
483
+
484
+ for (const key in headers) {
485
+ const value = headers[key]
486
+
487
+ if (value !== undefined && value !== null) {
488
+ this.res.writeHeader(key, value)
489
+ }
490
+ }
491
+ }
492
+
493
+ /**
494
+ * @param {any} result
495
+ * @returns {void}
496
+ */
497
+ send(result) {
498
+ if (result == null) {
499
+ return this.reply(204, TEXT_PLAIN_HEADER, null)
500
+ }
501
+
502
+ const type = typeof result
503
+
504
+ if (type === 'string') {
505
+ return this.reply(200, TEXT_PLAIN_HEADER, result)
506
+ }
507
+
508
+ if (type === 'object') {
509
+ if (ArrayBuffer.isView(result) || result instanceof ArrayBuffer) {
510
+ return this.reply(200, OCTET_STREAM_HEADER, result)
511
+ }
512
+
513
+ return this.reply(200, JSON_HEADER, JSON.stringify(result))
514
+ }
515
+
516
+ return this.reply(200, TEXT_PLAIN_HEADER, String(result))
517
+ }
518
+
519
+ /**
520
+ * @param {object | Array} data
521
+ * @param {number} [status]
522
+ * @returns {void}
523
+ */
524
+ sendJson(data, status = 200) {
525
+ this.reply(status, JSON_HEADER, JSON.stringify(data))
526
+ }
527
+
528
+ /**
529
+ * @param {string} text
530
+ * @param {number} [status]
531
+ * @returns {void}
532
+ */
533
+ sendText(text, status = 200) {
534
+ this.reply(status, TEXT_PLAIN_HEADER, text)
535
+ }
536
+
537
+ /**
538
+ * @param {Buffer|Uint8Array|ArrayBuffer} buffer
539
+ * @param {number} [status]
540
+ * @returns {void}
541
+ */
542
+ sendBuffer(buffer, status = 200) {
543
+ this.reply(status, OCTET_STREAM_HEADER, buffer)
544
+ }
545
+
546
+ /**
547
+ * @param {number} status
548
+ * @param {Record<string,string>} headers
549
+ * @param {string|ArrayBuffer|Uint8Array|Buffer|null|undefined} body
550
+ */
551
+ reply(status = 200, headers = null, body = null) {
552
+ if (this.replied || this.aborted) {
553
+ return
554
+ }
555
+
556
+ this.replied = true
557
+
558
+ this.res.cork(() => {
559
+ if (this.aborted) {
560
+ return
561
+ }
562
+
563
+ this.res.writeStatus(this.getStatus(status))
564
+ this.setHeaders(headers)
565
+
566
+ if (body != null) {
567
+ this.res.end(body)
568
+ } else {
569
+ this.res.end()
570
+ }
571
+ })
572
+ }
573
+
574
+ /**
575
+ * @param {number} status
576
+ * @param {Record<string,string>} headers
577
+ * @returns {HttpContext}
578
+ */
579
+ startStreaming(status = 200, headers = null) {
580
+ if (this.replied || this.aborted) {
581
+ return this
582
+ }
583
+
584
+ this.replied = true
585
+ this.streaming = true
586
+
587
+ this.res.cork(() => {
588
+ this.res.writeStatus(this.getStatus(status))
589
+ this.setHeaders(headers)
590
+ })
591
+
592
+ return this
593
+ }
594
+
595
+ /**
596
+ * @param {string|ArrayBuffer|Uint8Array|Buffer} chunk
597
+ * @returns {boolean}
598
+ */
599
+ write(chunk) {
600
+ if (this.aborted) {
601
+ return false
602
+ }
603
+
604
+ if (!this.streaming) {
605
+ throw new Error('Must call startStreaming() before write()')
606
+ }
607
+
608
+ this.streamingStarted = true
609
+
610
+ let ok = false
611
+
612
+ this.res.cork(() => {
613
+ ok = this.res.write(chunk)
614
+ })
615
+
616
+ return ok
617
+ }
618
+
619
+ /**
620
+ * @param {string|ArrayBuffer|Uint8Array|Buffer} [chunk]
621
+ * @returns {[boolean, boolean]}
622
+ */
623
+ tryEnd(chunk) {
624
+ if (this.aborted) {
625
+ return [false, false]
626
+ }
627
+
628
+ if (!this.streaming) {
629
+ throw new Error('Must call startStreaming() before tryEnd()')
630
+ }
631
+
632
+ let result = [false, false]
633
+
634
+ this.res.cork(() => {
635
+ const offset = this.res.getWriteOffset()
636
+ const chunkLen = typeof chunk === 'string' ? Buffer.byteLength(chunk) : chunk.byteLength
637
+ const totalSize = offset + chunkLen
638
+ const [ok, done] = this.res.tryEnd(chunk, totalSize)
639
+
640
+ result = [ok, done]
641
+
642
+ if (done) {
643
+ this.streaming = false
644
+
645
+ if (this.#finalize) {
646
+ this.#finalize()
647
+ }
648
+ }
649
+ })
650
+
651
+ return result
652
+ }
653
+
654
+ /**
655
+ * @param {string|ArrayBuffer|Uint8Array|Buffer} [chunk]
656
+ */
657
+ end(chunk) {
658
+ if (this.aborted) {
659
+ return
660
+ }
661
+
662
+ if (!this.streaming) {
663
+ throw new Error('Must call startStreaming() before end()')
664
+ }
665
+
666
+ this.res.cork(() => {
667
+ if (chunk != null) {
668
+ this.res.end(chunk)
669
+ } else {
670
+ this.res.end()
671
+ }
672
+ })
673
+
674
+ this.streaming = false
675
+
676
+ if (this.#finalize) {
677
+ this.#finalize()
678
+ }
679
+ }
680
+
681
+ /**
682
+ * @param {Function} callback
683
+ */
684
+ onWritable(callback) {
685
+ if (this.aborted) {
686
+ return
687
+ }
688
+
689
+ this.onWritableCallback = callback
690
+
691
+ this.res.onWritable((offset) => {
692
+ const cb = this.onWritableCallback
693
+
694
+ if (!cb) {
695
+ return true
696
+ }
697
+
698
+ this.onWritableCallback = null
699
+ cb(offset)
700
+
701
+ return false
702
+ })
703
+ }
704
+
705
+ /**
706
+ * @returns {number}
707
+ */
708
+ getWriteOffset() {
709
+ if (this.aborted) {
710
+ return 0
711
+ }
712
+
713
+ return this.res.getWriteOffset()
714
+ }
715
+
716
+ /**
717
+ * @param {import('stream').Readable} readable
718
+ * @param {number} status
719
+ * @param {Record<string,string>} headers
720
+ * @returns {Promise<void>}
721
+ */
722
+ async stream(readable, status = 200, headers = null) {
723
+ this.startStreaming(status, headers)
724
+
725
+ return new Promise((resolve, reject) => {
726
+ let paused = false
727
+
728
+ readable.on('data', (chunk) => {
729
+ if (this.aborted) {
730
+ readable.destroy()
731
+ resolve()
732
+ return
733
+ }
734
+
735
+ const ok = this.write(chunk)
736
+
737
+ if (!ok && !paused) {
738
+ paused = true
739
+ readable.pause()
740
+
741
+ this.onWritable(() => {
742
+ paused = false
743
+ readable.resume()
744
+ })
745
+ }
746
+ })
747
+
748
+ readable.on('end', () => {
749
+ this.end()
750
+ resolve()
751
+ })
752
+
753
+ readable.on('error', (err) => {
754
+ if (!this.aborted) {
755
+ this.end()
756
+ }
757
+ reject(err)
758
+ })
759
+ })
760
+ }
761
+ }