@nxtedition/lib 28.0.5 → 28.0.7

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/package.json +2 -2
  2. package/sequence.js +62 -6
  3. package/shared.js +127 -190
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/lib",
3
- "version": "28.0.5",
3
+ "version": "28.0.7",
4
4
  "license": "UNLICENSED",
5
5
  "author": "Robert Nagy <robert.nagy@boffins.se>",
6
6
  "type": "module",
@@ -92,5 +92,5 @@
92
92
  "pino": ">=7.0.0",
93
93
  "rxjs": "^7.0.0"
94
94
  },
95
- "gitHead": "f778b4c525a240411f681137d8dd0fe19031c7b0"
95
+ "gitHead": "545d4052432b61ff13ba81fa5a69644cf4901103"
96
96
  }
package/sequence.js CHANGED
@@ -26,14 +26,14 @@ export class Sequence {
26
26
  #identity
27
27
  #count = 0
28
28
 
29
- // TODO (perf): Optimize
30
29
  /**
31
30
  *
32
31
  * @param {string|Sequence|null|undefined} a
33
32
  * @param {string|Sequence|null|undefined} b
34
33
  * @param {boolean} [strict=true]
35
- * @returns
34
+ * @returns {-1|0|1}
36
35
  */
36
+ /** @deprecated */
37
37
  static compare(a, b, strict) {
38
38
  if (!a && !b) {
39
39
  return 0
@@ -58,6 +58,17 @@ export class Sequence {
58
58
  return a.compare(b, strict)
59
59
  }
60
60
 
61
+ /**
62
+ *
63
+ * @param {string|Sequence|null|undefined} a
64
+ * @param {string|Sequence|null|undefined} b
65
+ * @param {boolean} [strict=true]
66
+ * @returns {boolean}
67
+ */
68
+ static has(a, b, strict) {
69
+ return Sequence.compare(a, b, strict) >= 0
70
+ }
71
+
61
72
  /**
62
73
  *
63
74
  * @param {string} seq
@@ -75,13 +86,16 @@ export class Sequence {
75
86
  }
76
87
  }
77
88
 
89
+ /**
90
+ * @param {null|undefined|string|Sequence|Array<string|number|{id:string,sequence:number}>>} value
91
+ * @param {null|undefined|number|Array<string|{id:string}>>} [identity]
92
+ */
78
93
  constructor(value, identity) {
79
94
  try {
80
95
  if (!value) {
81
96
  this.#value = '0'
82
97
  } else if (Array.isArray(value)) {
83
98
  this.#identity = identity
84
- this.#count = 0
85
99
  for (const part of value) {
86
100
  if (typeof part === 'string') {
87
101
  const [sequenceStr, id] = part.split(ID_SEP)
@@ -103,7 +117,6 @@ export class Sequence {
103
117
  assert(this.#identity === 0 || this.#parts.length > 0)
104
118
  } else if (typeof value === 'string') {
105
119
  const [countStr, token] = value.split('-')
106
- const count = parseInt(countStr)
107
120
  if (token) {
108
121
  for (const str of token.split('_')) {
109
122
  const [sequenceStr, id] = str.split(ID_SEP)
@@ -111,7 +124,7 @@ export class Sequence {
111
124
  this.#parts.push(id, sequence)
112
125
  }
113
126
  }
114
- this.#count = count
127
+ this.#count = parseInt(countStr)
115
128
  this.#value = value
116
129
  } else if (value instanceof Sequence) {
117
130
  this.#count = value.#count
@@ -174,6 +187,9 @@ export class Sequence {
174
187
  }
175
188
  }
176
189
 
190
+ /**
191
+ * @returns {number}
192
+ */
177
193
  get identity() {
178
194
  if (this.#identity == null) {
179
195
  if (this.#parts.length === 0) {
@@ -190,6 +206,9 @@ export class Sequence {
190
206
  return this.#identity
191
207
  }
192
208
 
209
+ /**
210
+ * @returns {number}
211
+ */
193
212
  get count() {
194
213
  if (this.#count == null) {
195
214
  let count = 0
@@ -201,10 +220,17 @@ export class Sequence {
201
220
  return this.#count
202
221
  }
203
222
 
223
+ /**
224
+ * @returns {number}
225
+ */
204
226
  get length() {
205
227
  return this.#parts.length / 2
206
228
  }
207
229
 
230
+ /**
231
+ * @param {number} index
232
+ * @returns {number}
233
+ */
208
234
  at(index) {
209
235
  if (!Number.isInteger(index)) {
210
236
  throw new TypeError('index must be an integer')
@@ -216,6 +242,10 @@ export class Sequence {
216
242
  return this.#parts[index * 2 + 1]
217
243
  }
218
244
 
245
+ /**
246
+ * @param {number} index
247
+ * @param {number} sequence
248
+ */
219
249
  set(index, sequence) {
220
250
  if (!Number.isInteger(index)) {
221
251
  throw new TypeError('index must be an integer')
@@ -236,6 +266,7 @@ export class Sequence {
236
266
  }
237
267
  }
238
268
 
269
+ /** @deprecated */
239
270
  compare(other, strict) {
240
271
  if (strict === undefined) {
241
272
  strict = true
@@ -256,7 +287,13 @@ export class Sequence {
256
287
  throw new TypeError('strict must be a boolean')
257
288
  }
258
289
 
259
- if (strict && other.identity && this.identity && other.identity !== this.identity) {
290
+ if (
291
+ strict &&
292
+ (other.identity || this.identity) &&
293
+ other.identity !== this.identity &&
294
+ this.#value !== '0' &&
295
+ other.#value !== '0'
296
+ ) {
260
297
  throw new Error('Cannot compare sequences with different identities')
261
298
  }
262
299
 
@@ -273,6 +310,19 @@ export class Sequence {
273
310
  return 0
274
311
  }
275
312
 
313
+ /**
314
+ *
315
+ * @param {string|Sequence|null|undefined} other
316
+ * @param {boolean} [strict=true]
317
+ * @returns {boolean}
318
+ */
319
+ has(other, strict = true) {
320
+ return this.compare(other, strict) >= 0
321
+ }
322
+
323
+ /**
324
+ * @returns {string}
325
+ */
276
326
  toString() {
277
327
  if (!this.#value) {
278
328
  let count = 0
@@ -286,10 +336,16 @@ export class Sequence {
286
336
  return this.#value
287
337
  }
288
338
 
339
+ /**
340
+ * @returns {string}
341
+ */
289
342
  [Symbol.toStringTag]() {
290
343
  return this.toString()
291
344
  }
292
345
 
346
+ /**
347
+ * @returns {string}
348
+ */
293
349
  [util.inspect.custom](depth, options, inspect) {
294
350
  return `Sequence: "${this.toString()}"`
295
351
  }
package/shared.js CHANGED
@@ -1,6 +1,3 @@
1
- import stream from 'node:stream'
2
- import assert from 'node:assert'
3
-
4
1
  // By placing the read and write indices far apart (multiples of a common
5
2
  // cache line size, 64 bytes), we prevent "false sharing". This is a
6
3
  // low-level CPU optimization where two cores writing to different variables
@@ -35,6 +32,16 @@ export function alloc(size) {
35
32
  * @returns {{ readSome: function }}
36
33
  */
37
34
  export function reader({ sharedState, sharedBuffer }) {
35
+ if (!(sharedState instanceof SharedArrayBuffer)) {
36
+ throw new TypeError('sharedState must be a SharedArrayBuffer')
37
+ }
38
+ if (!(sharedBuffer instanceof SharedArrayBuffer)) {
39
+ throw new TypeError('sharedBuffer must be a SharedArrayBuffer')
40
+ }
41
+ if (sharedBuffer.byteLength >= 2 ** 31) {
42
+ throw new RangeError('Shared buffer size exceeds maximum of 2GB')
43
+ }
44
+
38
45
  const state = new Int32Array(sharedState)
39
46
  const size = sharedBuffer.byteLength
40
47
  const buffer = Buffer.from(sharedBuffer)
@@ -52,7 +59,7 @@ export function reader({ sharedState, sharedBuffer }) {
52
59
 
53
60
  /**
54
61
  * Reads a batch of messages from the buffer.
55
- * @param {(data: {buffer: Buffer, view: DataView, offset: number, length: number}) => void} next Callback to process a message.
62
+ * @param {(data: {buffer: Buffer, view: DataView, offset: number, length: number}) => void|boolean} next Callback to process a message.
56
63
  * @returns {number} The number of messages read.
57
64
  */
58
65
  function readSome(next) {
@@ -70,10 +77,11 @@ export function reader({ sharedState, sharedBuffer }) {
70
77
  const dataPos = readPos + 4
71
78
  const dataLen = view.getInt32(dataPos - 4, true) | 0
72
79
 
80
+ bytes += 4
81
+
73
82
  // A length of -1 is a special marker indicating the writer has
74
83
  // wrapped around to the beginning of the buffer.
75
84
  if (dataLen === -1) {
76
- bytes += 4
77
85
  readPos = 0
78
86
  // After wrapping, we must re-check against the writer's position.
79
87
  // It's possible the writer is now at a position > 0.
@@ -86,22 +94,17 @@ export function reader({ sharedState, sharedBuffer }) {
86
94
  throw new Error('Data exceeds buffer size')
87
95
  }
88
96
 
89
- bytes += dataLen
90
97
  readPos += 4 + dataLen
98
+
99
+ bytes += dataLen
91
100
  count += 1
92
101
 
93
102
  // This is a "zero-copy" operation. We don't copy the data out.
94
103
  // Instead, we pass a "view" into the shared buffer.
95
104
  data.offset = dataPos
96
105
  data.length = dataLen
97
- const cont = next(data)
98
-
99
- if (readPos === writePos) {
100
- // If we reach the end of the buffer, we must re-check the writer's position.
101
- writePos = Atomics.load(state, WRITE_INDEX) | 0
102
- }
103
106
 
104
- if (cont === false) {
107
+ if (next(data) === false) {
105
108
  break
106
109
  }
107
110
  }
@@ -125,6 +128,16 @@ export function reader({ sharedState, sharedBuffer }) {
125
128
  * @returns {{ write: function, cork: function(function) }}
126
129
  */
127
130
  export function writer({ sharedState, sharedBuffer }, { yield: onYield, logger } = {}) {
131
+ if (!(sharedState instanceof SharedArrayBuffer)) {
132
+ throw new TypeError('sharedState must be a SharedArrayBuffer')
133
+ }
134
+ if (!(sharedBuffer instanceof SharedArrayBuffer)) {
135
+ throw new TypeError('sharedBuffer must be a SharedArrayBuffer')
136
+ }
137
+ if (sharedBuffer.byteLength >= 2 ** 31) {
138
+ throw new RangeError('Shared buffer size exceeds maximum of 2GB')
139
+ }
140
+
128
141
  const state = new Int32Array(sharedState)
129
142
  const size = sharedBuffer.byteLength
130
143
  const buffer = Buffer.from(sharedBuffer)
@@ -140,7 +153,7 @@ export function writer({ sharedState, sharedBuffer }, { yield: onYield, logger }
140
153
  let readPos = Atomics.load(state, READ_INDEX) | 0
141
154
  let writePos = Atomics.load(state, WRITE_INDEX) | 0
142
155
 
143
- let yielding = false
156
+ let yielding = 0
144
157
  let corked = 0
145
158
  let pending = 0
146
159
 
@@ -153,22 +166,22 @@ export function writer({ sharedState, sharedBuffer }, { yield: onYield, logger }
153
166
  * @param {number} delay The timeout for Atomics.wait.
154
167
  */
155
168
  function _yield(delay) {
156
- if (yielding) {
157
- throw new Error('Cannot yield while yielding')
169
+ if (yielding > 128) {
170
+ throw new Error('Detected possible deadlock: writer yielding too many times')
158
171
  }
159
172
 
160
173
  // First, ensure the very latest write position is visible to the reader.
161
174
  _flush()
162
175
 
163
176
  if (onYield) {
164
- yielding = true
177
+ yielding += 1
165
178
  try {
166
179
  // Call the user-provided yield function, if any. This can be important
167
180
  // if the writer is waiting for the reader to process data which would
168
181
  // otherwise deadlock.
169
182
  onYield()
170
183
  } finally {
171
- yielding = false
184
+ yielding -= 1
172
185
  }
173
186
  }
174
187
 
@@ -176,6 +189,8 @@ export function writer({ sharedState, sharedBuffer }, { yield: onYield, logger }
176
189
  // to sleep, consuming no CPU, until the reader changes the READ_INDEX.
177
190
  if (delay > 0) {
178
191
  Atomics.wait(state, READ_INDEX, readPos, delay)
192
+ } else {
193
+ Atomics.pause()
179
194
  }
180
195
 
181
196
  // After waking up, refresh the local view of the reader's position.
@@ -192,24 +207,22 @@ export function writer({ sharedState, sharedBuffer }, { yield: onYield, logger }
192
207
  // 4-byte header for the *next* message (for wrap-around check).
193
208
  const required = len + 4 + 4
194
209
 
195
- if (required < 0) {
196
- throw new Error(`Required length ${required} is negative, expected at least 0`)
197
- }
198
- if (required > size) {
199
- throw new Error(`Required length ${required} exceeds buffer size ${size}`)
200
- }
201
-
202
210
  if (writePos >= readPos) {
203
- // Case 1: The writer is ahead of the reader. [ 0 ---- R ... W ---- S ]
204
- // There is free space from W to the end (S) and from 0 to R.
211
+ // Case 1: The writer is ahead of the reader. [ 0 - R ... W - size ]
212
+ // There is free space from W to the end (s) and from 0 to R.
213
+
205
214
  if (size - writePos >= required) {
206
215
  // Enough space at the end of the buffer.
207
216
  return true
208
217
  }
209
218
 
219
+ if (readPos === 0) {
220
+ readPos = Atomics.load(state, READ_INDEX) | 0
221
+ }
222
+
210
223
  // Not enough space at the end. Check if there's space at the beginning.
211
224
  if (readPos === 0) {
212
- // Reader is at the very beginning, so no space to wrap around into.
225
+ // Reader is at the beginning, so no space to wrap around into.
213
226
  return false
214
227
  }
215
228
 
@@ -220,17 +233,24 @@ export function writer({ sharedState, sharedBuffer }, { yield: onYield, logger }
220
233
  writePos = 0
221
234
 
222
235
  if (writePos + 4 > size) {
236
+ // assertion
223
237
  throw new Error(`Write position ${writePos} with next header exceeds buffer size ${size}`)
224
238
  }
225
239
  if (writePos === readPos) {
240
+ // assertion
226
241
  throw new Error(`Write position ${writePos} cannot equal read position ${readPos}`)
227
242
  }
228
243
 
229
244
  Atomics.store(state, WRITE_INDEX, writePos)
230
245
  }
231
246
 
232
- // Case 2: The writer has wrapped around. [ 0 ... W ---- R ... S ]
247
+ // Case 2: The writer has wrapped around. [ 0 ... W - R ... s ]
233
248
  // The only free space is between W and R.
249
+
250
+ if (readPos - writePos < required) {
251
+ readPos = Atomics.load(state, READ_INDEX) | 0
252
+ }
253
+
234
254
  return readPos - writePos >= required
235
255
  }
236
256
 
@@ -245,84 +265,114 @@ export function writer({ sharedState, sharedBuffer }, { yield: onYield, logger }
245
265
  }
246
266
  }
247
267
 
268
+ function _flush() {
269
+ if (pending > 0) {
270
+ Atomics.store(state, WRITE_INDEX, writePos)
271
+ pending = 0
272
+ }
273
+ }
274
+
248
275
  /**
249
276
  * Performs the actual write into the buffer after space has been acquired.
250
- * @param {number} len The exact length of the payload.
277
+ * @param {number} dataCap Max length of the payload.
251
278
  * @param {({ buffer, view, offset, length }) => number} fn The callback that writes the data.
252
279
  * @returns {void}
253
280
  */
254
- function _write(len, fn) {
281
+ function _write(dataCap, fn) {
255
282
  const dataPos = writePos + 4
256
283
 
257
284
  data.offset = dataPos
258
- data.length = len
285
+ data.length = dataCap
259
286
 
260
287
  // The user-provided function writes the data and returns the final position.
261
288
  // We calculate the actual bytes written from that.
289
+ // NOTE: This is unsafe as the user function can write beyond the reserved length.
262
290
  const dataLen = fn(data) - dataPos
263
291
 
292
+ if (typeof dataLen !== 'number') {
293
+ throw new TypeError('"fn" must return the number of bytes written')
294
+ }
264
295
  if (dataLen < 0) {
265
- throw new Error(`Data length ${dataLen} is negative`)
296
+ throw new RangeError(`"fn" returned a negative number ${dataLen}`)
266
297
  }
267
- if (dataLen > len) {
268
- throw new Error(`Data length ${dataLen} exceeds expected length ${len}`)
298
+ if (dataLen > dataCap) {
299
+ throw new RangeError(`"fn" returned a number ${dataLen} that exceeds capacity ${dataCap}`)
269
300
  }
301
+
270
302
  if (dataPos + dataLen > size) {
303
+ // assertion
271
304
  throw new Error(`Data position ${dataPos} with length ${dataLen} exceeds buffer size ${size}`)
272
305
  }
273
306
 
274
- // Write the actual length of the data into the 4-byte header.
275
- view.setInt32(writePos, dataLen, true)
276
- writePos += 4 + dataLen
307
+ const nextPos = writePos + 4 + dataLen
277
308
 
278
- if (writePos + 4 > size) {
279
- throw new Error(`Write position ${writePos} with next header exceeds buffer size ${size}`)
309
+ if (nextPos + 4 > size) {
310
+ // assertion
311
+ throw new Error(`Write position ${nextPos} with next header exceeds buffer size ${size}`)
280
312
  }
281
- if (writePos === readPos) {
282
- throw new Error(`Write position ${writePos} cannot equal read position ${readPos}`)
313
+ if (nextPos === readPos) {
314
+ // assertion
315
+ throw new Error(`Write position ${nextPos} cannot equal read position ${readPos}`)
283
316
  }
284
317
 
318
+ // Write the actual length of the data into the 4-byte header.
319
+ view.setInt32(writePos, dataLen, true)
320
+ writePos += 4 + dataLen
321
+ pending += 4 + dataLen
322
+
285
323
  // This is the "corking" optimization. Instead of calling Atomics.store
286
324
  // on every write, we batch them. We either write when a certain
287
325
  // amount of data is pending (HWM_BYTES) or at the end of the current
288
- // JS microtask. This drastically reduces atomic operation overhead.
326
+ // event loop tick. This drastically reduces atomic operation overhead.
289
327
  if (pending >= HWM_BYTES) {
290
328
  Atomics.store(state, WRITE_INDEX, writePos)
291
329
  pending = 0
292
- } else {
293
- pending += 4 + dataLen
294
- if (corked === 0) {
295
- corked += 1
296
- setImmediate(_uncork)
297
- }
330
+ } else if (corked === 0) {
331
+ corked += 1
332
+ setImmediate(_uncork)
298
333
  }
299
334
  }
300
335
 
301
336
  /**
302
- * Public write method. Acquires space and writes data.
337
+ * Public write method. Acquires space and synchronously writes data with a timeout. Will
338
+ * wait until space is available.
339
+ * Writing more than "len" bytes in the callback will cause undefined behavior.
303
340
  * @param {number} len The maximum expected length of the payload.
304
341
  * @param {({ buffer, view, offset, length }) => number} fn The callback that writes the data.
342
+ * @param {number} [timeout=60000] The maximum time to wait for space in milliseconds.
305
343
  */
306
- function writeSync(len, fn) {
344
+ function writeSync(len, fn, timeout = 60e3) {
345
+ if (typeof len !== 'number') {
346
+ throw new TypeError('"len" must be a non-negative number')
347
+ }
307
348
  if (len < 0) {
308
- throw new Error(`Length ${len} is negative`)
349
+ throw new RangeError(`"len" ${len} is negative`)
309
350
  }
310
351
  if (len >= 2 ** 31 || len > size - 8) {
311
- throw new Error(`Length ${len} exceeds maximum allowed size`)
352
+ throw new Error(`"len" ${len} exceeds maximum allowed size ${size - 8}`)
353
+ }
354
+ if (typeof fn !== 'function') {
355
+ throw new TypeError('"fn" must be a function')
356
+ }
357
+ if (typeof timeout !== 'number') {
358
+ throw new TypeError('"timeout" must be a non-negative number')
359
+ }
360
+ if (timeout < 0) {
361
+ throw new RangeError('"timeout" must be a non-negative number')
312
362
  }
313
363
 
314
364
  if (!_acquire(len)) {
315
- readPos = Atomics.load(state, READ_INDEX) | 0
316
- if (!_acquire(len)) {
317
- const startTime = performance.now()
318
- logger?.warn({ readPos, writePos }, 'yield started')
319
- _yield(0)
320
- while (!_acquire(len)) {
321
- _yield(3)
365
+ const startTime = performance.now()
366
+ logger?.warn({ readPos, writePos }, 'yield started')
367
+ _yield(0)
368
+ for (let n = 0; !_acquire(len); n++) {
369
+ if (performance.now() - startTime > timeout) {
370
+ throw new Error('Timeout while waiting for space in the buffer')
322
371
  }
323
- const elapsedTime = performance.now() - startTime
324
- logger?.warn({ readPos, writePos, elapsedTime }, 'yield completed')
372
+ _yield(3)
325
373
  }
374
+ const elapsedTime = performance.now() - startTime
375
+ logger?.warn({ readPos, writePos, elapsedTime }, 'yield completed')
326
376
  }
327
377
 
328
378
  _write(len, fn)
@@ -332,19 +382,28 @@ export function writer({ sharedState, sharedBuffer }, { yield: onYield, logger }
332
382
  }
333
383
  }
334
384
 
385
+ /**
386
+ * Public write method. Acquires space and tries to writes data.
387
+ * Writing more than "len" bytes in the callback will cause undefined behavior.
388
+ * @param {number} len The maximum expected length of the payload.
389
+ * @param {({ buffer, view, offset, length }) => number} fn The callback that writes the data.
390
+ */
335
391
  function tryWrite(len, fn) {
392
+ if (typeof len !== 'number') {
393
+ throw new TypeError('"len" must be a non-negative number')
394
+ }
336
395
  if (len < 0) {
337
- throw new Error(`Length ${len} is negative`)
396
+ throw new RangeError(`"len" ${len} is negative`)
338
397
  }
339
398
  if (len >= 2 ** 31 || len > size - 8) {
340
- throw new Error(`Length ${len} exceeds maximum allowed size`)
399
+ throw new Error(`"len" ${len} exceeds maximum allowed size ${size - 8}`)
400
+ }
401
+ if (typeof fn !== 'function') {
402
+ throw new TypeError('"fn" must be a function')
341
403
  }
342
404
 
343
405
  if (!_acquire(len)) {
344
- readPos = Atomics.load(state, READ_INDEX) | 0
345
- if (!_acquire(len)) {
346
- return false
347
- }
406
+ return false
348
407
  }
349
408
 
350
409
  _write(len, fn)
@@ -356,13 +415,6 @@ export function writer({ sharedState, sharedBuffer }, { yield: onYield, logger }
356
415
  return true
357
416
  }
358
417
 
359
- function _flush() {
360
- if (pending > 0) {
361
- Atomics.store(state, WRITE_INDEX, writePos)
362
- pending = 0
363
- }
364
- }
365
-
366
418
  function cork(callback) {
367
419
  corked += 1
368
420
  try {
@@ -374,118 +426,3 @@ export function writer({ sharedState, sharedBuffer }, { yield: onYield, logger }
374
426
 
375
427
  return { tryWrite, writeSync, cork }
376
428
  }
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
- }
491
- }