-
-
Notifications
You must be signed in to change notification settings - Fork 3
/
Thread.ts
162 lines (146 loc) · 4.58 KB
/
Thread.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
/**
* > Type T -> return type
*
* > Type K -> data type of MessageEvent
*/
export default class Thread<T = unknown, K = unknown> {
public worker: Promise<Worker>;
private imports: Array<string>;
private blob: Promise<Blob>;
private blobURL = "";
public debugMode: boolean;
/**
* Tells if the worker has been stopped
*/
public stopped = false;
/**
* @param operation The method to be used in the thread
* @param imports Modules to import in the worker. only JS files allowed (over the net import allowed)
*/
constructor(
operation: (
e: MessageEvent<K>,
globalObject?: Record<string, unknown>,
) => T | Promise<T>,
type?: "classic" | "module",
imports?: Array<string>,
opts: { debug?: boolean } = { debug: false },
) {
this.debugMode = opts.debug ?? false;
this.imports = imports || [];
// these methods are asynchronous, because we're in the constructor, we must make sure they're at the end
this.blob = this.populateFile(operation);
this.worker = this.makeWorker(type);
}
private async makeWorker(type?: "classic" | "module") {
this.blobURL = URL.createObjectURL(await this.blob);
return new Worker(
this.blobURL,
{
type: type || "module",
},
);
}
// deno-lint-ignore ban-types
private async populateFile(code: Function) {
const imported = this.imports?.flatMap(async (val) =>
(await this.copyDep(val)).join("\n")
);
const blobContent = `
${(await Promise.all(imported)).join("\n")}
var global = {};
var userCode = ${code.toString()}
onmessage = async function(e) {
postMessage(await userCode(e, global));
}
`;
this.debug(`Blob content:${blobContent}\n\n\n`);
return new Blob([blobContent]);
}
/**
* Handles a single import line
* @param str the import line (eg: import {som} from "lorem/ipsum.js";)
*/
private async copyDep(str: string) {
const importPathRegex = /('|"|`)(.+(\.js|\.ts))(\1)/ig; // for the path string ("lorem/ipsum.js")
const importInsRegex = /(import( |))({.+}|.+)(from( |))/ig; // for the instruction before the path (import {som} from)
const matchedPath = importPathRegex.exec(str) || "";
this.debug("attempting to import: ", str);
let file = false;
let fqfn = "";
if (
!matchedPath[0].includes("http://") &&
!matchedPath[0].includes("https://")
) {
file = true;
fqfn = matchedPath[0].replaceAll(/('|"|`)/ig, "");
this.debug("file identified as local file");
}
const matchedIns = importInsRegex.exec(str) || ""; // matchedIns[0] > import {sss} from
if (!matchedIns) {
throw new Error(
"The import instruction seems to be unreadable try formatting it, for example: \n" +
"import { something } from './somet.js' \n ",
);
}
if (file) {
this.debug(
"importing file: ",
import.meta.resolve("file://" + Deno.realPathSync(fqfn)),
);
const x = await import("file://" + Deno.realPathSync(fqfn));
this.debug(
"file imported, inlining the following: ",
Object.keys(x).join(","),
);
return Object.keys(x).map((v) => x[v].toString());
} else {
const filePath = matchedPath[0].replaceAll(/'|"/g, "");
this.debug("importing from the net: ", filePath);
if (filePath.endsWith(".ts")) {
this.debug("filePath ends with .ts, returning: ", str);
return [str]; // do nothing if plain import string
}
const x = await import(filePath);
this.debug(
"imported from the net, inlining the following: ",
Object.keys(x).join(","),
);
return Object.keys(x).map((v) => x[v].toString());
}
}
private debug(...msg: unknown[]) {
if (this.debugMode) console.debug(`[${new Date()}]\t`, ...msg);
}
/**
* Sends data to the Thread
* @param msg
*/
public postMessage(msg: K): this {
this.worker.then((w) => w.postMessage(msg));
return this;
}
/**
* Handbrakes are very handy you know
*/
public async stop() {
this.stopped = true;
(await this.worker).terminate();
}
/**
* Stops the worker and revokes the blob URL.
* NOTE: Can be used while the program is running (calls stop()..)
*/
public async remove() {
if (this.stopped == false) await this.stop();
URL.revokeObjectURL(this.blobURL);
}
/**
* Bind to the worker to receive messages
* @param callback Function that is called when the worker sends data back
*/
public onMessage(callback: (e: T) => void): this {
this.worker.then((w) => w.onmessage = (e) => callback(e.data));
return this;
}
}