space-data-module-sdk 0.8.23 → 0.8.25
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/docs/isomorphic-pthreads.html +29 -5
- package/docs/isomorphic-pthreads.md +98 -8
- package/package.json +1 -1
- package/src/compiler/compileModule.js +378 -385
- package/src/compiler/emceptionNode.js +339 -70
- package/src/host/browserModuleHarness.js +13 -0
- package/src/host/hostWorkerBundleSources.js +3 -3
- package/src/host/nodeSyncFsIo.js +3 -1
- package/src/host/wasiThreadBrowserWorker.mjs +41 -21
- package/src/host/wasiThreadHost.js +279 -150
- package/src/host/wasiThreadPool.js +389 -0
- package/src/host/wasiThreadWorker.mjs +62 -20
- package/src/host/wasiThreadWorkerRuntime.js +6 -1
- package/src/index.d.ts +31 -0
- package/src/testing/index.d.ts +2 -0
|
@@ -1,17 +1,41 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
4
|
+
import {
|
|
5
|
+
cp,
|
|
6
|
+
lstat,
|
|
7
|
+
mkdtemp,
|
|
8
|
+
readFile,
|
|
9
|
+
readdir,
|
|
10
|
+
rename,
|
|
11
|
+
rm,
|
|
12
|
+
stat,
|
|
13
|
+
writeFile,
|
|
14
|
+
} from "node:fs/promises";
|
|
15
|
+
import { constants as fsConstants, createReadStream, readFileSync } from "node:fs";
|
|
5
16
|
import { createRequire } from "node:module";
|
|
6
17
|
import { pathToFileURL } from "node:url";
|
|
7
18
|
|
|
8
19
|
const require = createRequire(import.meta.url);
|
|
20
|
+
// Bump when the patched tree's layout or meaning changes. The patch transform's
|
|
21
|
+
// own source text is part of the cache key as well (see
|
|
22
|
+
// computeEmceptionIdentity), so editing patchEmceptionModuleSource cannot reuse
|
|
23
|
+
// a tree patched by an older transform even if this string is left alone.
|
|
9
24
|
const PATCH_VERSION = "space-data-module-sdk-emception-node-v1";
|
|
25
|
+
const PATCH_KEY_TAG = "node-v1";
|
|
10
26
|
const PATCH_MARKER_FILENAME = ".space-data-module-sdk-emception-patch";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
27
|
+
// One shared root per (patch, emception build) lives at
|
|
28
|
+
// <tmpdir>/space-data-module-sdk-emception-<tag>-<version>-<fingerprint>-u<uid>.
|
|
29
|
+
// Every process of every consumer run by that user reuses it. Builds happen in a sibling
|
|
30
|
+
// staging dir that is renamed into place only once it is complete.
|
|
31
|
+
const PATCHED_ROOT_PREFIX = "space-data-module-sdk-emception-";
|
|
32
|
+
const STAGING_MARKER = ".staging-";
|
|
33
|
+
const STALE_MARKER = ".stale-";
|
|
34
|
+
const ORPHAN_NAME_PATTERN = new RegExp(
|
|
35
|
+
`^${PATCHED_ROOT_PREFIX.replaceAll("-", "\\-")}.+\\.(?:staging|stale)-(\\d+)-[A-Za-z0-9]+$`,
|
|
14
36
|
);
|
|
37
|
+
const RENAME_COLLISION_CODES = new Set(["ENOTEMPTY", "EEXIST", "EPERM", "EBUSY"]);
|
|
38
|
+
const MAX_PUBLISH_ATTEMPTS = 4;
|
|
15
39
|
const FILE_URL_FETCH_PATCH_FLAG =
|
|
16
40
|
"__spaceDataModuleSdkFileUrlFetchPatched";
|
|
17
41
|
|
|
@@ -84,90 +108,335 @@ function installNodeRuntimeShims() {
|
|
|
84
108
|
}
|
|
85
109
|
}
|
|
86
110
|
|
|
87
|
-
|
|
88
|
-
|
|
111
|
+
// Pure transform of one emception .mjs file. Its source text is hashed into
|
|
112
|
+
// the cache key, so any edit here yields a fresh patched root.
|
|
113
|
+
function patchEmceptionModuleSource(fileName, source) {
|
|
114
|
+
let patched = source.replaceAll(
|
|
115
|
+
'scriptDirectory=__dirname+"/"',
|
|
116
|
+
'scriptDirectory=(new URL(".", import.meta.url)).pathname',
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
if (fileName === "emception.mjs") {
|
|
120
|
+
patched = patched.replace(
|
|
121
|
+
"this.#fs = await new FileSystem();",
|
|
122
|
+
[
|
|
123
|
+
"this.#fs = await new FileSystem({",
|
|
124
|
+
" locateFile: (file, scriptDirectory) => scriptDirectory + file,",
|
|
125
|
+
' cache: "/tmp/emception-cache",',
|
|
126
|
+
" });",
|
|
127
|
+
].join("\n"),
|
|
128
|
+
);
|
|
129
|
+
patched = patched.replace(
|
|
130
|
+
"const config = {",
|
|
131
|
+
[
|
|
132
|
+
"const config = {",
|
|
133
|
+
" locateFile: (file, scriptDirectory) => scriptDirectory + file,",
|
|
134
|
+
].join("\n"),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (fileName === "FileSystem.mjs") {
|
|
139
|
+
patched = patched.replace(
|
|
140
|
+
[
|
|
141
|
+
" if (!this.exists(cache)) {",
|
|
142
|
+
" this.persist(cache);",
|
|
143
|
+
" }",
|
|
144
|
+
" await this.pull();",
|
|
145
|
+
].join("\n"),
|
|
146
|
+
[
|
|
147
|
+
' if (typeof indexedDB !== "undefined") {',
|
|
148
|
+
" if (!this.exists(cache)) {",
|
|
149
|
+
" this.persist(cache);",
|
|
150
|
+
" }",
|
|
151
|
+
" await this.pull();",
|
|
152
|
+
" }",
|
|
153
|
+
].join("\n"),
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return patched;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Relative POSIX paths of every regular file under rootDir, sorted.
|
|
161
|
+
async function listTreeFiles(rootDir, relativeDir = "") {
|
|
162
|
+
const entries = await readdir(path.join(rootDir, relativeDir), {
|
|
163
|
+
withFileTypes: true,
|
|
164
|
+
});
|
|
165
|
+
const files = [];
|
|
89
166
|
for (const entry of entries) {
|
|
90
|
-
const
|
|
167
|
+
const relativePath = relativeDir
|
|
168
|
+
? path.posix.join(relativeDir, entry.name)
|
|
169
|
+
: entry.name;
|
|
91
170
|
if (entry.isDirectory()) {
|
|
92
|
-
await
|
|
93
|
-
|
|
171
|
+
files.push(...(await listTreeFiles(rootDir, relativePath)));
|
|
172
|
+
} else if (entry.isFile()) {
|
|
173
|
+
files.push(relativePath);
|
|
94
174
|
}
|
|
95
|
-
|
|
175
|
+
}
|
|
176
|
+
return files.sort();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function patchEmceptionModuleTree(rootDir, files) {
|
|
180
|
+
for (const relativePath of files) {
|
|
181
|
+
if (!relativePath.endsWith(".mjs")) {
|
|
96
182
|
continue;
|
|
97
183
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
184
|
+
const fullPath = path.join(rootDir, relativePath);
|
|
185
|
+
const source = await readFile(fullPath, "utf8");
|
|
186
|
+
const patched = patchEmceptionModuleSource(
|
|
187
|
+
path.posix.basename(relativePath),
|
|
188
|
+
source,
|
|
103
189
|
);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
source = source.replace(
|
|
107
|
-
"this.#fs = await new FileSystem();",
|
|
108
|
-
[
|
|
109
|
-
"this.#fs = await new FileSystem({",
|
|
110
|
-
" locateFile: (file, scriptDirectory) => scriptDirectory + file,",
|
|
111
|
-
' cache: "/tmp/emception-cache",',
|
|
112
|
-
" });",
|
|
113
|
-
].join("\n"),
|
|
114
|
-
);
|
|
115
|
-
source = source.replace(
|
|
116
|
-
"const config = {",
|
|
117
|
-
[
|
|
118
|
-
"const config = {",
|
|
119
|
-
" locateFile: (file, scriptDirectory) => scriptDirectory + file,",
|
|
120
|
-
].join("\n"),
|
|
121
|
-
);
|
|
190
|
+
if (patched !== source) {
|
|
191
|
+
await writeFile(fullPath, patched, "utf8");
|
|
122
192
|
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
123
195
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
" await this.pull();",
|
|
131
|
-
].join("\n"),
|
|
132
|
-
[
|
|
133
|
-
' if (typeof indexedDB !== "undefined") {',
|
|
134
|
-
" if (!this.exists(cache)) {",
|
|
135
|
-
" this.persist(cache);",
|
|
136
|
-
" }",
|
|
137
|
-
" await this.pull();",
|
|
138
|
-
" }",
|
|
139
|
-
].join("\n"),
|
|
196
|
+
async function readEmceptionPackageVersion(sourceRoot) {
|
|
197
|
+
let directory = sourceRoot;
|
|
198
|
+
for (;;) {
|
|
199
|
+
try {
|
|
200
|
+
const packageJson = JSON.parse(
|
|
201
|
+
await readFile(path.join(directory, "package.json"), "utf8"),
|
|
140
202
|
);
|
|
203
|
+
if (packageJson?.name === "sdn-emception") {
|
|
204
|
+
return String(packageJson.version ?? "unknown");
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
207
|
+
// Keep walking up.
|
|
141
208
|
}
|
|
209
|
+
const parent = path.dirname(directory);
|
|
210
|
+
if (parent === directory) {
|
|
211
|
+
return "unknown";
|
|
212
|
+
}
|
|
213
|
+
directory = parent;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
142
216
|
|
|
143
|
-
|
|
217
|
+
// The cache key: the patch version, the patch transform, the emception
|
|
218
|
+
// package version, and a sha256 over every source file's path, size and
|
|
219
|
+
// bytes. Two installs with the same version but different bytes never share
|
|
220
|
+
// a root.
|
|
221
|
+
async function computeEmceptionIdentity(sourceRoot) {
|
|
222
|
+
const version = await readEmceptionPackageVersion(sourceRoot);
|
|
223
|
+
const files = await listTreeFiles(sourceRoot);
|
|
224
|
+
const hash = createHash("sha256");
|
|
225
|
+
hash.update(`${PATCH_VERSION}\0${patchEmceptionModuleSource.toString()}\0`);
|
|
226
|
+
hash.update(`${version}\0`);
|
|
227
|
+
for (const relativePath of files) {
|
|
228
|
+
const fullPath = path.join(sourceRoot, relativePath);
|
|
229
|
+
const { size } = await stat(fullPath);
|
|
230
|
+
hash.update(`${relativePath}\0${size}\0`);
|
|
231
|
+
for await (const chunk of createReadStream(fullPath)) {
|
|
232
|
+
hash.update(chunk);
|
|
233
|
+
}
|
|
234
|
+
hash.update("\0");
|
|
144
235
|
}
|
|
236
|
+
const fingerprint = hash.digest("hex");
|
|
237
|
+
const safeVersion = version.replace(/[^A-Za-z0-9._]/g, "_");
|
|
238
|
+
// Per user: where the tmp dir is shared (Linux /tmp), users never collide.
|
|
239
|
+
const uid = currentUid();
|
|
240
|
+
return {
|
|
241
|
+
version,
|
|
242
|
+
fingerprint,
|
|
243
|
+
files,
|
|
244
|
+
key: `${PATCH_KEY_TAG}-${safeVersion}-${fingerprint.slice(0, 16)}${uid === null ? "" : `-u${uid}`}`,
|
|
245
|
+
};
|
|
145
246
|
}
|
|
146
247
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
248
|
+
// The content check on reuse. The marker is written last inside the staging
|
|
249
|
+
// dir, so a published root always has one; this also catches a root whose
|
|
250
|
+
// files were later deleted or truncated (tmp cleaners purge by age).
|
|
251
|
+
async function isPatchedRootValid(rootDir, identity) {
|
|
252
|
+
// The root is imported as code, so it must be a real directory that this
|
|
253
|
+
// user owns and nobody else can write. Anything else is never trusted.
|
|
254
|
+
try {
|
|
255
|
+
const rootStat = await lstat(rootDir);
|
|
256
|
+
if (!rootStat.isDirectory()) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
const uid = currentUid();
|
|
260
|
+
if (uid !== null && (rootStat.uid !== uid || (rootStat.mode & 0o022) !== 0)) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
} catch {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
let marker;
|
|
267
|
+
try {
|
|
268
|
+
marker = JSON.parse(
|
|
269
|
+
await readFile(path.join(rootDir, PATCH_MARKER_FILENAME), "utf8"),
|
|
270
|
+
);
|
|
271
|
+
} catch {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
if (
|
|
275
|
+
marker?.patchVersion !== PATCH_VERSION ||
|
|
276
|
+
marker.emceptionVersion !== identity.version ||
|
|
277
|
+
marker.fingerprint !== identity.fingerprint ||
|
|
278
|
+
!Array.isArray(marker.files) ||
|
|
279
|
+
marker.files.length !== identity.files.length
|
|
280
|
+
) {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
for (let index = 0; index < marker.files.length; index += 1) {
|
|
284
|
+
const [relativePath, size] = marker.files[index] ?? [];
|
|
285
|
+
if (relativePath !== identity.files[index]) {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
const entry = await stat(path.join(rootDir, relativePath));
|
|
290
|
+
if (!entry.isFile() || entry.size !== size) {
|
|
291
|
+
return false;
|
|
157
292
|
}
|
|
293
|
+
} catch {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function currentUid() {
|
|
301
|
+
return typeof process.getuid === "function" ? process.getuid() : null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function isProcessAlive(pid) {
|
|
305
|
+
try {
|
|
306
|
+
process.kill(pid, 0);
|
|
307
|
+
return true;
|
|
308
|
+
} catch (error) {
|
|
309
|
+
return error?.code === "EPERM";
|
|
310
|
+
}
|
|
311
|
+
}
|
|
158
312
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
313
|
+
// Staging and stale dirs carry their creator's pid. A process that died
|
|
314
|
+
// mid-build leaves one behind; nothing else would ever remove it.
|
|
315
|
+
async function removeOrphanedBuildDirs(cacheRoot) {
|
|
316
|
+
let names;
|
|
317
|
+
try {
|
|
318
|
+
names = await readdir(cacheRoot);
|
|
319
|
+
} catch {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
for (const name of names) {
|
|
323
|
+
const match = ORPHAN_NAME_PATTERN.exec(name);
|
|
324
|
+
if (!match) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
const pid = Number(match[1]);
|
|
328
|
+
if (pid === process.pid || isProcessAlive(pid)) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
const uid = currentUid();
|
|
332
|
+
if (uid !== null) {
|
|
333
|
+
const entry = await lstat(path.join(cacheRoot, name)).catch(() => null);
|
|
334
|
+
if (!entry || entry.uid !== uid) {
|
|
335
|
+
continue;
|
|
164
336
|
}
|
|
337
|
+
}
|
|
338
|
+
await rm(path.join(cacheRoot, name), { recursive: true, force: true }).catch(
|
|
339
|
+
() => {},
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
165
343
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
344
|
+
// rename(2) of a directory is atomic: the root either does not exist or is
|
|
345
|
+
// complete. Losing the race to another process is success when its root
|
|
346
|
+
// passes the content check.
|
|
347
|
+
async function publishPatchedRoot(stagingDir, finalDir, identity) {
|
|
348
|
+
let lastError = null;
|
|
349
|
+
for (let attempt = 0; attempt < MAX_PUBLISH_ATTEMPTS; attempt += 1) {
|
|
350
|
+
try {
|
|
351
|
+
await rename(stagingDir, finalDir);
|
|
352
|
+
return;
|
|
353
|
+
} catch (error) {
|
|
354
|
+
if (!RENAME_COLLISION_CODES.has(error?.code)) {
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
357
|
+
lastError = error;
|
|
358
|
+
}
|
|
359
|
+
if (await isPatchedRootValid(finalDir, identity)) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
// What sits at finalDir is incomplete. Move it aside in one rename so no
|
|
363
|
+
// reader ever sees it half-deleted, then retry.
|
|
364
|
+
const staleDir = `${finalDir}${STALE_MARKER}${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
365
|
+
try {
|
|
366
|
+
await rename(finalDir, staleDir);
|
|
367
|
+
} catch (error) {
|
|
368
|
+
if (error?.code === "ENOENT") {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
169
371
|
throw error;
|
|
372
|
+
}
|
|
373
|
+
await rm(staleDir, { recursive: true, force: true });
|
|
374
|
+
}
|
|
375
|
+
throw new Error(
|
|
376
|
+
`Could not publish the patched emception root at ${finalDir}.`,
|
|
377
|
+
{ cause: lastError },
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Returns the shared patched emception root under `cacheRoot` (default
|
|
383
|
+
* `os.tmpdir()`), building it first if it is missing or fails the content
|
|
384
|
+
* check. Safe to call from any number of processes at once.
|
|
385
|
+
*/
|
|
386
|
+
export async function preparePatchedEmceptionRoot(options = {}) {
|
|
387
|
+
const cacheRoot = options.cacheRoot ?? os.tmpdir();
|
|
388
|
+
const sourceRoot =
|
|
389
|
+
options.sourceRoot ?? path.dirname(require.resolve("sdn-emception"));
|
|
390
|
+
const identity = await computeEmceptionIdentity(sourceRoot);
|
|
391
|
+
const finalDir = path.join(cacheRoot, `${PATCHED_ROOT_PREFIX}${identity.key}`);
|
|
392
|
+
if (await isPatchedRootValid(finalDir, identity)) {
|
|
393
|
+
return finalDir;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
await removeOrphanedBuildDirs(cacheRoot);
|
|
397
|
+
const stagingDir = await mkdtemp(
|
|
398
|
+
`${finalDir}${STAGING_MARKER}${process.pid}-`,
|
|
399
|
+
);
|
|
400
|
+
try {
|
|
401
|
+
// Copy-on-write clone where the filesystem supports it (APFS, btrfs);
|
|
402
|
+
// a plain copy elsewhere.
|
|
403
|
+
await cp(sourceRoot, stagingDir, {
|
|
404
|
+
recursive: true,
|
|
405
|
+
mode: fsConstants.COPYFILE_FICLONE,
|
|
170
406
|
});
|
|
407
|
+
await patchEmceptionModuleTree(stagingDir, identity.files);
|
|
408
|
+
const files = [];
|
|
409
|
+
for (const relativePath of identity.files) {
|
|
410
|
+
const { size } = await stat(path.join(stagingDir, relativePath));
|
|
411
|
+
files.push([relativePath, size]);
|
|
412
|
+
}
|
|
413
|
+
await writeFile(
|
|
414
|
+
path.join(stagingDir, PATCH_MARKER_FILENAME),
|
|
415
|
+
`${JSON.stringify({
|
|
416
|
+
patchVersion: PATCH_VERSION,
|
|
417
|
+
emceptionVersion: identity.version,
|
|
418
|
+
fingerprint: identity.fingerprint,
|
|
419
|
+
files,
|
|
420
|
+
})}\n`,
|
|
421
|
+
"utf8",
|
|
422
|
+
);
|
|
423
|
+
await publishPatchedRoot(stagingDir, finalDir, identity);
|
|
424
|
+
} finally {
|
|
425
|
+
// A no-op after a successful rename; removes the copy after a lost race
|
|
426
|
+
// or a failure.
|
|
427
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
428
|
+
}
|
|
429
|
+
return finalDir;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function getPatchedEmceptionRoot() {
|
|
433
|
+
if (!patchedEmceptionRootPromise) {
|
|
434
|
+
patchedEmceptionRootPromise = preparePatchedEmceptionRoot().catch(
|
|
435
|
+
(error) => {
|
|
436
|
+
patchedEmceptionRootPromise = null;
|
|
437
|
+
throw error;
|
|
438
|
+
},
|
|
439
|
+
);
|
|
171
440
|
}
|
|
172
441
|
|
|
173
442
|
return patchedEmceptionRootPromise;
|
|
@@ -181,7 +450,7 @@ class EmceptionController {
|
|
|
181
450
|
if (!this.#instancePromise) {
|
|
182
451
|
this.#instancePromise = (async () => {
|
|
183
452
|
installNodeRuntimeShims();
|
|
184
|
-
const patchedRoot = await
|
|
453
|
+
const patchedRoot = await getPatchedEmceptionRoot();
|
|
185
454
|
const moduleUrl = pathToFileURL(path.join(patchedRoot, "emception.mjs")).href;
|
|
186
455
|
const { default: Emception } = await import(moduleUrl);
|
|
187
456
|
const emception = new Emception({
|
|
@@ -384,6 +384,7 @@ async function instantiateBrowserModule(options = {}) {
|
|
|
384
384
|
// Browser warm-pool sizing: cap pooled workers at how many guest threads
|
|
385
385
|
// the module will actually ask for. Ignored by the Node lazy path.
|
|
386
386
|
requestedThreads: options.maxThreads,
|
|
387
|
+
poolSize: options.wasiThreadPoolSize,
|
|
387
388
|
hostcallChannel: options.threadHostcallChannel,
|
|
388
389
|
requiresHostcalls: needsHostBridge,
|
|
389
390
|
enableBrowserThreads: options.enableBrowserWasiThreads,
|
|
@@ -395,6 +396,7 @@ async function instantiateBrowserModule(options = {}) {
|
|
|
395
396
|
// packaged sibling, i.e. today's unbundled behavior.
|
|
396
397
|
browserWorkerBaseUrl: options.wasiThreadWorkerBaseUrl,
|
|
397
398
|
browserWorkerUrl: options.wasiThreadWorkerUrl,
|
|
399
|
+
spawnWaitMs: options.wasiThreadSpawnWaitMs,
|
|
398
400
|
});
|
|
399
401
|
importObject.wasi = {
|
|
400
402
|
...(importObject.wasi ?? {}),
|
|
@@ -469,6 +471,13 @@ async function instantiateBrowserModule(options = {}) {
|
|
|
469
471
|
* @param {number} [options.maxThreads] - Upper bound on guest threads; sizes the
|
|
470
472
|
* browser wasi-threads warm pool. Ignored by single-thread artifacts and by
|
|
471
473
|
* the Node lazy worker path.
|
|
474
|
+
* @param {number} [options.wasiThreadPoolSize] - Explicit wasi-threads pool size
|
|
475
|
+
* (createWasiThreadSpawn `poolSize`): the browser pre-starts exactly this many
|
|
476
|
+
* workers; Node caps live guest threads at it. Unset: the browser pool is
|
|
477
|
+
* min(hardwareConcurrency - 1, maxThreads) and Node spawns without a cap.
|
|
478
|
+
* @param {number} [options.wasiThreadSpawnWaitMs] - How long a guest spawn that
|
|
479
|
+
* finds every pooled thread busy waits for one to finish before
|
|
480
|
+
* pthread_create fails (createWasiThreadSpawn `spawnWaitMs`, default 250).
|
|
472
481
|
*/
|
|
473
482
|
export async function createBrowserModuleHarness(options = {}) {
|
|
474
483
|
let wasmSource = options.wasmSource;
|
|
@@ -578,6 +587,8 @@ export async function createBrowserModuleHarness(options = {}) {
|
|
|
578
587
|
enableBrowserWasiThreads: options.enableBrowserWasiThreads,
|
|
579
588
|
wasiThreadWorkerBaseUrl: options.wasiThreadWorkerBaseUrl,
|
|
580
589
|
wasiThreadWorkerUrl: options.wasiThreadWorkerUrl,
|
|
590
|
+
wasiThreadSpawnWaitMs: options.wasiThreadSpawnWaitMs,
|
|
591
|
+
wasiThreadPoolSize: options.wasiThreadPoolSize,
|
|
581
592
|
});
|
|
582
593
|
const { instance, bridge, wasi, memory, threadHost } = activeContext;
|
|
583
594
|
let lastCommandContext = null;
|
|
@@ -842,6 +853,8 @@ export async function createBrowserModuleHarness(options = {}) {
|
|
|
842
853
|
enableBrowserWasiThreads: options.enableBrowserWasiThreads,
|
|
843
854
|
wasiThreadWorkerBaseUrl: options.wasiThreadWorkerBaseUrl,
|
|
844
855
|
wasiThreadWorkerUrl: options.wasiThreadWorkerUrl,
|
|
856
|
+
wasiThreadSpawnWaitMs: options.wasiThreadSpawnWaitMs,
|
|
857
|
+
wasiThreadPoolSize: options.wasiThreadPoolSize,
|
|
845
858
|
});
|
|
846
859
|
lastCommandContext = commandContext;
|
|
847
860
|
try {
|