thread-stream 0.11.2 → 0.13.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.
- package/.github/dependabot.yml +4 -0
- package/.github/workflows/ci.yml +3 -3
- package/.github/workflows/package-manager-ci.yml +4 -4
- package/index.js +284 -216
- package/lib/worker.js +9 -2
- package/package.json +4 -1
- package/test/base.test.js +38 -102
- package/test/bundlers.test.js +33 -0
- package/test/close-on-gc.js +8 -10
- package/test/commonjs-fallback.test.js +30 -3
- package/test/create-and-exit.js +5 -7
- package/test/custom-worker.js +9 -0
- package/test/dir with spaces/test-package.zip +0 -0
- package/test/end.test.js +22 -38
- package/test/esm.test.mjs +6 -14
- package/test/multibyte-chars.test.mjs +6 -10
- package/test/string-limit.test.js +10 -14
- package/test/thread-management.test.js +5 -5
package/index.js
CHANGED
|
@@ -10,6 +10,9 @@ const {
|
|
|
10
10
|
READ_INDEX
|
|
11
11
|
} = require('./lib/indexes')
|
|
12
12
|
const buffer = require('buffer')
|
|
13
|
+
const assert = require('assert')
|
|
14
|
+
|
|
15
|
+
const kImpl = Symbol('kImpl')
|
|
13
16
|
|
|
14
17
|
// V8 limit for string size
|
|
15
18
|
const MAX_STRING = buffer.constants.MAX_STRING_LENGTH
|
|
@@ -41,7 +44,8 @@ const registry = new FinalizationRegistry((worker) => {
|
|
|
41
44
|
function createWorker (stream, opts) {
|
|
42
45
|
const { filename, workerData } = opts
|
|
43
46
|
|
|
44
|
-
const
|
|
47
|
+
const bundlerOverrides = '__bundlerPathsOverrides' in globalThis ? globalThis.__bundlerPathsOverrides : {}
|
|
48
|
+
const toExecute = bundlerOverrides['thread-stream-worker'] || join(__dirname, 'lib', 'worker.js')
|
|
45
49
|
|
|
46
50
|
const worker = new Worker(toExecute, {
|
|
47
51
|
...opts.workerOpts,
|
|
@@ -49,8 +53,8 @@ function createWorker (stream, opts) {
|
|
|
49
53
|
filename: filename.indexOf('file://') === 0
|
|
50
54
|
? filename
|
|
51
55
|
: pathToFileURL(filename).href,
|
|
52
|
-
dataBuf: stream.
|
|
53
|
-
stateBuf: stream.
|
|
56
|
+
dataBuf: stream[kImpl].dataBuf,
|
|
57
|
+
stateBuf: stream[kImpl].stateBuf,
|
|
54
58
|
workerData
|
|
55
59
|
}
|
|
56
60
|
})
|
|
@@ -67,57 +71,62 @@ function createWorker (stream, opts) {
|
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
function drain (stream) {
|
|
70
|
-
stream.
|
|
71
|
-
stream.
|
|
74
|
+
assert(!stream[kImpl].sync)
|
|
75
|
+
if (stream[kImpl].needDrain) {
|
|
76
|
+
stream[kImpl].needDrain = false
|
|
77
|
+
stream.emit('drain')
|
|
78
|
+
}
|
|
72
79
|
}
|
|
73
80
|
|
|
74
81
|
function nextFlush (stream) {
|
|
75
|
-
const writeIndex = Atomics.load(stream.
|
|
76
|
-
let leftover = stream.
|
|
82
|
+
const writeIndex = Atomics.load(stream[kImpl].state, WRITE_INDEX)
|
|
83
|
+
let leftover = stream[kImpl].data.length - writeIndex
|
|
77
84
|
|
|
78
85
|
if (leftover > 0) {
|
|
79
|
-
if (stream.buf.length === 0) {
|
|
80
|
-
stream.flushing = false
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
stream
|
|
86
|
+
if (stream[kImpl].buf.length === 0) {
|
|
87
|
+
stream[kImpl].flushing = false
|
|
88
|
+
|
|
89
|
+
if (stream[kImpl].ending) {
|
|
90
|
+
end(stream)
|
|
91
|
+
} else if (stream[kImpl].needDrain) {
|
|
84
92
|
process.nextTick(drain, stream)
|
|
85
93
|
}
|
|
94
|
+
|
|
86
95
|
return
|
|
87
96
|
}
|
|
88
97
|
|
|
89
|
-
let toWrite = stream.buf.slice(0, leftover)
|
|
98
|
+
let toWrite = stream[kImpl].buf.slice(0, leftover)
|
|
90
99
|
let toWriteBytes = Buffer.byteLength(toWrite)
|
|
91
100
|
if (toWriteBytes <= leftover) {
|
|
92
|
-
stream.buf = stream.buf.slice(leftover)
|
|
101
|
+
stream[kImpl].buf = stream[kImpl].buf.slice(leftover)
|
|
93
102
|
// process._rawDebug('writing ' + toWrite.length)
|
|
94
|
-
stream
|
|
103
|
+
write(stream, toWrite, nextFlush.bind(null, stream))
|
|
95
104
|
} else {
|
|
96
105
|
// multi-byte utf-8
|
|
97
106
|
stream.flush(() => {
|
|
98
|
-
Atomics.store(stream.
|
|
99
|
-
Atomics.store(stream.
|
|
107
|
+
Atomics.store(stream[kImpl].state, READ_INDEX, 0)
|
|
108
|
+
Atomics.store(stream[kImpl].state, WRITE_INDEX, 0)
|
|
100
109
|
|
|
101
110
|
// Find a toWrite length that fits the buffer
|
|
102
111
|
// it must exists as the buffer is at least 4 bytes length
|
|
103
112
|
// and the max utf-8 length for a char is 4 bytes.
|
|
104
|
-
while (toWriteBytes > stream.buf.length) {
|
|
113
|
+
while (toWriteBytes > stream[kImpl].buf.length) {
|
|
105
114
|
leftover = leftover / 2
|
|
106
|
-
toWrite = stream.buf.slice(0, leftover)
|
|
115
|
+
toWrite = stream[kImpl].buf.slice(0, leftover)
|
|
107
116
|
toWriteBytes = Buffer.byteLength(toWrite)
|
|
108
117
|
}
|
|
109
|
-
stream.buf = stream.buf.slice(leftover)
|
|
110
|
-
stream
|
|
118
|
+
stream[kImpl].buf = stream[kImpl].buf.slice(leftover)
|
|
119
|
+
write(stream, toWrite, nextFlush.bind(null, stream))
|
|
111
120
|
})
|
|
112
121
|
}
|
|
113
122
|
} else if (leftover === 0) {
|
|
114
|
-
if (writeIndex === 0 && stream.buf.length === 0) {
|
|
123
|
+
if (writeIndex === 0 && stream[kImpl].buf.length === 0) {
|
|
115
124
|
// we had a flushSync in the meanwhile
|
|
116
125
|
return
|
|
117
126
|
}
|
|
118
127
|
stream.flush(() => {
|
|
119
|
-
Atomics.store(stream.
|
|
120
|
-
Atomics.store(stream.
|
|
128
|
+
Atomics.store(stream[kImpl].state, READ_INDEX, 0)
|
|
129
|
+
Atomics.store(stream[kImpl].state, WRITE_INDEX, 0)
|
|
121
130
|
nextFlush(stream)
|
|
122
131
|
})
|
|
123
132
|
} else {
|
|
@@ -140,29 +149,14 @@ function onWorkerMessage (msg) {
|
|
|
140
149
|
// Replace the FakeWeakRef with a
|
|
141
150
|
// proper one.
|
|
142
151
|
this.stream = new WeakRef(stream)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
stream.
|
|
152
|
+
|
|
153
|
+
stream.flush(() => {
|
|
154
|
+
stream[kImpl].ready = true
|
|
146
155
|
stream.emit('ready')
|
|
147
|
-
}
|
|
148
|
-
stream.once('drain', function () {
|
|
149
|
-
stream.flush(() => {
|
|
150
|
-
stream.ready = true
|
|
151
|
-
stream.emit('ready')
|
|
152
|
-
})
|
|
153
|
-
})
|
|
154
|
-
nextFlush(stream)
|
|
155
|
-
}
|
|
156
|
+
})
|
|
156
157
|
break
|
|
157
158
|
case 'ERROR':
|
|
158
|
-
stream.
|
|
159
|
-
stream.worker.exited = true
|
|
160
|
-
// TODO only remove our own
|
|
161
|
-
stream.worker.removeAllListeners('exit')
|
|
162
|
-
stream.worker.terminate().then(null, () => {})
|
|
163
|
-
process.nextTick(() => {
|
|
164
|
-
stream.emit('error', msg.err)
|
|
165
|
-
})
|
|
159
|
+
destroy(stream, msg.err)
|
|
166
160
|
break
|
|
167
161
|
default:
|
|
168
162
|
throw new Error('this should not happen: ' + msg.code)
|
|
@@ -176,14 +170,9 @@ function onWorkerExit (code) {
|
|
|
176
170
|
return
|
|
177
171
|
}
|
|
178
172
|
registry.unregister(stream)
|
|
179
|
-
stream.closed = true
|
|
180
173
|
stream.worker.exited = true
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
stream.emit('error', new Error('The worker thread exited'))
|
|
184
|
-
}
|
|
185
|
-
stream.emit('close')
|
|
186
|
-
})
|
|
174
|
+
stream.worker.off('exit', onWorkerExit)
|
|
175
|
+
destroy(stream, code !== 0 ? new Error('The worker thread exited') : null)
|
|
187
176
|
}
|
|
188
177
|
|
|
189
178
|
class ThreadStream extends EventEmitter {
|
|
@@ -194,125 +183,88 @@ class ThreadStream extends EventEmitter {
|
|
|
194
183
|
throw new Error('bufferSize must at least fit a 4-byte utf-8 char')
|
|
195
184
|
}
|
|
196
185
|
|
|
197
|
-
this
|
|
198
|
-
this.
|
|
199
|
-
this.
|
|
200
|
-
this.
|
|
201
|
-
this.
|
|
202
|
-
this.
|
|
203
|
-
this.
|
|
204
|
-
this.
|
|
205
|
-
this.needDrain = false
|
|
206
|
-
this.
|
|
207
|
-
|
|
208
|
-
this.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
this.
|
|
216
|
-
Atomics.store(this._state, WRITE_INDEX, current + length)
|
|
217
|
-
Atomics.notify(this._state, WRITE_INDEX)
|
|
218
|
-
cb()
|
|
219
|
-
return true
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
_hasSpace () {
|
|
223
|
-
const current = Atomics.load(this._state, WRITE_INDEX)
|
|
224
|
-
return this._data.length - this.buf.length - current > 0
|
|
186
|
+
this[kImpl] = {}
|
|
187
|
+
this[kImpl].stateBuf = new SharedArrayBuffer(128)
|
|
188
|
+
this[kImpl].state = new Int32Array(this[kImpl].stateBuf)
|
|
189
|
+
this[kImpl].dataBuf = new SharedArrayBuffer(opts.bufferSize || 4 * 1024 * 1024)
|
|
190
|
+
this[kImpl].data = Buffer.from(this[kImpl].dataBuf)
|
|
191
|
+
this[kImpl].sync = opts.sync || false
|
|
192
|
+
this[kImpl].ending = false
|
|
193
|
+
this[kImpl].ended = false
|
|
194
|
+
this[kImpl].needDrain = false
|
|
195
|
+
this[kImpl].destroyed = false
|
|
196
|
+
this[kImpl].flushing = false
|
|
197
|
+
this[kImpl].ready = false
|
|
198
|
+
this[kImpl].finished = false
|
|
199
|
+
this[kImpl].errored = null
|
|
200
|
+
this[kImpl].closed = false
|
|
201
|
+
this[kImpl].buf = ''
|
|
202
|
+
|
|
203
|
+
// TODO (fix): Make private?
|
|
204
|
+
this.worker = createWorker(this, opts) // TODO (fix): make private
|
|
225
205
|
}
|
|
226
206
|
|
|
227
207
|
write (data) {
|
|
228
|
-
if (this.
|
|
208
|
+
if (this[kImpl].destroyed) {
|
|
229
209
|
throw new Error('the worker has exited')
|
|
230
210
|
}
|
|
231
211
|
|
|
232
|
-
if (this.
|
|
233
|
-
|
|
234
|
-
this._writeSync()
|
|
235
|
-
this.flushing = true // we are still flushing
|
|
212
|
+
if (this[kImpl].ending) {
|
|
213
|
+
throw new Error('the worker is ending')
|
|
236
214
|
}
|
|
237
215
|
|
|
238
|
-
if (
|
|
239
|
-
|
|
240
|
-
|
|
216
|
+
if (this[kImpl].flushing && this[kImpl].buf.length + data.length >= MAX_STRING) {
|
|
217
|
+
try {
|
|
218
|
+
writeSync(this)
|
|
219
|
+
this[kImpl].flushing = true
|
|
220
|
+
} catch (err) {
|
|
221
|
+
destroy(this, err)
|
|
222
|
+
return false
|
|
223
|
+
}
|
|
241
224
|
}
|
|
242
225
|
|
|
243
|
-
|
|
244
|
-
this.buf += data
|
|
245
|
-
this._writeSync()
|
|
226
|
+
this[kImpl].buf += data
|
|
246
227
|
|
|
247
|
-
|
|
228
|
+
if (this[kImpl].sync) {
|
|
229
|
+
try {
|
|
230
|
+
writeSync(this)
|
|
231
|
+
return true
|
|
232
|
+
} catch (err) {
|
|
233
|
+
destroy(this, err)
|
|
234
|
+
return false
|
|
235
|
+
}
|
|
248
236
|
}
|
|
249
237
|
|
|
250
|
-
this.
|
|
251
|
-
|
|
252
|
-
|
|
238
|
+
if (!this[kImpl].flushing) {
|
|
239
|
+
this[kImpl].flushing = true
|
|
240
|
+
setImmediate(nextFlush, this)
|
|
241
|
+
}
|
|
253
242
|
|
|
254
|
-
|
|
243
|
+
this[kImpl].needDrain = this[kImpl].data.length - this[kImpl].buf.length - Atomics.load(this[kImpl].state, WRITE_INDEX) <= 0
|
|
244
|
+
return !this[kImpl].needDrain
|
|
255
245
|
}
|
|
256
246
|
|
|
257
247
|
end () {
|
|
258
|
-
if (this.
|
|
248
|
+
if (this[kImpl].destroyed) {
|
|
259
249
|
throw new Error('the worker has exited')
|
|
260
250
|
}
|
|
261
251
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
return
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (this.flushing) {
|
|
268
|
-
this.once('drain', this.end.bind(this))
|
|
269
|
-
return
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
if (this.ending) {
|
|
273
|
-
return
|
|
274
|
-
}
|
|
275
|
-
this.ending = true
|
|
276
|
-
|
|
277
|
-
this.flushSync()
|
|
278
|
-
|
|
279
|
-
let read = Atomics.load(this._state, READ_INDEX)
|
|
280
|
-
|
|
281
|
-
// process._rawDebug('writing index')
|
|
282
|
-
Atomics.store(this._state, WRITE_INDEX, -1)
|
|
283
|
-
// process._rawDebug(`(end) readIndex (${Atomics.load(this._state, READ_INDEX)}) writeIndex (${Atomics.load(this._state, WRITE_INDEX)})`)
|
|
284
|
-
Atomics.notify(this._state, WRITE_INDEX)
|
|
285
|
-
|
|
286
|
-
// Wait for the process to complete
|
|
287
|
-
let spins = 0
|
|
288
|
-
while (read !== -1) {
|
|
289
|
-
// process._rawDebug(`read = ${read}`)
|
|
290
|
-
Atomics.wait(this._state, READ_INDEX, read, 1000)
|
|
291
|
-
read = Atomics.load(this._state, READ_INDEX)
|
|
292
|
-
|
|
293
|
-
if (++spins === 10) {
|
|
294
|
-
throw new Error('end() took too long (10s)')
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
process.nextTick(() => {
|
|
299
|
-
this.emit('finish')
|
|
300
|
-
})
|
|
301
|
-
// process._rawDebug('end finished...')
|
|
252
|
+
this[kImpl].ending = true
|
|
253
|
+
end(this)
|
|
302
254
|
}
|
|
303
255
|
|
|
304
256
|
flush (cb) {
|
|
305
|
-
if (this.
|
|
257
|
+
if (this[kImpl].destroyed) {
|
|
306
258
|
throw new Error('the worker has exited')
|
|
307
259
|
}
|
|
308
260
|
|
|
309
261
|
// TODO write all .buf
|
|
310
|
-
const writeIndex = Atomics.load(this.
|
|
311
|
-
// process._rawDebug(`(flush) readIndex (${Atomics.load(this.
|
|
312
|
-
wait(this.
|
|
262
|
+
const writeIndex = Atomics.load(this[kImpl].state, WRITE_INDEX)
|
|
263
|
+
// process._rawDebug(`(flush) readIndex (${Atomics.load(this.state, READ_INDEX)}) writeIndex (${Atomics.load(this.state, WRITE_INDEX)})`)
|
|
264
|
+
wait(this[kImpl].state, READ_INDEX, writeIndex, Infinity, (err, res) => {
|
|
313
265
|
if (err) {
|
|
314
|
-
this
|
|
315
|
-
cb
|
|
266
|
+
destroy(this, err)
|
|
267
|
+
process.nextTick(cb, err)
|
|
316
268
|
return
|
|
317
269
|
}
|
|
318
270
|
if (res === 'not-equal') {
|
|
@@ -320,108 +272,224 @@ class ThreadStream extends EventEmitter {
|
|
|
320
272
|
this.flush(cb)
|
|
321
273
|
return
|
|
322
274
|
}
|
|
323
|
-
cb
|
|
275
|
+
process.nextTick(cb)
|
|
324
276
|
})
|
|
325
277
|
}
|
|
326
278
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
// process._rawDebug('emitting drain')
|
|
331
|
-
this.needDrain = true
|
|
332
|
-
process.nextTick(drain, this)
|
|
333
|
-
}
|
|
279
|
+
flushSync () {
|
|
280
|
+
if (this[kImpl].destroyed) {
|
|
281
|
+
throw new Error('the worker has exited')
|
|
334
282
|
}
|
|
335
|
-
this.flushing = false
|
|
336
|
-
|
|
337
|
-
while (this.buf.length !== 0) {
|
|
338
|
-
const writeIndex = Atomics.load(this._state, WRITE_INDEX)
|
|
339
|
-
let leftover = this._data.length - writeIndex
|
|
340
|
-
if (leftover === 0) {
|
|
341
|
-
this._flushSync()
|
|
342
|
-
Atomics.store(this._state, READ_INDEX, 0)
|
|
343
|
-
Atomics.store(this._state, WRITE_INDEX, 0)
|
|
344
|
-
continue
|
|
345
|
-
} else if (leftover < 0) {
|
|
346
|
-
// This should never happen
|
|
347
|
-
throw new Error('overwritten')
|
|
348
|
-
}
|
|
349
283
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
this.buf = this.buf.slice(leftover)
|
|
354
|
-
// process._rawDebug('writing ' + toWrite.length)
|
|
355
|
-
this._write(toWrite, cb)
|
|
356
|
-
} else {
|
|
357
|
-
// multi-byte utf-8
|
|
358
|
-
this._flushSync()
|
|
359
|
-
Atomics.store(this._state, READ_INDEX, 0)
|
|
360
|
-
Atomics.store(this._state, WRITE_INDEX, 0)
|
|
284
|
+
writeSync(this)
|
|
285
|
+
flushSync(this)
|
|
286
|
+
}
|
|
361
287
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
// and the max utf-8 length for a char is 4 bytes.
|
|
365
|
-
while (toWriteBytes > this.buf.length) {
|
|
366
|
-
leftover = leftover / 2
|
|
367
|
-
toWrite = this.buf.slice(0, leftover)
|
|
368
|
-
toWriteBytes = Buffer.byteLength(toWrite)
|
|
369
|
-
}
|
|
370
|
-
this.buf = this.buf.slice(leftover)
|
|
371
|
-
this._write(toWrite, cb)
|
|
372
|
-
}
|
|
373
|
-
}
|
|
288
|
+
unref () {
|
|
289
|
+
this.worker.unref()
|
|
374
290
|
}
|
|
375
291
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
292
|
+
ref () {
|
|
293
|
+
this.worker.ref()
|
|
294
|
+
}
|
|
380
295
|
|
|
381
|
-
|
|
382
|
-
this.
|
|
296
|
+
get ready () {
|
|
297
|
+
return this[kImpl].ready
|
|
383
298
|
}
|
|
384
299
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
}
|
|
300
|
+
get destroyed () {
|
|
301
|
+
return this[kImpl].destroyed
|
|
302
|
+
}
|
|
389
303
|
|
|
390
|
-
|
|
304
|
+
get closed () {
|
|
305
|
+
return this[kImpl].closed
|
|
306
|
+
}
|
|
391
307
|
|
|
392
|
-
|
|
308
|
+
get writable () {
|
|
309
|
+
return !this[kImpl].destroyed && !this[kImpl].ending
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
get writableEnded () {
|
|
313
|
+
return this[kImpl].ending
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
get writableFinished () {
|
|
317
|
+
return this[kImpl].finished
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
get writableNeedDrain () {
|
|
321
|
+
return this[kImpl].needDrain
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
get writableObjectMode () {
|
|
325
|
+
return false
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
get writableErrored () {
|
|
329
|
+
return this[kImpl].errored
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function destroy (stream, err) {
|
|
334
|
+
if (stream[kImpl].destroyed) {
|
|
335
|
+
return
|
|
336
|
+
}
|
|
337
|
+
stream[kImpl].destroyed = true
|
|
338
|
+
|
|
339
|
+
if (err) {
|
|
340
|
+
stream[kImpl].errored = err
|
|
341
|
+
stream.emit('error', err)
|
|
342
|
+
}
|
|
393
343
|
|
|
344
|
+
if (!stream.worker.exited) {
|
|
345
|
+
stream.worker.terminate()
|
|
346
|
+
.catch(() => {})
|
|
347
|
+
.then(() => {
|
|
348
|
+
stream[kImpl].closed = true
|
|
349
|
+
stream.emit('close')
|
|
350
|
+
})
|
|
351
|
+
} else {
|
|
352
|
+
setImmediate(() => {
|
|
353
|
+
stream[kImpl].closed = true
|
|
354
|
+
stream.emit('close')
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function write (stream, data, cb) {
|
|
360
|
+
// data is smaller than the shared buffer length
|
|
361
|
+
const current = Atomics.load(stream[kImpl].state, WRITE_INDEX)
|
|
362
|
+
const length = Buffer.byteLength(data)
|
|
363
|
+
stream[kImpl].data.write(data, current)
|
|
364
|
+
Atomics.store(stream[kImpl].state, WRITE_INDEX, current + length)
|
|
365
|
+
Atomics.notify(stream[kImpl].state, WRITE_INDEX)
|
|
366
|
+
cb()
|
|
367
|
+
return true
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function end (stream) {
|
|
371
|
+
if (stream[kImpl].ended || !stream[kImpl].ending || stream[kImpl].flushing) {
|
|
372
|
+
return
|
|
373
|
+
}
|
|
374
|
+
stream[kImpl].ended = true
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
stream.flushSync()
|
|
378
|
+
|
|
379
|
+
let readIndex = Atomics.load(stream[kImpl].state, READ_INDEX)
|
|
380
|
+
|
|
381
|
+
// process._rawDebug('writing index')
|
|
382
|
+
Atomics.store(stream[kImpl].state, WRITE_INDEX, -1)
|
|
383
|
+
// process._rawDebug(`(end) readIndex (${Atomics.load(stream.state, READ_INDEX)}) writeIndex (${Atomics.load(stream.state, WRITE_INDEX)})`)
|
|
384
|
+
Atomics.notify(stream[kImpl].state, WRITE_INDEX)
|
|
385
|
+
|
|
386
|
+
// Wait for the process to complete
|
|
394
387
|
let spins = 0
|
|
388
|
+
while (readIndex !== -1) {
|
|
389
|
+
// process._rawDebug(`read = ${read}`)
|
|
390
|
+
Atomics.wait(stream[kImpl].state, READ_INDEX, readIndex, 1000)
|
|
391
|
+
readIndex = Atomics.load(stream[kImpl].state, READ_INDEX)
|
|
395
392
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
const readIndex = Atomics.load(this._state, READ_INDEX)
|
|
399
|
-
// process._rawDebug(`(flushSync) readIndex (${readIndex}) writeIndex (${writeIndex})`)
|
|
400
|
-
if (readIndex !== writeIndex) {
|
|
401
|
-
// TODO this timeouts for some reason.
|
|
402
|
-
Atomics.wait(this._state, READ_INDEX, readIndex, 1000)
|
|
403
|
-
} else {
|
|
404
|
-
break
|
|
393
|
+
if (readIndex === -2) {
|
|
394
|
+
throw new Error('end() failed')
|
|
405
395
|
}
|
|
406
396
|
|
|
407
397
|
if (++spins === 10) {
|
|
408
|
-
throw new Error('
|
|
398
|
+
throw new Error('end() took too long (10s)')
|
|
409
399
|
}
|
|
410
400
|
}
|
|
411
|
-
|
|
401
|
+
|
|
402
|
+
process.nextTick(() => {
|
|
403
|
+
stream[kImpl].finished = true
|
|
404
|
+
stream.emit('finish')
|
|
405
|
+
})
|
|
406
|
+
} catch (err) {
|
|
407
|
+
destroy(stream, err)
|
|
412
408
|
}
|
|
409
|
+
// process._rawDebug('end finished...')
|
|
410
|
+
}
|
|
413
411
|
|
|
414
|
-
|
|
415
|
-
|
|
412
|
+
function writeSync (stream) {
|
|
413
|
+
const cb = () => {
|
|
414
|
+
if (stream[kImpl].ending) {
|
|
415
|
+
end(stream)
|
|
416
|
+
} else if (stream[kImpl].needDrain) {
|
|
417
|
+
process.nextTick(drain, stream)
|
|
418
|
+
}
|
|
416
419
|
}
|
|
420
|
+
stream[kImpl].flushing = false
|
|
421
|
+
|
|
422
|
+
while (stream[kImpl].buf.length !== 0) {
|
|
423
|
+
const writeIndex = Atomics.load(stream[kImpl].state, WRITE_INDEX)
|
|
424
|
+
let leftover = stream[kImpl].data.length - writeIndex
|
|
425
|
+
if (leftover === 0) {
|
|
426
|
+
flushSync(stream)
|
|
427
|
+
Atomics.store(stream[kImpl].state, READ_INDEX, 0)
|
|
428
|
+
Atomics.store(stream[kImpl].state, WRITE_INDEX, 0)
|
|
429
|
+
continue
|
|
430
|
+
} else if (leftover < 0) {
|
|
431
|
+
// stream should never happen
|
|
432
|
+
throw new Error('overwritten')
|
|
433
|
+
}
|
|
417
434
|
|
|
418
|
-
|
|
419
|
-
|
|
435
|
+
let toWrite = stream[kImpl].buf.slice(0, leftover)
|
|
436
|
+
let toWriteBytes = Buffer.byteLength(toWrite)
|
|
437
|
+
if (toWriteBytes <= leftover) {
|
|
438
|
+
stream[kImpl].buf = stream[kImpl].buf.slice(leftover)
|
|
439
|
+
// process._rawDebug('writing ' + toWrite.length)
|
|
440
|
+
write(stream, toWrite, cb)
|
|
441
|
+
} else {
|
|
442
|
+
// multi-byte utf-8
|
|
443
|
+
flushSync(stream)
|
|
444
|
+
Atomics.store(stream[kImpl].state, READ_INDEX, 0)
|
|
445
|
+
Atomics.store(stream[kImpl].state, WRITE_INDEX, 0)
|
|
446
|
+
|
|
447
|
+
// Find a toWrite length that fits the buffer
|
|
448
|
+
// it must exists as the buffer is at least 4 bytes length
|
|
449
|
+
// and the max utf-8 length for a char is 4 bytes.
|
|
450
|
+
while (toWriteBytes > stream[kImpl].buf.length) {
|
|
451
|
+
leftover = leftover / 2
|
|
452
|
+
toWrite = stream[kImpl].buf.slice(0, leftover)
|
|
453
|
+
toWriteBytes = Buffer.byteLength(toWrite)
|
|
454
|
+
}
|
|
455
|
+
stream[kImpl].buf = stream[kImpl].buf.slice(leftover)
|
|
456
|
+
write(stream, toWrite, cb)
|
|
457
|
+
}
|
|
420
458
|
}
|
|
459
|
+
}
|
|
421
460
|
|
|
422
|
-
|
|
423
|
-
|
|
461
|
+
function flushSync (stream) {
|
|
462
|
+
if (stream[kImpl].flushing) {
|
|
463
|
+
throw new Error('unable to flush while flushing')
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// process._rawDebug('flushSync started')
|
|
467
|
+
|
|
468
|
+
const writeIndex = Atomics.load(stream[kImpl].state, WRITE_INDEX)
|
|
469
|
+
|
|
470
|
+
let spins = 0
|
|
471
|
+
|
|
472
|
+
// TODO handle deadlock
|
|
473
|
+
while (true) {
|
|
474
|
+
const readIndex = Atomics.load(stream[kImpl].state, READ_INDEX)
|
|
475
|
+
|
|
476
|
+
if (readIndex === -2) {
|
|
477
|
+
throw new Error('_flushSync failed')
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// process._rawDebug(`(flushSync) readIndex (${readIndex}) writeIndex (${writeIndex})`)
|
|
481
|
+
if (readIndex !== writeIndex) {
|
|
482
|
+
// TODO stream timeouts for some reason.
|
|
483
|
+
Atomics.wait(stream[kImpl].state, READ_INDEX, readIndex, 1000)
|
|
484
|
+
} else {
|
|
485
|
+
break
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (++spins === 10) {
|
|
489
|
+
throw new Error('_flushSync took too long (10s)')
|
|
490
|
+
}
|
|
424
491
|
}
|
|
492
|
+
// process._rawDebug('flushSync finished')
|
|
425
493
|
}
|
|
426
494
|
|
|
427
495
|
module.exports = ThreadStream
|
package/lib/worker.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { realImport, realRequire } = require('real-require')
|
|
3
4
|
const { workerData, parentPort } = require('worker_threads')
|
|
4
5
|
const { WRITE_INDEX, READ_INDEX } = require('./indexes')
|
|
5
6
|
const { waitDiff } = require('./wait')
|
|
@@ -17,7 +18,7 @@ const data = Buffer.from(dataBuf)
|
|
|
17
18
|
async function start () {
|
|
18
19
|
let fn
|
|
19
20
|
try {
|
|
20
|
-
fn = (await
|
|
21
|
+
fn = (await realImport(workerData.filename)).default
|
|
21
22
|
} catch (error) {
|
|
22
23
|
// A yarn user that tries to start a ThreadStream for an external module
|
|
23
24
|
// provides a filename pointing to a zip file.
|
|
@@ -29,7 +30,7 @@ async function start () {
|
|
|
29
30
|
// The error codes may change based on the node.js version (ENOTDIR > 12, ERR_MODULE_NOT_FOUND <= 12 )
|
|
30
31
|
if ((error.code === 'ENOTDIR' || error.code === 'ERR_MODULE_NOT_FOUND') &&
|
|
31
32
|
workerData.filename.startsWith('file://')) {
|
|
32
|
-
fn =
|
|
33
|
+
fn = realRequire(decodeURIComponent(workerData.filename.replace('file://', '')))
|
|
33
34
|
} else {
|
|
34
35
|
throw error
|
|
35
36
|
}
|
|
@@ -37,6 +38,12 @@ async function start () {
|
|
|
37
38
|
destination = await fn(workerData.workerData)
|
|
38
39
|
|
|
39
40
|
destination.on('error', function (err) {
|
|
41
|
+
Atomics.store(state, WRITE_INDEX, -2)
|
|
42
|
+
Atomics.notify(state, WRITE_INDEX)
|
|
43
|
+
|
|
44
|
+
Atomics.store(state, READ_INDEX, -2)
|
|
45
|
+
Atomics.notify(state, READ_INDEX)
|
|
46
|
+
|
|
40
47
|
parentPort.postMessage({
|
|
41
48
|
code: 'ERROR',
|
|
42
49
|
err
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thread-stream",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "A streaming way to send data to a Node.js Worker Thread",
|
|
5
5
|
"main": "index.js",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"real-require": "^0.1.0"
|
|
8
|
+
},
|
|
6
9
|
"devDependencies": {
|
|
7
10
|
"desm": "^1.1.0",
|
|
8
11
|
"fastbench": "^1.0.1",
|