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/README.md
CHANGED
|
@@ -900,6 +900,40 @@ The pack contains the same locked upstream files, not an import-based subset. Ea
|
|
|
900
900
|
|
|
901
901
|
`node scripts/sync-runtime.mjs wasm-ruby` regenerates both profiles from the pinned producer inputs. `node scripts/sync-wasm-ruby-split.mjs --verify` additionally checks split-profile freshness without replacing published files.
|
|
902
902
|
|
|
903
|
+
### Retaining compiler caches across editor navigation
|
|
904
|
+
|
|
905
|
+
Create one `createRuntimeSession(assets)` owner per browser application and use
|
|
906
|
+
`session.createBinding()` for each editor or execution panel. C/C++ and Python
|
|
907
|
+
keep immutable compiler/runtime modules outside their disposable execution Workers:
|
|
908
|
+
|
|
909
|
+
```ts
|
|
910
|
+
import { createRuntimeSession } from 'wasm-idle';
|
|
911
|
+
|
|
912
|
+
const session = createRuntimeSession(assets); // browser scope, not an SSR singleton
|
|
913
|
+
const panel = session.createBinding();
|
|
914
|
+
const sandbox = await panel.load('PYTHON');
|
|
915
|
+
await sandbox.load('print("hi")', false);
|
|
916
|
+
await sandbox.clear(); // reset execution state; retain compiler/runtime cache
|
|
917
|
+
await panel.dispose(); // close this panel; retain the session's cache
|
|
918
|
+
|
|
919
|
+
const nextPanel = session.createBinding(); // another problem, same language
|
|
920
|
+
await nextPanel.load('PYTHON');
|
|
921
|
+
await session.selectLanguage('CPP'); // release old-language panels and cache
|
|
922
|
+
await session.dispose(); // full, terminal release of this owner
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
Selecting a language does not download assets. The owner retains only one language;
|
|
926
|
+
selecting an unsupported/server-only language also releases the previous cache.
|
|
927
|
+
Executions never share mutable Wasm instances, user files or stdin. C/C++ additionally
|
|
928
|
+
reuse compiled programs when source, workspace and compiler options match, while
|
|
929
|
+
custom loaders and debugging retain the original execution path. Python still creates
|
|
930
|
+
a fresh interpreter after `clear()`; module reuse does not eliminate interpreter startup.
|
|
931
|
+
|
|
932
|
+
The asset cache is LRU-bounded to 64 entries and 256 MiB of logical asset bytes
|
|
933
|
+
(not a measurement of the browser engine's compiled-code memory). C/C++ program
|
|
934
|
+
artifacts are independently bounded to 8 entries and 32 MiB. `getCacheStats()` exposes
|
|
935
|
+
cache counters for diagnostics. Other languages currently keep their existing lifecycle.
|
|
936
|
+
|
|
903
937
|
### Optional idle runtime prewarming
|
|
904
938
|
|
|
905
939
|
`createPlaygroundBinding(assets, { prewarm: true })` enables explicit
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import playground, { createPlaygroundBinding } from './playground';
|
|
2
2
|
export default playground;
|
|
3
3
|
export { createPlaygroundBinding, playground };
|
|
4
|
+
export { createRuntimeSession } from './playground/runtimeSession';
|
|
5
|
+
export type { PlaygroundRuntimeSession } from './playground/runtimeSession';
|
|
4
6
|
export { isSharedArrayBufferAvailable, requireSharedArrayBuffer } from './playground/sharedBuffer';
|
|
5
7
|
export type { DebugCommand, DebugFrame, DebugSessionEvent, DebugVariable, TerminalControl } from '@wasm-idle/core';
|
|
6
8
|
export type { CompilerDiagnostic, SandboxExecutionOptions } from './playground/options';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import playground, { createPlaygroundBinding } from './playground';
|
|
2
2
|
export default playground;
|
|
3
3
|
export { createPlaygroundBinding, playground };
|
|
4
|
+
export { createRuntimeSession } from './playground/runtimeSession';
|
|
4
5
|
export { isSharedArrayBufferAvailable, requireSharedArrayBuffer } from './playground/sharedBuffer';
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { type ResolvedRuntimeAssetConfig, type RuntimeAssetRuntime } from './assets';
|
|
2
2
|
import { type ProgressLike } from '@wasm-idle/core';
|
|
3
|
+
import { RuntimeAssetCache } from './runtimeAssetCache';
|
|
3
4
|
export declare const boundedUtf8ByteLength: (value: string, maxBytes?: number) => number;
|
|
4
5
|
export declare class WorkerAssetBridge {
|
|
6
|
+
private readonly cache?;
|
|
5
7
|
private worker;
|
|
6
8
|
private readonly runtime;
|
|
7
9
|
private config;
|
|
@@ -13,7 +15,7 @@ export declare class WorkerAssetBridge {
|
|
|
13
15
|
private state;
|
|
14
16
|
private readonly activeLoads;
|
|
15
17
|
private maxAssetBytes;
|
|
16
|
-
constructor(worker: Worker, runtime: RuntimeAssetRuntime, config: ResolvedRuntimeAssetConfig, progress?: ProgressLike, maxAssetBytes?: number, languageSysroots?: boolean);
|
|
18
|
+
constructor(worker: Worker, runtime: RuntimeAssetRuntime, config: ResolvedRuntimeAssetConfig, progress?: ProgressLike, maxAssetBytes?: number, languageSysroots?: boolean, cache?: RuntimeAssetCache | undefined);
|
|
17
19
|
matches(config: ResolvedRuntimeAssetConfig, maxAssetBytes?: number, languageSysroots?: boolean): boolean;
|
|
18
20
|
rebind(worker: Worker, config: ResolvedRuntimeAssetConfig, progress?: ProgressLike, maxAssetBytes?: number, languageSysroots?: boolean): void;
|
|
19
21
|
dispose(): void;
|
|
@@ -23,6 +25,8 @@ export declare class WorkerAssetBridge {
|
|
|
23
25
|
private sourceAssetByteLimit;
|
|
24
26
|
handleMessage(event: MessageEvent<any>): boolean;
|
|
25
27
|
private respond;
|
|
28
|
+
private loadVerifiedAsset;
|
|
29
|
+
private validateAssetRequest;
|
|
26
30
|
private loadAsset;
|
|
27
31
|
private verifyIntegrity;
|
|
28
32
|
private normalizeLoaderResult;
|
|
@@ -4,6 +4,9 @@ import { decompressGzip } from '@wasm-idle/llvm-core';
|
|
|
4
4
|
import { readPythonPackageAssets } from './pythonPackageLock';
|
|
5
5
|
import { BUNDLED_CLANG_LANGUAGE_SYSROOT_PROFILES } from './clangAssetIntegrity';
|
|
6
6
|
import { shouldStreamBundledClang } from './clangStreamingPolicy';
|
|
7
|
+
import { compileVerifiedWasmAsset } from '@wasm-idle/llvm-core/core/verified-wasm';
|
|
8
|
+
import { BUNDLED_CLANG_ASSET_INTEGRITY } from './clangAssetIntegrity';
|
|
9
|
+
import { RuntimeAssetCache } from './runtimeAssetCache';
|
|
7
10
|
const encoder = new TextEncoder();
|
|
8
11
|
const DEFAULT_STREAM_BUFFER_BYTES = 64 * 1024;
|
|
9
12
|
const MAX_RUNTIME_ASSET_BYTES = 128 * 1024 * 1024;
|
|
@@ -372,6 +375,7 @@ class RuntimeLoadProgress {
|
|
|
372
375
|
}
|
|
373
376
|
}
|
|
374
377
|
export class WorkerAssetBridge {
|
|
378
|
+
cache;
|
|
375
379
|
worker;
|
|
376
380
|
runtime;
|
|
377
381
|
config;
|
|
@@ -383,7 +387,8 @@ export class WorkerAssetBridge {
|
|
|
383
387
|
state = 'active';
|
|
384
388
|
activeLoads = new Set();
|
|
385
389
|
maxAssetBytes;
|
|
386
|
-
constructor(worker, runtime, config, progress, maxAssetBytes = MAX_RUNTIME_ASSET_BYTES, languageSysroots = false) {
|
|
390
|
+
constructor(worker, runtime, config, progress, maxAssetBytes = MAX_RUNTIME_ASSET_BYTES, languageSysroots = false, cache) {
|
|
391
|
+
this.cache = cache;
|
|
387
392
|
this.worker = worker;
|
|
388
393
|
this.runtime = runtime;
|
|
389
394
|
this.config = config;
|
|
@@ -522,51 +527,71 @@ export class WorkerAssetBridge {
|
|
|
522
527
|
const controller = new AbortController();
|
|
523
528
|
this.activeLoads.add(controller);
|
|
524
529
|
try {
|
|
525
|
-
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
this.
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
.toLowerCase()
|
|
542
|
-
.split(',')
|
|
543
|
-
.map((encoding) => encoding.trim())
|
|
544
|
-
.includes('gzip');
|
|
545
|
-
if (controller.signal.aborted || generation !== this.generation)
|
|
546
|
-
return;
|
|
547
|
-
let cancelOnAbort;
|
|
548
|
-
const aborted = new Promise((_resolve, reject) => {
|
|
549
|
-
cancelOnAbort = () => reject(runtimeAssetAbortReason(controller.signal));
|
|
550
|
-
controller.signal.addEventListener('abort', cancelOnAbort, { once: true });
|
|
551
|
-
});
|
|
552
|
-
try {
|
|
553
|
-
if (this.config.integrity?.[request.asset]) {
|
|
554
|
-
this.progress.activity('verifying', request.asset);
|
|
530
|
+
this.validateAssetRequest(request.asset);
|
|
531
|
+
const key = JSON.stringify([
|
|
532
|
+
this.runtime,
|
|
533
|
+
this.config.baseUrl,
|
|
534
|
+
this.cache?.identity(this.config.loader),
|
|
535
|
+
integrityKey(this.config),
|
|
536
|
+
allowedBaseUrlsKey(this.config),
|
|
537
|
+
this.config.useAssetBridge,
|
|
538
|
+
this.maxAssetBytes,
|
|
539
|
+
this.languageSysroots,
|
|
540
|
+
request.asset,
|
|
541
|
+
request.module === true ? 'module' : 'bytes'
|
|
542
|
+
]);
|
|
543
|
+
if (request.module === true) {
|
|
544
|
+
if (!/\.wasm(?:\.gz)?$/u.test(request.asset)) {
|
|
545
|
+
throw new Error('Only runtime Wasm assets can be compiled');
|
|
555
546
|
}
|
|
556
|
-
|
|
557
|
-
|
|
547
|
+
let module = this.cache?.get(key);
|
|
548
|
+
if (!module) {
|
|
549
|
+
const streaming = this.runtime === 'clang' &&
|
|
550
|
+
shouldStreamBundledClang(this.config) &&
|
|
551
|
+
(request.asset === 'bin/clang.wasm.gz' ||
|
|
552
|
+
request.asset === 'bin/lld.wasm.gz');
|
|
553
|
+
let bytes;
|
|
554
|
+
if (streaming) {
|
|
555
|
+
const receipt = BUNDLED_CLANG_ASSET_INTEGRITY[request.asset];
|
|
556
|
+
module = await compileVerifiedWasmAsset(new URL(request.asset, this.config.baseUrl).href, receipt, {
|
|
557
|
+
fetch: globalThis.fetch.bind(globalThis),
|
|
558
|
+
maxAssetBytes: this.maxAssetBytes,
|
|
559
|
+
signal: controller.signal,
|
|
560
|
+
onProgress: (loaded, total) => this.progress.update(request.asset, loaded, total)
|
|
561
|
+
});
|
|
562
|
+
bytes = receipt.uncompressedBytes;
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
const loaded = await this.loadVerifiedAsset(request.asset, controller.signal);
|
|
566
|
+
bytes = loaded.bytes.byteLength;
|
|
567
|
+
module = await WebAssembly.compile(Uint8Array.from(loaded.bytes));
|
|
568
|
+
}
|
|
569
|
+
if (controller.signal.aborted || generation !== this.generation)
|
|
570
|
+
return;
|
|
571
|
+
this.cache?.set(key, module, bytes);
|
|
572
|
+
}
|
|
573
|
+
if (controller.signal.aborted || generation !== this.generation)
|
|
574
|
+
return;
|
|
575
|
+
worker.postMessage({ assetResponse: { id: request.id, ok: true, module } });
|
|
576
|
+
return;
|
|
558
577
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
578
|
+
let loaded = this.cache?.get(key);
|
|
579
|
+
if (!loaded) {
|
|
580
|
+
loaded = await this.loadVerifiedAsset(request.asset, controller.signal);
|
|
581
|
+
if (controller.signal.aborted || generation !== this.generation)
|
|
582
|
+
return;
|
|
583
|
+
// Keep private copies: never transfer or lend the owner cache's backing buffer.
|
|
584
|
+
if (this.cache) {
|
|
585
|
+
loaded = { bytes: Uint8Array.from(loaded.bytes), mimeType: loaded.mimeType };
|
|
586
|
+
this.cache.set(key, loaded, loaded.bytes.byteLength);
|
|
562
587
|
}
|
|
563
588
|
}
|
|
564
|
-
if (controller.signal.aborted || generation !== this.generation)
|
|
565
|
-
return;
|
|
566
589
|
if (this.runtime === 'python' && request.asset === 'pyodide-lock.json') {
|
|
567
|
-
this.pythonPackageAssets = readPythonPackageAssets(
|
|
590
|
+
this.pythonPackageAssets = readPythonPackageAssets(loaded.bytes);
|
|
568
591
|
}
|
|
569
|
-
|
|
592
|
+
if (controller.signal.aborted || generation !== this.generation)
|
|
593
|
+
return;
|
|
594
|
+
const buffer = transferBuffer(loaded.bytes, this.cache ? false : loaded.transferOwnership);
|
|
570
595
|
worker.postMessage({
|
|
571
596
|
assetResponse: {
|
|
572
597
|
id: request.id,
|
|
@@ -589,20 +614,73 @@ export class WorkerAssetBridge {
|
|
|
589
614
|
});
|
|
590
615
|
}
|
|
591
616
|
catch {
|
|
592
|
-
// The worker may already be terminated.
|
|
617
|
+
// The worker may already be terminated.
|
|
593
618
|
}
|
|
594
619
|
}
|
|
595
620
|
finally {
|
|
596
621
|
this.activeLoads.delete(controller);
|
|
597
622
|
}
|
|
598
623
|
}
|
|
599
|
-
async
|
|
624
|
+
async loadVerifiedAsset(asset, signal) {
|
|
625
|
+
const loaded = await this.loadAsset(asset, signal);
|
|
626
|
+
const deliveryBytes = canonicalUint8Array(loaded.bytes);
|
|
627
|
+
const sourceAssetByteLimit = this.sourceAssetByteLimit(asset);
|
|
628
|
+
const runtimeAssetByteLimit = this.runtimeAssetByteLimit(asset);
|
|
629
|
+
requireRuntimeAssetSize(asset, deliveryBytes.byteLength, sourceAssetByteLimit);
|
|
630
|
+
let normalizedRuntimeBytes;
|
|
631
|
+
if (asset.endsWith('.gz')) {
|
|
632
|
+
this.progress.activity('decompressing', asset);
|
|
633
|
+
normalizedRuntimeBytes = await decompressGzip(deliveryBytes, asset, runtimeAssetByteLimit, signal);
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
normalizedRuntimeBytes = deliveryBytes;
|
|
637
|
+
}
|
|
638
|
+
const runtimeBytes = canonicalUint8Array(normalizedRuntimeBytes);
|
|
639
|
+
requireRuntimeAssetSize(asset, runtimeBytes.byteLength, runtimeAssetByteLimit);
|
|
640
|
+
const httpDecodedGzip = (loaded.contentEncoding || '')
|
|
641
|
+
.toLowerCase()
|
|
642
|
+
.split(',')
|
|
643
|
+
.map((encoding) => encoding.trim())
|
|
644
|
+
.includes('gzip');
|
|
645
|
+
if (signal.aborted)
|
|
646
|
+
throw runtimeAssetAbortReason(signal);
|
|
647
|
+
let cancelOnAbort;
|
|
648
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
649
|
+
cancelOnAbort = () => reject(runtimeAssetAbortReason(signal));
|
|
650
|
+
signal.addEventListener('abort', cancelOnAbort, { once: true });
|
|
651
|
+
});
|
|
652
|
+
try {
|
|
653
|
+
if (this.config.integrity?.[asset]) {
|
|
654
|
+
this.progress.activity('verifying', asset);
|
|
655
|
+
}
|
|
656
|
+
const verification = this.verifyIntegrity(asset, deliveryBytes, runtimeBytes, loaded.mimeType, !httpDecodedGzip);
|
|
657
|
+
await Promise.race([verification, aborted]);
|
|
658
|
+
}
|
|
659
|
+
finally {
|
|
660
|
+
if (cancelOnAbort) {
|
|
661
|
+
signal.removeEventListener('abort', cancelOnAbort);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (signal.aborted)
|
|
665
|
+
throw runtimeAssetAbortReason(signal);
|
|
666
|
+
if (this.runtime === 'python' && asset === 'pyodide-lock.json')
|
|
667
|
+
readPythonPackageAssets(runtimeBytes);
|
|
668
|
+
return {
|
|
669
|
+
bytes: runtimeBytes,
|
|
670
|
+
mimeType: loaded.mimeType,
|
|
671
|
+
transferOwnership: normalizedRuntimeBytes === deliveryBytes ? loaded.transferOwnership : true
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
validateAssetRequest(asset) {
|
|
600
675
|
if (!this.expectedAssets.has(asset) && !this.pythonPackageAssets.has(asset)) {
|
|
601
676
|
throw new Error(`Unexpected ${this.runtime} runtime asset: ${asset}`);
|
|
602
677
|
}
|
|
603
678
|
if (this.config.integrity && !Object.hasOwn(this.config.integrity, asset)) {
|
|
604
679
|
throw new Error(`Runtime asset ${asset} is missing integrity metadata`);
|
|
605
680
|
}
|
|
681
|
+
}
|
|
682
|
+
async loadAsset(asset, signal) {
|
|
683
|
+
this.validateAssetRequest(asset);
|
|
606
684
|
if (signal.aborted) {
|
|
607
685
|
throw runtimeAssetAbortReason(signal);
|
|
608
686
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Sandbox } from '@wasm-idle/core';
|
|
2
|
+
import type { RuntimeAssetCache } from './runtimeAssetCache';
|
|
3
|
+
import { CompiledArtifactCache } from './compiledArtifactCache';
|
|
4
|
+
type WorkerFactories = {
|
|
5
|
+
compile(): Worker | Promise<Worker>;
|
|
6
|
+
execute(): Worker | Promise<Worker>;
|
|
7
|
+
};
|
|
8
|
+
/** Wrap the public sandbox contract; all other languages and debugger operations stay unchanged. */
|
|
9
|
+
export declare function createCachedClangSandbox(legacy: Sandbox, language: 'C' | 'CPP', runtimeCache: RuntimeAssetCache, cache?: CompiledArtifactCache, factories?: WorkerFactories): Sandbox;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { resolveExecutionLimits } from '@wasm-idle/core';
|
|
2
|
+
import { resolveRuntimeAssetConfig } from './assets';
|
|
3
|
+
import { WorkerAssetBridge } from './assetBridge';
|
|
4
|
+
import { shouldStreamBundledClang } from './clangStreamingPolicy';
|
|
5
|
+
import { CompiledArtifactCache, clangCompileKey } from './compiledArtifactCache';
|
|
6
|
+
const workers = {
|
|
7
|
+
async compile() {
|
|
8
|
+
const { default: CompileWorker } = await import('./worker/cachedClangCompile?worker');
|
|
9
|
+
return new CompileWorker();
|
|
10
|
+
},
|
|
11
|
+
async execute() {
|
|
12
|
+
const { default: ExecuteWorker } = await import('./worker/cachedClangExecute?worker');
|
|
13
|
+
return new ExecuteWorker();
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
/** Wrap the public sandbox contract; all other languages and debugger operations stay unchanged. */
|
|
17
|
+
export function createCachedClangSandbox(legacy, language, runtimeCache, cache = new CompiledArtifactCache(), factories = workers) {
|
|
18
|
+
let cached = false;
|
|
19
|
+
let loadedAssets = '';
|
|
20
|
+
let runtimeBaseUrl = '';
|
|
21
|
+
let assetConfig;
|
|
22
|
+
let maxAssetBytes = 0;
|
|
23
|
+
let languageSysroots = false;
|
|
24
|
+
let activeWorker;
|
|
25
|
+
let cancelWorker;
|
|
26
|
+
let generation = 0;
|
|
27
|
+
let running = false;
|
|
28
|
+
let disposed = false;
|
|
29
|
+
let disposal;
|
|
30
|
+
let inputBuffer;
|
|
31
|
+
let inputRequested = false;
|
|
32
|
+
let pendingEof = false;
|
|
33
|
+
const pendingInput = [];
|
|
34
|
+
const encoder = new TextEncoder();
|
|
35
|
+
function resetInput() {
|
|
36
|
+
pendingInput.length = 0;
|
|
37
|
+
pendingEof = false;
|
|
38
|
+
inputRequested = false;
|
|
39
|
+
inputBuffer = undefined;
|
|
40
|
+
}
|
|
41
|
+
function flushInput() {
|
|
42
|
+
if (!inputRequested || !inputBuffer)
|
|
43
|
+
return;
|
|
44
|
+
const control = new Int32Array(inputBuffer, 0, 2);
|
|
45
|
+
const payload = new Uint8Array(inputBuffer, 8);
|
|
46
|
+
const next = pendingInput[0];
|
|
47
|
+
if (next) {
|
|
48
|
+
const length = Math.min(next.byteLength, payload.byteLength);
|
|
49
|
+
payload.set(next.subarray(0, length));
|
|
50
|
+
if (length === next.byteLength)
|
|
51
|
+
pendingInput.shift();
|
|
52
|
+
else
|
|
53
|
+
pendingInput[0] = next.subarray(length);
|
|
54
|
+
Atomics.store(control, 1, length);
|
|
55
|
+
Atomics.store(control, 0, 1);
|
|
56
|
+
}
|
|
57
|
+
else if (pendingEof) {
|
|
58
|
+
Atomics.store(control, 1, 0);
|
|
59
|
+
Atomics.store(control, 0, 2);
|
|
60
|
+
}
|
|
61
|
+
else
|
|
62
|
+
return;
|
|
63
|
+
inputRequested = false;
|
|
64
|
+
Atomics.notify(control, 0);
|
|
65
|
+
}
|
|
66
|
+
function stop() {
|
|
67
|
+
generation++;
|
|
68
|
+
running = false;
|
|
69
|
+
cancelWorker?.(new Error('Process terminated'));
|
|
70
|
+
resetInput();
|
|
71
|
+
}
|
|
72
|
+
async function workerOperation(factory, message, token, progress, compileLimit = 0, signal) {
|
|
73
|
+
const worker = await factory();
|
|
74
|
+
if (token !== generation || signal?.aborted) {
|
|
75
|
+
worker.terminate();
|
|
76
|
+
throw signal?.reason ?? new Error('Process terminated');
|
|
77
|
+
}
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
let bridge;
|
|
80
|
+
try {
|
|
81
|
+
if (compileLimit)
|
|
82
|
+
bridge = new WorkerAssetBridge(worker, 'clang', assetConfig, progress, compileLimit, languageSysroots, runtimeCache);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
worker.terminate();
|
|
86
|
+
reject(error);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
activeWorker = worker;
|
|
90
|
+
const abort = () => finish(signal?.reason instanceof Error
|
|
91
|
+
? signal.reason
|
|
92
|
+
: new Error('Process terminated'));
|
|
93
|
+
const finish = (error, value) => {
|
|
94
|
+
if (activeWorker !== worker)
|
|
95
|
+
return;
|
|
96
|
+
activeWorker = undefined;
|
|
97
|
+
cancelWorker = undefined;
|
|
98
|
+
signal?.removeEventListener('abort', abort);
|
|
99
|
+
bridge?.dispose();
|
|
100
|
+
worker.onmessage = null;
|
|
101
|
+
worker.onerror = null;
|
|
102
|
+
worker.onmessageerror = null;
|
|
103
|
+
worker.terminate();
|
|
104
|
+
if (error)
|
|
105
|
+
reject(error);
|
|
106
|
+
else
|
|
107
|
+
resolve(value);
|
|
108
|
+
};
|
|
109
|
+
cancelWorker = finish;
|
|
110
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
111
|
+
worker.onerror = (event) => finish(new Error(event.message || 'C/C++ worker failed'));
|
|
112
|
+
worker.onmessageerror = () => finish(new Error('C/C++ worker message could not be decoded'));
|
|
113
|
+
worker.onmessage = (event) => {
|
|
114
|
+
if (activeWorker !== worker)
|
|
115
|
+
return;
|
|
116
|
+
try {
|
|
117
|
+
if (bridge?.handleMessage(event))
|
|
118
|
+
return;
|
|
119
|
+
const { data } = event;
|
|
120
|
+
if (data.type === 'output')
|
|
121
|
+
legacy.output?.(data.output);
|
|
122
|
+
else if (data.type === 'progress') {
|
|
123
|
+
progress?.set?.(data.progress.percent / 100);
|
|
124
|
+
progress?.report?.({
|
|
125
|
+
kind: 'activity',
|
|
126
|
+
phase: 'compiling',
|
|
127
|
+
label: data.progress.message
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
else if (data.type === 'stdin') {
|
|
131
|
+
inputRequested = true;
|
|
132
|
+
progress?.report?.({
|
|
133
|
+
kind: 'ready',
|
|
134
|
+
state: 'waiting-input',
|
|
135
|
+
reason: 'stdin-request',
|
|
136
|
+
label: 'Runtime ready for input'
|
|
137
|
+
});
|
|
138
|
+
flushInput();
|
|
139
|
+
}
|
|
140
|
+
else if (data.type === 'compiled' || data.type === 'error') {
|
|
141
|
+
if (data.stdout)
|
|
142
|
+
legacy.output?.(data.stdout);
|
|
143
|
+
if (data.stderr)
|
|
144
|
+
legacy.output?.(data.stderr);
|
|
145
|
+
if (data.type === 'error')
|
|
146
|
+
finish(new Error(data.error));
|
|
147
|
+
else
|
|
148
|
+
finish(undefined, data.artifact);
|
|
149
|
+
}
|
|
150
|
+
else if (data.type === 'done') {
|
|
151
|
+
if (data.exitCode !== 0 && data.exitCode !== null)
|
|
152
|
+
finish(new Error(`Program exited with code ${data.exitCode}`));
|
|
153
|
+
else
|
|
154
|
+
finish(undefined, true);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
finish(error instanceof Error ? error : new Error(String(error)));
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
try {
|
|
162
|
+
worker.postMessage(message);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
finish(error instanceof Error ? error : new Error(String(error)));
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
const overrides = {
|
|
170
|
+
async load(assets = '', code = '', log = true, args = [], options = {}, progress) {
|
|
171
|
+
if (disposed)
|
|
172
|
+
throw new Error('C/C++ sandbox was disposed');
|
|
173
|
+
loadedAssets = assets;
|
|
174
|
+
const config = typeof assets === 'object' ? assets : undefined;
|
|
175
|
+
assetConfig = resolveRuntimeAssetConfig('clang', assets, globalThis.location?.href || 'http://localhost/');
|
|
176
|
+
const debugMode = options.debugMode || (options.debug ? 'trace' : 'none');
|
|
177
|
+
const cacheEligible = debugMode === 'none' &&
|
|
178
|
+
shouldStreamBundledClang(assetConfig) &&
|
|
179
|
+
!options.workspaceFiles?.some((file) => typeof file.content !== 'string') &&
|
|
180
|
+
(options.stdin === undefined || typeof options.stdin === 'string') &&
|
|
181
|
+
typeof SharedArrayBuffer !== 'undefined';
|
|
182
|
+
if (!cached)
|
|
183
|
+
await legacy.clear();
|
|
184
|
+
if (disposed)
|
|
185
|
+
throw new Error('C/C++ sandbox was disposed');
|
|
186
|
+
stop();
|
|
187
|
+
cached = cacheEligible;
|
|
188
|
+
if (!cached)
|
|
189
|
+
return legacy.load(assets, code, log, args, options, progress);
|
|
190
|
+
runtimeBaseUrl = assetConfig.baseUrl;
|
|
191
|
+
maxAssetBytes = resolveExecutionLimits(options.limits).maxAssetBytes;
|
|
192
|
+
languageSysroots = config?.clang?.bundledLanguageSysroots === true;
|
|
193
|
+
},
|
|
194
|
+
async run(code, prepare, log = true, progress, args = [], options = {}) {
|
|
195
|
+
if (disposed)
|
|
196
|
+
throw new Error('C/C++ sandbox was disposed');
|
|
197
|
+
if (cached &&
|
|
198
|
+
((options.debugMode || (options.debug ? 'trace' : 'none')) !== 'none' ||
|
|
199
|
+
(options.stdin !== undefined && typeof options.stdin !== 'string') ||
|
|
200
|
+
options.workspaceFiles?.some((file) => typeof file.content !== 'string'))) {
|
|
201
|
+
await overrides.load(loadedAssets, code, log, args, options, progress);
|
|
202
|
+
}
|
|
203
|
+
if (!cached)
|
|
204
|
+
return legacy.run(code, prepare, log, progress, args, options);
|
|
205
|
+
if (running)
|
|
206
|
+
throw new Error('C/C++ sandbox is already running');
|
|
207
|
+
options.signal?.throwIfAborted();
|
|
208
|
+
const token = ++generation;
|
|
209
|
+
// Reactive UI arrays/records are Proxies and cannot cross postMessage.
|
|
210
|
+
// Snapshot known fields before awaiting compilation, so the cache key,
|
|
211
|
+
// compiler and executor also observe the same input values.
|
|
212
|
+
const request = {
|
|
213
|
+
code,
|
|
214
|
+
language,
|
|
215
|
+
activePath: options.activePath,
|
|
216
|
+
workspaceFiles: options.workspaceFiles === undefined
|
|
217
|
+
? undefined
|
|
218
|
+
: Array.from(options.workspaceFiles, (file) => ({
|
|
219
|
+
path: file.path,
|
|
220
|
+
content: file.content
|
|
221
|
+
})),
|
|
222
|
+
compileArgs: Array.from(options.compileArgs ?? args),
|
|
223
|
+
cVersion: options.cVersion,
|
|
224
|
+
cppVersion: options.cppVersion
|
|
225
|
+
};
|
|
226
|
+
const programArgs = Array.from(options.programArgs ?? []);
|
|
227
|
+
const env = options.env === undefined
|
|
228
|
+
? undefined
|
|
229
|
+
: Object.fromEntries(Object.entries(options.env));
|
|
230
|
+
const stdin = options.stdin;
|
|
231
|
+
const limit = Math.min(maxAssetBytes, resolveExecutionLimits(options.limits).maxAssetBytes);
|
|
232
|
+
const key = clangCompileKey(runtimeBaseUrl, limit, request, languageSysroots);
|
|
233
|
+
let artifact = cache.get(key);
|
|
234
|
+
running = true;
|
|
235
|
+
try {
|
|
236
|
+
if (!artifact) {
|
|
237
|
+
artifact = await workerOperation(() => factories.compile(), {
|
|
238
|
+
request,
|
|
239
|
+
runtimeBaseUrl,
|
|
240
|
+
maxAssetBytes: limit,
|
|
241
|
+
languageSysroots,
|
|
242
|
+
log,
|
|
243
|
+
assets: {
|
|
244
|
+
baseUrl: runtimeBaseUrl,
|
|
245
|
+
maxAssetBytes: limit,
|
|
246
|
+
useAssetBridge: assetConfig.useAssetBridge,
|
|
247
|
+
useModuleBridge: true
|
|
248
|
+
}
|
|
249
|
+
}, token, progress, limit, options.signal);
|
|
250
|
+
if (token !== generation)
|
|
251
|
+
throw new Error('Process terminated');
|
|
252
|
+
cache.set(key, artifact);
|
|
253
|
+
}
|
|
254
|
+
progress?.set?.(1);
|
|
255
|
+
if (prepare)
|
|
256
|
+
return true;
|
|
257
|
+
inputBuffer = new SharedArrayBuffer(65536 + 8);
|
|
258
|
+
return await workerOperation(() => factories.execute(), {
|
|
259
|
+
artifact,
|
|
260
|
+
code,
|
|
261
|
+
activePath: request.activePath,
|
|
262
|
+
workspaceFiles: request.workspaceFiles,
|
|
263
|
+
programArgs,
|
|
264
|
+
env,
|
|
265
|
+
stdin,
|
|
266
|
+
inputBuffer
|
|
267
|
+
}, token, progress, 0, options.signal);
|
|
268
|
+
}
|
|
269
|
+
finally {
|
|
270
|
+
if (token === generation) {
|
|
271
|
+
running = false;
|
|
272
|
+
if (!prepare)
|
|
273
|
+
resetInput();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
write(input) {
|
|
278
|
+
if (!cached)
|
|
279
|
+
return legacy.write?.(input);
|
|
280
|
+
if (input)
|
|
281
|
+
pendingInput.push(encoder.encode(input));
|
|
282
|
+
flushInput();
|
|
283
|
+
},
|
|
284
|
+
eof() {
|
|
285
|
+
if (!cached)
|
|
286
|
+
return legacy.eof();
|
|
287
|
+
pendingEof = true;
|
|
288
|
+
flushInput();
|
|
289
|
+
},
|
|
290
|
+
async clear() {
|
|
291
|
+
stop();
|
|
292
|
+
if (!cached)
|
|
293
|
+
await legacy.clear();
|
|
294
|
+
},
|
|
295
|
+
async terminate() {
|
|
296
|
+
stop();
|
|
297
|
+
if (!cached)
|
|
298
|
+
await legacy.terminate();
|
|
299
|
+
},
|
|
300
|
+
async kill() {
|
|
301
|
+
await overrides.terminate();
|
|
302
|
+
},
|
|
303
|
+
async cancel() {
|
|
304
|
+
await overrides.terminate();
|
|
305
|
+
},
|
|
306
|
+
dispose() {
|
|
307
|
+
if (disposal)
|
|
308
|
+
return disposal;
|
|
309
|
+
disposed = true;
|
|
310
|
+
stop();
|
|
311
|
+
disposal = Promise.resolve(legacy.dispose ? legacy.dispose() : legacy.terminate());
|
|
312
|
+
return disposal;
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
return new Proxy(legacy, {
|
|
316
|
+
get(target, property) {
|
|
317
|
+
if (Object.hasOwn(overrides, property))
|
|
318
|
+
return overrides[property];
|
|
319
|
+
const value = Reflect.get(target, property);
|
|
320
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DebugCommand, DebugDataBreakpoint, DebugDataBreakpointInfoArguments, DebugSessionEvent, SandboxExecutionOptions } from './options';
|
|
2
2
|
import { WorkerAssetBridge } from './assetBridge';
|
|
3
|
+
import { RuntimeAssetCache } from './runtimeAssetCache';
|
|
3
4
|
import { type PlaygroundRuntimeAssets, type RuntimeAssetIntegrityEntry } from './assets';
|
|
4
5
|
import type { Sandbox } from './sandbox';
|
|
5
6
|
import { type ProgressSink } from './workerProgress';
|
|
@@ -27,6 +28,10 @@ declare class Clang implements Sandbox {
|
|
|
27
28
|
debugManifestUrl: string;
|
|
28
29
|
debugManifestReceipt?: Readonly<RuntimeAssetIntegrityEntry>;
|
|
29
30
|
private lldbSession?;
|
|
31
|
+
private runtimeAssetCache;
|
|
32
|
+
private ownsRuntimeAssetCache;
|
|
33
|
+
private disposed;
|
|
34
|
+
private disposal?;
|
|
30
35
|
private debugMode;
|
|
31
36
|
private readonly lldbBreakpoints;
|
|
32
37
|
private debugEvaluationQueue;
|
|
@@ -56,5 +61,7 @@ declare class Clang implements Sandbox {
|
|
|
56
61
|
kill(): Promise<void>;
|
|
57
62
|
terminate(): Promise<void>;
|
|
58
63
|
clear(): Promise<void>;
|
|
64
|
+
setRuntimeAssetCache(cache: RuntimeAssetCache): void;
|
|
65
|
+
dispose(): Promise<void>;
|
|
59
66
|
}
|
|
60
67
|
export default Clang;
|