@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.
Files changed (171) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +227 -0
  4. package/dist/common.d.ts +4 -0
  5. package/dist/common.js +18 -0
  6. package/dist/index.d.ts +7 -0
  7. package/dist/index.js +27 -0
  8. package/dist/master/get-bundle-url.browser.d.ts +3 -0
  9. package/dist/master/get-bundle-url.browser.js +29 -0
  10. package/dist/master/implementation.browser.d.ts +4 -0
  11. package/dist/master/implementation.browser.js +69 -0
  12. package/dist/master/implementation.d.ts +6 -0
  13. package/dist/master/implementation.js +41 -0
  14. package/dist/master/implementation.node.d.ts +5 -0
  15. package/dist/master/implementation.node.js +255 -0
  16. package/dist/master/index.d.ts +13 -0
  17. package/dist/master/index.js +16 -0
  18. package/dist/master/invocation-proxy.d.ts +3 -0
  19. package/dist/master/invocation-proxy.js +130 -0
  20. package/dist/master/pool-types.d.ts +65 -0
  21. package/dist/master/pool-types.js +15 -0
  22. package/dist/master/pool.d.ts +90 -0
  23. package/dist/master/pool.js +281 -0
  24. package/dist/master/register.d.ts +1 -0
  25. package/dist/master/register.js +12 -0
  26. package/dist/master/spawn.d.ts +20 -0
  27. package/dist/master/spawn.js +130 -0
  28. package/dist/master/thread.d.ts +12 -0
  29. package/dist/master/thread.js +22 -0
  30. package/dist/observable-promise.d.ts +38 -0
  31. package/dist/observable-promise.js +156 -0
  32. package/dist/observable.d.ts +19 -0
  33. package/dist/observable.js +43 -0
  34. package/dist/ponyfills.d.ts +8 -0
  35. package/dist/ponyfills.js +22 -0
  36. package/dist/promise.d.ts +5 -0
  37. package/dist/promise.js +29 -0
  38. package/dist/serializers.d.ts +16 -0
  39. package/dist/serializers.js +41 -0
  40. package/dist/symbols.d.ts +5 -0
  41. package/dist/symbols.js +8 -0
  42. package/dist/transferable.d.ts +42 -0
  43. package/dist/transferable.js +28 -0
  44. package/dist/types/master.d.ts +99 -0
  45. package/dist/types/master.js +14 -0
  46. package/dist/types/messages.d.ts +62 -0
  47. package/dist/types/messages.js +20 -0
  48. package/dist/types/worker.d.ts +11 -0
  49. package/dist/types/worker.js +2 -0
  50. package/dist/worker/bundle-entry.d.ts +1 -0
  51. package/dist/worker/bundle-entry.js +27 -0
  52. package/dist/worker/implementation.browser.d.ts +7 -0
  53. package/dist/worker/implementation.browser.js +28 -0
  54. package/dist/worker/implementation.d.ts +3 -0
  55. package/dist/worker/implementation.js +24 -0
  56. package/dist/worker/implementation.tiny-worker.d.ts +7 -0
  57. package/dist/worker/implementation.tiny-worker.js +38 -0
  58. package/dist/worker/implementation.worker_threads.d.ts +8 -0
  59. package/dist/worker/implementation.worker_threads.js +42 -0
  60. package/dist/worker/index.d.ts +13 -0
  61. package/dist/worker/index.js +195 -0
  62. package/dist/worker_threads.d.ts +8 -0
  63. package/dist/worker_threads.js +17 -0
  64. package/dist-esm/common.js +12 -0
  65. package/dist-esm/index.js +6 -0
  66. package/dist-esm/master/get-bundle-url.browser.js +25 -0
  67. package/dist-esm/master/implementation.browser.js +64 -0
  68. package/dist-esm/master/implementation.js +15 -0
  69. package/dist-esm/master/implementation.node.js +224 -0
  70. package/dist-esm/master/index.js +9 -0
  71. package/dist-esm/master/invocation-proxy.js +122 -0
  72. package/dist-esm/master/pool-types.js +12 -0
  73. package/dist-esm/master/pool.js +273 -0
  74. package/dist-esm/master/register.js +10 -0
  75. package/dist-esm/master/spawn.js +123 -0
  76. package/dist-esm/master/thread.js +19 -0
  77. package/dist-esm/observable-promise.js +152 -0
  78. package/dist-esm/observable.js +38 -0
  79. package/dist-esm/ponyfills.js +18 -0
  80. package/dist-esm/promise.js +25 -0
  81. package/dist-esm/serializers.js +37 -0
  82. package/dist-esm/symbols.js +5 -0
  83. package/dist-esm/transferable.js +23 -0
  84. package/dist-esm/types/master.js +11 -0
  85. package/dist-esm/types/messages.js +17 -0
  86. package/dist-esm/types/worker.js +1 -0
  87. package/dist-esm/worker/bundle-entry.js +11 -0
  88. package/dist-esm/worker/implementation.browser.js +26 -0
  89. package/dist-esm/worker/implementation.js +19 -0
  90. package/dist-esm/worker/implementation.tiny-worker.js +36 -0
  91. package/dist-esm/worker/implementation.worker_threads.js +37 -0
  92. package/dist-esm/worker/index.js +186 -0
  93. package/dist-esm/worker_threads.js +14 -0
  94. package/index.mjs +11 -0
  95. package/observable.d.ts +2 -0
  96. package/observable.js +3 -0
  97. package/observable.mjs +5 -0
  98. package/package.json +141 -0
  99. package/register.d.ts +3 -0
  100. package/register.js +3 -0
  101. package/register.mjs +2 -0
  102. package/rollup.config.js +16 -0
  103. package/src/common.ts +16 -0
  104. package/src/index.ts +8 -0
  105. package/src/master/get-bundle-url.browser.ts +31 -0
  106. package/src/master/implementation.browser.ts +80 -0
  107. package/src/master/implementation.node.ts +284 -0
  108. package/src/master/implementation.ts +21 -0
  109. package/src/master/index.ts +20 -0
  110. package/src/master/invocation-proxy.ts +146 -0
  111. package/src/master/pool-types.ts +83 -0
  112. package/src/master/pool.ts +391 -0
  113. package/src/master/register.ts +10 -0
  114. package/src/master/spawn.ts +172 -0
  115. package/src/master/thread.ts +26 -0
  116. package/src/observable-promise.ts +181 -0
  117. package/src/observable.ts +43 -0
  118. package/src/ponyfills.ts +31 -0
  119. package/src/promise.ts +26 -0
  120. package/src/serializers.ts +67 -0
  121. package/src/symbols.ts +5 -0
  122. package/src/transferable.ts +68 -0
  123. package/src/types/master.ts +130 -0
  124. package/src/types/messages.ts +81 -0
  125. package/src/types/worker.ts +14 -0
  126. package/src/worker/bundle-entry.ts +10 -0
  127. package/src/worker/implementation.browser.ts +40 -0
  128. package/src/worker/implementation.tiny-worker.ts +52 -0
  129. package/src/worker/implementation.ts +23 -0
  130. package/src/worker/implementation.worker_threads.ts +50 -0
  131. package/src/worker/index.ts +228 -0
  132. package/src/worker_threads.ts +28 -0
  133. package/test/lib/serialization.ts +38 -0
  134. package/test/observable-promise.test.ts +189 -0
  135. package/test/observable.test.ts +86 -0
  136. package/test/pool.test.ts +173 -0
  137. package/test/serialization.test.ts +21 -0
  138. package/test/spawn.chromium.mocha.ts +49 -0
  139. package/test/spawn.test.ts +71 -0
  140. package/test/streaming.test.ts +27 -0
  141. package/test/transferables.test.ts +69 -0
  142. package/test/workers/arraybuffer-xor.ts +11 -0
  143. package/test/workers/count-to-five.ts +13 -0
  144. package/test/workers/counter.ts +20 -0
  145. package/test/workers/faulty-function.ts +6 -0
  146. package/test/workers/hello-world.ts +6 -0
  147. package/test/workers/increment.ts +9 -0
  148. package/test/workers/minmax.ts +25 -0
  149. package/test/workers/serialization.ts +12 -0
  150. package/test/workers/top-level-throw.ts +1 -0
  151. package/test-tooling/rollup/app.js +20 -0
  152. package/test-tooling/rollup/rollup.config.ts +15 -0
  153. package/test-tooling/rollup/rollup.test.ts +44 -0
  154. package/test-tooling/rollup/worker.js +7 -0
  155. package/test-tooling/tsconfig/minimal-tsconfig.test.ts +7 -0
  156. package/test-tooling/tsconfig/minimal.ts +10 -0
  157. package/test-tooling/webpack/addition-worker.ts +10 -0
  158. package/test-tooling/webpack/app-with-inlined-worker.ts +29 -0
  159. package/test-tooling/webpack/app.ts +58 -0
  160. package/test-tooling/webpack/pool-worker.ts +6 -0
  161. package/test-tooling/webpack/raw-loader.d.ts +4 -0
  162. package/test-tooling/webpack/webpack.chromium.mocha.ts +21 -0
  163. package/test-tooling/webpack/webpack.node.config.js +38 -0
  164. package/test-tooling/webpack/webpack.test.ts +90 -0
  165. package/test-tooling/webpack/webpack.web.config.js +35 -0
  166. package/types/is-observable.d.ts +7 -0
  167. package/types/tiny-worker.d.ts +4 -0
  168. package/types/webworker.d.ts +9 -0
  169. package/worker.d.ts +2 -0
  170. package/worker.js +3 -0
  171. package/worker.mjs +7 -0
@@ -0,0 +1,281 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Thread = exports.PoolEventType = exports.Pool = void 0;
7
+ /* eslint-disable unicorn/no-thenable */
8
+ /* eslint-disable @typescript-eslint/no-floating-promises */
9
+ /* eslint-disable require-await */
10
+ /* eslint-disable @typescript-eslint/member-ordering */
11
+ /* eslint-disable unicorn/no-array-reduce */
12
+ /* eslint-disable @typescript-eslint/no-explicit-any */
13
+ /* eslint-disable import/export */
14
+ /* eslint-disable @typescript-eslint/no-namespace */
15
+ const debug_1 = __importDefault(require("debug"));
16
+ const observable_fns_1 = require("observable-fns");
17
+ const ponyfills_1 = require("../ponyfills");
18
+ const implementation_1 = require("./implementation");
19
+ const pool_types_1 = require("./pool-types");
20
+ const thread_1 = require("./thread");
21
+ let nextPoolID = 1;
22
+ function createArray(size) {
23
+ const array = [];
24
+ for (let index = 0; index < size; index++) {
25
+ array.push(index);
26
+ }
27
+ return array;
28
+ }
29
+ function delay(ms) {
30
+ return new Promise((resolve) => setTimeout(resolve, ms));
31
+ }
32
+ function flatMap(array, mapper) {
33
+ return array.reduce((flattened, element) => [...flattened, ...mapper(element)], []);
34
+ }
35
+ function slugify(text) {
36
+ return text.replaceAll(/\W/g, ' ').trim().replaceAll(/\s+/g, '-');
37
+ }
38
+ function spawnWorkers(spawnWorker, count) {
39
+ return createArray(count).map(() => ({
40
+ init: spawnWorker(),
41
+ runningTasks: [],
42
+ }));
43
+ }
44
+ class WorkerPool {
45
+ static EventType = pool_types_1.PoolEventType;
46
+ debug;
47
+ eventObservable;
48
+ options;
49
+ workers;
50
+ eventSubject = new observable_fns_1.Subject();
51
+ initErrors = [];
52
+ isClosing = false;
53
+ nextTaskID = 1;
54
+ taskQueue = [];
55
+ constructor(spawnWorker, optionsOrSize) {
56
+ const options = typeof optionsOrSize === 'number' ? { size: optionsOrSize } : optionsOrSize || {};
57
+ const { size = implementation_1.defaultPoolSize } = options;
58
+ this.debug = (0, debug_1.default)(`threads:pool:${slugify(options.name || String(nextPoolID++))}`);
59
+ this.options = options;
60
+ this.workers = spawnWorkers(spawnWorker, size);
61
+ this.eventObservable = (0, observable_fns_1.multicast)(observable_fns_1.Observable.from(this.eventSubject));
62
+ Promise.all(this.workers.map((worker) => worker.init)).then(() => this.eventSubject.next({
63
+ size: this.workers.length,
64
+ type: pool_types_1.PoolEventType.initialized,
65
+ }), (error) => {
66
+ this.debug('Error while initializing pool worker:', error);
67
+ this.eventSubject.error(error);
68
+ this.initErrors.push(error);
69
+ });
70
+ }
71
+ findIdlingWorker() {
72
+ const { concurrency = 1 } = this.options;
73
+ return this.workers.find((worker) => worker.runningTasks.length < concurrency);
74
+ }
75
+ async runPoolTask(worker, task) {
76
+ const workerID = this.workers.indexOf(worker) + 1;
77
+ this.debug(`Running task #${task.id} on worker #${workerID}...`);
78
+ this.eventSubject.next({
79
+ taskID: task.id,
80
+ type: pool_types_1.PoolEventType.taskStart,
81
+ workerID,
82
+ });
83
+ try {
84
+ const returnValue = await task.run(await worker.init);
85
+ this.debug(`Task #${task.id} completed successfully`);
86
+ this.eventSubject.next({
87
+ returnValue,
88
+ taskID: task.id,
89
+ type: pool_types_1.PoolEventType.taskCompleted,
90
+ workerID,
91
+ });
92
+ }
93
+ catch (error) {
94
+ this.debug(`Task #${task.id} failed`);
95
+ this.eventSubject.next({
96
+ error,
97
+ taskID: task.id,
98
+ type: pool_types_1.PoolEventType.taskFailed,
99
+ workerID,
100
+ });
101
+ }
102
+ }
103
+ async run(worker, task) {
104
+ const runPromise = (async () => {
105
+ const removeTaskFromWorkersRunningTasks = () => {
106
+ worker.runningTasks = worker.runningTasks.filter((someRunPromise) => someRunPromise !== runPromise);
107
+ };
108
+ // Defer task execution by one tick to give handlers time to subscribe
109
+ await delay(0);
110
+ try {
111
+ await this.runPoolTask(worker, task);
112
+ }
113
+ finally {
114
+ removeTaskFromWorkersRunningTasks();
115
+ if (!this.isClosing) {
116
+ this.scheduleWork();
117
+ }
118
+ }
119
+ })();
120
+ worker.runningTasks.push(runPromise);
121
+ }
122
+ scheduleWork() {
123
+ this.debug('Attempt de-queueing a task in order to run it...');
124
+ const availableWorker = this.findIdlingWorker();
125
+ if (!availableWorker)
126
+ return;
127
+ const nextTask = this.taskQueue.shift();
128
+ if (!nextTask) {
129
+ this.debug('Task queue is empty');
130
+ this.eventSubject.next({ type: pool_types_1.PoolEventType.taskQueueDrained });
131
+ return;
132
+ }
133
+ this.run(availableWorker, nextTask);
134
+ }
135
+ taskCompletion(taskID) {
136
+ return new Promise((resolve, reject) => {
137
+ const eventSubscription = this.events().subscribe((event) => {
138
+ if (event.type === pool_types_1.PoolEventType.taskCompleted && event.taskID === taskID) {
139
+ eventSubscription.unsubscribe();
140
+ resolve(event.returnValue);
141
+ }
142
+ else if (event.type === pool_types_1.PoolEventType.taskFailed && event.taskID === taskID) {
143
+ eventSubscription.unsubscribe();
144
+ reject(event.error);
145
+ }
146
+ else if (event.type === pool_types_1.PoolEventType.terminated) {
147
+ eventSubscription.unsubscribe();
148
+ reject(Error('Pool has been terminated before task was run.'));
149
+ }
150
+ });
151
+ });
152
+ }
153
+ async settled(allowResolvingImmediately = false) {
154
+ const getCurrentlyRunningTasks = () => flatMap(this.workers, (worker) => worker.runningTasks);
155
+ const taskFailures = [];
156
+ const failureSubscription = this.eventObservable.subscribe((event) => {
157
+ if (event.type === pool_types_1.PoolEventType.taskFailed) {
158
+ taskFailures.push(event.error);
159
+ }
160
+ });
161
+ if (this.initErrors.length > 0) {
162
+ throw this.initErrors[0];
163
+ }
164
+ if (allowResolvingImmediately && this.taskQueue.length === 0) {
165
+ await (0, ponyfills_1.allSettled)(getCurrentlyRunningTasks());
166
+ return taskFailures;
167
+ }
168
+ await new Promise((resolve, reject) => {
169
+ const subscription = this.eventObservable.subscribe({
170
+ error: reject,
171
+ next(event) {
172
+ if (event.type === pool_types_1.PoolEventType.taskQueueDrained) {
173
+ subscription.unsubscribe();
174
+ resolve(void 0);
175
+ }
176
+ }, // make a pool-wide error reject the completed() result promise
177
+ });
178
+ });
179
+ await (0, ponyfills_1.allSettled)(getCurrentlyRunningTasks());
180
+ failureSubscription.unsubscribe();
181
+ return taskFailures;
182
+ }
183
+ async completed(allowResolvingImmediately = false) {
184
+ const settlementPromise = this.settled(allowResolvingImmediately);
185
+ const earlyExitPromise = new Promise((resolve, reject) => {
186
+ const subscription = this.eventObservable.subscribe({
187
+ error: reject,
188
+ next(event) {
189
+ if (event.type === pool_types_1.PoolEventType.taskQueueDrained) {
190
+ subscription.unsubscribe();
191
+ resolve(settlementPromise);
192
+ }
193
+ else if (event.type === pool_types_1.PoolEventType.taskFailed) {
194
+ subscription.unsubscribe();
195
+ reject(event.error);
196
+ }
197
+ }, // make a pool-wide error reject the completed() result promise
198
+ });
199
+ });
200
+ const errors = await Promise.race([settlementPromise, earlyExitPromise]);
201
+ if (errors.length > 0) {
202
+ throw errors[0];
203
+ }
204
+ }
205
+ events() {
206
+ return this.eventObservable;
207
+ }
208
+ queue(taskFunction) {
209
+ const { maxQueuedJobs = Number.POSITIVE_INFINITY } = this.options;
210
+ if (this.isClosing) {
211
+ throw new Error('Cannot schedule pool tasks after terminate() has been called.');
212
+ }
213
+ if (this.initErrors.length > 0) {
214
+ throw this.initErrors[0];
215
+ }
216
+ const taskID = this.nextTaskID++;
217
+ const taskCompletion = this.taskCompletion(taskID);
218
+ taskCompletion.catch((error) => {
219
+ // Prevent unhandled rejections here as we assume the user will use
220
+ // `pool.completed()`, `pool.settled()` or `task.catch()` to handle errors
221
+ this.debug(`Task #${taskID} errored:`, error);
222
+ });
223
+ const task = {
224
+ cancel: () => {
225
+ if (!this.taskQueue.includes(task))
226
+ return;
227
+ this.taskQueue = this.taskQueue.filter((someTask) => someTask !== task);
228
+ this.eventSubject.next({
229
+ taskID: task.id,
230
+ type: pool_types_1.PoolEventType.taskCanceled,
231
+ });
232
+ },
233
+ id: taskID,
234
+ run: taskFunction,
235
+ then: taskCompletion.then.bind(taskCompletion),
236
+ };
237
+ if (this.taskQueue.length >= maxQueuedJobs) {
238
+ throw new Error('Maximum number of pool tasks queued. Refusing to queue another one.\n' +
239
+ 'This usually happens for one of two reasons: We are either at peak ' +
240
+ "workload right now or some tasks just won't finish, thus blocking the pool.");
241
+ }
242
+ this.debug(`Queueing task #${task.id}...`);
243
+ this.taskQueue.push(task);
244
+ this.eventSubject.next({
245
+ taskID: task.id,
246
+ type: pool_types_1.PoolEventType.taskQueued,
247
+ });
248
+ this.scheduleWork();
249
+ return task;
250
+ }
251
+ async terminate(force) {
252
+ this.isClosing = true;
253
+ if (!force) {
254
+ await this.completed(true);
255
+ }
256
+ this.eventSubject.next({
257
+ remainingQueue: [...this.taskQueue],
258
+ type: pool_types_1.PoolEventType.terminated,
259
+ });
260
+ this.eventSubject.complete();
261
+ await Promise.all(this.workers.map(async (worker) => thread_1.Thread.terminate(await worker.init)));
262
+ }
263
+ }
264
+ /**
265
+ * Thread pool constructor. Creates a new pool and spawns its worker threads.
266
+ */
267
+ function PoolConstructor(spawnWorker, optionsOrSize) {
268
+ // The function exists only so we don't need to use `new` to create a pool (we still can, though).
269
+ // If the Pool is a class or not is an implementation detail that should not concern the user.
270
+ return new WorkerPool(spawnWorker, optionsOrSize);
271
+ }
272
+ ;
273
+ PoolConstructor.EventType = pool_types_1.PoolEventType;
274
+ /**
275
+ * Thread pool constructor. Creates a new pool and spawns its worker threads.
276
+ */
277
+ exports.Pool = PoolConstructor;
278
+ var pool_types_2 = require("./pool-types");
279
+ Object.defineProperty(exports, "PoolEventType", { enumerable: true, get: function () { return pool_types_2.PoolEventType; } });
280
+ var thread_2 = require("./thread");
281
+ Object.defineProperty(exports, "Thread", { enumerable: true, get: function () { return thread_2.Thread; } });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /* eslint-disable @typescript-eslint/no-explicit-any */
4
+ const index_1 = require("./index");
5
+ if (typeof global !== 'undefined') {
6
+ ;
7
+ global.Worker = index_1.Worker;
8
+ }
9
+ else if (window !== undefined) {
10
+ ;
11
+ window.Worker = index_1.Worker;
12
+ }
@@ -0,0 +1,20 @@
1
+ import { FunctionThread, ModuleThread, StripAsync, Worker as WorkerType } from '../types/master';
2
+ import { WorkerFunction, WorkerModule } from '../types/worker';
3
+ type ArbitraryWorkerInterface = WorkerFunction & WorkerModule<string> & {
4
+ somekeythatisneverusedinproductioncode123: 'magicmarker123';
5
+ };
6
+ type ArbitraryThreadType = FunctionThread<any, any> & ModuleThread<any>;
7
+ export type ExposedToThreadType<Exposed extends WorkerFunction | WorkerModule<any>> = Exposed extends ArbitraryWorkerInterface ? ArbitraryThreadType : Exposed extends WorkerFunction ? FunctionThread<Parameters<Exposed>, StripAsync<ReturnType<Exposed>>> : Exposed extends WorkerModule<any> ? ModuleThread<Exposed> : never;
8
+ /**
9
+ * Spawn a new thread. Takes a fresh worker instance, wraps it in a thin
10
+ * abstraction layer to provide the transparent API and verifies that
11
+ * the worker has initialized successfully.
12
+ *
13
+ * @param worker Instance of `Worker`. Either a web worker, `worker_threads` worker or `tiny-worker` worker.
14
+ * @param [options]
15
+ * @param [options.timeout] Init message timeout. Default: 10000 or set by environment variable.
16
+ */
17
+ export declare function spawn<Exposed extends WorkerFunction | WorkerModule<any> = ArbitraryWorkerInterface>(worker: WorkerType, options?: {
18
+ timeout?: number;
19
+ }): Promise<ExposedToThreadType<Exposed>>;
20
+ export {};
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.spawn = void 0;
7
+ /* eslint-disable @typescript-eslint/no-floating-promises */
8
+ /* eslint-disable @typescript-eslint/no-explicit-any */
9
+ const debug_1 = __importDefault(require("debug"));
10
+ const observable_fns_1 = require("observable-fns");
11
+ const common_1 = require("../common");
12
+ const promise_1 = require("../promise");
13
+ const symbols_1 = require("../symbols");
14
+ const master_1 = require("../types/master");
15
+ const invocation_proxy_1 = require("./invocation-proxy");
16
+ const debugMessages = (0, debug_1.default)('threads:master:messages');
17
+ const debugSpawn = (0, debug_1.default)('threads:master:spawn');
18
+ const debugThreadUtils = (0, debug_1.default)('threads:master:thread-utils');
19
+ const isInitMessage = (data) => data && data.type === 'init';
20
+ const isUncaughtErrorMessage = (data) => data && data.type === 'uncaughtError';
21
+ const initMessageTimeout = typeof process !== 'undefined' && process.env !== undefined && process.env.THREADS_WORKER_INIT_TIMEOUT ?
22
+ Number.parseInt(process.env.THREADS_WORKER_INIT_TIMEOUT, 10)
23
+ : 10000;
24
+ async function withTimeout(promise, timeoutInMs, errorMessage) {
25
+ let timeoutHandle;
26
+ const timeout = new Promise((resolve, reject) => {
27
+ timeoutHandle = setTimeout(() => reject(Error(errorMessage)), timeoutInMs);
28
+ });
29
+ const result = await Promise.race([promise, timeout]);
30
+ clearTimeout(timeoutHandle);
31
+ return result;
32
+ }
33
+ function receiveInitMessage(worker) {
34
+ return new Promise((resolve, reject) => {
35
+ const messageHandler = ((event) => {
36
+ debugMessages('Message from worker before finishing initialization:', event.data);
37
+ if (isInitMessage(event.data)) {
38
+ worker.removeEventListener('message', messageHandler);
39
+ resolve(event.data);
40
+ }
41
+ else if (isUncaughtErrorMessage(event.data)) {
42
+ worker.removeEventListener('message', messageHandler);
43
+ reject((0, common_1.deserialize)(event.data.error));
44
+ }
45
+ });
46
+ worker.addEventListener('message', messageHandler);
47
+ });
48
+ }
49
+ function createEventObservable(worker, workerTermination) {
50
+ return new observable_fns_1.Observable((observer) => {
51
+ const messageHandler = ((messageEvent) => {
52
+ const workerEvent = {
53
+ data: messageEvent.data,
54
+ type: master_1.WorkerEventType.message,
55
+ };
56
+ observer.next(workerEvent);
57
+ });
58
+ const rejectionHandler = ((errorEvent) => {
59
+ debugThreadUtils('Unhandled promise rejection event in thread:', errorEvent);
60
+ const workerEvent = {
61
+ error: Error(errorEvent.reason),
62
+ type: master_1.WorkerEventType.internalError,
63
+ };
64
+ observer.next(workerEvent);
65
+ });
66
+ worker.addEventListener('message', messageHandler);
67
+ worker.addEventListener('unhandledrejection', rejectionHandler);
68
+ workerTermination.then(() => {
69
+ const terminationEvent = {
70
+ type: master_1.WorkerEventType.termination,
71
+ };
72
+ worker.removeEventListener('message', messageHandler);
73
+ worker.removeEventListener('unhandledrejection', rejectionHandler);
74
+ observer.next(terminationEvent);
75
+ observer.complete();
76
+ });
77
+ });
78
+ }
79
+ function createTerminator(worker) {
80
+ const [termination, resolver] = (0, promise_1.createPromiseWithResolver)();
81
+ const terminate = async () => {
82
+ debugThreadUtils('Terminating worker');
83
+ // Newer versions of worker_threads workers return a promise
84
+ await worker.terminate();
85
+ resolver();
86
+ };
87
+ return { terminate, termination };
88
+ }
89
+ function setPrivateThreadProps(raw, worker, workerEvents, terminate) {
90
+ const workerErrors = workerEvents
91
+ .filter((event) => event.type === master_1.WorkerEventType.internalError)
92
+ .map((errorEvent) => errorEvent.error);
93
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
94
+ return Object.assign(raw, {
95
+ [symbols_1.$errors]: workerErrors,
96
+ [symbols_1.$events]: workerEvents,
97
+ [symbols_1.$terminate]: terminate,
98
+ [symbols_1.$worker]: worker,
99
+ });
100
+ }
101
+ /**
102
+ * Spawn a new thread. Takes a fresh worker instance, wraps it in a thin
103
+ * abstraction layer to provide the transparent API and verifies that
104
+ * the worker has initialized successfully.
105
+ *
106
+ * @param worker Instance of `Worker`. Either a web worker, `worker_threads` worker or `tiny-worker` worker.
107
+ * @param [options]
108
+ * @param [options.timeout] Init message timeout. Default: 10000 or set by environment variable.
109
+ */
110
+ async function spawn(worker, options) {
111
+ debugSpawn('Initializing new thread');
112
+ const timeout = options && options.timeout ? options.timeout : initMessageTimeout;
113
+ const initMessage = await withTimeout(receiveInitMessage(worker), timeout, `Timeout: Did not receive an init message from worker after ${timeout}ms. Make sure the worker calls expose().`);
114
+ const exposed = initMessage.exposed;
115
+ const { termination, terminate } = createTerminator(worker);
116
+ const events = createEventObservable(worker, termination);
117
+ if (exposed.type === 'function') {
118
+ const proxy = (0, invocation_proxy_1.createProxyFunction)(worker);
119
+ return setPrivateThreadProps(proxy, worker, events, terminate);
120
+ }
121
+ else if (exposed.type === 'module') {
122
+ const proxy = (0, invocation_proxy_1.createProxyModule)(worker, exposed.methods);
123
+ return setPrivateThreadProps(proxy, worker, events, terminate);
124
+ }
125
+ else {
126
+ const type = exposed.type;
127
+ throw new Error(`Worker init message states unexpected type of expose(): ${type}`);
128
+ }
129
+ }
130
+ exports.spawn = spawn;
@@ -0,0 +1,12 @@
1
+ import { Observable } from 'observable-fns';
2
+ import { Thread as ThreadType, WorkerEvent } from '../types/master';
3
+ export type Thread = ThreadType;
4
+ /** Thread utility functions. Use them to manage or inspect a `spawn()`-ed thread. */
5
+ export declare const Thread: {
6
+ /** Return an observable that can be used to subscribe to all errors happening in the thread. */
7
+ errors<ThreadT extends ThreadType>(thread: ThreadT): Observable<Error>;
8
+ /** Return an observable that can be used to subscribe to internal events happening in the thread. Useful for debugging. */
9
+ events<ThreadT_1 extends ThreadType>(thread: ThreadT_1): Observable<WorkerEvent>;
10
+ /** Terminate a thread. Remember to terminate every thread when you are done using it. */
11
+ terminate<ThreadT_2 extends ThreadType>(thread: ThreadT_2): Promise<void>;
12
+ };
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Thread = void 0;
4
+ const symbols_1 = require("../symbols");
5
+ function fail(message) {
6
+ throw new Error(message);
7
+ }
8
+ /** Thread utility functions. Use them to manage or inspect a `spawn()`-ed thread. */
9
+ exports.Thread = {
10
+ /** Return an observable that can be used to subscribe to all errors happening in the thread. */
11
+ errors(thread) {
12
+ return thread[symbols_1.$errors] || fail('Error observable not found. Make sure to pass a thread instance as returned by the spawn() promise.');
13
+ },
14
+ /** Return an observable that can be used to subscribe to internal events happening in the thread. Useful for debugging. */
15
+ events(thread) {
16
+ return thread[symbols_1.$events] || fail('Events observable not found. Make sure to pass a thread instance as returned by the spawn() promise.');
17
+ },
18
+ /** Terminate a thread. Remember to terminate every thread when you are done using it. */
19
+ terminate(thread) {
20
+ return thread[symbols_1.$terminate]();
21
+ },
22
+ };
@@ -0,0 +1,38 @@
1
+ import { Observable, ObservableLike, SubscriptionObserver } from 'observable-fns';
2
+ type Initializer<T> = (observer: SubscriptionObserver<T>) => UnsubscribeFn | void;
3
+ type Thenable<T> = {
4
+ then: (onFulfilled?: (value: T) => any, onRejected?: (error: any) => any) => any;
5
+ };
6
+ type UnsubscribeFn = () => void;
7
+ /**
8
+ * Creates a hybrid, combining the APIs of an Observable and a Promise.
9
+ *
10
+ * It is used to proxy async process states when we are initially not sure
11
+ * if that async process will yield values once (-> Promise) or multiple
12
+ * times (-> Observable).
13
+ *
14
+ * Note that the observable promise inherits some of the observable's characteristics:
15
+ * The `init` function will be called *once for every time anyone subscribes to it*.
16
+ *
17
+ * If this is undesired, derive a hot observable from it using `makeHot()` and
18
+ * subscribe to that.
19
+ */
20
+ export declare class ObservablePromise<T> extends Observable<T> implements Promise<T> {
21
+ readonly [Symbol.toStringTag] = "[object ObservablePromise]";
22
+ private initHasRun;
23
+ private fulfillmentCallbacks;
24
+ private rejectionCallbacks;
25
+ private firstValue;
26
+ private firstValueSet;
27
+ private rejection;
28
+ private state;
29
+ constructor(init: Initializer<T>);
30
+ private onNext;
31
+ private onError;
32
+ private onCompletion;
33
+ then<TResult1 = T, TResult2 = never>(onFulfilledRaw?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onRejectedRaw?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): Promise<TResult1 | TResult2>;
34
+ catch<Result = never>(onRejected: ((error: Error) => Promise<Result> | Result) | null | undefined): Promise<Result>;
35
+ finally(onCompleted?: (() => void) | null | undefined): Promise<T>;
36
+ static from<T>(thing: Observable<T> | ObservableLike<T> | ArrayLike<T> | Thenable<T>): ObservablePromise<T>;
37
+ }
38
+ export {};
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ObservablePromise = void 0;
4
+ /* eslint-disable unicorn/no-thenable */
5
+ /* eslint-disable @typescript-eslint/member-ordering */
6
+ /* eslint-disable @typescript-eslint/no-floating-promises */
7
+ /* eslint-disable @typescript-eslint/no-explicit-any */
8
+ /* eslint-disable @typescript-eslint/no-this-alias */
9
+ /* eslint-disable unicorn/no-this-assignment */
10
+ const observable_fns_1 = require("observable-fns");
11
+ const doNothing = () => { };
12
+ const returnInput = (input) => input;
13
+ const runDeferred = (fn) => Promise.resolve().then(fn);
14
+ function fail(error) {
15
+ throw error;
16
+ }
17
+ function isThenable(thing) {
18
+ return thing && typeof thing.then === 'function';
19
+ }
20
+ /**
21
+ * Creates a hybrid, combining the APIs of an Observable and a Promise.
22
+ *
23
+ * It is used to proxy async process states when we are initially not sure
24
+ * if that async process will yield values once (-> Promise) or multiple
25
+ * times (-> Observable).
26
+ *
27
+ * Note that the observable promise inherits some of the observable's characteristics:
28
+ * The `init` function will be called *once for every time anyone subscribes to it*.
29
+ *
30
+ * If this is undesired, derive a hot observable from it using `makeHot()` and
31
+ * subscribe to that.
32
+ */
33
+ class ObservablePromise extends observable_fns_1.Observable {
34
+ [Symbol.toStringTag] = '[object ObservablePromise]';
35
+ initHasRun = false;
36
+ fulfillmentCallbacks = [];
37
+ rejectionCallbacks = [];
38
+ firstValue;
39
+ firstValueSet = false;
40
+ rejection;
41
+ state = 'pending';
42
+ constructor(init) {
43
+ super((originalObserver) => {
44
+ // tslint:disable-next-line no-this-assignment
45
+ const self = this;
46
+ const observer = {
47
+ ...originalObserver,
48
+ complete() {
49
+ originalObserver.complete();
50
+ self.onCompletion();
51
+ },
52
+ error(error) {
53
+ originalObserver.error(error);
54
+ self.onError(error);
55
+ },
56
+ next(value) {
57
+ originalObserver.next(value);
58
+ self.onNext(value);
59
+ },
60
+ };
61
+ try {
62
+ this.initHasRun = true;
63
+ return init(observer);
64
+ }
65
+ catch (error) {
66
+ observer.error(error);
67
+ }
68
+ });
69
+ }
70
+ onNext(value) {
71
+ if (!this.firstValueSet) {
72
+ this.firstValue = value;
73
+ this.firstValueSet = true;
74
+ }
75
+ }
76
+ onError(error) {
77
+ this.state = 'rejected';
78
+ this.rejection = error;
79
+ for (const onRejected of this.rejectionCallbacks) {
80
+ // Promisifying the call to turn errors into unhandled promise rejections
81
+ // instead of them failing sync and cancelling the iteration
82
+ runDeferred(() => onRejected(error));
83
+ }
84
+ }
85
+ onCompletion() {
86
+ this.state = 'fulfilled';
87
+ for (const onFulfilled of this.fulfillmentCallbacks) {
88
+ // Promisifying the call to turn errors into unhandled promise rejections
89
+ // instead of them failing sync and cancelling the iteration
90
+ runDeferred(() => onFulfilled(this.firstValue));
91
+ }
92
+ }
93
+ then(onFulfilledRaw, onRejectedRaw) {
94
+ const onFulfilled = onFulfilledRaw || returnInput;
95
+ const onRejected = onRejectedRaw || fail;
96
+ let onRejectedCalled = false;
97
+ return new Promise((resolve, reject) => {
98
+ const rejectionCallback = (error) => {
99
+ if (onRejectedCalled)
100
+ return;
101
+ onRejectedCalled = true;
102
+ try {
103
+ resolve(onRejected(error));
104
+ }
105
+ catch (anotherError) {
106
+ reject(anotherError);
107
+ }
108
+ };
109
+ const fulfillmentCallback = (value) => {
110
+ try {
111
+ resolve(onFulfilled(value));
112
+ }
113
+ catch (error) {
114
+ rejectionCallback(error);
115
+ }
116
+ };
117
+ if (!this.initHasRun) {
118
+ this.subscribe({ error: rejectionCallback });
119
+ }
120
+ if (this.state === 'fulfilled') {
121
+ return resolve(onFulfilled(this.firstValue));
122
+ }
123
+ if (this.state === 'rejected') {
124
+ onRejectedCalled = true;
125
+ return resolve(onRejected(this.rejection));
126
+ }
127
+ this.fulfillmentCallbacks.push(fulfillmentCallback);
128
+ this.rejectionCallbacks.push(rejectionCallback);
129
+ });
130
+ }
131
+ catch(onRejected) {
132
+ return this.then(undefined, onRejected);
133
+ }
134
+ finally(onCompleted) {
135
+ const handler = onCompleted || doNothing;
136
+ return this.then((value) => {
137
+ handler();
138
+ return value;
139
+ }, () => handler());
140
+ }
141
+ static from(thing) {
142
+ return isThenable(thing) ?
143
+ new ObservablePromise((observer) => {
144
+ const onFulfilled = (value) => {
145
+ observer.next(value);
146
+ observer.complete();
147
+ };
148
+ const onRejected = (error) => {
149
+ observer.error(error);
150
+ };
151
+ thing.then(onFulfilled, onRejected);
152
+ })
153
+ : super.from(thing);
154
+ }
155
+ }
156
+ exports.ObservablePromise = ObservablePromise;