@xylabs/threads 4.8.0-rc.2 → 4.8.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/dist/browser/index-browser.mjs +827 -0
- package/dist/browser/index-browser.mjs.map +1 -0
- package/dist/browser/master/index-browser.mjs +793 -0
- package/dist/browser/master/index-browser.mjs.map +1 -0
- package/dist/browser/master/pool-browser.mjs +303 -0
- package/dist/browser/master/pool-browser.mjs.map +1 -0
- package/dist/browser/worker/worker.browser.mjs +3 -3
- package/dist/browser/worker/worker.browser.mjs.map +1 -1
- package/dist/neutral/master/register.mjs +21 -131
- package/dist/neutral/master/register.mjs.map +1 -1
- package/dist/{neutral/master/index.mjs → node/index-node.mjs} +81 -174
- package/dist/node/index-node.mjs.map +1 -0
- package/dist/{neutral/index.mjs → node/master/index-node.mjs} +56 -217
- package/dist/node/master/index-node.mjs.map +1 -0
- package/dist/{neutral/master/pool.mjs → node/master/pool-node.mjs} +22 -152
- package/dist/node/master/pool-node.mjs.map +1 -0
- package/dist/types/index-browser.d.ts +9 -0
- package/dist/types/index-browser.d.ts.map +1 -0
- package/dist/types/{index.d.ts → index-node.d.ts} +3 -3
- package/dist/types/index-node.d.ts.map +1 -0
- package/dist/types/master/index-browser.d.ts +13 -0
- package/dist/types/master/index-browser.d.ts.map +1 -0
- package/dist/types/master/{index.d.ts → index-node.d.ts} +3 -3
- package/dist/types/master/index-node.d.ts.map +1 -0
- package/dist/types/master/pool-browser.d.ts +93 -0
- package/dist/types/master/pool-browser.d.ts.map +1 -0
- package/dist/types/master/{pool.d.ts → pool-node.d.ts} +1 -1
- package/dist/types/master/pool-node.d.ts.map +1 -0
- package/dist/types/worker/worker.browser.d.ts.map +1 -1
- package/dist/types/worker/worker.node.d.ts.map +1 -1
- package/package.json +37 -27
- package/src/common.ts +23 -0
- package/src/index-browser.ts +11 -0
- package/src/index-node.ts +11 -0
- package/src/master/get-bundle-url.browser.ts +32 -0
- package/src/master/implementation.browser.ts +82 -0
- package/src/master/implementation.node.ts +208 -0
- package/src/master/index-browser.ts +19 -0
- package/src/master/index-node.ts +19 -0
- package/src/master/invocation-proxy.ts +151 -0
- package/src/master/pool-browser.ts +399 -0
- package/src/master/pool-node.ts +399 -0
- package/src/master/pool-types.ts +83 -0
- package/src/master/register.ts +11 -0
- package/src/master/spawn.ts +172 -0
- package/src/master/thread.ts +29 -0
- package/src/observable-promise.ts +184 -0
- package/src/observable.ts +44 -0
- package/src/promise.ts +26 -0
- package/src/serializers.ts +68 -0
- package/src/symbols.ts +5 -0
- package/src/transferable.ts +69 -0
- package/src/types/master.ts +132 -0
- package/src/types/messages.ts +72 -0
- package/src/types/worker.ts +14 -0
- package/src/worker/WorkerGlobalScope.ts +5 -0
- package/src/worker/expose.ts +234 -0
- package/src/worker/is-observable.d.ts +7 -0
- package/src/worker/worker.browser.ts +56 -0
- package/src/worker/worker.node.ts +68 -0
- package/CHANGELOG.md +0 -11
- package/dist/neutral/index.mjs.map +0 -1
- package/dist/neutral/master/implementation.mjs +0 -264
- package/dist/neutral/master/implementation.mjs.map +0 -1
- package/dist/neutral/master/index.mjs.map +0 -1
- package/dist/neutral/master/pool.mjs.map +0 -1
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/master/implementation.d.ts +0 -7
- package/dist/types/master/implementation.d.ts.map +0 -1
- package/dist/types/master/index.d.ts.map +0 -1
- package/dist/types/master/pool.d.ts.map +0 -1
- package/dist/types/ponyfills.d.ts +0 -9
- package/dist/types/ponyfills.d.ts.map +0 -1
package/package.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xylabs/threads",
|
|
3
|
-
"version": "4.8.0
|
|
4
|
-
"stableVersion": "4.7.1",
|
|
3
|
+
"version": "4.8.0",
|
|
5
4
|
"description": "Web workers & worker threads as simple as a function call",
|
|
6
5
|
"keywords": [
|
|
7
6
|
"thread",
|
|
@@ -24,17 +23,18 @@
|
|
|
24
23
|
"funding": "https://github.com/andywer/threads.js?sponsor=1",
|
|
25
24
|
"license": "MIT",
|
|
26
25
|
"author": "Andy Wermke (https://github.com/andywer)",
|
|
27
|
-
"sideEffects":
|
|
28
|
-
"./dist*/master/register.js",
|
|
29
|
-
"./dist*/worker/index.js",
|
|
30
|
-
"./register.*js",
|
|
31
|
-
"./worker.*js"
|
|
32
|
-
],
|
|
26
|
+
"sideEffects": false,
|
|
33
27
|
"type": "module",
|
|
34
28
|
"exports": {
|
|
35
29
|
".": {
|
|
36
|
-
"
|
|
37
|
-
|
|
30
|
+
"browser": {
|
|
31
|
+
"types": "./dist/types/index-browser.d.ts",
|
|
32
|
+
"import": "./dist/browser/index-browser.mjs"
|
|
33
|
+
},
|
|
34
|
+
"node": {
|
|
35
|
+
"types": "./dist/types/index-node.d.ts",
|
|
36
|
+
"import": "./dist/node/index-node.mjs"
|
|
37
|
+
}
|
|
38
38
|
},
|
|
39
39
|
"./implementation": {
|
|
40
40
|
"browser": {
|
|
@@ -44,13 +44,17 @@
|
|
|
44
44
|
"node": {
|
|
45
45
|
"types": "./dist/types/master/implementation.node.d.ts",
|
|
46
46
|
"import": "./dist/node/master/implementation.node.mjs"
|
|
47
|
-
}
|
|
48
|
-
"types": "./dist/types/master/implementation.d.ts",
|
|
49
|
-
"import": "./dist/neutral/master/implementation.mjs"
|
|
47
|
+
}
|
|
50
48
|
},
|
|
51
49
|
"./master": {
|
|
52
|
-
"
|
|
53
|
-
|
|
50
|
+
"browser": {
|
|
51
|
+
"types": "./dist/types/master/index-browser.d.ts",
|
|
52
|
+
"import": "./dist/browser/master/index-browser.mjs"
|
|
53
|
+
},
|
|
54
|
+
"node": {
|
|
55
|
+
"types": "./dist/types/master/index-node.d.ts",
|
|
56
|
+
"import": "./dist/node/master/index-node.mjs"
|
|
57
|
+
}
|
|
54
58
|
},
|
|
55
59
|
"./spawn": {
|
|
56
60
|
"types": "./dist/types/master/spawn.d.ts",
|
|
@@ -68,8 +72,14 @@
|
|
|
68
72
|
"import": "./dist/neutral/observable.mjs"
|
|
69
73
|
},
|
|
70
74
|
"./pool": {
|
|
71
|
-
"
|
|
72
|
-
|
|
75
|
+
"browser": {
|
|
76
|
+
"types": "./dist/types/master/pool-browser.d.ts",
|
|
77
|
+
"import": "./dist/browser/master/pool-browser.mjs"
|
|
78
|
+
},
|
|
79
|
+
"node": {
|
|
80
|
+
"types": "./dist/types/master/pool-node.d.ts",
|
|
81
|
+
"import": "./dist/node/master/pool-node.mjs"
|
|
82
|
+
}
|
|
73
83
|
},
|
|
74
84
|
"./observable-promise": {
|
|
75
85
|
"types": "./dist/types/observable-promise.d.ts",
|
|
@@ -90,10 +100,13 @@
|
|
|
90
100
|
}
|
|
91
101
|
}
|
|
92
102
|
},
|
|
93
|
-
"module": "dist/
|
|
94
|
-
"types": "dist/types/index.d.ts",
|
|
103
|
+
"module": "dist/browser/index-browser.mjs",
|
|
104
|
+
"types": "dist/types/index-browser.d.ts",
|
|
95
105
|
"files": [
|
|
96
|
-
"dist"
|
|
106
|
+
"dist",
|
|
107
|
+
"src",
|
|
108
|
+
"README.md",
|
|
109
|
+
"LICENSE"
|
|
97
110
|
],
|
|
98
111
|
"dependencies": {
|
|
99
112
|
"debug": "^4.4.0",
|
|
@@ -102,13 +115,10 @@
|
|
|
102
115
|
},
|
|
103
116
|
"devDependencies": {
|
|
104
117
|
"@types/debug": "^4.1.12",
|
|
105
|
-
"@types/node": "^22.
|
|
106
|
-
"@xylabs/eslint-config-flat": "^6.1
|
|
107
|
-
"@xylabs/ts-scripts-yarn3": "^6.1
|
|
108
|
-
"typescript": "^5.8.
|
|
109
|
-
},
|
|
110
|
-
"optionalDependencies": {
|
|
111
|
-
"tiny-worker": "^2.3.0"
|
|
118
|
+
"@types/node": "^22.14.0",
|
|
119
|
+
"@xylabs/eslint-config-flat": "^6.2.1",
|
|
120
|
+
"@xylabs/ts-scripts-yarn3": "^6.2.1",
|
|
121
|
+
"typescript": "^5.8.3"
|
|
112
122
|
},
|
|
113
123
|
"publishConfig": {
|
|
114
124
|
"access": "public"
|
package/src/common.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import type {
|
|
3
|
+
JsonSerializable, Serializer, SerializerImplementation,
|
|
4
|
+
} from './serializers.ts'
|
|
5
|
+
import { DefaultSerializer, extendSerializer } from './serializers.ts'
|
|
6
|
+
|
|
7
|
+
declare global {
|
|
8
|
+
var registeredSerializer: Serializer<JsonSerializable>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
globalThis.registeredSerializer = globalThis.registeredSerializer ?? DefaultSerializer
|
|
12
|
+
|
|
13
|
+
export function registerSerializer(serializer: SerializerImplementation<JsonSerializable>) {
|
|
14
|
+
globalThis.registeredSerializer = extendSerializer(globalThis.registeredSerializer, serializer)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function deserialize(message: JsonSerializable): any {
|
|
18
|
+
return globalThis.registeredSerializer.deserialize(message)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function serialize(input: any): JsonSerializable {
|
|
22
|
+
return globalThis.registeredSerializer.serialize(input)
|
|
23
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/* eslint-disable import-x/no-internal-modules */
|
|
2
|
+
export { registerSerializer } from './common.ts'
|
|
3
|
+
export * from './master/index-browser.ts'
|
|
4
|
+
export type { QueuedTask } from './master/pool-node.ts'
|
|
5
|
+
export type { ExposedToThreadType as ExposedAs } from './master/spawn.ts'
|
|
6
|
+
export type {
|
|
7
|
+
JsonSerializable, Serializer, SerializerImplementation,
|
|
8
|
+
} from './serializers.ts'
|
|
9
|
+
export { DefaultSerializer } from './serializers.ts'
|
|
10
|
+
export type { TransferDescriptor } from './transferable.ts'
|
|
11
|
+
export { Transfer } from './transferable.ts'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/* eslint-disable import-x/no-internal-modules */
|
|
2
|
+
export { registerSerializer } from './common.ts'
|
|
3
|
+
export * from './master/index-node.ts'
|
|
4
|
+
export type { QueuedTask } from './master/pool-node.ts'
|
|
5
|
+
export type { ExposedToThreadType as ExposedAs } from './master/spawn.ts'
|
|
6
|
+
export type {
|
|
7
|
+
JsonSerializable, Serializer, SerializerImplementation,
|
|
8
|
+
} from './serializers.ts'
|
|
9
|
+
export { DefaultSerializer } from './serializers.ts'
|
|
10
|
+
export type { TransferDescriptor } from './transferable.ts'
|
|
11
|
+
export { Transfer } from './transferable.ts'
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Source: <https://github.com/parcel-bundler/parcel/blob/master/packages/core/parcel-bundler/src/builtins/bundle-url.js>
|
|
2
|
+
|
|
3
|
+
let bundleURL: string | undefined
|
|
4
|
+
|
|
5
|
+
function getBundleURLCached(): string {
|
|
6
|
+
if (!bundleURL) {
|
|
7
|
+
bundleURL = getBundleURL()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return bundleURL
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getBundleURL(): string {
|
|
14
|
+
// Attempt to find the URL of the current script and use that as the base URL
|
|
15
|
+
try {
|
|
16
|
+
throw new Error('getBundleURL failed')
|
|
17
|
+
} catch (ex) {
|
|
18
|
+
const err = ex as Error
|
|
19
|
+
const matches = ('' + err.stack).match(/(https?|file|ftp|chrome-extension|moz-extension):\/\/[^\n)]+/g)
|
|
20
|
+
if (matches) {
|
|
21
|
+
return getBaseURL(matches[0])
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return '/'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getBaseURL(url: string): string {
|
|
29
|
+
return ('' + url).replace(/^((?:https?|file|ftp|chrome-extension|moz-extension):\/\/.+)?\/[^/]+(?:\?.*)?$/, '$1') + '/'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { getBundleURLCached as getBundleURL }
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/* eslint-disable @stylistic/max-len */
|
|
2
|
+
/* eslint-disable import-x/no-internal-modules */
|
|
3
|
+
// tslint:disable max-classes-per-file
|
|
4
|
+
|
|
5
|
+
import type { ImplementationExport, ThreadsWorkerOptions } from '../types/master.ts'
|
|
6
|
+
import { getBundleURL } from './get-bundle-url.browser.ts'
|
|
7
|
+
|
|
8
|
+
export const defaultPoolSize = typeof navigator !== 'undefined' && navigator.hardwareConcurrency ? navigator.hardwareConcurrency : 4
|
|
9
|
+
|
|
10
|
+
const isAbsoluteURL = (value: string) => /^[A-Za-z][\d+.A-Za-z\-]*:/.test(value)
|
|
11
|
+
|
|
12
|
+
function createSourceBlobURL(code: string): string {
|
|
13
|
+
const blob = new Blob([code], { type: 'application/javascript' })
|
|
14
|
+
return URL.createObjectURL(blob)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function selectWorkerImplementation(): ImplementationExport {
|
|
18
|
+
if (typeof Worker === 'undefined') {
|
|
19
|
+
// Might happen on Safari, for instance
|
|
20
|
+
// The idea is to only fail if the constructor is actually used
|
|
21
|
+
return class NoWebWorker {
|
|
22
|
+
constructor() {
|
|
23
|
+
throw new Error(
|
|
24
|
+
"No web worker implementation available. You might have tried to spawn a worker within a worker in a browser that doesn't support workers in workers.",
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
} as unknown as ImplementationExport
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class WebWorker extends Worker {
|
|
31
|
+
constructor(url: string | URL, options?: ThreadsWorkerOptions) {
|
|
32
|
+
if (typeof url === 'string' && options && options._baseURL) {
|
|
33
|
+
url = new URL(url, options._baseURL)
|
|
34
|
+
} else if (typeof url === 'string' && !isAbsoluteURL(url) && /^file:\/\//i.test(getBundleURL())) {
|
|
35
|
+
url = new URL(url, getBundleURL().replace(/\/[^/]+$/, '/'))
|
|
36
|
+
if (options?.CORSWorkaround ?? true) {
|
|
37
|
+
url = createSourceBlobURL(`importScripts(${JSON.stringify(url)});`)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (
|
|
41
|
+
typeof url === 'string'
|
|
42
|
+
&& isAbsoluteURL(url) // Create source code blob loading JS file via `importScripts()`
|
|
43
|
+
// to circumvent worker CORS restrictions
|
|
44
|
+
&& (options?.CORSWorkaround ?? true)
|
|
45
|
+
) {
|
|
46
|
+
url = createSourceBlobURL(`importScripts(${JSON.stringify(url)});`)
|
|
47
|
+
}
|
|
48
|
+
super(url, options)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class BlobWorker extends WebWorker {
|
|
53
|
+
constructor(blob: Blob, options?: ThreadsWorkerOptions) {
|
|
54
|
+
const url = globalThis.URL.createObjectURL(blob)
|
|
55
|
+
super(url, options)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static fromText(source: string, options?: ThreadsWorkerOptions): WebWorker {
|
|
59
|
+
const blob = new globalThis.Blob([source], { type: 'text/javascript' })
|
|
60
|
+
return new BlobWorker(blob, options)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
blob: BlobWorker,
|
|
66
|
+
default: WebWorker,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let implementation: ImplementationExport
|
|
71
|
+
|
|
72
|
+
export function getWorkerImplementation(): ImplementationExport {
|
|
73
|
+
if (!implementation) {
|
|
74
|
+
implementation = selectWorkerImplementation()
|
|
75
|
+
}
|
|
76
|
+
return implementation
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function isWorkerRuntime() {
|
|
80
|
+
const isWindowContext = typeof globalThis !== 'undefined' && typeof Window !== 'undefined' && globalThis instanceof Window
|
|
81
|
+
return typeof globalThis !== 'undefined' && self['postMessage'] && !isWindowContext ? true : false
|
|
82
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
2
|
+
|
|
3
|
+
/* eslint-disable unicorn/prefer-add-event-listener */
|
|
4
|
+
/* eslint-disable unicorn/prefer-event-target */
|
|
5
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
6
|
+
/* eslint-disable unicorn/text-encoding-identifier-case */
|
|
7
|
+
|
|
8
|
+
import { EventEmitter } from 'node:events'
|
|
9
|
+
import { cpus } from 'node:os'
|
|
10
|
+
import path from 'node:path'
|
|
11
|
+
import { cwd } from 'node:process'
|
|
12
|
+
import { Worker as NativeWorker } from 'node:worker_threads'
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
ImplementationExport, ThreadsWorkerOptions, WorkerImplementation,
|
|
16
|
+
// eslint-disable-next-line import-x/no-internal-modules
|
|
17
|
+
} from '../types/master.ts'
|
|
18
|
+
|
|
19
|
+
declare const __non_webpack_require__: typeof require
|
|
20
|
+
|
|
21
|
+
type WorkerEventName = 'error' | 'message'
|
|
22
|
+
|
|
23
|
+
export const defaultPoolSize = cpus().length
|
|
24
|
+
|
|
25
|
+
function resolveScriptPath(scriptPath: string, baseURL?: string | undefined) {
|
|
26
|
+
const makeAbsolute = (filePath: string) => {
|
|
27
|
+
return path.isAbsolute(filePath) ? filePath : path.join(baseURL ?? cwd(), filePath)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const absolutePath = makeAbsolute(scriptPath)
|
|
31
|
+
return absolutePath
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function initWorkerThreadsWorker(): ImplementationExport {
|
|
35
|
+
let allWorkers: Array<NativeWorker> = []
|
|
36
|
+
|
|
37
|
+
class Worker extends NativeWorker {
|
|
38
|
+
private mappedEventListeners: WeakMap<EventListener, EventListener>
|
|
39
|
+
|
|
40
|
+
constructor(scriptPath: string, options?: ThreadsWorkerOptions & { fromSource: boolean }) {
|
|
41
|
+
const resolvedScriptPath = options && options.fromSource ? null : resolveScriptPath(scriptPath, (options ?? {})._baseURL)
|
|
42
|
+
if (resolvedScriptPath) {
|
|
43
|
+
super(resolvedScriptPath, options)
|
|
44
|
+
} else {
|
|
45
|
+
// `options.fromSource` is true
|
|
46
|
+
const sourceCode = scriptPath
|
|
47
|
+
super(sourceCode, { ...options, eval: true })
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.mappedEventListeners = new WeakMap()
|
|
51
|
+
allWorkers.push(this)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
addEventListener(eventName: string, rawListener: EventListener) {
|
|
55
|
+
const listener = (message: any) => {
|
|
56
|
+
rawListener({ data: message } as any)
|
|
57
|
+
}
|
|
58
|
+
this.mappedEventListeners.set(rawListener, listener)
|
|
59
|
+
this.on(eventName, listener)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
removeEventListener(eventName: string, rawListener: EventListener) {
|
|
63
|
+
const listener = this.mappedEventListeners.get(rawListener) || rawListener
|
|
64
|
+
this.off(eventName, listener)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const terminateWorkersAndMaster = () => {
|
|
69
|
+
// we should terminate all workers and then gracefully shutdown self process
|
|
70
|
+
Promise.all(allWorkers.map(worker => worker.terminate())).then(
|
|
71
|
+
() => process.exit(0),
|
|
72
|
+
() => process.exit(1),
|
|
73
|
+
)
|
|
74
|
+
allWorkers = []
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Take care to not leave orphaned processes behind. See #147.
|
|
78
|
+
process.on('SIGINT', () => terminateWorkersAndMaster())
|
|
79
|
+
process.on('SIGTERM', () => terminateWorkersAndMaster())
|
|
80
|
+
|
|
81
|
+
class BlobWorker extends Worker {
|
|
82
|
+
constructor(blob: Uint8Array, options?: ThreadsWorkerOptions) {
|
|
83
|
+
super(Buffer.from(blob).toString('utf-8'), { ...options, fromSource: true })
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
static fromText(source: string, options?: ThreadsWorkerOptions): WorkerImplementation {
|
|
87
|
+
return new Worker(source, { ...options, fromSource: true }) as any
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
blob: BlobWorker as any,
|
|
93
|
+
default: Worker as any,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function initTinyWorker(): ImplementationExport {
|
|
98
|
+
const TinyWorker = require('tiny-worker')
|
|
99
|
+
|
|
100
|
+
let allWorkers: Array<typeof TinyWorker> = []
|
|
101
|
+
|
|
102
|
+
class Worker extends TinyWorker {
|
|
103
|
+
private emitter: EventEmitter
|
|
104
|
+
|
|
105
|
+
constructor(scriptPath: string, options?: ThreadsWorkerOptions & { fromSource?: boolean }) {
|
|
106
|
+
// Need to apply a work-around for Windows or it will choke upon the absolute path
|
|
107
|
+
// (`Error [ERR_INVALID_PROTOCOL]: Protocol 'c:' not supported`)
|
|
108
|
+
const resolvedScriptPath
|
|
109
|
+
= options && options.fromSource
|
|
110
|
+
? null
|
|
111
|
+
: process.platform === 'win32'
|
|
112
|
+
? `file:///${resolveScriptPath(scriptPath).replaceAll('\\', '/')}`
|
|
113
|
+
: resolveScriptPath(scriptPath)
|
|
114
|
+
|
|
115
|
+
if (resolvedScriptPath) {
|
|
116
|
+
super(resolvedScriptPath, [], { esm: true })
|
|
117
|
+
} else {
|
|
118
|
+
// `options.fromSource` is true
|
|
119
|
+
const sourceCode = scriptPath
|
|
120
|
+
super(new Function(sourceCode), [], { esm: true })
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
allWorkers.push(this)
|
|
124
|
+
|
|
125
|
+
this.emitter = new EventEmitter()
|
|
126
|
+
this.onerror = (error: Error) => this.emitter.emit('error', error)
|
|
127
|
+
this.onmessage = (message: MessageEvent) => this.emitter.emit('message', message)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
addEventListener(eventName: WorkerEventName, listener: EventListener) {
|
|
131
|
+
this.emitter.addListener(eventName, listener)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
removeEventListener(eventName: WorkerEventName, listener: EventListener) {
|
|
135
|
+
this.emitter.removeListener(eventName, listener)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
terminate() {
|
|
139
|
+
allWorkers = allWorkers.filter(worker => worker !== this)
|
|
140
|
+
return super.terminate()
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const terminateWorkersAndMaster = () => {
|
|
145
|
+
// we should terminate all workers and then gracefully shutdown self process
|
|
146
|
+
Promise.all(allWorkers.map(worker => worker.terminate())).then(
|
|
147
|
+
() => process.exit(0),
|
|
148
|
+
() => process.exit(1),
|
|
149
|
+
)
|
|
150
|
+
allWorkers = []
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Take care to not leave orphaned processes behind
|
|
154
|
+
// See <https://github.com/avoidwork/tiny-worker#faq>
|
|
155
|
+
process.on('SIGINT', () => terminateWorkersAndMaster())
|
|
156
|
+
process.on('SIGTERM', () => terminateWorkersAndMaster())
|
|
157
|
+
|
|
158
|
+
class BlobWorker extends Worker {
|
|
159
|
+
constructor(blob: Uint8Array, options?: ThreadsWorkerOptions) {
|
|
160
|
+
super(Buffer.from(blob).toString('utf-8'), { ...options, fromSource: true })
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
static fromText(source: string, options?: ThreadsWorkerOptions): WorkerImplementation {
|
|
164
|
+
return new Worker(source, { ...options, fromSource: true }) as any
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
blob: BlobWorker as any,
|
|
170
|
+
default: Worker as any,
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let implementation: ImplementationExport
|
|
175
|
+
let isTinyWorker: boolean
|
|
176
|
+
|
|
177
|
+
function selectWorkerImplementation(): ImplementationExport {
|
|
178
|
+
try {
|
|
179
|
+
isTinyWorker = false
|
|
180
|
+
return initWorkerThreadsWorker()
|
|
181
|
+
} catch (ex) {
|
|
182
|
+
console.error(ex)
|
|
183
|
+
// tslint:disable-next-line no-console
|
|
184
|
+
console.debug('Node worker_threads not available. Trying to fall back to tiny-worker polyfill...')
|
|
185
|
+
isTinyWorker = true
|
|
186
|
+
return initTinyWorker()
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function getWorkerImplementation(): ImplementationExport {
|
|
191
|
+
if (!implementation) {
|
|
192
|
+
implementation = selectWorkerImplementation()
|
|
193
|
+
}
|
|
194
|
+
return implementation
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function isWorkerRuntime() {
|
|
198
|
+
if (isTinyWorker) {
|
|
199
|
+
return globalThis !== undefined && self['postMessage'] ? true : false
|
|
200
|
+
} else {
|
|
201
|
+
// Webpack hack
|
|
202
|
+
const isMainThread
|
|
203
|
+
= typeof __non_webpack_require__ === 'function'
|
|
204
|
+
? __non_webpack_require__('worker_threads').isMainThread
|
|
205
|
+
: eval('require')('worker_threads').isMainThread
|
|
206
|
+
return !isMainThread
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/* eslint-disable import-x/no-internal-modules */
|
|
2
|
+
import type { BlobWorker as BlobWorkerClass, Worker as WorkerType } from '../types/master.ts'
|
|
3
|
+
import * as BrowserImplementation from './implementation.browser.ts'
|
|
4
|
+
|
|
5
|
+
export type { FunctionThread, ModuleThread } from '../types/master.ts'
|
|
6
|
+
export { Pool } from './pool-browser.ts'
|
|
7
|
+
export { spawn } from './spawn.ts'
|
|
8
|
+
export { Thread } from './thread.ts'
|
|
9
|
+
|
|
10
|
+
export type BlobWorker = typeof BlobWorkerClass
|
|
11
|
+
export type Worker = WorkerType
|
|
12
|
+
|
|
13
|
+
/** Separate class to spawn workers from source code blobs or strings. */
|
|
14
|
+
export const BlobWorker = BrowserImplementation.getWorkerImplementation().blob
|
|
15
|
+
|
|
16
|
+
/** Worker implementation. Either web worker or a node.js Worker class. */
|
|
17
|
+
export const Worker = BrowserImplementation.getWorkerImplementation().default
|
|
18
|
+
|
|
19
|
+
export { isWorkerRuntime } from './implementation.browser.ts'
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/* eslint-disable import-x/no-internal-modules */
|
|
2
|
+
import type { BlobWorker as BlobWorkerClass, Worker as WorkerType } from '../types/master.ts'
|
|
3
|
+
import * as NodeImplementation from './implementation.node.ts'
|
|
4
|
+
|
|
5
|
+
export type { FunctionThread, ModuleThread } from '../types/master.ts'
|
|
6
|
+
export { Pool } from './pool-browser.ts'
|
|
7
|
+
export { spawn } from './spawn.ts'
|
|
8
|
+
export { Thread } from './thread.ts'
|
|
9
|
+
|
|
10
|
+
export type BlobWorker = typeof BlobWorkerClass
|
|
11
|
+
export type Worker = WorkerType
|
|
12
|
+
|
|
13
|
+
/** Separate class to spawn workers from source code blobs or strings. */
|
|
14
|
+
export const BlobWorker = NodeImplementation.getWorkerImplementation().blob
|
|
15
|
+
|
|
16
|
+
/** Worker implementation. Either web worker or a node.js Worker class. */
|
|
17
|
+
export const Worker = NodeImplementation.getWorkerImplementation().default
|
|
18
|
+
|
|
19
|
+
export { isWorkerRuntime } from './implementation.node.ts'
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/* eslint-disable import-x/no-internal-modules */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
+
/*
|
|
4
|
+
* This source file contains the code for proxying calls in the master thread to calls in the workers
|
|
5
|
+
* by `.postMessage()`-ing.
|
|
6
|
+
*
|
|
7
|
+
* Keep in mind that this code can make or break the program's performance! Need to optimize more…
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import DebugLogger from 'debug'
|
|
11
|
+
import { multicast, Observable } from 'observable-fns'
|
|
12
|
+
|
|
13
|
+
import { deserialize, serialize } from '../common.ts'
|
|
14
|
+
import { ObservablePromise } from '../observable-promise.ts'
|
|
15
|
+
import { isTransferDescriptor } from '../transferable.ts'
|
|
16
|
+
import type {
|
|
17
|
+
ModuleMethods, ModuleProxy, ProxyableFunction, Worker as WorkerType,
|
|
18
|
+
} from '../types/master.ts'
|
|
19
|
+
import type {
|
|
20
|
+
MasterJobCancelMessage,
|
|
21
|
+
MasterJobRunMessage,
|
|
22
|
+
WorkerJobErrorMessage,
|
|
23
|
+
WorkerJobResultMessage,
|
|
24
|
+
WorkerJobStartMessage,
|
|
25
|
+
} from '../types/messages.ts'
|
|
26
|
+
import {
|
|
27
|
+
MasterMessageType,
|
|
28
|
+
WorkerMessageType,
|
|
29
|
+
} from '../types/messages.ts'
|
|
30
|
+
|
|
31
|
+
const debugMessages = DebugLogger('threads:master:messages')
|
|
32
|
+
|
|
33
|
+
let nextJobUID = 1
|
|
34
|
+
|
|
35
|
+
const dedupe = <T>(array: T[]): T[] => [...new Set(array)]
|
|
36
|
+
|
|
37
|
+
const isJobErrorMessage = (data: any): data is WorkerJobErrorMessage => data && data.type === WorkerMessageType.error
|
|
38
|
+
const isJobResultMessage = (data: any): data is WorkerJobResultMessage => data && data.type === WorkerMessageType.result
|
|
39
|
+
const isJobStartMessage = (data: any): data is WorkerJobStartMessage => data && data.type === WorkerMessageType.running
|
|
40
|
+
|
|
41
|
+
function createObservableForJob<ResultType>(worker: WorkerType, jobUID: number): Observable<ResultType> {
|
|
42
|
+
return new Observable((observer) => {
|
|
43
|
+
let asyncType: 'observable' | 'promise' | undefined
|
|
44
|
+
|
|
45
|
+
const messageHandler = ((event: MessageEvent) => {
|
|
46
|
+
debugMessages('Message from worker:', event.data)
|
|
47
|
+
if (!event.data || event.data.uid !== jobUID) return
|
|
48
|
+
|
|
49
|
+
if (isJobStartMessage(event.data)) {
|
|
50
|
+
asyncType = event.data.resultType
|
|
51
|
+
} else if (isJobResultMessage(event.data)) {
|
|
52
|
+
if (asyncType === 'promise') {
|
|
53
|
+
if (event.data.payload !== undefined) {
|
|
54
|
+
observer.next(deserialize(event.data.payload))
|
|
55
|
+
}
|
|
56
|
+
observer.complete()
|
|
57
|
+
worker.removeEventListener('message', messageHandler)
|
|
58
|
+
} else {
|
|
59
|
+
if (event.data.payload) {
|
|
60
|
+
observer.next(deserialize(event.data.payload))
|
|
61
|
+
}
|
|
62
|
+
if (event.data.complete) {
|
|
63
|
+
observer.complete()
|
|
64
|
+
worker.removeEventListener('message', messageHandler)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} else if (isJobErrorMessage(event.data)) {
|
|
68
|
+
const error = deserialize(event.data.error as any)
|
|
69
|
+
if (asyncType === 'promise' || !asyncType) {
|
|
70
|
+
observer.error(error)
|
|
71
|
+
} else {
|
|
72
|
+
observer.error(error)
|
|
73
|
+
}
|
|
74
|
+
worker.removeEventListener('message', messageHandler)
|
|
75
|
+
}
|
|
76
|
+
}) as EventListener
|
|
77
|
+
|
|
78
|
+
worker.addEventListener('message', messageHandler)
|
|
79
|
+
|
|
80
|
+
return () => {
|
|
81
|
+
if (asyncType === 'observable' || !asyncType) {
|
|
82
|
+
const cancelMessage: MasterJobCancelMessage = {
|
|
83
|
+
type: MasterMessageType.cancel,
|
|
84
|
+
uid: jobUID,
|
|
85
|
+
}
|
|
86
|
+
worker.postMessage(cancelMessage)
|
|
87
|
+
}
|
|
88
|
+
worker.removeEventListener('message', messageHandler)
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function prepareArguments(rawArgs: any[]): { args: any[]; transferables: Transferable[] } {
|
|
94
|
+
if (rawArgs.length === 0) {
|
|
95
|
+
// Exit early if possible
|
|
96
|
+
return {
|
|
97
|
+
args: [],
|
|
98
|
+
transferables: [],
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const args: any[] = []
|
|
103
|
+
const transferables: Transferable[] = []
|
|
104
|
+
|
|
105
|
+
for (const arg of rawArgs) {
|
|
106
|
+
if (isTransferDescriptor(arg)) {
|
|
107
|
+
args.push(serialize(arg.send))
|
|
108
|
+
transferables.push(...arg.transferables)
|
|
109
|
+
} else {
|
|
110
|
+
args.push(serialize(arg))
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
args,
|
|
116
|
+
transferables: transferables.length === 0 ? transferables : dedupe(transferables),
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function createProxyFunction<Args extends any[], ReturnType>(worker: WorkerType, method?: string) {
|
|
121
|
+
return ((...rawArgs: Args) => {
|
|
122
|
+
const uid = nextJobUID++
|
|
123
|
+
const { args, transferables } = prepareArguments(rawArgs)
|
|
124
|
+
const runMessage: MasterJobRunMessage = {
|
|
125
|
+
args,
|
|
126
|
+
method,
|
|
127
|
+
type: MasterMessageType.run,
|
|
128
|
+
uid,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
debugMessages('Sending command to run function to worker:', runMessage)
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
worker.postMessage(runMessage, transferables)
|
|
135
|
+
} catch (error) {
|
|
136
|
+
return ObservablePromise.from(Promise.reject(error))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return ObservablePromise.from(multicast(createObservableForJob<ReturnType>(worker, uid)))
|
|
140
|
+
}) as any as ProxyableFunction<Args, ReturnType>
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function createProxyModule<Methods extends ModuleMethods>(worker: WorkerType, methodNames: string[]): ModuleProxy<Methods> {
|
|
144
|
+
const proxy: any = {}
|
|
145
|
+
|
|
146
|
+
for (const methodName of methodNames) {
|
|
147
|
+
proxy[methodName] = createProxyFunction(worker, methodName)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return proxy
|
|
151
|
+
}
|