thread-stream 0.11.1 → 0.13.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.
- package/.github/workflows/ci.yml +3 -3
- package/.github/workflows/package-manager-ci.yml +4 -4
- package/.taprc +2 -0
- package/index.js +295 -211
- package/lib/worker.js +9 -2
- package/package.json +7 -3
- 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 +3 -3
- package/test/create-and-exit.js +5 -7
- package/test/custom-worker.js +9 -0
- package/test/end.test.js +22 -38
- package/test/esm.test.mjs +8 -36
- package/test/helper.js +8 -1
- package/test/multibyte-chars.test.mjs +6 -10
- package/test/string-limit.test.js +36 -0
- package/test/thread-management.test.js +5 -5
package/index.js
CHANGED
|
@@ -9,6 +9,13 @@ const {
|
|
|
9
9
|
WRITE_INDEX,
|
|
10
10
|
READ_INDEX
|
|
11
11
|
} = require('./lib/indexes')
|
|
12
|
+
const buffer = require('buffer')
|
|
13
|
+
const assert = require('assert')
|
|
14
|
+
|
|
15
|
+
const kImpl = Symbol('kImpl')
|
|
16
|
+
|
|
17
|
+
// V8 limit for string size
|
|
18
|
+
const MAX_STRING = buffer.constants.MAX_STRING_LENGTH
|
|
12
19
|
|
|
13
20
|
class FakeWeakRef {
|
|
14
21
|
constructor (value) {
|
|
@@ -28,13 +35,17 @@ const FinalizationRegistry = global.FinalizationRegistry || class FakeFinalizati
|
|
|
28
35
|
const WeakRef = global.WeakRef || FakeWeakRef
|
|
29
36
|
|
|
30
37
|
const registry = new FinalizationRegistry((worker) => {
|
|
38
|
+
if (worker.exited) {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
31
41
|
worker.terminate()
|
|
32
42
|
})
|
|
33
43
|
|
|
34
44
|
function createWorker (stream, opts) {
|
|
35
45
|
const { filename, workerData } = opts
|
|
36
46
|
|
|
37
|
-
const
|
|
47
|
+
const bundlerOverrides = '__bundlerPathsOverrides' in globalThis ? globalThis.__bundlerPathsOverrides : {}
|
|
48
|
+
const toExecute = bundlerOverrides['thread-stream-worker'] || join(__dirname, 'lib', 'worker.js')
|
|
38
49
|
|
|
39
50
|
const worker = new Worker(toExecute, {
|
|
40
51
|
...opts.workerOpts,
|
|
@@ -42,8 +53,8 @@ function createWorker (stream, opts) {
|
|
|
42
53
|
filename: filename.indexOf('file://') === 0
|
|
43
54
|
? filename
|
|
44
55
|
: pathToFileURL(filename).href,
|
|
45
|
-
dataBuf: stream.
|
|
46
|
-
stateBuf: stream.
|
|
56
|
+
dataBuf: stream[kImpl].dataBuf,
|
|
57
|
+
stateBuf: stream[kImpl].stateBuf,
|
|
47
58
|
workerData
|
|
48
59
|
}
|
|
49
60
|
})
|
|
@@ -60,57 +71,62 @@ function createWorker (stream, opts) {
|
|
|
60
71
|
}
|
|
61
72
|
|
|
62
73
|
function drain (stream) {
|
|
63
|
-
stream.
|
|
64
|
-
stream.
|
|
74
|
+
assert(!stream[kImpl].sync)
|
|
75
|
+
if (stream[kImpl].needDrain) {
|
|
76
|
+
stream[kImpl].needDrain = false
|
|
77
|
+
stream.emit('drain')
|
|
78
|
+
}
|
|
65
79
|
}
|
|
66
80
|
|
|
67
81
|
function nextFlush (stream) {
|
|
68
|
-
const writeIndex = Atomics.load(stream.
|
|
69
|
-
let leftover = stream.
|
|
82
|
+
const writeIndex = Atomics.load(stream[kImpl].state, WRITE_INDEX)
|
|
83
|
+
let leftover = stream[kImpl].data.length - writeIndex
|
|
70
84
|
|
|
71
85
|
if (leftover > 0) {
|
|
72
|
-
if (stream.buf.length === 0) {
|
|
73
|
-
stream.flushing = false
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
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) {
|
|
77
92
|
process.nextTick(drain, stream)
|
|
78
93
|
}
|
|
94
|
+
|
|
79
95
|
return
|
|
80
96
|
}
|
|
81
97
|
|
|
82
|
-
let toWrite = stream.buf.slice(0, leftover)
|
|
98
|
+
let toWrite = stream[kImpl].buf.slice(0, leftover)
|
|
83
99
|
let toWriteBytes = Buffer.byteLength(toWrite)
|
|
84
100
|
if (toWriteBytes <= leftover) {
|
|
85
|
-
stream.buf = stream.buf.slice(leftover)
|
|
101
|
+
stream[kImpl].buf = stream[kImpl].buf.slice(leftover)
|
|
86
102
|
// process._rawDebug('writing ' + toWrite.length)
|
|
87
|
-
stream
|
|
103
|
+
write(stream, toWrite, nextFlush.bind(null, stream))
|
|
88
104
|
} else {
|
|
89
105
|
// multi-byte utf-8
|
|
90
106
|
stream.flush(() => {
|
|
91
|
-
Atomics.store(stream.
|
|
92
|
-
Atomics.store(stream.
|
|
107
|
+
Atomics.store(stream[kImpl].state, READ_INDEX, 0)
|
|
108
|
+
Atomics.store(stream[kImpl].state, WRITE_INDEX, 0)
|
|
93
109
|
|
|
94
110
|
// Find a toWrite length that fits the buffer
|
|
95
111
|
// it must exists as the buffer is at least 4 bytes length
|
|
96
112
|
// and the max utf-8 length for a char is 4 bytes.
|
|
97
|
-
while (toWriteBytes > stream.buf.length) {
|
|
113
|
+
while (toWriteBytes > stream[kImpl].buf.length) {
|
|
98
114
|
leftover = leftover / 2
|
|
99
|
-
toWrite = stream.buf.slice(0, leftover)
|
|
115
|
+
toWrite = stream[kImpl].buf.slice(0, leftover)
|
|
100
116
|
toWriteBytes = Buffer.byteLength(toWrite)
|
|
101
117
|
}
|
|
102
|
-
stream.buf = stream.buf.slice(leftover)
|
|
103
|
-
stream
|
|
118
|
+
stream[kImpl].buf = stream[kImpl].buf.slice(leftover)
|
|
119
|
+
write(stream, toWrite, nextFlush.bind(null, stream))
|
|
104
120
|
})
|
|
105
121
|
}
|
|
106
122
|
} else if (leftover === 0) {
|
|
107
|
-
if (writeIndex === 0 && stream.buf.length === 0) {
|
|
123
|
+
if (writeIndex === 0 && stream[kImpl].buf.length === 0) {
|
|
108
124
|
// we had a flushSync in the meanwhile
|
|
109
125
|
return
|
|
110
126
|
}
|
|
111
127
|
stream.flush(() => {
|
|
112
|
-
Atomics.store(stream.
|
|
113
|
-
Atomics.store(stream.
|
|
128
|
+
Atomics.store(stream[kImpl].state, READ_INDEX, 0)
|
|
129
|
+
Atomics.store(stream[kImpl].state, WRITE_INDEX, 0)
|
|
114
130
|
nextFlush(stream)
|
|
115
131
|
})
|
|
116
132
|
} else {
|
|
@@ -122,6 +138,7 @@ function nextFlush (stream) {
|
|
|
122
138
|
function onWorkerMessage (msg) {
|
|
123
139
|
const stream = this.stream.deref()
|
|
124
140
|
if (stream === undefined) {
|
|
141
|
+
this.exited = true
|
|
125
142
|
// Terminate the worker.
|
|
126
143
|
this.terminate()
|
|
127
144
|
return
|
|
@@ -132,28 +149,14 @@ function onWorkerMessage (msg) {
|
|
|
132
149
|
// Replace the FakeWeakRef with a
|
|
133
150
|
// proper one.
|
|
134
151
|
this.stream = new WeakRef(stream)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
stream.
|
|
152
|
+
|
|
153
|
+
stream.flush(() => {
|
|
154
|
+
stream[kImpl].ready = true
|
|
138
155
|
stream.emit('ready')
|
|
139
|
-
}
|
|
140
|
-
stream.once('drain', function () {
|
|
141
|
-
stream.flush(() => {
|
|
142
|
-
stream.ready = true
|
|
143
|
-
stream.emit('ready')
|
|
144
|
-
})
|
|
145
|
-
})
|
|
146
|
-
nextFlush(stream)
|
|
147
|
-
}
|
|
156
|
+
})
|
|
148
157
|
break
|
|
149
158
|
case 'ERROR':
|
|
150
|
-
stream.
|
|
151
|
-
// TODO only remove our own
|
|
152
|
-
stream.worker.removeAllListeners('exit')
|
|
153
|
-
stream.worker.terminate().then(null, () => {})
|
|
154
|
-
process.nextTick(() => {
|
|
155
|
-
stream.emit('error', msg.err)
|
|
156
|
-
})
|
|
159
|
+
destroy(stream, msg.err)
|
|
157
160
|
break
|
|
158
161
|
default:
|
|
159
162
|
throw new Error('this should not happen: ' + msg.code)
|
|
@@ -167,13 +170,9 @@ function onWorkerExit (code) {
|
|
|
167
170
|
return
|
|
168
171
|
}
|
|
169
172
|
registry.unregister(stream)
|
|
170
|
-
stream.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
stream.emit('error', new Error('The worker thread exited'))
|
|
174
|
-
}
|
|
175
|
-
stream.emit('close')
|
|
176
|
-
})
|
|
173
|
+
stream.worker.exited = true
|
|
174
|
+
stream.worker.off('exit', onWorkerExit)
|
|
175
|
+
destroy(stream, code !== 0 ? new Error('The worker thread exited') : null)
|
|
177
176
|
}
|
|
178
177
|
|
|
179
178
|
class ThreadStream extends EventEmitter {
|
|
@@ -184,119 +183,88 @@ class ThreadStream extends EventEmitter {
|
|
|
184
183
|
throw new Error('bufferSize must at least fit a 4-byte utf-8 char')
|
|
185
184
|
}
|
|
186
185
|
|
|
187
|
-
this
|
|
188
|
-
this.
|
|
189
|
-
this.
|
|
190
|
-
this.
|
|
191
|
-
this.
|
|
192
|
-
this.
|
|
193
|
-
this.
|
|
194
|
-
this.
|
|
195
|
-
this.needDrain = false
|
|
196
|
-
this.
|
|
197
|
-
|
|
198
|
-
this.
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
this.
|
|
206
|
-
Atomics.store(this._state, WRITE_INDEX, current + length)
|
|
207
|
-
Atomics.notify(this._state, WRITE_INDEX)
|
|
208
|
-
cb()
|
|
209
|
-
return true
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
_hasSpace () {
|
|
213
|
-
const current = Atomics.load(this._state, WRITE_INDEX)
|
|
214
|
-
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
|
|
215
205
|
}
|
|
216
206
|
|
|
217
207
|
write (data) {
|
|
218
|
-
if (this.
|
|
208
|
+
if (this[kImpl].destroyed) {
|
|
219
209
|
throw new Error('the worker has exited')
|
|
220
210
|
}
|
|
221
211
|
|
|
222
|
-
if (
|
|
223
|
-
|
|
224
|
-
return this._hasSpace()
|
|
212
|
+
if (this[kImpl].ending) {
|
|
213
|
+
throw new Error('the worker is ending')
|
|
225
214
|
}
|
|
226
215
|
|
|
227
|
-
if (this.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
this.flushing = true
|
|
236
|
-
setImmediate(nextFlush, this)
|
|
237
|
-
|
|
238
|
-
return this._hasSpace()
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
end () {
|
|
242
|
-
if (this.closed) {
|
|
243
|
-
throw new Error('the worker has exited')
|
|
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
|
+
}
|
|
244
224
|
}
|
|
245
225
|
|
|
246
|
-
|
|
247
|
-
this.once('ready', this.end.bind(this))
|
|
248
|
-
return
|
|
249
|
-
}
|
|
226
|
+
this[kImpl].buf += data
|
|
250
227
|
|
|
251
|
-
if (this.
|
|
252
|
-
|
|
253
|
-
|
|
228
|
+
if (this[kImpl].sync) {
|
|
229
|
+
try {
|
|
230
|
+
writeSync(this)
|
|
231
|
+
return true
|
|
232
|
+
} catch (err) {
|
|
233
|
+
destroy(this, err)
|
|
234
|
+
return false
|
|
235
|
+
}
|
|
254
236
|
}
|
|
255
237
|
|
|
256
|
-
if (this.
|
|
257
|
-
|
|
238
|
+
if (!this[kImpl].flushing) {
|
|
239
|
+
this[kImpl].flushing = true
|
|
240
|
+
setImmediate(nextFlush, this)
|
|
258
241
|
}
|
|
259
|
-
this.ending = true
|
|
260
|
-
|
|
261
|
-
this.flushSync()
|
|
262
|
-
|
|
263
|
-
let read = Atomics.load(this._state, READ_INDEX)
|
|
264
242
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
Atomics.notify(this._state, WRITE_INDEX)
|
|
269
|
-
|
|
270
|
-
// Wait for the process to complete
|
|
271
|
-
let spins = 0
|
|
272
|
-
while (read !== -1) {
|
|
273
|
-
// process._rawDebug(`read = ${read}`)
|
|
274
|
-
Atomics.wait(this._state, READ_INDEX, read, 1000)
|
|
275
|
-
read = Atomics.load(this._state, READ_INDEX)
|
|
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
|
|
245
|
+
}
|
|
276
246
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
247
|
+
end () {
|
|
248
|
+
if (this[kImpl].destroyed) {
|
|
249
|
+
throw new Error('the worker has exited')
|
|
280
250
|
}
|
|
281
251
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
})
|
|
285
|
-
// process._rawDebug('end finished...')
|
|
252
|
+
this[kImpl].ending = true
|
|
253
|
+
end(this)
|
|
286
254
|
}
|
|
287
255
|
|
|
288
256
|
flush (cb) {
|
|
289
|
-
if (this.
|
|
257
|
+
if (this[kImpl].destroyed) {
|
|
290
258
|
throw new Error('the worker has exited')
|
|
291
259
|
}
|
|
292
260
|
|
|
293
261
|
// TODO write all .buf
|
|
294
|
-
const writeIndex = Atomics.load(this.
|
|
295
|
-
// process._rawDebug(`(flush) readIndex (${Atomics.load(this.
|
|
296
|
-
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) => {
|
|
297
265
|
if (err) {
|
|
298
|
-
this
|
|
299
|
-
cb
|
|
266
|
+
destroy(this, err)
|
|
267
|
+
process.nextTick(cb, err)
|
|
300
268
|
return
|
|
301
269
|
}
|
|
302
270
|
if (res === 'not-equal') {
|
|
@@ -304,108 +272,224 @@ class ThreadStream extends EventEmitter {
|
|
|
304
272
|
this.flush(cb)
|
|
305
273
|
return
|
|
306
274
|
}
|
|
307
|
-
cb
|
|
275
|
+
process.nextTick(cb)
|
|
308
276
|
})
|
|
309
277
|
}
|
|
310
278
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
// process._rawDebug('emitting drain')
|
|
315
|
-
this.needDrain = true
|
|
316
|
-
process.nextTick(drain, this)
|
|
317
|
-
}
|
|
279
|
+
flushSync () {
|
|
280
|
+
if (this[kImpl].destroyed) {
|
|
281
|
+
throw new Error('the worker has exited')
|
|
318
282
|
}
|
|
319
|
-
this.flushing = false
|
|
320
|
-
|
|
321
|
-
while (this.buf.length !== 0) {
|
|
322
|
-
const writeIndex = Atomics.load(this._state, WRITE_INDEX)
|
|
323
|
-
let leftover = this._data.length - writeIndex
|
|
324
|
-
if (leftover === 0) {
|
|
325
|
-
this._flushSync()
|
|
326
|
-
Atomics.store(this._state, READ_INDEX, 0)
|
|
327
|
-
Atomics.store(this._state, WRITE_INDEX, 0)
|
|
328
|
-
continue
|
|
329
|
-
} else if (leftover < 0) {
|
|
330
|
-
// This should never happen
|
|
331
|
-
throw new Error('overwritten')
|
|
332
|
-
}
|
|
333
283
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
this.buf = this.buf.slice(leftover)
|
|
338
|
-
// process._rawDebug('writing ' + toWrite.length)
|
|
339
|
-
this._write(toWrite, cb)
|
|
340
|
-
} else {
|
|
341
|
-
// multi-byte utf-8
|
|
342
|
-
this._flushSync()
|
|
343
|
-
Atomics.store(this._state, READ_INDEX, 0)
|
|
344
|
-
Atomics.store(this._state, WRITE_INDEX, 0)
|
|
284
|
+
writeSync(this)
|
|
285
|
+
flushSync(this)
|
|
286
|
+
}
|
|
345
287
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
// and the max utf-8 length for a char is 4 bytes.
|
|
349
|
-
while (toWriteBytes > this.buf.length) {
|
|
350
|
-
leftover = leftover / 2
|
|
351
|
-
toWrite = this.buf.slice(0, leftover)
|
|
352
|
-
toWriteBytes = Buffer.byteLength(toWrite)
|
|
353
|
-
}
|
|
354
|
-
this.buf = this.buf.slice(leftover)
|
|
355
|
-
this._write(toWrite, cb)
|
|
356
|
-
}
|
|
357
|
-
}
|
|
288
|
+
unref () {
|
|
289
|
+
this.worker.unref()
|
|
358
290
|
}
|
|
359
291
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
292
|
+
ref () {
|
|
293
|
+
this.worker.ref()
|
|
294
|
+
}
|
|
364
295
|
|
|
365
|
-
|
|
366
|
-
this.
|
|
296
|
+
get ready () {
|
|
297
|
+
return this[kImpl].ready
|
|
367
298
|
}
|
|
368
299
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
300
|
+
get destroyed () {
|
|
301
|
+
return this[kImpl].destroyed
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
get closed () {
|
|
305
|
+
return this[kImpl].closed
|
|
306
|
+
}
|
|
307
|
+
|
|
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
|
+
}
|
|
373
319
|
|
|
374
|
-
|
|
320
|
+
get writableNeedDrain () {
|
|
321
|
+
return this[kImpl].needDrain
|
|
322
|
+
}
|
|
375
323
|
|
|
376
|
-
|
|
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
|
+
}
|
|
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)
|
|
377
385
|
|
|
386
|
+
// Wait for the process to complete
|
|
378
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)
|
|
379
392
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const readIndex = Atomics.load(this._state, READ_INDEX)
|
|
383
|
-
// process._rawDebug(`(flushSync) readIndex (${readIndex}) writeIndex (${writeIndex})`)
|
|
384
|
-
if (readIndex !== writeIndex) {
|
|
385
|
-
// TODO this timeouts for some reason.
|
|
386
|
-
Atomics.wait(this._state, READ_INDEX, readIndex, 1000)
|
|
387
|
-
} else {
|
|
388
|
-
break
|
|
393
|
+
if (readIndex === -2) {
|
|
394
|
+
throw new Error('end() failed')
|
|
389
395
|
}
|
|
390
396
|
|
|
391
397
|
if (++spins === 10) {
|
|
392
|
-
throw new Error('
|
|
398
|
+
throw new Error('end() took too long (10s)')
|
|
393
399
|
}
|
|
394
400
|
}
|
|
395
|
-
|
|
401
|
+
|
|
402
|
+
process.nextTick(() => {
|
|
403
|
+
stream[kImpl].finished = true
|
|
404
|
+
stream.emit('finish')
|
|
405
|
+
})
|
|
406
|
+
} catch (err) {
|
|
407
|
+
destroy(stream, err)
|
|
396
408
|
}
|
|
409
|
+
// process._rawDebug('end finished...')
|
|
410
|
+
}
|
|
397
411
|
|
|
398
|
-
|
|
399
|
-
|
|
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
|
+
}
|
|
400
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
|
+
}
|
|
401
434
|
|
|
402
|
-
|
|
403
|
-
|
|
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
|
+
}
|
|
404
458
|
}
|
|
459
|
+
}
|
|
405
460
|
|
|
406
|
-
|
|
407
|
-
|
|
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
|
+
}
|
|
408
491
|
}
|
|
492
|
+
// process._rawDebug('flushSync finished')
|
|
409
493
|
}
|
|
410
494
|
|
|
411
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(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
|