@xylabs/threads 3.0.4
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/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +227 -0
- package/dist/common.d.ts +4 -0
- package/dist/common.js +18 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +27 -0
- package/dist/master/get-bundle-url.browser.d.ts +3 -0
- package/dist/master/get-bundle-url.browser.js +29 -0
- package/dist/master/implementation.browser.d.ts +4 -0
- package/dist/master/implementation.browser.js +69 -0
- package/dist/master/implementation.d.ts +6 -0
- package/dist/master/implementation.js +41 -0
- package/dist/master/implementation.node.d.ts +5 -0
- package/dist/master/implementation.node.js +255 -0
- package/dist/master/index.d.ts +13 -0
- package/dist/master/index.js +16 -0
- package/dist/master/invocation-proxy.d.ts +3 -0
- package/dist/master/invocation-proxy.js +130 -0
- package/dist/master/pool-types.d.ts +65 -0
- package/dist/master/pool-types.js +15 -0
- package/dist/master/pool.d.ts +90 -0
- package/dist/master/pool.js +281 -0
- package/dist/master/register.d.ts +1 -0
- package/dist/master/register.js +12 -0
- package/dist/master/spawn.d.ts +20 -0
- package/dist/master/spawn.js +130 -0
- package/dist/master/thread.d.ts +12 -0
- package/dist/master/thread.js +22 -0
- package/dist/observable-promise.d.ts +38 -0
- package/dist/observable-promise.js +156 -0
- package/dist/observable.d.ts +19 -0
- package/dist/observable.js +43 -0
- package/dist/ponyfills.d.ts +8 -0
- package/dist/ponyfills.js +22 -0
- package/dist/promise.d.ts +5 -0
- package/dist/promise.js +29 -0
- package/dist/serializers.d.ts +16 -0
- package/dist/serializers.js +41 -0
- package/dist/symbols.d.ts +5 -0
- package/dist/symbols.js +8 -0
- package/dist/transferable.d.ts +42 -0
- package/dist/transferable.js +28 -0
- package/dist/types/master.d.ts +99 -0
- package/dist/types/master.js +14 -0
- package/dist/types/messages.d.ts +62 -0
- package/dist/types/messages.js +20 -0
- package/dist/types/worker.d.ts +11 -0
- package/dist/types/worker.js +2 -0
- package/dist/worker/bundle-entry.d.ts +1 -0
- package/dist/worker/bundle-entry.js +27 -0
- package/dist/worker/implementation.browser.d.ts +7 -0
- package/dist/worker/implementation.browser.js +28 -0
- package/dist/worker/implementation.d.ts +3 -0
- package/dist/worker/implementation.js +24 -0
- package/dist/worker/implementation.tiny-worker.d.ts +7 -0
- package/dist/worker/implementation.tiny-worker.js +38 -0
- package/dist/worker/implementation.worker_threads.d.ts +8 -0
- package/dist/worker/implementation.worker_threads.js +42 -0
- package/dist/worker/index.d.ts +13 -0
- package/dist/worker/index.js +195 -0
- package/dist/worker_threads.d.ts +8 -0
- package/dist/worker_threads.js +17 -0
- package/dist-esm/common.js +12 -0
- package/dist-esm/index.js +6 -0
- package/dist-esm/master/get-bundle-url.browser.js +25 -0
- package/dist-esm/master/implementation.browser.js +64 -0
- package/dist-esm/master/implementation.js +15 -0
- package/dist-esm/master/implementation.node.js +224 -0
- package/dist-esm/master/index.js +9 -0
- package/dist-esm/master/invocation-proxy.js +122 -0
- package/dist-esm/master/pool-types.js +12 -0
- package/dist-esm/master/pool.js +273 -0
- package/dist-esm/master/register.js +10 -0
- package/dist-esm/master/spawn.js +123 -0
- package/dist-esm/master/thread.js +19 -0
- package/dist-esm/observable-promise.js +152 -0
- package/dist-esm/observable.js +38 -0
- package/dist-esm/ponyfills.js +18 -0
- package/dist-esm/promise.js +25 -0
- package/dist-esm/serializers.js +37 -0
- package/dist-esm/symbols.js +5 -0
- package/dist-esm/transferable.js +23 -0
- package/dist-esm/types/master.js +11 -0
- package/dist-esm/types/messages.js +17 -0
- package/dist-esm/types/worker.js +1 -0
- package/dist-esm/worker/bundle-entry.js +11 -0
- package/dist-esm/worker/implementation.browser.js +26 -0
- package/dist-esm/worker/implementation.js +19 -0
- package/dist-esm/worker/implementation.tiny-worker.js +36 -0
- package/dist-esm/worker/implementation.worker_threads.js +37 -0
- package/dist-esm/worker/index.js +186 -0
- package/dist-esm/worker_threads.js +14 -0
- package/index.mjs +11 -0
- package/observable.d.ts +2 -0
- package/observable.js +3 -0
- package/observable.mjs +5 -0
- package/package.json +141 -0
- package/register.d.ts +3 -0
- package/register.js +3 -0
- package/register.mjs +2 -0
- package/rollup.config.js +16 -0
- package/src/common.ts +16 -0
- package/src/index.ts +8 -0
- package/src/master/get-bundle-url.browser.ts +31 -0
- package/src/master/implementation.browser.ts +80 -0
- package/src/master/implementation.node.ts +284 -0
- package/src/master/implementation.ts +21 -0
- package/src/master/index.ts +20 -0
- package/src/master/invocation-proxy.ts +146 -0
- package/src/master/pool-types.ts +83 -0
- package/src/master/pool.ts +391 -0
- package/src/master/register.ts +10 -0
- package/src/master/spawn.ts +172 -0
- package/src/master/thread.ts +26 -0
- package/src/observable-promise.ts +181 -0
- package/src/observable.ts +43 -0
- package/src/ponyfills.ts +31 -0
- package/src/promise.ts +26 -0
- package/src/serializers.ts +67 -0
- package/src/symbols.ts +5 -0
- package/src/transferable.ts +68 -0
- package/src/types/master.ts +130 -0
- package/src/types/messages.ts +81 -0
- package/src/types/worker.ts +14 -0
- package/src/worker/bundle-entry.ts +10 -0
- package/src/worker/implementation.browser.ts +40 -0
- package/src/worker/implementation.tiny-worker.ts +52 -0
- package/src/worker/implementation.ts +23 -0
- package/src/worker/implementation.worker_threads.ts +50 -0
- package/src/worker/index.ts +228 -0
- package/src/worker_threads.ts +28 -0
- package/test/lib/serialization.ts +38 -0
- package/test/observable-promise.test.ts +189 -0
- package/test/observable.test.ts +86 -0
- package/test/pool.test.ts +173 -0
- package/test/serialization.test.ts +21 -0
- package/test/spawn.chromium.mocha.ts +49 -0
- package/test/spawn.test.ts +71 -0
- package/test/streaming.test.ts +27 -0
- package/test/transferables.test.ts +69 -0
- package/test/workers/arraybuffer-xor.ts +11 -0
- package/test/workers/count-to-five.ts +13 -0
- package/test/workers/counter.ts +20 -0
- package/test/workers/faulty-function.ts +6 -0
- package/test/workers/hello-world.ts +6 -0
- package/test/workers/increment.ts +9 -0
- package/test/workers/minmax.ts +25 -0
- package/test/workers/serialization.ts +12 -0
- package/test/workers/top-level-throw.ts +1 -0
- package/test-tooling/rollup/app.js +20 -0
- package/test-tooling/rollup/rollup.config.ts +15 -0
- package/test-tooling/rollup/rollup.test.ts +44 -0
- package/test-tooling/rollup/worker.js +7 -0
- package/test-tooling/tsconfig/minimal-tsconfig.test.ts +7 -0
- package/test-tooling/tsconfig/minimal.ts +10 -0
- package/test-tooling/webpack/addition-worker.ts +10 -0
- package/test-tooling/webpack/app-with-inlined-worker.ts +29 -0
- package/test-tooling/webpack/app.ts +58 -0
- package/test-tooling/webpack/pool-worker.ts +6 -0
- package/test-tooling/webpack/raw-loader.d.ts +4 -0
- package/test-tooling/webpack/webpack.chromium.mocha.ts +21 -0
- package/test-tooling/webpack/webpack.node.config.js +38 -0
- package/test-tooling/webpack/webpack.test.ts +90 -0
- package/test-tooling/webpack/webpack.web.config.js +35 -0
- package/types/is-observable.d.ts +7 -0
- package/types/tiny-worker.d.ts +4 -0
- package/types/webworker.d.ts +9 -0
- package/worker.d.ts +2 -0
- package/worker.js +3 -0
- package/worker.mjs +7 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
/* eslint-disable import/no-internal-modules */
|
|
3
|
+
import { JsonSerializable, SerializerImplementation } from '../../src/index'
|
|
4
|
+
|
|
5
|
+
export class Foo<T> {
|
|
6
|
+
private readonly value: T
|
|
7
|
+
|
|
8
|
+
constructor(value: T) {
|
|
9
|
+
this.value = value
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
getValue() {
|
|
13
|
+
return this.value
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface SerializedFoo<T extends JsonSerializable> {
|
|
18
|
+
__type: '$$foo'
|
|
19
|
+
val: T
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const isSerializedFoo = (thing: any): thing is SerializedFoo<JsonSerializable> =>
|
|
23
|
+
thing && typeof thing === 'object' && '__type' in thing && thing.__type === '$$foo'
|
|
24
|
+
|
|
25
|
+
export const fooSerializer: SerializerImplementation = {
|
|
26
|
+
deserialize(serialized, fallback) {
|
|
27
|
+
return isSerializedFoo(serialized) ? new Foo(serialized.val) : fallback(serialized)
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
serialize(data, fallback) {
|
|
31
|
+
return data instanceof Foo ?
|
|
32
|
+
{
|
|
33
|
+
__type: '$$foo',
|
|
34
|
+
val: data.getValue(),
|
|
35
|
+
}
|
|
36
|
+
: fallback(data)
|
|
37
|
+
},
|
|
38
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
/* eslint-disable import/no-internal-modules */
|
|
3
|
+
import test from 'ava'
|
|
4
|
+
import { Observable } from 'observable-fns'
|
|
5
|
+
|
|
6
|
+
import { ObservablePromise } from '../src/observable-promise'
|
|
7
|
+
|
|
8
|
+
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
9
|
+
|
|
10
|
+
test('can create an observable promise', async (t) => {
|
|
11
|
+
t.plan(1)
|
|
12
|
+
|
|
13
|
+
await new ObservablePromise((observer) => {
|
|
14
|
+
t.true(true)
|
|
15
|
+
observer.complete()
|
|
16
|
+
})
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('init function is only called once', async (t) => {
|
|
20
|
+
let initCallCount = 0
|
|
21
|
+
|
|
22
|
+
const async = new ObservablePromise((observer) => {
|
|
23
|
+
initCallCount++
|
|
24
|
+
setTimeout(() => observer.complete(), 10)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
await Promise.all([async.then(() => t.true(true)), async.then(() => t.true(true)), async.then(() => t.true(true))])
|
|
28
|
+
|
|
29
|
+
t.is(initCallCount, 1)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('can proxy a promise fulfillment', async (t) => {
|
|
33
|
+
t.plan(2)
|
|
34
|
+
|
|
35
|
+
const async = new ObservablePromise((observer) => {
|
|
36
|
+
setTimeout(() => {
|
|
37
|
+
observer.next(123)
|
|
38
|
+
|
|
39
|
+
// Ignore all values after the first one
|
|
40
|
+
observer.next(456)
|
|
41
|
+
observer.complete()
|
|
42
|
+
}, 1)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const promise1 = async.then((value) => t.is(value, 123), t.fail)
|
|
46
|
+
await delay(10)
|
|
47
|
+
const promise2 = async.then((value) => t.is(value, 123), t.fail)
|
|
48
|
+
|
|
49
|
+
await Promise.all([promise1, promise2])
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('can proxy a promise rejection', async (t) => {
|
|
53
|
+
let handlerCallCount = 0
|
|
54
|
+
|
|
55
|
+
const async = new ObservablePromise((observer) => {
|
|
56
|
+
setTimeout(() => observer.error(Error('I am supposed to be rejected.')), 1)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const promise1 = async.then(
|
|
60
|
+
() => t.fail('Promise should not become fulfilled'),
|
|
61
|
+
() => handlerCallCount++,
|
|
62
|
+
)
|
|
63
|
+
await delay(10)
|
|
64
|
+
const promise2 = async.then(
|
|
65
|
+
() => t.fail('Promise should not become fulfilled'),
|
|
66
|
+
() => handlerCallCount++,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
await Promise.all([promise1.catch(() => true), promise2.catch(() => true)])
|
|
70
|
+
t.is(handlerCallCount, 2)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('can proxy a promise rejection caused by a sync throw', async (t) => {
|
|
74
|
+
let handlerCallCount = 0
|
|
75
|
+
|
|
76
|
+
const async = new ObservablePromise(() => {
|
|
77
|
+
throw new Error('I am supposed to be rejected.')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const promise1 = async.then(
|
|
81
|
+
() => t.fail('Promise should not become fulfilled'),
|
|
82
|
+
() => handlerCallCount++,
|
|
83
|
+
)
|
|
84
|
+
await delay(10)
|
|
85
|
+
const promise2 = async.then(
|
|
86
|
+
() => t.fail('Promise should not become fulfilled'),
|
|
87
|
+
() => handlerCallCount++,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
await Promise.all([promise1, promise2])
|
|
91
|
+
t.is(handlerCallCount, 2)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('can subscribe to values and completion', async (t) => {
|
|
95
|
+
const capturedValues: any[] = []
|
|
96
|
+
let capturedCompletions = 0
|
|
97
|
+
|
|
98
|
+
const async = new ObservablePromise((observer) => {
|
|
99
|
+
setTimeout(() => observer.next(1), 10)
|
|
100
|
+
setTimeout(() => observer.next(2), 20)
|
|
101
|
+
setTimeout(() => observer.complete(), 30)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
for (let index = 0; index < 2; index++) {
|
|
105
|
+
async.subscribe(
|
|
106
|
+
(value) => capturedValues.push(value),
|
|
107
|
+
() => {},
|
|
108
|
+
() => capturedCompletions++,
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await async.finally()
|
|
113
|
+
await delay(1)
|
|
114
|
+
|
|
115
|
+
t.deepEqual(capturedValues, [1, 1, 2, 2])
|
|
116
|
+
t.is(capturedCompletions, 2)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('can subscribe to errors', async (t) => {
|
|
120
|
+
const capturedErrorMessages: string[] = []
|
|
121
|
+
const capturedValues: any[] = []
|
|
122
|
+
let capturedCompletions = 0
|
|
123
|
+
|
|
124
|
+
const async = new ObservablePromise((observer) => {
|
|
125
|
+
setTimeout(() => observer.next(1), 10)
|
|
126
|
+
setTimeout(() => observer.error(Error('Fails as expected.')), 20)
|
|
127
|
+
setTimeout(() => observer.next(2), 30)
|
|
128
|
+
setTimeout(() => observer.complete(), 40)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
for (let index = 0; index < 2; index++) {
|
|
132
|
+
async.subscribe(
|
|
133
|
+
(value) => capturedValues.push(value),
|
|
134
|
+
(error) => capturedErrorMessages.push(error.message),
|
|
135
|
+
() => capturedCompletions++,
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
await async.finally()
|
|
140
|
+
await delay(35)
|
|
141
|
+
|
|
142
|
+
t.deepEqual(capturedValues, [1, 1])
|
|
143
|
+
t.deepEqual(capturedErrorMessages, ['Fails as expected.', 'Fails as expected.'])
|
|
144
|
+
t.is(capturedCompletions, 0)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test('from(Observable) works', async (t) => {
|
|
148
|
+
const capturedErrorMessages: string[] = []
|
|
149
|
+
const capturedValues: any[] = []
|
|
150
|
+
let capturedCompletions = 0
|
|
151
|
+
|
|
152
|
+
const async = ObservablePromise.from(
|
|
153
|
+
new Observable((observer) => {
|
|
154
|
+
setTimeout(() => observer.next(1), 10)
|
|
155
|
+
setTimeout(() => observer.error(Error('Fails as expected.')), 20)
|
|
156
|
+
setTimeout(() => observer.next(2), 30)
|
|
157
|
+
setTimeout(() => observer.complete(), 40)
|
|
158
|
+
}),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
for (let index = 0; index < 2; index++) {
|
|
162
|
+
async.subscribe(
|
|
163
|
+
(value) => capturedValues.push(value),
|
|
164
|
+
(error) => capturedErrorMessages.push(error.message),
|
|
165
|
+
() => capturedCompletions++,
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
await async.finally()
|
|
170
|
+
await delay(35)
|
|
171
|
+
|
|
172
|
+
t.deepEqual(capturedValues, [1, 1])
|
|
173
|
+
t.deepEqual(capturedErrorMessages, ['Fails as expected.', 'Fails as expected.'])
|
|
174
|
+
t.is(capturedCompletions, 0)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test('from(Promise) works', async (t) => {
|
|
178
|
+
const resolved = ObservablePromise.from(
|
|
179
|
+
new Promise((resolve) => {
|
|
180
|
+
setTimeout(() => resolve('Works'), 10)
|
|
181
|
+
}),
|
|
182
|
+
)
|
|
183
|
+
t.is(await resolved, 'Works')
|
|
184
|
+
|
|
185
|
+
const rejected = ObservablePromise.from(Promise.reject(Error('Fails')))
|
|
186
|
+
const error = await t.throwsAsync(rejected)
|
|
187
|
+
|
|
188
|
+
t.is(error.message, 'Fails')
|
|
189
|
+
})
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/* eslint-disable require-await */
|
|
2
|
+
/* eslint-disable import/no-internal-modules */
|
|
3
|
+
import test from 'ava'
|
|
4
|
+
|
|
5
|
+
import { Observable, Subject } from '../src/observable'
|
|
6
|
+
|
|
7
|
+
test('Observable subject emits values and completion event', async (t) => {
|
|
8
|
+
let completed1 = false
|
|
9
|
+
const values1: number[] = []
|
|
10
|
+
let completed2 = false
|
|
11
|
+
const values2: number[] = []
|
|
12
|
+
let completed3 = false
|
|
13
|
+
const values3: number[] = []
|
|
14
|
+
|
|
15
|
+
const subject = new Subject<number>()
|
|
16
|
+
const observable = Observable.from(subject)
|
|
17
|
+
|
|
18
|
+
const subscription1 = subject.subscribe(
|
|
19
|
+
(value) => values1.push(value),
|
|
20
|
+
undefined,
|
|
21
|
+
() => (completed1 = true),
|
|
22
|
+
)
|
|
23
|
+
subject.subscribe(
|
|
24
|
+
(value) => values2.push(value),
|
|
25
|
+
undefined,
|
|
26
|
+
() => (completed2 = true),
|
|
27
|
+
)
|
|
28
|
+
observable.subscribe(
|
|
29
|
+
(value) => values3.push(value),
|
|
30
|
+
undefined,
|
|
31
|
+
() => (completed3 = true),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
subject.next(1)
|
|
35
|
+
subscription1.unsubscribe()
|
|
36
|
+
|
|
37
|
+
subject.next(2)
|
|
38
|
+
subject.complete()
|
|
39
|
+
|
|
40
|
+
t.deepEqual(values1, [1])
|
|
41
|
+
t.deepEqual(values2, [1, 2])
|
|
42
|
+
t.deepEqual(values3, [1, 2])
|
|
43
|
+
t.is(completed1, false)
|
|
44
|
+
t.is(completed2, true)
|
|
45
|
+
t.is(completed3, true)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('Observable subject propagates errors', async (t) => {
|
|
49
|
+
let completed1 = false
|
|
50
|
+
let error1: Error | undefined
|
|
51
|
+
let completed2 = false
|
|
52
|
+
let error2: Error | undefined
|
|
53
|
+
let completed3 = false
|
|
54
|
+
let error3: Error | undefined
|
|
55
|
+
|
|
56
|
+
const subject = new Subject<number>()
|
|
57
|
+
const observable = Observable.from(subject)
|
|
58
|
+
|
|
59
|
+
const subscription1 = subject.subscribe(
|
|
60
|
+
() => {},
|
|
61
|
+
(error) => (error1 = error),
|
|
62
|
+
() => (completed1 = true),
|
|
63
|
+
)
|
|
64
|
+
subject.subscribe(
|
|
65
|
+
() => {},
|
|
66
|
+
(error) => (error2 = error),
|
|
67
|
+
() => (completed2 = true),
|
|
68
|
+
)
|
|
69
|
+
observable.subscribe(
|
|
70
|
+
() => {},
|
|
71
|
+
(error) => (error3 = error),
|
|
72
|
+
() => (completed3 = true),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
const testingError = Error('Test, test!')
|
|
76
|
+
|
|
77
|
+
subscription1.unsubscribe()
|
|
78
|
+
subject.error(testingError)
|
|
79
|
+
|
|
80
|
+
t.is(completed1, false)
|
|
81
|
+
t.is(error1, undefined)
|
|
82
|
+
t.is(completed2, false)
|
|
83
|
+
t.is(error2, testingError)
|
|
84
|
+
t.is(completed3, false)
|
|
85
|
+
t.is(error3, testingError)
|
|
86
|
+
})
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/* eslint-disable require-await */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
+
/* eslint-disable @typescript-eslint/no-floating-promises */
|
|
4
|
+
/* eslint-disable import/no-internal-modules */
|
|
5
|
+
import test from 'ava'
|
|
6
|
+
|
|
7
|
+
import { Pool, spawn, Worker } from '../src/index'
|
|
8
|
+
import { PoolEventType, QueuedTask } from '../src/master/pool'
|
|
9
|
+
|
|
10
|
+
test.serial('thread pool basics work and events are emitted', async (t) => {
|
|
11
|
+
const events: Pool.Event[] = []
|
|
12
|
+
let spawnCalled = 0
|
|
13
|
+
let taskFnCalled = 0
|
|
14
|
+
|
|
15
|
+
const spawnHelloWorld = () => {
|
|
16
|
+
spawnCalled++
|
|
17
|
+
return spawn<() => string>(new Worker('./workers/hello-world'))
|
|
18
|
+
}
|
|
19
|
+
const pool = Pool(spawnHelloWorld, 3)
|
|
20
|
+
pool.events().subscribe((event) => events.push(event))
|
|
21
|
+
|
|
22
|
+
// Just to make sure all worker threads are initialized before starting to queue
|
|
23
|
+
// This is only necessary for testing to make sure that this is the first event recorded
|
|
24
|
+
await new Promise((resolve, reject) => {
|
|
25
|
+
pool
|
|
26
|
+
.events()
|
|
27
|
+
.filter((event) => event.type === PoolEventType.initialized)
|
|
28
|
+
.subscribe(resolve, reject)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
await pool.queue(async (helloWorld) => {
|
|
32
|
+
taskFnCalled++
|
|
33
|
+
const result = await helloWorld()
|
|
34
|
+
t.is(result, 'Hello World')
|
|
35
|
+
return result
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
await pool.terminate()
|
|
39
|
+
t.is(spawnCalled, 3)
|
|
40
|
+
t.is(taskFnCalled, 1)
|
|
41
|
+
|
|
42
|
+
t.deepEqual(events, [
|
|
43
|
+
{
|
|
44
|
+
size: 3,
|
|
45
|
+
type: Pool.EventType.initialized,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
taskID: 1,
|
|
49
|
+
type: Pool.EventType.taskQueued,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
taskID: 1,
|
|
53
|
+
type: Pool.EventType.taskStart,
|
|
54
|
+
workerID: 1,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
returnValue: 'Hello World',
|
|
58
|
+
taskID: 1,
|
|
59
|
+
type: Pool.EventType.taskCompleted,
|
|
60
|
+
workerID: 1,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
type: Pool.EventType.taskQueueDrained,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
remainingQueue: [],
|
|
67
|
+
type: Pool.EventType.terminated,
|
|
68
|
+
},
|
|
69
|
+
])
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test.serial('pool.completed() works', async (t) => {
|
|
73
|
+
const returned: any[] = []
|
|
74
|
+
|
|
75
|
+
const spawnHelloWorld = () => spawn(new Worker('./workers/hello-world'))
|
|
76
|
+
const pool = Pool(spawnHelloWorld, 2)
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < 3; i++) {
|
|
79
|
+
pool.queue(async (helloWorld) => {
|
|
80
|
+
returned.push(await helloWorld())
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await pool.completed()
|
|
85
|
+
|
|
86
|
+
t.deepEqual(returned, ['Hello World', 'Hello World', 'Hello World'])
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test.serial('pool.completed() proxies errors', async (t) => {
|
|
90
|
+
const spawnHelloWorld = () => spawn(new Worker('./workers/hello-world'))
|
|
91
|
+
const pool = Pool(spawnHelloWorld, 2)
|
|
92
|
+
|
|
93
|
+
pool.queue(async () => {
|
|
94
|
+
throw new Error('Ooopsie')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const error = await t.throwsAsync(() => pool.completed())
|
|
98
|
+
t.is(error.message, 'Ooopsie')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test.serial('pool.completed(true) works', async (t) => {
|
|
102
|
+
const spawnHelloWorld = () => spawn(new Worker('./workers/hello-world'))
|
|
103
|
+
const pool = Pool(spawnHelloWorld, 2)
|
|
104
|
+
|
|
105
|
+
await pool.completed(true)
|
|
106
|
+
t.pass()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test.serial('pool.settled() does not reject on task failure', async (t) => {
|
|
110
|
+
const returned: any[] = []
|
|
111
|
+
|
|
112
|
+
const spawnHelloWorld = () => spawn(new Worker('./workers/hello-world'))
|
|
113
|
+
const pool = Pool(spawnHelloWorld, 2)
|
|
114
|
+
|
|
115
|
+
pool.queue(async (helloWorld) => {
|
|
116
|
+
returned.push(await helloWorld())
|
|
117
|
+
})
|
|
118
|
+
pool.queue(async () => {
|
|
119
|
+
throw new Error('Test error one')
|
|
120
|
+
})
|
|
121
|
+
pool.queue(async () => {
|
|
122
|
+
throw new Error('Test error two')
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const errors = await pool.settled()
|
|
126
|
+
t.is(errors.length, 2)
|
|
127
|
+
t.deepEqual(errors.map((error) => error.message).sort(), ['Test error one', 'Test error two'])
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test.serial('pool.settled(true) works', async (t) => {
|
|
131
|
+
const spawnHelloWorld = () => spawn(new Worker('./workers/hello-world'))
|
|
132
|
+
const pool = Pool(spawnHelloWorld, 2)
|
|
133
|
+
|
|
134
|
+
await pool.settled(true)
|
|
135
|
+
t.pass()
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test.serial('task.cancel() works', async (t) => {
|
|
139
|
+
const events: Pool.Event[] = []
|
|
140
|
+
const spawnHelloWorld = () => spawn(new Worker('./workers/hello-world'))
|
|
141
|
+
const pool = Pool(spawnHelloWorld, 1)
|
|
142
|
+
|
|
143
|
+
pool.events().subscribe((event) => events.push(event))
|
|
144
|
+
|
|
145
|
+
let executionCount = 0
|
|
146
|
+
const tasks: QueuedTask<any, any>[] = []
|
|
147
|
+
|
|
148
|
+
for (let i = 0; i < 4; i++) {
|
|
149
|
+
const task = pool.queue((helloWorld) => {
|
|
150
|
+
executionCount++
|
|
151
|
+
return helloWorld()
|
|
152
|
+
})
|
|
153
|
+
tasks.push(task)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
tasks[2].cancel()
|
|
157
|
+
tasks[3].cancel()
|
|
158
|
+
|
|
159
|
+
await pool.completed()
|
|
160
|
+
t.is(executionCount, 2)
|
|
161
|
+
|
|
162
|
+
const cancellationEvents = events.filter((event) => event.type === 'taskCanceled')
|
|
163
|
+
t.deepEqual(cancellationEvents, [
|
|
164
|
+
{
|
|
165
|
+
taskID: 3,
|
|
166
|
+
type: PoolEventType.taskCanceled,
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
taskID: 4,
|
|
170
|
+
type: PoolEventType.taskCanceled,
|
|
171
|
+
},
|
|
172
|
+
])
|
|
173
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/* eslint-disable import/no-internal-modules */
|
|
2
|
+
import test from 'ava'
|
|
3
|
+
|
|
4
|
+
import { registerSerializer, spawn, Thread, Worker } from '../src/index'
|
|
5
|
+
import { Foo, fooSerializer } from './lib/serialization'
|
|
6
|
+
|
|
7
|
+
registerSerializer(fooSerializer)
|
|
8
|
+
|
|
9
|
+
test('can use a custom serializer', async (t) => {
|
|
10
|
+
const run = await spawn(new Worker('./workers/serialization.ts'))
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const input = new Foo('Test')
|
|
14
|
+
const result: Foo<string> = await run(input)
|
|
15
|
+
|
|
16
|
+
t.true(result instanceof Foo)
|
|
17
|
+
t.is(result.getValue(), 'TestTest')
|
|
18
|
+
} finally {
|
|
19
|
+
await Thread.terminate(run)
|
|
20
|
+
}
|
|
21
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/* eslint-disable no-restricted-imports */
|
|
2
|
+
/* eslint-disable import/no-internal-modules */
|
|
3
|
+
/*
|
|
4
|
+
* This code here will be run in a headless Chromium browser using `puppet-run`.
|
|
5
|
+
* Check the package.json scripts `test:puppeteer:*`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// We need this as a work-around to make our threads Worker global, since
|
|
9
|
+
// the bundler would otherwise not recognize `new Worker()` as a web worker
|
|
10
|
+
import '../src/master/register'
|
|
11
|
+
|
|
12
|
+
import { expect } from 'chai'
|
|
13
|
+
|
|
14
|
+
import { BlobWorker, spawn, Thread } from '..'
|
|
15
|
+
|
|
16
|
+
describe('threads in browser', function () {
|
|
17
|
+
it('can spawn and terminate a thread', async function () {
|
|
18
|
+
const helloWorld = await spawn<() => string>(new Worker('./workers/hello-world.js'))
|
|
19
|
+
expect(await helloWorld()).to.equal('Hello World')
|
|
20
|
+
await Thread.terminate(helloWorld)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('can call a function thread more than once', async function () {
|
|
24
|
+
const increment = await spawn<() => number>(new Worker('./workers/increment.js'))
|
|
25
|
+
expect(await increment()).to.equal(1)
|
|
26
|
+
expect(await increment()).to.equal(2)
|
|
27
|
+
expect(await increment()).to.equal(3)
|
|
28
|
+
await Thread.terminate(increment)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('can spawn and use a blob worker', async function () {
|
|
32
|
+
const baseUrl = new URL(window.location.href).origin
|
|
33
|
+
const workerSource = `
|
|
34
|
+
// Makes expose() available on global scope
|
|
35
|
+
importScripts(${JSON.stringify(baseUrl + '/worker.js')})
|
|
36
|
+
|
|
37
|
+
let counter = 0
|
|
38
|
+
|
|
39
|
+
expose(function() {
|
|
40
|
+
return ++counter
|
|
41
|
+
})
|
|
42
|
+
`
|
|
43
|
+
const increment = await spawn<() => number>(BlobWorker.fromText(workerSource))
|
|
44
|
+
expect(await increment()).to.equal(1)
|
|
45
|
+
expect(await increment()).to.equal(2)
|
|
46
|
+
expect(await increment()).to.equal(3)
|
|
47
|
+
await Thread.terminate(increment)
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
+
/* eslint-disable import/no-internal-modules */
|
|
4
|
+
import test from 'ava'
|
|
5
|
+
import { Observable } from 'observable-fns'
|
|
6
|
+
|
|
7
|
+
import { spawn, Thread, Worker } from '../src/index'
|
|
8
|
+
import { Counter } from './workers/counter'
|
|
9
|
+
|
|
10
|
+
test('can spawn and terminate a thread', async (t) => {
|
|
11
|
+
// We also test here that running spawn() without type parameters works
|
|
12
|
+
const helloWorld = await spawn(new Worker('./workers/hello-world'))
|
|
13
|
+
t.is(await helloWorld(), 'Hello World')
|
|
14
|
+
await Thread.terminate(helloWorld)
|
|
15
|
+
t.pass()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('can call a function thread more than once', async (t) => {
|
|
19
|
+
const increment = await spawn<() => number>(new Worker('./workers/increment'))
|
|
20
|
+
t.is(await increment(), 1)
|
|
21
|
+
t.is(await increment(), 2)
|
|
22
|
+
t.is(await increment(), 3)
|
|
23
|
+
await Thread.terminate(increment)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('can subscribe to an observable returned by a thread call', async (t) => {
|
|
27
|
+
const countToFive = await spawn<() => Observable<number>>(new Worker('./workers/count-to-five'))
|
|
28
|
+
const encounteredValues: any[] = []
|
|
29
|
+
|
|
30
|
+
const observable = countToFive()
|
|
31
|
+
observable.subscribe((value) => encounteredValues.push(value))
|
|
32
|
+
await observable
|
|
33
|
+
|
|
34
|
+
t.deepEqual(encounteredValues, [1, 2, 3, 4, 5])
|
|
35
|
+
await Thread.terminate(countToFive)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('can spawn a module thread', async (t) => {
|
|
39
|
+
const counter = await spawn<Counter>(new Worker('./workers/counter'))
|
|
40
|
+
t.is(await counter.getCount(), 0)
|
|
41
|
+
await Promise.all([counter.increment(), counter.increment()])
|
|
42
|
+
t.is(await counter.getCount(), 2)
|
|
43
|
+
await counter.decrement()
|
|
44
|
+
t.is(await counter.getCount(), 1)
|
|
45
|
+
await Thread.terminate(counter)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('thread job errors are handled', async (t) => {
|
|
49
|
+
const fail = await spawn<() => Promise<never>>(new Worker('./workers/faulty-function'))
|
|
50
|
+
await t.throwsAsync(fail(), null, 'I am supposed to fail.')
|
|
51
|
+
await Thread.terminate(fail)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('thread transfer errors are handled', async (t) => {
|
|
55
|
+
const builtin = require('node:module').builtinModules
|
|
56
|
+
if (builtin.includes('worker_threads')) {
|
|
57
|
+
// test is actual for native worker_threads only
|
|
58
|
+
const helloWorld = await spawn(new Worker('./workers/hello-world'))
|
|
59
|
+
const badTransferObj = { fn: () => {} }
|
|
60
|
+
await t.throwsAsync(helloWorld(badTransferObj), { name: 'DataCloneError' })
|
|
61
|
+
await Thread.terminate(helloWorld)
|
|
62
|
+
} else {
|
|
63
|
+
t.pass()
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('catches top-level thread errors', async (t) => {
|
|
68
|
+
await t.throwsAsync(spawn(new Worker('./workers/top-level-throw')), null, 'Top-level worker error')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test.todo('can subscribe to thread events')
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/* eslint-disable import/no-internal-modules */
|
|
2
|
+
import test from 'ava'
|
|
3
|
+
|
|
4
|
+
import { spawn, Thread, Worker } from '../src/index'
|
|
5
|
+
|
|
6
|
+
test('can use worker returning an observable subject', async (t) => {
|
|
7
|
+
const captured: Array<{ max: number; min: number }> = []
|
|
8
|
+
|
|
9
|
+
const minmax = await spawn(new Worker('./workers/minmax'))
|
|
10
|
+
minmax.values().subscribe((values) => captured.push(values))
|
|
11
|
+
|
|
12
|
+
await minmax.push(2)
|
|
13
|
+
await minmax.push(3)
|
|
14
|
+
await minmax.push(4)
|
|
15
|
+
await minmax.push(1)
|
|
16
|
+
await minmax.push(5)
|
|
17
|
+
await minmax.finish()
|
|
18
|
+
|
|
19
|
+
await Thread.terminate(minmax)
|
|
20
|
+
t.deepEqual(captured, [
|
|
21
|
+
{ max: 2, min: 2 },
|
|
22
|
+
{ max: 3, min: 2 },
|
|
23
|
+
{ max: 4, min: 2 },
|
|
24
|
+
{ max: 4, min: 1 },
|
|
25
|
+
{ max: 5, min: 1 },
|
|
26
|
+
])
|
|
27
|
+
})
|