@nxtedition/lib 26.5.0 → 26.7.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.
Files changed (3) hide show
  1. package/http.js +18 -1
  2. package/package.json +1 -1
  3. package/shared.js +149 -3
package/http.js CHANGED
@@ -35,6 +35,9 @@ function onTimeout() {
35
35
  this.destroy((timeoutError ??= new createError.RequestTimeout()))
36
36
  }
37
37
 
38
+ const pending = (globalThis.__nxt_lib_http_pending = [])
39
+ const kPendingIndex = Symbol('pendingIndex')
40
+
38
41
  // TODO (fix): Make custom ServerRequest class with the properties
39
42
  // that is currently deprecated by Context.
40
43
 
@@ -45,7 +48,9 @@ export class Context {
45
48
  #ac
46
49
  #logger
47
50
  #query
48
- #target
51
+ #target;
52
+
53
+ [kPendingIndex] = -1
49
54
 
50
55
  constructor(req, res, logger) {
51
56
  assert(req)
@@ -210,6 +215,8 @@ export async function requestMiddleware(ctx, next) {
210
215
  if (stats?.pending != null) {
211
216
  stats.pending++
212
217
  }
218
+
219
+ ctx[kPendingIndex] = pending.push(ctx) - 1
213
220
  try {
214
221
  const isHealthcheck = req.url === '/healthcheck' || req.url === '/_up'
215
222
  if (!isHealthcheck) {
@@ -364,6 +371,16 @@ export async function requestMiddleware(ctx, next) {
364
371
  res.destroy()
365
372
  ctx.logger?.warn('request destroyed')
366
373
  }
374
+
375
+ {
376
+ const idx = ctx[kPendingIndex]
377
+ const tmp = pending.pop()
378
+ if (tmp !== ctx) {
379
+ pending[idx] = tmp
380
+ pending[idx][kPendingIndex] = idx
381
+ tmp[kPendingIndex] = -1
382
+ }
383
+ }
367
384
  }
368
385
  }
369
386
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/lib",
3
- "version": "26.5.0",
3
+ "version": "26.7.0",
4
4
  "license": "MIT",
5
5
  "author": "Robert Nagy <robert.nagy@boffins.se>",
6
6
  "type": "module",
package/shared.js CHANGED
@@ -1,3 +1,6 @@
1
+ import stream from 'node:stream'
2
+ import assert from 'node:assert'
3
+
1
4
  // By placing the read and write indices far apart (multiples of a common
2
5
  // cache line size, 64 bytes), we prevent "false sharing". This is a
3
6
  // low-level CPU optimization where two cores writing to different variables
@@ -91,12 +94,16 @@ export function reader({ sharedState, sharedBuffer }) {
91
94
  // Instead, we pass a "view" into the shared buffer.
92
95
  data.offset = dataPos
93
96
  data.length = dataLen
94
- next(data)
97
+ const cont = next(data)
95
98
 
96
99
  if (readPos === writePos) {
97
100
  // If we reach the end of the buffer, we must re-check the writer's position.
98
101
  writePos = Atomics.load(state, WRITE_INDEX) | 0
99
102
  }
103
+
104
+ if (cont === false) {
105
+ break
106
+ }
100
107
  }
101
108
  }
102
109
 
@@ -296,7 +303,7 @@ export function writer({ sharedState, sharedBuffer }, { yield: onYield, logger }
296
303
  * @param {number} len The maximum expected length of the payload.
297
304
  * @param {({ buffer, view, offset, length }) => number} fn The callback that writes the data.
298
305
  */
299
- function write(len, fn) {
306
+ function writeSync(len, fn) {
300
307
  if (len < 0) {
301
308
  throw new Error(`Length ${len} is negative`)
302
309
  }
@@ -325,6 +332,30 @@ export function writer({ sharedState, sharedBuffer }, { yield: onYield, logger }
325
332
  }
326
333
  }
327
334
 
335
+ function tryWrite(len, fn) {
336
+ if (len < 0) {
337
+ throw new Error(`Length ${len} is negative`)
338
+ }
339
+ if (len >= 2 ** 31 || len > size - 8) {
340
+ throw new Error(`Length ${len} exceeds maximum allowed size`)
341
+ }
342
+
343
+ if (!_acquire(len)) {
344
+ readPos = Atomics.load(state, READ_INDEX) | 0
345
+ if (!_acquire(len)) {
346
+ return false
347
+ }
348
+ }
349
+
350
+ _write(len, fn)
351
+
352
+ if (writePos === readPos) {
353
+ throw new Error(`Write position ${writePos} cannot equal read position ${readPos}`)
354
+ }
355
+
356
+ return true
357
+ }
358
+
328
359
  function _flush() {
329
360
  if (pending > 0) {
330
361
  Atomics.store(state, WRITE_INDEX, writePos)
@@ -341,5 +372,120 @@ export function writer({ sharedState, sharedBuffer }, { yield: onYield, logger }
341
372
  }
342
373
  }
343
374
 
344
- return { write, cork }
375
+ return { tryWrite, writeSync, cork }
376
+ }
377
+
378
+ export class Writable extends stream.Writable {
379
+ #writer
380
+ #retries = 0
381
+ #timeout = null
382
+
383
+ #chunk
384
+ #encoding
385
+ #callback
386
+
387
+ constructor({ state, ...options }) {
388
+ super({ ...options })
389
+ this.#writer = writer(state)
390
+ }
391
+
392
+ _write(chunk, encoding, callback) {
393
+ if (chunk.byteLength === 0) {
394
+ callback(null)
395
+ return
396
+ }
397
+
398
+ assert(!this.#timeout)
399
+
400
+ this.#chunk = chunk
401
+ this.#encoding = encoding
402
+ this.#callback = callback
403
+ this._writeSome()
404
+ }
405
+
406
+ _final(callback) {
407
+ this.#chunk = Buffer.allocUnsafe(0)
408
+ this.#encoding = null
409
+ this.#callback = callback
410
+ this._writeSome()
411
+ }
412
+
413
+ _writeSome = () => {
414
+ this.#timeout = null
415
+
416
+ if (this.#writer.tryWrite(this.#chunk.byteLength, this._doWrite)) {
417
+ const callback = this.#callback
418
+ this.#retries = 0
419
+ this.#chunk = null
420
+ this.#encoding = null
421
+ this.#callback = null
422
+ callback(null)
423
+ } else {
424
+ this.#retries += 1
425
+ this.#timeout = setTimeout(this._writeSome, Math.min(10, this.#retries))
426
+ }
427
+ }
428
+
429
+ _doWrite = (data) => {
430
+ const written =
431
+ typeof this.#chunk === 'string'
432
+ ? data.buffer.write(this.#chunk, data.offset, data.length, this.#encoding)
433
+ : this.#chunk.copy(data.buffer, data.offset, 0, this.#chunk.length)
434
+ return data.offset + written
435
+ }
436
+
437
+ _destroy(err, callback) {
438
+ if (this.#timeout != null) {
439
+ clearTimeout(this.#timeout)
440
+ this.#timeout = null
441
+ }
442
+ callback(err)
443
+ }
444
+ }
445
+
446
+ export class Readable extends stream.Readable {
447
+ #reader
448
+ #retries = 0
449
+ #timeout
450
+
451
+ constructor({ state, ...options }) {
452
+ super(options)
453
+ this.#reader = reader(state)
454
+ }
455
+
456
+ _read() {
457
+ assert(!this.#timeout)
458
+
459
+ this._readSome()
460
+ }
461
+
462
+ _readSome = () => {
463
+ this.#timeout = null
464
+
465
+ const count = this.#reader.readSome((data) => {
466
+ if (data.length === 0) {
467
+ this.push(null)
468
+ return false
469
+ }
470
+
471
+ const chunk = Buffer.allocUnsafe(data.length)
472
+ data.buffer.copy(chunk, 0, data.offset, data.offset + data.length)
473
+ return this.push(chunk)
474
+ })
475
+
476
+ if (count > 0 || this.readableEnded) {
477
+ this.#retries = 0
478
+ } else {
479
+ this.#retries += 1
480
+ this.#timeout = setTimeout(this._readSome, Math.min(10, this.#retries))
481
+ }
482
+ }
483
+
484
+ _destroy(err, callback) {
485
+ if (this.#timeout != null) {
486
+ clearTimeout(this.#timeout)
487
+ this.#timeout = null
488
+ }
489
+ callback(err)
490
+ }
345
491
  }