wasm-idle 1.0.4 → 1.0.6
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/README.md +34 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/playground/assetBridge.d.ts +5 -1
- package/dist/playground/assetBridge.js +120 -42
- package/dist/playground/cachedClang.d.ts +10 -0
- package/dist/playground/cachedClang.js +323 -0
- package/dist/playground/clang.d.ts +7 -0
- package/dist/playground/clang.js +39 -7
- package/dist/playground/clangWorkerProtocol.d.ts +57 -0
- package/dist/playground/clangWorkerProtocol.js +11 -0
- package/dist/playground/compiledArtifactCache.d.ts +21 -0
- package/dist/playground/compiledArtifactCache.js +67 -0
- package/dist/playground/python.d.ts +7 -0
- package/dist/playground/python.js +38 -7
- package/dist/playground/runtimeAssetCache.d.ts +26 -0
- package/dist/playground/runtimeAssetCache.js +84 -0
- package/dist/playground/runtimeSession.d.ts +20 -0
- package/dist/playground/runtimeSession.js +119 -0
- package/dist/playground/worker/assets.d.ts +3 -0
- package/dist/playground/worker/assets.js +41 -0
- package/dist/playground/worker/cachedClangCompile.d.ts +1 -0
- package/dist/playground/worker/cachedClangCompile.js +85 -0
- package/dist/playground/worker/cachedClangExecute.d.ts +1 -0
- package/dist/playground/worker/cachedClangExecute.js +67 -0
- package/dist/playground/worker/clang.js +1 -3
- package/dist/playground/worker/clangStreaming.d.ts +1 -1
- package/dist/playground/worker/clangStreaming.js +20 -2
- package/dist/playground/worker/python.js +3 -6
- package/dist/playground/worker/runtimeModule.d.ts +8 -0
- package/dist/playground/worker/runtimeModule.js +49 -0
- package/package.json +6 -6
package/dist/playground/clang.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { shouldStreamBundledClang } from './clangStreamingPolicy';
|
|
2
2
|
import { WorkerAssetBridge } from './assetBridge';
|
|
3
|
+
import { RuntimeAssetCache } from './runtimeAssetCache';
|
|
3
4
|
import { resolveDebugRuntimeUrls, resolveRuntimeAssetConfig } from './assets';
|
|
4
5
|
import { LldbSandboxSession } from './lldbSession';
|
|
5
6
|
import { normalizeDwarfWorkspacePath } from '@wasm-idle/llvm-core/clang';
|
|
@@ -39,6 +40,10 @@ class Clang {
|
|
|
39
40
|
debugManifestUrl = '';
|
|
40
41
|
debugManifestReceipt;
|
|
41
42
|
lldbSession;
|
|
43
|
+
runtimeAssetCache = new RuntimeAssetCache();
|
|
44
|
+
ownsRuntimeAssetCache = true;
|
|
45
|
+
disposed = false;
|
|
46
|
+
disposal;
|
|
42
47
|
debugMode = 'none';
|
|
43
48
|
lldbBreakpoints = new Map();
|
|
44
49
|
debugEvaluationQueue = Promise.resolve();
|
|
@@ -74,6 +79,8 @@ class Clang {
|
|
|
74
79
|
load(runtimeAssets = '', code = '', log = true, args = [], options = {}, progress) {
|
|
75
80
|
let limits;
|
|
76
81
|
try {
|
|
82
|
+
if (this.disposed)
|
|
83
|
+
throw new Error('Clang runtime has been disposed');
|
|
77
84
|
limits = resolveExecutionLimits(options.limits);
|
|
78
85
|
}
|
|
79
86
|
catch (error) {
|
|
@@ -107,7 +114,7 @@ class Clang {
|
|
|
107
114
|
if (!this.workerSession.attach(worker, operation))
|
|
108
115
|
return;
|
|
109
116
|
this.worker = worker;
|
|
110
|
-
this.assetBridge = new WorkerAssetBridge(this.worker, 'clang', assetConfig, progress, limits.maxAssetBytes, languageSysrootProfiles);
|
|
117
|
+
this.assetBridge = new WorkerAssetBridge(this.worker, 'clang', assetConfig, progress, limits.maxAssetBytes, languageSysrootProfiles, this.runtimeAssetCache);
|
|
111
118
|
this.worker.onmessage = (event) => {
|
|
112
119
|
if (!this.workerSession.isActive(operation))
|
|
113
120
|
return;
|
|
@@ -129,7 +136,8 @@ class Clang {
|
|
|
129
136
|
assets: {
|
|
130
137
|
baseUrl: assetConfig.baseUrl,
|
|
131
138
|
maxAssetBytes: limits.maxAssetBytes,
|
|
132
|
-
useAssetBridge: assetConfig.useAssetBridge
|
|
139
|
+
useAssetBridge: assetConfig.useAssetBridge,
|
|
140
|
+
useModuleBridge: true
|
|
133
141
|
},
|
|
134
142
|
maxAssetBytes: limits.maxAssetBytes
|
|
135
143
|
});
|
|
@@ -172,6 +180,8 @@ class Clang {
|
|
|
172
180
|
}
|
|
173
181
|
}
|
|
174
182
|
run(code, prepare, log = this.log, prog, args = [], options = {}) {
|
|
183
|
+
if (this.disposed)
|
|
184
|
+
return Promise.reject(new Error('Clang runtime has been disposed'));
|
|
175
185
|
const debugMode = options.debugMode || (options.debug ? 'trace' : 'none');
|
|
176
186
|
this.debugMode = debugMode;
|
|
177
187
|
if (debugMode !== 'none')
|
|
@@ -422,11 +432,33 @@ class Clang {
|
|
|
422
432
|
resetBufferedStdin(this.watchResultBuffer);
|
|
423
433
|
const debugBuffer = new Int32Array(this.debugBuffer);
|
|
424
434
|
debugBuffer.fill(0);
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
435
|
+
new Uint8Array(this.interruptBuffer).fill(0);
|
|
436
|
+
}
|
|
437
|
+
setRuntimeAssetCache(cache) {
|
|
438
|
+
if (this.disposed)
|
|
439
|
+
throw new Error('Clang runtime has been disposed');
|
|
440
|
+
if (this.worker)
|
|
441
|
+
throw new Error('Cannot replace an active runtime cache');
|
|
442
|
+
if (cache === this.runtimeAssetCache)
|
|
443
|
+
return;
|
|
444
|
+
if (this.ownsRuntimeAssetCache)
|
|
445
|
+
this.runtimeAssetCache.dispose();
|
|
446
|
+
this.runtimeAssetCache = cache;
|
|
447
|
+
this.ownsRuntimeAssetCache = false;
|
|
448
|
+
}
|
|
449
|
+
dispose() {
|
|
450
|
+
if (this.disposal)
|
|
451
|
+
return this.disposal;
|
|
452
|
+
this.disposed = true;
|
|
453
|
+
this.disposal = Promise.resolve()
|
|
454
|
+
.then(() => this.clear())
|
|
455
|
+
.finally(() => {
|
|
456
|
+
if (this.ownsRuntimeAssetCache)
|
|
457
|
+
this.runtimeAssetCache.dispose();
|
|
458
|
+
this.output = null;
|
|
459
|
+
this.ondebug = undefined;
|
|
460
|
+
});
|
|
461
|
+
return this.disposal;
|
|
430
462
|
}
|
|
431
463
|
}
|
|
432
464
|
export default Clang;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { BrowserClangArtifact, BrowserClangCompileProgress, BrowserClangCompileRequest, BrowserClangWorkspaceFile } from '@wasm-idle/llvm-core/clang';
|
|
2
|
+
import type { WorkerRuntimeAssetConfig } from './worker/assets';
|
|
3
|
+
export type ClangCompileWorkerRequest = {
|
|
4
|
+
request: Omit<BrowserClangCompileRequest, 'onProgress'>;
|
|
5
|
+
runtimeBaseUrl: string;
|
|
6
|
+
maxAssetBytes?: number;
|
|
7
|
+
languageSysroots?: boolean;
|
|
8
|
+
log?: boolean;
|
|
9
|
+
assets: WorkerRuntimeAssetConfig;
|
|
10
|
+
};
|
|
11
|
+
export type ClangCompileWorkerResponse = {
|
|
12
|
+
type: 'progress';
|
|
13
|
+
progress: BrowserClangCompileProgress;
|
|
14
|
+
} | {
|
|
15
|
+
type: 'compiled';
|
|
16
|
+
artifact: BrowserClangArtifact;
|
|
17
|
+
stdout: string;
|
|
18
|
+
stderr: string;
|
|
19
|
+
} | {
|
|
20
|
+
type: 'error';
|
|
21
|
+
error: string;
|
|
22
|
+
stdout: string;
|
|
23
|
+
stderr: string;
|
|
24
|
+
};
|
|
25
|
+
export type ClangExecuteWorkerRequest = {
|
|
26
|
+
artifact: BrowserClangArtifact;
|
|
27
|
+
programArgs?: string[];
|
|
28
|
+
env?: Record<string, string>;
|
|
29
|
+
workspaceFiles?: BrowserClangWorkspaceFile[];
|
|
30
|
+
activePath?: string;
|
|
31
|
+
code?: string;
|
|
32
|
+
inputBuffer: SharedArrayBuffer;
|
|
33
|
+
stdin?: string;
|
|
34
|
+
};
|
|
35
|
+
export type ClangExecuteWorkerResponse = {
|
|
36
|
+
type: 'stdin';
|
|
37
|
+
} | {
|
|
38
|
+
type: 'output';
|
|
39
|
+
output: string;
|
|
40
|
+
} | {
|
|
41
|
+
type: 'done';
|
|
42
|
+
exitCode: number | null;
|
|
43
|
+
} | {
|
|
44
|
+
type: 'error';
|
|
45
|
+
error: string;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* One stdin response occupies two Int32 control words followed by UTF-8 bytes.
|
|
49
|
+
* Before requesting input the worker sets state to WAITING, posts `stdin`, and
|
|
50
|
+
* waits on word 0. The parent writes bytes and length, then atomically publishes
|
|
51
|
+
* DATA or EOF and notifies word 0. It writes only in response to a request.
|
|
52
|
+
* The worker copies DATA before making another request; EOF remains latched.
|
|
53
|
+
*/
|
|
54
|
+
export declare const CLANG_STDIN_HEADER_BYTES: number;
|
|
55
|
+
export declare const CLANG_STDIN_WAITING = 0;
|
|
56
|
+
export declare const CLANG_STDIN_DATA = 1;
|
|
57
|
+
export declare const CLANG_STDIN_EOF = 2;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One stdin response occupies two Int32 control words followed by UTF-8 bytes.
|
|
3
|
+
* Before requesting input the worker sets state to WAITING, posts `stdin`, and
|
|
4
|
+
* waits on word 0. The parent writes bytes and length, then atomically publishes
|
|
5
|
+
* DATA or EOF and notifies word 0. It writes only in response to a request.
|
|
6
|
+
* The worker copies DATA before making another request; EOF remains latched.
|
|
7
|
+
*/
|
|
8
|
+
export const CLANG_STDIN_HEADER_BYTES = Int32Array.BYTES_PER_ELEMENT * 2;
|
|
9
|
+
export const CLANG_STDIN_WAITING = 0;
|
|
10
|
+
export const CLANG_STDIN_DATA = 1;
|
|
11
|
+
export const CLANG_STDIN_EOF = 2;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { BrowserClangArtifact, BrowserClangCompileRequest } from '@wasm-idle/llvm-core/clang';
|
|
2
|
+
/** Only immutable compiler output lives here, never a worker, instance, or filesystem. */
|
|
3
|
+
export declare class CompiledArtifactCache {
|
|
4
|
+
private entries;
|
|
5
|
+
private bytes;
|
|
6
|
+
private hits;
|
|
7
|
+
private misses;
|
|
8
|
+
private maxEntries;
|
|
9
|
+
private maxBytes;
|
|
10
|
+
constructor(maxEntries?: number, maxBytes?: number);
|
|
11
|
+
get(key: string): BrowserClangArtifact | undefined;
|
|
12
|
+
set(key: string, artifact: BrowserClangArtifact): void;
|
|
13
|
+
clear(): void;
|
|
14
|
+
stats(): {
|
|
15
|
+
hits: number;
|
|
16
|
+
misses: number;
|
|
17
|
+
entries: number;
|
|
18
|
+
bytes: number;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export declare function clangCompileKey(runtimeBaseUrl: string, maxAssetBytes: number, request: BrowserClangCompileRequest, languageSysroots?: boolean): string;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** Only immutable compiler output lives here, never a worker, instance, or filesystem. */
|
|
2
|
+
export class CompiledArtifactCache {
|
|
3
|
+
entries = new Map();
|
|
4
|
+
bytes = 0;
|
|
5
|
+
hits = 0;
|
|
6
|
+
misses = 0;
|
|
7
|
+
maxEntries;
|
|
8
|
+
maxBytes;
|
|
9
|
+
constructor(maxEntries = 8, maxBytes = 32 * 1024 * 1024) {
|
|
10
|
+
this.maxEntries = maxEntries;
|
|
11
|
+
this.maxBytes = maxBytes;
|
|
12
|
+
}
|
|
13
|
+
get(key) {
|
|
14
|
+
const entry = this.entries.get(key);
|
|
15
|
+
if (!entry) {
|
|
16
|
+
this.misses++;
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
this.hits++;
|
|
20
|
+
this.entries.delete(key);
|
|
21
|
+
this.entries.set(key, entry);
|
|
22
|
+
return entry.artifact;
|
|
23
|
+
}
|
|
24
|
+
set(key, artifact) {
|
|
25
|
+
const size = artifact.bytes.byteLength + key.length * 2;
|
|
26
|
+
if (size > this.maxBytes)
|
|
27
|
+
return;
|
|
28
|
+
const old = this.entries.get(key);
|
|
29
|
+
if (old)
|
|
30
|
+
this.bytes -= old.size;
|
|
31
|
+
this.entries.delete(key);
|
|
32
|
+
this.entries.set(key, { artifact, size });
|
|
33
|
+
this.bytes += size;
|
|
34
|
+
while (this.entries.size > this.maxEntries || this.bytes > this.maxBytes) {
|
|
35
|
+
const oldest = this.entries.keys().next().value;
|
|
36
|
+
this.bytes -= this.entries.get(oldest).size;
|
|
37
|
+
this.entries.delete(oldest);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
clear() {
|
|
41
|
+
this.entries.clear();
|
|
42
|
+
this.bytes = 0;
|
|
43
|
+
}
|
|
44
|
+
stats() {
|
|
45
|
+
return {
|
|
46
|
+
hits: this.hits,
|
|
47
|
+
misses: this.misses,
|
|
48
|
+
entries: this.entries.size,
|
|
49
|
+
bytes: this.bytes
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function clangCompileKey(runtimeBaseUrl, maxAssetBytes, request, languageSysroots = false) {
|
|
54
|
+
return JSON.stringify([
|
|
55
|
+
'wasm-idle/clang-cache-v2',
|
|
56
|
+
runtimeBaseUrl,
|
|
57
|
+
maxAssetBytes,
|
|
58
|
+
languageSysroots,
|
|
59
|
+
request.language,
|
|
60
|
+
request.code,
|
|
61
|
+
request.activePath,
|
|
62
|
+
request.compileArgs,
|
|
63
|
+
request.cVersion,
|
|
64
|
+
request.cppVersion,
|
|
65
|
+
[...(request.workspaceFiles || [])].sort((a, b) => a.path.localeCompare(b.path))
|
|
66
|
+
]);
|
|
67
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DebugCommand, DebugSessionEvent, SandboxExecutionOptions } from './options';
|
|
2
2
|
import { WorkerAssetBridge } from './assetBridge';
|
|
3
|
+
import { RuntimeAssetCache } from './runtimeAssetCache';
|
|
3
4
|
import { type PlaygroundRuntimeAssets } from './assets';
|
|
4
5
|
import type { Sandbox, SandboxProgress } from './sandbox';
|
|
5
6
|
declare class Python implements Sandbox {
|
|
@@ -26,6 +27,10 @@ declare class Python implements Sandbox {
|
|
|
26
27
|
exit: boolean;
|
|
27
28
|
assetBridge: WorkerAssetBridge | null;
|
|
28
29
|
private activeOperation;
|
|
30
|
+
private runtimeAssetCache;
|
|
31
|
+
private ownsRuntimeAssetCache;
|
|
32
|
+
private disposed;
|
|
33
|
+
private disposal?;
|
|
29
34
|
private readonly workerSession;
|
|
30
35
|
private beginOperation;
|
|
31
36
|
private isOperationActive;
|
|
@@ -46,5 +51,7 @@ declare class Python implements Sandbox {
|
|
|
46
51
|
kill(): void;
|
|
47
52
|
terminate(reason?: unknown): void;
|
|
48
53
|
clear(): Promise<void>;
|
|
54
|
+
setRuntimeAssetCache(cache: RuntimeAssetCache): void;
|
|
55
|
+
dispose(): Promise<void>;
|
|
49
56
|
}
|
|
50
57
|
export default Python;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { WorkerAssetBridge } from './assetBridge';
|
|
2
|
+
import { RuntimeAssetCache } from './runtimeAssetCache';
|
|
2
3
|
import { resolveRuntimeAssetConfig } from './assets';
|
|
3
4
|
import { bufferedSequence, flushBufferedEof, flushQueuedStdin, resetBufferedStdin, waitForBufferedSequenceChange } from './stdinBuffer';
|
|
4
5
|
import { createWasmIdleSharedBuffer, requireSharedArrayBuffer } from './sharedBuffer';
|
|
@@ -29,6 +30,10 @@ class Python {
|
|
|
29
30
|
exit = true;
|
|
30
31
|
assetBridge = null;
|
|
31
32
|
activeOperation = null;
|
|
33
|
+
runtimeAssetCache = new RuntimeAssetCache();
|
|
34
|
+
ownsRuntimeAssetCache = true;
|
|
35
|
+
disposed = false;
|
|
36
|
+
disposal;
|
|
32
37
|
workerSession = new WorkerSession({
|
|
33
38
|
label: 'Python',
|
|
34
39
|
onDispose: (worker) => {
|
|
@@ -44,6 +49,8 @@ class Python {
|
|
|
44
49
|
}
|
|
45
50
|
});
|
|
46
51
|
beginOperation(phase) {
|
|
52
|
+
if (this.disposed)
|
|
53
|
+
throw new Error('Python runtime has been disposed');
|
|
47
54
|
if (this.activeOperation) {
|
|
48
55
|
throw new BusyError('Python runtime already has an active operation', {
|
|
49
56
|
runtimeId: 'PYTHON3',
|
|
@@ -220,7 +227,7 @@ class Python {
|
|
|
220
227
|
}
|
|
221
228
|
this.worker = worker;
|
|
222
229
|
this.workerSession.attach(worker);
|
|
223
|
-
const assetBridge = new WorkerAssetBridge(worker, 'python', assetConfig, progress, limits.maxAssetBytes);
|
|
230
|
+
const assetBridge = new WorkerAssetBridge(worker, 'python', assetConfig, progress, limits.maxAssetBytes, false, this.runtimeAssetCache);
|
|
224
231
|
if (!this.isOperationActive(operation) || this.worker !== worker) {
|
|
225
232
|
assetBridge.dispose();
|
|
226
233
|
return;
|
|
@@ -259,7 +266,8 @@ class Python {
|
|
|
259
266
|
assets: {
|
|
260
267
|
baseUrl: assetConfig.baseUrl,
|
|
261
268
|
maxAssetBytes: limits.maxAssetBytes,
|
|
262
|
-
useAssetBridge: assetConfig.useAssetBridge
|
|
269
|
+
useAssetBridge: assetConfig.useAssetBridge,
|
|
270
|
+
useModuleBridge: true
|
|
263
271
|
}
|
|
264
272
|
});
|
|
265
273
|
return;
|
|
@@ -545,11 +553,34 @@ class Python {
|
|
|
545
553
|
resetBufferedStdin(this.watchResultBuffer);
|
|
546
554
|
const debugBuffer = new Int32Array(this.debugBuffer);
|
|
547
555
|
debugBuffer.fill(0);
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
556
|
+
new Uint8Array(this.interruptBuffer).fill(0);
|
|
557
|
+
}
|
|
558
|
+
setRuntimeAssetCache(cache) {
|
|
559
|
+
if (this.disposed)
|
|
560
|
+
throw new Error('Python runtime has been disposed');
|
|
561
|
+
if (this.worker || this.activeOperation)
|
|
562
|
+
throw new Error('Cannot replace an active runtime cache');
|
|
563
|
+
if (cache === this.runtimeAssetCache)
|
|
564
|
+
return;
|
|
565
|
+
if (this.ownsRuntimeAssetCache)
|
|
566
|
+
this.runtimeAssetCache.dispose();
|
|
567
|
+
this.runtimeAssetCache = cache;
|
|
568
|
+
this.ownsRuntimeAssetCache = false;
|
|
569
|
+
}
|
|
570
|
+
dispose() {
|
|
571
|
+
if (this.disposal)
|
|
572
|
+
return this.disposal;
|
|
573
|
+
this.disposed = true;
|
|
574
|
+
this.disposal = Promise.resolve()
|
|
575
|
+
.then(() => this.clear())
|
|
576
|
+
.finally(() => {
|
|
577
|
+
if (this.ownsRuntimeAssetCache)
|
|
578
|
+
this.runtimeAssetCache.dispose();
|
|
579
|
+
this.output = null;
|
|
580
|
+
this.ondebug = undefined;
|
|
581
|
+
this.image = undefined;
|
|
582
|
+
});
|
|
583
|
+
return this.disposal;
|
|
553
584
|
}
|
|
554
585
|
}
|
|
555
586
|
export default Python;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Immutable bootstrap assets only: never store a worker, Wasm instance, or user filesystem. */
|
|
2
|
+
export declare class RuntimeAssetCache {
|
|
3
|
+
private readonly maxBytes;
|
|
4
|
+
private readonly maxEntries;
|
|
5
|
+
private entries;
|
|
6
|
+
private identities;
|
|
7
|
+
private nextIdentity;
|
|
8
|
+
private bytes;
|
|
9
|
+
private hits;
|
|
10
|
+
private misses;
|
|
11
|
+
private disposed;
|
|
12
|
+
constructor(maxBytes?: number, maxEntries?: number);
|
|
13
|
+
/** Identity is owner-local; a custom loader must never alias another loader. */
|
|
14
|
+
identity(value: object | undefined): number;
|
|
15
|
+
get<T>(key: string): T | undefined;
|
|
16
|
+
/** Only the trusted host publishes fully verified results; failures are never retained. */
|
|
17
|
+
set<T>(key: string, value: T, bytes: number): void;
|
|
18
|
+
stats(): {
|
|
19
|
+
hits: number;
|
|
20
|
+
misses: number;
|
|
21
|
+
entries: number;
|
|
22
|
+
bytes: number;
|
|
23
|
+
disposed: boolean;
|
|
24
|
+
};
|
|
25
|
+
dispose(): void;
|
|
26
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** Immutable bootstrap assets only: never store a worker, Wasm instance, or user filesystem. */
|
|
2
|
+
export class RuntimeAssetCache {
|
|
3
|
+
maxBytes;
|
|
4
|
+
maxEntries;
|
|
5
|
+
entries = new Map();
|
|
6
|
+
identities = new WeakMap();
|
|
7
|
+
nextIdentity = 0;
|
|
8
|
+
bytes = 0;
|
|
9
|
+
hits = 0;
|
|
10
|
+
misses = 0;
|
|
11
|
+
disposed = false;
|
|
12
|
+
constructor(maxBytes = 256 * 1024 * 1024, maxEntries = 64) {
|
|
13
|
+
this.maxBytes = maxBytes;
|
|
14
|
+
this.maxEntries = maxEntries;
|
|
15
|
+
if (!Number.isSafeInteger(maxBytes) ||
|
|
16
|
+
maxBytes < 0 ||
|
|
17
|
+
!Number.isSafeInteger(maxEntries) ||
|
|
18
|
+
maxEntries < 0) {
|
|
19
|
+
throw new TypeError('Runtime cache limits must be non-negative safe integers');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Identity is owner-local; a custom loader must never alias another loader. */
|
|
23
|
+
identity(value) {
|
|
24
|
+
if (!value)
|
|
25
|
+
return 0;
|
|
26
|
+
let identity = this.identities.get(value);
|
|
27
|
+
if (identity === undefined) {
|
|
28
|
+
identity = ++this.nextIdentity;
|
|
29
|
+
this.identities.set(value, identity);
|
|
30
|
+
}
|
|
31
|
+
return identity;
|
|
32
|
+
}
|
|
33
|
+
get(key) {
|
|
34
|
+
if (this.disposed)
|
|
35
|
+
return undefined;
|
|
36
|
+
const entry = this.entries.get(key);
|
|
37
|
+
if (!entry) {
|
|
38
|
+
this.misses++;
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
this.hits++;
|
|
42
|
+
this.entries.delete(key);
|
|
43
|
+
this.entries.set(key, entry);
|
|
44
|
+
return entry.value;
|
|
45
|
+
}
|
|
46
|
+
/** Only the trusted host publishes fully verified results; failures are never retained. */
|
|
47
|
+
set(key, value, bytes) {
|
|
48
|
+
if (this.disposed)
|
|
49
|
+
return;
|
|
50
|
+
if (!Number.isSafeInteger(bytes) || bytes < 0)
|
|
51
|
+
throw new TypeError('Invalid cache byte count');
|
|
52
|
+
const charge = bytes + key.length * 2;
|
|
53
|
+
if (this.maxEntries === 0 || charge > this.maxBytes)
|
|
54
|
+
return;
|
|
55
|
+
const previous = this.entries.get(key);
|
|
56
|
+
if (previous)
|
|
57
|
+
this.bytes -= previous.bytes;
|
|
58
|
+
this.entries.delete(key);
|
|
59
|
+
this.entries.set(key, { value, bytes: charge });
|
|
60
|
+
this.bytes += charge;
|
|
61
|
+
while (this.entries.size > this.maxEntries || this.bytes > this.maxBytes) {
|
|
62
|
+
const oldest = this.entries.keys().next().value;
|
|
63
|
+
this.bytes -= this.entries.get(oldest).bytes;
|
|
64
|
+
this.entries.delete(oldest);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
stats() {
|
|
68
|
+
return {
|
|
69
|
+
hits: this.hits,
|
|
70
|
+
misses: this.misses,
|
|
71
|
+
entries: this.entries.size,
|
|
72
|
+
bytes: this.bytes,
|
|
73
|
+
disposed: this.disposed
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
dispose() {
|
|
77
|
+
if (this.disposed)
|
|
78
|
+
return;
|
|
79
|
+
this.disposed = true;
|
|
80
|
+
this.entries.clear();
|
|
81
|
+
this.identities = new WeakMap();
|
|
82
|
+
this.bytes = 0;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PlaygroundBinding, SandboxRuntimeAssets } from './sandbox';
|
|
2
|
+
import { RuntimeAssetCache } from './runtimeAssetCache';
|
|
3
|
+
import { CompiledArtifactCache } from './compiledArtifactCache';
|
|
4
|
+
export interface PlaygroundRuntimeSession {
|
|
5
|
+
createBinding(): PlaygroundBinding & {
|
|
6
|
+
dispose(): Promise<void>;
|
|
7
|
+
};
|
|
8
|
+
selectLanguage(language: string): Promise<void>;
|
|
9
|
+
dispose(): Promise<void>;
|
|
10
|
+
getCacheStats(): {
|
|
11
|
+
language: string | undefined;
|
|
12
|
+
assets: ReturnType<RuntimeAssetCache['stats']>;
|
|
13
|
+
artifacts: ReturnType<CompiledArtifactCache['stats']>;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A browser-owned language session. Panels own disposable execution leases, not the
|
|
18
|
+
* compiler cache. A problem/editor change may release all leases without cooling it.
|
|
19
|
+
*/
|
|
20
|
+
export declare function createRuntimeSession(runtimeAssets: SandboxRuntimeAssets): PlaygroundRuntimeSession;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { createPlaygroundBinding, normalizeLanguageId, RuntimeConfigurationError } from '@wasm-idle/core';
|
|
2
|
+
import playground from './index';
|
|
3
|
+
import { RuntimeAssetCache } from './runtimeAssetCache';
|
|
4
|
+
import { CompiledArtifactCache } from './compiledArtifactCache';
|
|
5
|
+
import { createCachedClangSandbox } from './cachedClang';
|
|
6
|
+
/**
|
|
7
|
+
* A browser-owned language session. Panels own disposable execution leases, not the
|
|
8
|
+
* compiler cache. A problem/editor change may release all leases without cooling it.
|
|
9
|
+
*/
|
|
10
|
+
export function createRuntimeSession(runtimeAssets) {
|
|
11
|
+
let language;
|
|
12
|
+
let epoch = 0;
|
|
13
|
+
let disposed = false;
|
|
14
|
+
let disposal;
|
|
15
|
+
let assetCache = new RuntimeAssetCache();
|
|
16
|
+
let artifactCache = new CompiledArtifactCache();
|
|
17
|
+
const leases = new Set();
|
|
18
|
+
let retiring = Promise.resolve();
|
|
19
|
+
const stale = () => new RuntimeConfigurationError('Runtime session was disposed or its language changed', {
|
|
20
|
+
phase: 'dispose'
|
|
21
|
+
});
|
|
22
|
+
const release = (owned) => {
|
|
23
|
+
const previous = retiring;
|
|
24
|
+
retiring = Promise.allSettled([previous, ...owned.map((lease) => lease.dispose())]).then((results) => {
|
|
25
|
+
const failure = results.find((result) => result.status === 'rejected');
|
|
26
|
+
if (failure?.status === 'rejected')
|
|
27
|
+
throw failure.reason;
|
|
28
|
+
});
|
|
29
|
+
// Errors remain visible to the caller without unhandled background rejections.
|
|
30
|
+
void retiring.catch(() => undefined);
|
|
31
|
+
return retiring;
|
|
32
|
+
};
|
|
33
|
+
const session = {
|
|
34
|
+
async selectLanguage(value) {
|
|
35
|
+
if (disposed)
|
|
36
|
+
throw stale();
|
|
37
|
+
const next = normalizeLanguageId(value) || value.trim().toUpperCase();
|
|
38
|
+
if (!next)
|
|
39
|
+
return;
|
|
40
|
+
if (next === language)
|
|
41
|
+
return await retiring;
|
|
42
|
+
language = next;
|
|
43
|
+
epoch++;
|
|
44
|
+
assetCache.dispose();
|
|
45
|
+
artifactCache.clear();
|
|
46
|
+
assetCache = new RuntimeAssetCache();
|
|
47
|
+
artifactCache = new CompiledArtifactCache();
|
|
48
|
+
await release([...leases].filter((lease) => lease.epoch !== undefined));
|
|
49
|
+
},
|
|
50
|
+
createBinding() {
|
|
51
|
+
if (disposed)
|
|
52
|
+
throw stale();
|
|
53
|
+
let closed = false;
|
|
54
|
+
let closing;
|
|
55
|
+
const lease = {
|
|
56
|
+
epoch: undefined,
|
|
57
|
+
dispose() {
|
|
58
|
+
if (closing)
|
|
59
|
+
return closing;
|
|
60
|
+
closed = true;
|
|
61
|
+
leases.delete(lease);
|
|
62
|
+
closing = Promise.resolve().then(() => core.dispose());
|
|
63
|
+
return closing;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const core = createPlaygroundBinding(runtimeAssets, async (target) => {
|
|
67
|
+
if (closed || disposed)
|
|
68
|
+
throw stale();
|
|
69
|
+
const selected = session.selectLanguage(target);
|
|
70
|
+
const selectedEpoch = epoch;
|
|
71
|
+
await selected;
|
|
72
|
+
if (closed || disposed || epoch !== selectedEpoch)
|
|
73
|
+
throw stale();
|
|
74
|
+
lease.epoch = selectedEpoch;
|
|
75
|
+
const cache = assetCache;
|
|
76
|
+
const artifacts = artifactCache;
|
|
77
|
+
const sandbox = (await playground(target));
|
|
78
|
+
if (closed || disposed || epoch !== selectedEpoch) {
|
|
79
|
+
if (sandbox.dispose)
|
|
80
|
+
await sandbox.dispose();
|
|
81
|
+
else
|
|
82
|
+
await sandbox.terminate();
|
|
83
|
+
throw stale();
|
|
84
|
+
}
|
|
85
|
+
sandbox.setRuntimeAssetCache?.(cache);
|
|
86
|
+
return target === 'C' || target === 'CPP'
|
|
87
|
+
? createCachedClangSandbox(sandbox, target, cache, artifacts)
|
|
88
|
+
: sandbox;
|
|
89
|
+
});
|
|
90
|
+
leases.add(lease);
|
|
91
|
+
const binding = core;
|
|
92
|
+
// Do not override core.dispose in place: the lease calls that exact owner method.
|
|
93
|
+
return new Proxy(binding, {
|
|
94
|
+
get(target, property) {
|
|
95
|
+
if (property === 'dispose')
|
|
96
|
+
return lease.dispose;
|
|
97
|
+
const value = Reflect.get(target, property);
|
|
98
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
dispose() {
|
|
103
|
+
if (disposal)
|
|
104
|
+
return disposal;
|
|
105
|
+
disposed = true;
|
|
106
|
+
epoch++;
|
|
107
|
+
assetCache.dispose();
|
|
108
|
+
artifactCache.clear();
|
|
109
|
+
disposal = release([...leases]);
|
|
110
|
+
return disposal;
|
|
111
|
+
},
|
|
112
|
+
getCacheStats: () => ({
|
|
113
|
+
language,
|
|
114
|
+
assets: assetCache.stats(),
|
|
115
|
+
artifacts: artifactCache.stats()
|
|
116
|
+
})
|
|
117
|
+
};
|
|
118
|
+
return session;
|
|
119
|
+
}
|
|
@@ -2,6 +2,7 @@ export interface WorkerRuntimeAssetConfig {
|
|
|
2
2
|
baseUrl: string;
|
|
3
3
|
maxAssetBytes?: number;
|
|
4
4
|
useAssetBridge: boolean;
|
|
5
|
+
useModuleBridge?: boolean;
|
|
5
6
|
}
|
|
6
7
|
export interface WorkerRuntimeAssetAllowlist {
|
|
7
8
|
baseUrl: string;
|
|
@@ -16,4 +17,6 @@ export declare function configureWorkerRuntimeAssets(config: WorkerRuntimeAssetC
|
|
|
16
17
|
export declare function configureWorkerRuntimeAssetAllowlist(allowlist: WorkerRuntimeAssetAllowlist | null): void;
|
|
17
18
|
export declare function handleWorkerAssetMessage(data: any): boolean;
|
|
18
19
|
export declare function loadWorkerRuntimeAsset(asset: string): Promise<LoadedWorkerAsset>;
|
|
20
|
+
export declare function hasWorkerRuntimeModuleBridge(): boolean;
|
|
21
|
+
export declare function loadWorkerRuntimeModule(asset: string): Promise<WebAssembly.Module>;
|
|
19
22
|
export {};
|