@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.
- package/package.json +2 -2
- package/sequence.js +62 -6
- package/shared.js +127 -190
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxtedition/lib",
|
|
3
|
-
"version": "28.0.
|
|
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": "
|
|
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 =
|
|
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 (
|
|
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 (
|
|
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 =
|
|
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('
|
|
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
|
|
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
|
|
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
|
|
204
|
-
// There is free space from W to the end (
|
|
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
|
|
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
|
|
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}
|
|
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(
|
|
281
|
+
function _write(dataCap, fn) {
|
|
255
282
|
const dataPos = writePos + 4
|
|
256
283
|
|
|
257
284
|
data.offset = dataPos
|
|
258
|
-
data.length =
|
|
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
|
|
296
|
+
throw new RangeError(`"fn" returned a negative number ${dataLen}`)
|
|
266
297
|
}
|
|
267
|
-
if (dataLen >
|
|
268
|
-
throw new
|
|
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
|
-
|
|
275
|
-
view.setInt32(writePos, dataLen, true)
|
|
276
|
-
writePos += 4 + dataLen
|
|
307
|
+
const nextPos = writePos + 4 + dataLen
|
|
277
308
|
|
|
278
|
-
if (
|
|
279
|
-
|
|
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 (
|
|
282
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
294
|
-
|
|
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
|
|
349
|
+
throw new RangeError(`"len" ${len} is negative`)
|
|
309
350
|
}
|
|
310
351
|
if (len >= 2 ** 31 || len > size - 8) {
|
|
311
|
-
throw new Error(`
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
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
|
|
396
|
+
throw new RangeError(`"len" ${len} is negative`)
|
|
338
397
|
}
|
|
339
398
|
if (len >= 2 ** 31 || len > size - 8) {
|
|
340
|
-
throw new Error(`
|
|
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
|
-
|
|
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
|
-
}
|