Skip to content

Commit 79f0e7b

Browse files
committed
Implement BlobWorker
1 parent 5fd9188 commit 79f0e7b

File tree

4 files changed

+103
-23
lines changed

4 files changed

+103
-23
lines changed

src/master/implementation.browser.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// tslint:disable max-classes-per-file
22

3-
import { ThreadsWorkerOptions, WorkerImplementation } from "../types/master"
3+
import { ImplementationExport, ThreadsWorkerOptions } from "../types/master"
44
import { getBundleURL } from "./get-bundle-url.browser"
55

66
export const defaultPoolSize = typeof navigator !== "undefined" && navigator.hardwareConcurrency
@@ -17,7 +17,7 @@ function createSourceBlobURL(code: string): string {
1717
return URL.createObjectURL(blob)
1818
}
1919

20-
function selectWorkerImplementation(): typeof WorkerImplementation {
20+
function selectWorkerImplementation(): ImplementationExport {
2121
if (typeof Worker === "undefined") {
2222
// Might happen on Safari, for instance
2323
// The idea is to only fail if the constructor is actually used
@@ -28,7 +28,7 @@ function selectWorkerImplementation(): typeof WorkerImplementation {
2828
} as any
2929
}
3030

31-
return class WebWorker extends Worker {
31+
class WebWorker extends Worker {
3232
constructor(url: string | URL, options?: ThreadsWorkerOptions) {
3333
if (typeof url === "string" && options && options._baseURL) {
3434
url = new URL(url, options._baseURL)
@@ -44,11 +44,28 @@ function selectWorkerImplementation(): typeof WorkerImplementation {
4444
super(url, options)
4545
}
4646
}
47+
48+
class BlobWorker extends WebWorker {
49+
constructor(blob: Blob, options?: ThreadsWorkerOptions) {
50+
const url = window.URL.createObjectURL(blob)
51+
super(url, options)
52+
}
53+
54+
public static fromText(source: string, options?: ThreadsWorkerOptions): WebWorker {
55+
const blob = new window.Blob([source], { type: "text/javascript" })
56+
return new BlobWorker(blob, options)
57+
}
58+
}
59+
60+
return {
61+
blob: BlobWorker,
62+
default: WebWorker
63+
}
4764
}
4865

49-
let implementation: typeof WorkerImplementation
66+
let implementation: ImplementationExport
5067

51-
export function getWorkerImplementation(): typeof WorkerImplementation {
68+
export function getWorkerImplementation(): ImplementationExport {
5269
if (!implementation) {
5370
implementation = selectWorkerImplementation()
5471
}

src/master/implementation.node.ts

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import getCallsites, { CallSite } from "callsites"
55
import EventEmitter from "events"
66
import { cpus } from 'os'
77
import * as path from "path"
8-
import { ThreadsWorkerOptions, WorkerImplementation } from "../types/master"
8+
import {
9+
ImplementationExport,
10+
ThreadsWorkerOptions,
11+
WorkerImplementation
12+
} from "../types/master"
913

1014
interface WorkerGlobalScope {
1115
addEventListener(eventName: string, listener: (event: Event) => void): void
@@ -84,7 +88,7 @@ function resolveScriptPath(scriptPath: string, baseURL?: string | undefined) {
8488
return workerFilePath
8589
}
8690

87-
function initWorkerThreadsWorker(): typeof WorkerImplementation {
91+
function initWorkerThreadsWorker(): ImplementationExport {
8892
// Webpack hack
8993
const NativeWorker = typeof __non_webpack_require__ === "function"
9094
? __non_webpack_require__("worker_threads").Worker
@@ -95,10 +99,16 @@ function initWorkerThreadsWorker(): typeof WorkerImplementation {
9599
class Worker extends NativeWorker {
96100
private mappedEventListeners: WeakMap<EventListener, EventListener>
97101

98-
constructor(scriptPath: string, options?: ThreadsWorkerOptions) {
99-
const resolvedScriptPath = resolveScriptPath(scriptPath, (options || {})._baseURL)
102+
constructor(scriptPath: string, options?: ThreadsWorkerOptions & { fromSource: boolean }) {
103+
const resolvedScriptPath = options && options.fromSource
104+
? null
105+
: resolveScriptPath(scriptPath, (options || {})._baseURL)
100106

101-
if (resolvedScriptPath.match(/\.tsx?$/i) && detectTsNode()) {
107+
if (!resolvedScriptPath) {
108+
// `options.fromSource` is true
109+
const sourceCode = scriptPath
110+
super(sourceCode, { ...options, eval: true })
111+
} else if (resolvedScriptPath.match(/\.tsx?$/i) && detectTsNode()) {
102112
super(createTsNodeModule(resolvedScriptPath), { ...options, eval: true })
103113
} else if (resolvedScriptPath.match(/\.asar[\/\\]/)) {
104114
// See <https://github.com/andywer/threads-plugin/issues/17>
@@ -138,25 +148,44 @@ function initWorkerThreadsWorker(): typeof WorkerImplementation {
138148
process.on("SIGINT", () => terminateWorkersAndMaster())
139149
process.on("SIGTERM", () => terminateWorkersAndMaster())
140150

141-
return Worker as any
151+
class BlobWorker extends Worker {
152+
constructor(blob: Uint8Array, options?: ThreadsWorkerOptions) {
153+
super(Buffer.from(blob).toString("utf-8"), { ...options, fromSource: true })
154+
}
155+
156+
public static fromText(source: string, options?: ThreadsWorkerOptions): WorkerImplementation {
157+
return new Worker(source, { ...options, fromSource: true }) as any
158+
}
159+
}
160+
161+
return {
162+
blob: BlobWorker as any,
163+
default: Worker as any
164+
}
142165
}
143166

144-
function initTinyWorker(): typeof WorkerImplementation {
167+
function initTinyWorker(): ImplementationExport {
145168
const TinyWorker = require("tiny-worker")
146169

147170
let allWorkers: Array<typeof TinyWorker> = []
148171

149172
class Worker extends TinyWorker {
150173
private emitter: EventEmitter
151174

152-
constructor(scriptPath: string) {
175+
constructor(scriptPath: string, options?: ThreadsWorkerOptions & { fromSource?: boolean }) {
153176
// Need to apply a work-around for Windows or it will choke upon the absolute path
154177
// (`Error [ERR_INVALID_PROTOCOL]: Protocol 'c:' not supported`)
155-
const resolvedScriptPath = process.platform === "win32"
156-
? `file:///${resolveScriptPath(scriptPath).replace(/\\/g, "/")}`
157-
: resolveScriptPath(scriptPath)
158-
159-
if (resolvedScriptPath.match(/\.tsx?$/i) && detectTsNode()) {
178+
const resolvedScriptPath = options && options.fromSource
179+
? null
180+
: process.platform === "win32"
181+
? `file:///${resolveScriptPath(scriptPath).replace(/\\/g, "/")}`
182+
: resolveScriptPath(scriptPath)
183+
184+
if (!resolvedScriptPath) {
185+
// `options.fromSource` is true
186+
const sourceCode = scriptPath
187+
super(new Function(sourceCode), [], { esm: true })
188+
} else if (resolvedScriptPath.match(/\.tsx?$/i) && detectTsNode()) {
160189
super(new Function(createTsNodeModule(resolveScriptPath(scriptPath))), [], { esm: true })
161190
} else if (resolvedScriptPath.match(/\.asar[\/\\]/)) {
162191
// See <https://github.com/andywer/threads-plugin/issues/17>
@@ -171,12 +200,15 @@ function initTinyWorker(): typeof WorkerImplementation {
171200
this.onerror = (error: Error) => this.emitter.emit("error", error)
172201
this.onmessage = (message: MessageEvent) => this.emitter.emit("message", message)
173202
}
203+
174204
public addEventListener(eventName: WorkerEventName, listener: EventListener) {
175205
this.emitter.addListener(eventName, listener)
176206
}
207+
177208
public removeEventListener(eventName: WorkerEventName, listener: EventListener) {
178209
this.emitter.removeListener(eventName, listener)
179210
}
211+
180212
public terminate() {
181213
allWorkers = allWorkers.filter(worker => worker !== this)
182214
return super.terminate()
@@ -197,13 +229,26 @@ function initTinyWorker(): typeof WorkerImplementation {
197229
process.on("SIGINT", () => terminateWorkersAndMaster())
198230
process.on("SIGTERM", () => terminateWorkersAndMaster())
199231

200-
return Worker as any
232+
class BlobWorker extends Worker {
233+
constructor(blob: Uint8Array, options?: ThreadsWorkerOptions) {
234+
super(Buffer.from(blob).toString("utf-8"), { ...options, fromSource: true })
235+
}
236+
237+
public static fromText(source: string, options?: ThreadsWorkerOptions): WorkerImplementation {
238+
return new Worker(source, { ...options, fromSource: true }) as any
239+
}
240+
}
241+
242+
return {
243+
blob: BlobWorker as any,
244+
default: Worker as any
245+
}
201246
}
202247

203-
let implementation: typeof WorkerImplementation
248+
let implementation: ImplementationExport
204249
let isTinyWorker: boolean
205250

206-
function selectWorkerImplementation(): typeof WorkerImplementation {
251+
function selectWorkerImplementation(): ImplementationExport {
207252
try {
208253
isTinyWorker = false
209254
return initWorkerThreadsWorker()
@@ -215,7 +260,7 @@ function selectWorkerImplementation(): typeof WorkerImplementation {
215260
}
216261
}
217262

218-
export function getWorkerImplementation(): typeof WorkerImplementation {
263+
export function getWorkerImplementation(): ImplementationExport {
219264
if (!implementation) {
220265
implementation = selectWorkerImplementation()
221266
}

src/master/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// tslint:disable no-duplicate-imports
2+
import type { BlobWorker as BlobWorkerClass } from "../types/master"
13
import { Worker as WorkerType } from "../types/master"
24
import { getWorkerImplementation, isWorkerRuntime } from "./implementation"
35

@@ -7,7 +9,11 @@ export { spawn } from "./spawn"
79
export { Thread } from "./thread"
810
export { isWorkerRuntime }
911

12+
export type BlobWorker = typeof BlobWorkerClass
1013
export type Worker = WorkerType
1114

15+
/** Separate class to spawn workers from source code blobs or strings. */
16+
export const BlobWorker = getWorkerImplementation().blob
17+
1218
/** Worker implementation. Either web worker or a node.js Worker class. */
13-
export const Worker = getWorkerImplementation()
19+
export const Worker = getWorkerImplementation().default

src/types/master.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/// <reference lib="dom" />
2+
// tslint:disable max-classes-per-file
23

34
// Cannot use `compilerOptions.esModuleInterop` and default import syntax
45
// See <https://github.com/microsoft/TypeScript/issues/28009>
@@ -89,6 +90,17 @@ export declare class WorkerImplementation extends EventTarget implements Worker
8990
public terminate(): void
9091
}
9192

93+
/** Class to spawn workers from a blob or source string. */
94+
export declare class BlobWorker extends WorkerImplementation {
95+
constructor(blob: Blob, options?: ThreadsWorkerOptions)
96+
public static fromText(source: string, options?: ThreadsWorkerOptions): WorkerImplementation
97+
}
98+
99+
export interface ImplementationExport {
100+
blob: typeof BlobWorker
101+
default: typeof WorkerImplementation
102+
}
103+
92104
/** Event as emitted by worker thread. Subscribe to using `Thread.events(thread)`. */
93105
export enum WorkerEventType {
94106
internalError = "internalError",

0 commit comments

Comments
 (0)