@xylabs/threads 4.7.7 → 4.7.8

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.
@@ -0,0 +1,234 @@
1
+ /* eslint-disable import-x/no-internal-modules */
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
+ /* eslint-disable @typescript-eslint/no-floating-promises */
4
+
5
+ import isSomeObservable from 'is-observable-2-1-0'
6
+ import type { Observable, Subscription } from 'observable-fns'
7
+
8
+ import { deserialize, serialize } from '../common.ts'
9
+ import type { TransferDescriptor } from '../transferable.ts'
10
+ import { isTransferDescriptor } from '../transferable.ts'
11
+ import type {
12
+ MasterJobCancelMessage,
13
+ MasterJobRunMessage,
14
+ SerializedError,
15
+ WorkerInitMessage,
16
+ WorkerJobErrorMessage,
17
+ WorkerJobResultMessage,
18
+ WorkerJobStartMessage,
19
+ WorkerUncaughtErrorMessage,
20
+ } from '../types/messages.ts'
21
+ import {
22
+ MasterMessageType,
23
+ WorkerMessageType,
24
+ } from '../types/messages.ts'
25
+ import type {
26
+ AbstractedWorkerAPI, WorkerFunction, WorkerModule,
27
+ } from '../types/worker.ts'
28
+ import type { WorkerGlobalScope } from './WorkerGlobalScope.ts'
29
+
30
+ const isErrorEvent = (value: Event): value is ErrorEvent => value && (value as ErrorEvent).error
31
+
32
+ export function createExpose(implementation: AbstractedWorkerAPI, self: WorkerGlobalScope) {
33
+ let exposeCalled = false
34
+
35
+ const activeSubscriptions = new Map<number, Subscription<any>>()
36
+
37
+ const isMasterJobCancelMessage = (thing: any): thing is MasterJobCancelMessage => thing && thing.type === MasterMessageType.cancel
38
+ const isMasterJobRunMessage = (thing: any): thing is MasterJobRunMessage => thing && thing.type === MasterMessageType.run
39
+
40
+ /**
41
+ * There are issues with `is-observable` not recognizing zen-observable's instances.
42
+ * We are using `observable-fns`, but it's based on zen-observable, too.
43
+ */
44
+ const isObservable = (thing: any): thing is Observable<any> => isSomeObservable(thing) || isZenObservable(thing)
45
+
46
+ function isZenObservable(thing: any): thing is Observable<any> {
47
+ return thing && typeof thing === 'object' && typeof thing.subscribe === 'function'
48
+ }
49
+
50
+ function deconstructTransfer(thing: any) {
51
+ return isTransferDescriptor(thing) ? { payload: thing.send, transferables: thing.transferables } : { payload: thing, transferables: undefined }
52
+ }
53
+
54
+ function postFunctionInitMessage() {
55
+ const initMessage: WorkerInitMessage = {
56
+ exposed: { type: 'function' },
57
+ type: WorkerMessageType.init,
58
+ }
59
+ implementation.postMessageToMaster(initMessage)
60
+ }
61
+
62
+ function postModuleInitMessage(methodNames: string[]) {
63
+ const initMessage: WorkerInitMessage = {
64
+ exposed: {
65
+ methods: methodNames,
66
+ type: 'module',
67
+ },
68
+ type: WorkerMessageType.init,
69
+ }
70
+ implementation.postMessageToMaster(initMessage)
71
+ }
72
+
73
+ function postJobErrorMessage(uid: number, rawError: Error | TransferDescriptor<Error>) {
74
+ const { payload: error, transferables } = deconstructTransfer(rawError)
75
+ const errorMessage: WorkerJobErrorMessage = {
76
+ error: serialize(error) as any as SerializedError,
77
+ type: WorkerMessageType.error,
78
+ uid,
79
+ }
80
+ implementation.postMessageToMaster(errorMessage, transferables)
81
+ }
82
+
83
+ function postJobResultMessage(uid: number, completed: boolean, resultValue?: any) {
84
+ const { payload, transferables } = deconstructTransfer(resultValue)
85
+ const resultMessage: WorkerJobResultMessage = {
86
+ complete: completed ? true : undefined,
87
+ payload,
88
+ type: WorkerMessageType.result,
89
+ uid,
90
+ }
91
+ implementation.postMessageToMaster(resultMessage, transferables)
92
+ }
93
+
94
+ function postJobStartMessage(uid: number, resultType: WorkerJobStartMessage['resultType']) {
95
+ const startMessage: WorkerJobStartMessage = {
96
+ resultType,
97
+ type: WorkerMessageType.running,
98
+ uid,
99
+ }
100
+ implementation.postMessageToMaster(startMessage)
101
+ }
102
+
103
+ function postUncaughtErrorMessage(error: Error) {
104
+ try {
105
+ const errorMessage: WorkerUncaughtErrorMessage = {
106
+ error: serialize(error) as any as SerializedError,
107
+ type: WorkerMessageType.uncaughtError,
108
+ }
109
+ implementation.postMessageToMaster(errorMessage)
110
+ } catch (subError) {
111
+ // tslint:disable-next-line no-console
112
+ console.error(
113
+ 'Not reporting uncaught error back to master thread as it ' + 'occured while reporting an uncaught error already.' + '\nLatest error:',
114
+ subError,
115
+ '\nOriginal error:',
116
+ error,
117
+ )
118
+ }
119
+ }
120
+
121
+ async function runFunction(jobUID: number, fn: WorkerFunction, args: any[]) {
122
+ let syncResult: any
123
+
124
+ try {
125
+ syncResult = fn(...args)
126
+ } catch (ex) {
127
+ const error = ex as Error
128
+ return postJobErrorMessage(jobUID, error)
129
+ }
130
+
131
+ const resultType = isObservable(syncResult) ? 'observable' : 'promise'
132
+ postJobStartMessage(jobUID, resultType)
133
+
134
+ if (isObservable(syncResult)) {
135
+ const subscription = syncResult.subscribe(
136
+ value => postJobResultMessage(jobUID, false, serialize(value)),
137
+ (error) => {
138
+ postJobErrorMessage(jobUID, serialize(error) as any)
139
+ activeSubscriptions.delete(jobUID)
140
+ },
141
+ () => {
142
+ postJobResultMessage(jobUID, true)
143
+ activeSubscriptions.delete(jobUID)
144
+ },
145
+ )
146
+ activeSubscriptions.set(jobUID, subscription)
147
+ } else {
148
+ try {
149
+ const result = await syncResult
150
+ postJobResultMessage(jobUID, true, serialize(result))
151
+ } catch (error) {
152
+ postJobErrorMessage(jobUID, serialize(error) as any)
153
+ }
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Expose a function or a module (an object whose values are functions)
159
+ * to the main thread. Must be called exactly once in every worker thread
160
+ * to signal its API to the main thread.
161
+ *
162
+ * @param exposed Function or object whose values are functions
163
+ */
164
+ const expose = (exposed: WorkerFunction | WorkerModule<any>) => {
165
+ if (!implementation.isWorkerRuntime()) {
166
+ throw new Error('expose() called in the master thread.')
167
+ }
168
+ if (exposeCalled) {
169
+ throw new Error('expose() called more than once. This is not possible. Pass an object to expose() if you want to expose multiple functions.')
170
+ }
171
+ exposeCalled = true
172
+
173
+ if (typeof exposed === 'function') {
174
+ implementation.subscribeToMasterMessages((messageData: unknown) => {
175
+ if (isMasterJobRunMessage(messageData) && !messageData.method) {
176
+ runFunction(messageData.uid, exposed, messageData.args.map(deserialize))
177
+ }
178
+ })
179
+ postFunctionInitMessage()
180
+ } else if (typeof exposed === 'object' && exposed) {
181
+ implementation.subscribeToMasterMessages((messageData: unknown) => {
182
+ if (isMasterJobRunMessage(messageData) && messageData.method) {
183
+ runFunction(messageData.uid, exposed[messageData.method], messageData.args.map(deserialize))
184
+ }
185
+ })
186
+
187
+ const methodNames = Object.keys(exposed).filter(key => typeof exposed[key] === 'function')
188
+ postModuleInitMessage(methodNames)
189
+ } else {
190
+ throw new Error(`Invalid argument passed to expose(). Expected a function or an object, got: ${exposed}`)
191
+ }
192
+
193
+ implementation.subscribeToMasterMessages((messageData: unknown) => {
194
+ if (isMasterJobCancelMessage(messageData)) {
195
+ const jobUID = messageData.uid
196
+ const subscription = activeSubscriptions.get(jobUID)
197
+
198
+ if (subscription) {
199
+ subscription.unsubscribe()
200
+ activeSubscriptions.delete(jobUID)
201
+ }
202
+ }
203
+ })
204
+ }
205
+
206
+ if (typeof globalThis !== 'undefined' && typeof self.addEventListener === 'function' && implementation.isWorkerRuntime()) {
207
+ self.addEventListener('error', (event) => {
208
+ // Post with some delay, so the master had some time to subscribe to messages
209
+ setTimeout(() => postUncaughtErrorMessage(isErrorEvent(event) ? event.error : event), 250)
210
+ })
211
+ self.addEventListener('unhandledrejection', (event) => {
212
+ const error = (event as any).reason
213
+ if (error && typeof (error as any).message === 'string') {
214
+ // Post with some delay, so the master had some time to subscribe to messages
215
+ setTimeout(() => postUncaughtErrorMessage(error), 250)
216
+ }
217
+ })
218
+ }
219
+
220
+ if (typeof process !== 'undefined' && typeof process.on === 'function' && implementation.isWorkerRuntime()) {
221
+ process.on('uncaughtException', (error) => {
222
+ // Post with some delay, so the master had some time to subscribe to messages
223
+ setTimeout(() => postUncaughtErrorMessage(error), 250)
224
+ })
225
+ process.on('unhandledRejection', (error) => {
226
+ if (error && typeof (error as any).message === 'string') {
227
+ // Post with some delay, so the master had some time to subscribe to messages
228
+ setTimeout(() => postUncaughtErrorMessage(error as any), 250)
229
+ }
230
+ })
231
+ }
232
+
233
+ return expose
234
+ }
@@ -0,0 +1,7 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ declare module 'is-observable-2-1-0' {
3
+ import type { Observable } from 'observable-fns'
4
+
5
+ function isObservable(thing: any): thing is Observable<any>
6
+ export = isObservable
7
+ }
@@ -0,0 +1,56 @@
1
+ /* eslint-disable import-x/no-internal-modules */
2
+
3
+ /// <reference lib="webworker" />
4
+ // tslint:disable no-shadowed-variable
5
+
6
+ import type { AbstractedWorkerAPI } from '../types/worker.ts'
7
+ import { createExpose } from './expose.ts'
8
+ import type { WorkerGlobalScope } from './WorkerGlobalScope.ts'
9
+
10
+ declare const self: WorkerGlobalScope
11
+
12
+ const isWorkerRuntime: AbstractedWorkerAPI['isWorkerRuntime'] = function isWorkerRuntime() {
13
+ const isWindowContext = self !== undefined && typeof Window !== 'undefined' && self instanceof Window
14
+ return self !== undefined && self['postMessage'] && !isWindowContext ? true : false
15
+ }
16
+
17
+ const postMessageToMaster: AbstractedWorkerAPI['postMessageToMaster'] = function postMessageToMaster(data, transferList?) {
18
+ self.postMessage(data, transferList)
19
+ }
20
+
21
+ const subscribeToMasterMessages: AbstractedWorkerAPI['subscribeToMasterMessages'] = function subscribeToMasterMessages(onMessage) {
22
+ const messageHandler = (messageEvent: MessageEvent) => {
23
+ onMessage(messageEvent.data)
24
+ }
25
+ const unsubscribe = () => {
26
+ self.removeEventListener('message', messageHandler as EventListener)
27
+ }
28
+ self.addEventListener('message', messageHandler as EventListener)
29
+ return unsubscribe
30
+ }
31
+
32
+ const addEventListener = self.addEventListener.bind(self)
33
+ const postMessage = self.postMessage.bind(self)
34
+ const removeEventListener = self.removeEventListener.bind(self)
35
+
36
+ export {
37
+ addEventListener,
38
+ postMessage,
39
+ removeEventListener,
40
+ }
41
+
42
+ const expose = createExpose({
43
+ isWorkerRuntime, postMessageToMaster, subscribeToMasterMessages,
44
+ }, {
45
+ addEventListener, postMessage, removeEventListener,
46
+ })
47
+
48
+ export {
49
+ isWorkerRuntime,
50
+ postMessageToMaster,
51
+ subscribeToMasterMessages,
52
+ }
53
+
54
+ export { registerSerializer } from '../common.ts'
55
+ export { Transfer } from '../transferable.ts'
56
+ export { expose }
@@ -0,0 +1,68 @@
1
+ /* eslint-disable import-x/no-internal-modules */
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
+ // tslint:disable no-shadowed-variable
4
+
5
+ import type { MessagePort, TransferListItem } from 'node:worker_threads'
6
+ import { parentPort as optionalParentPort } from 'node:worker_threads'
7
+
8
+ import { assertEx } from '@xylabs/assert'
9
+
10
+ import type { AbstractedWorkerAPI } from '../types/worker.ts'
11
+ import { createExpose } from './expose.ts'
12
+
13
+ const parentPort = assertEx(optionalParentPort, () => 'Invariant violation: MessagePort to parent is not available.')
14
+
15
+ function assertMessagePort(port: MessagePort | null | undefined): MessagePort {
16
+ if (!port) {
17
+ throw new Error('Invariant violation: MessagePort to parent is not available.')
18
+ }
19
+ return port
20
+ }
21
+
22
+ const isWorkerRuntime: AbstractedWorkerAPI['isWorkerRuntime'] = function isWorkerRuntime() {
23
+ return true // isMainThread
24
+ }
25
+
26
+ const postMessageToMaster: AbstractedWorkerAPI['postMessageToMaster'] = function postMessageToMaster(data, transferList) {
27
+ assertMessagePort(parentPort).postMessage(data, transferList as TransferListItem[])
28
+ }
29
+
30
+ const subscribeToMasterMessages: AbstractedWorkerAPI['subscribeToMasterMessages'] = function subscribeToMasterMessages(onMessage) {
31
+ if (!parentPort) {
32
+ throw new Error('Invariant violation: MessagePort to parent is not available.')
33
+ }
34
+ const messageHandler = (message: any) => {
35
+ onMessage(message)
36
+ }
37
+ const unsubscribe = () => {
38
+ assertMessagePort(parentPort).off('message', messageHandler)
39
+ }
40
+ assertMessagePort(parentPort).on('message', messageHandler)
41
+ return unsubscribe
42
+ }
43
+
44
+ const addEventListener = parentPort?.on.bind(parentPort)
45
+ const postMessage = parentPort?.postMessage.bind(parentPort)
46
+ const removeEventListener = parentPort?.off.bind(parentPort)
47
+
48
+ export {
49
+ addEventListener,
50
+ postMessage,
51
+ removeEventListener,
52
+ }
53
+
54
+ const expose = createExpose({
55
+ isWorkerRuntime, postMessageToMaster, subscribeToMasterMessages,
56
+ }, {
57
+ addEventListener, postMessage, removeEventListener,
58
+ })
59
+
60
+ export {
61
+ isWorkerRuntime,
62
+ postMessageToMaster,
63
+ subscribeToMasterMessages,
64
+ }
65
+
66
+ export { registerSerializer } from '../common.ts'
67
+ export { Transfer } from '../transferable.ts'
68
+ export { expose }
package/CHANGELOG.md DELETED
@@ -1,11 +0,0 @@
1
- # Changelog - threads.js
2
-
3
- ## v1.0
4
-
5
- For newer versions of `threads`, visit the [releases page](https://github.com/andywer/threads.js/releases) on GitHub.
6
-
7
- You will find a description of each release's changes there.
8
-
9
- ## v0.x
10
-
11
- For old versions of `threads`, see the [CHANGELOG.md on the `v0` branch](https://github.com/andywer/threads.js/blob/v0/CHANGELOG.md).