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.
Files changed (32) hide show
  1. package/README.md +34 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +1 -0
  4. package/dist/playground/assetBridge.d.ts +5 -1
  5. package/dist/playground/assetBridge.js +120 -42
  6. package/dist/playground/cachedClang.d.ts +10 -0
  7. package/dist/playground/cachedClang.js +323 -0
  8. package/dist/playground/clang.d.ts +7 -0
  9. package/dist/playground/clang.js +39 -7
  10. package/dist/playground/clangWorkerProtocol.d.ts +57 -0
  11. package/dist/playground/clangWorkerProtocol.js +11 -0
  12. package/dist/playground/compiledArtifactCache.d.ts +21 -0
  13. package/dist/playground/compiledArtifactCache.js +67 -0
  14. package/dist/playground/python.d.ts +7 -0
  15. package/dist/playground/python.js +38 -7
  16. package/dist/playground/runtimeAssetCache.d.ts +26 -0
  17. package/dist/playground/runtimeAssetCache.js +84 -0
  18. package/dist/playground/runtimeSession.d.ts +20 -0
  19. package/dist/playground/runtimeSession.js +119 -0
  20. package/dist/playground/worker/assets.d.ts +3 -0
  21. package/dist/playground/worker/assets.js +41 -0
  22. package/dist/playground/worker/cachedClangCompile.d.ts +1 -0
  23. package/dist/playground/worker/cachedClangCompile.js +85 -0
  24. package/dist/playground/worker/cachedClangExecute.d.ts +1 -0
  25. package/dist/playground/worker/cachedClangExecute.js +67 -0
  26. package/dist/playground/worker/clang.js +1 -3
  27. package/dist/playground/worker/clangStreaming.d.ts +1 -1
  28. package/dist/playground/worker/clangStreaming.js +20 -2
  29. package/dist/playground/worker/python.js +3 -6
  30. package/dist/playground/worker/runtimeModule.d.ts +8 -0
  31. package/dist/playground/worker/runtimeModule.js +49 -0
  32. 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
- const loaded = await this.loadAsset(request.asset, controller.signal);
526
- const deliveryBytes = canonicalUint8Array(loaded.bytes);
527
- const sourceAssetByteLimit = this.sourceAssetByteLimit(request.asset);
528
- const runtimeAssetByteLimit = this.runtimeAssetByteLimit(request.asset);
529
- requireRuntimeAssetSize(request.asset, deliveryBytes.byteLength, sourceAssetByteLimit);
530
- let normalizedRuntimeBytes;
531
- if (request.asset.endsWith('.gz')) {
532
- this.progress.activity('decompressing', request.asset);
533
- normalizedRuntimeBytes = await decompressGzip(deliveryBytes, request.asset, runtimeAssetByteLimit, controller.signal);
534
- }
535
- else {
536
- normalizedRuntimeBytes = deliveryBytes;
537
- }
538
- const runtimeBytes = canonicalUint8Array(normalizedRuntimeBytes);
539
- requireRuntimeAssetSize(request.asset, runtimeBytes.byteLength, runtimeAssetByteLimit);
540
- const httpDecodedGzip = (loaded.contentEncoding || '')
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
- const verification = this.verifyIntegrity(request.asset, deliveryBytes, runtimeBytes, loaded.mimeType, !httpDecodedGzip);
557
- await Promise.race([verification, aborted]);
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
- finally {
560
- if (cancelOnAbort) {
561
- controller.signal.removeEventListener('abort', cancelOnAbort);
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(runtimeBytes);
590
+ this.pythonPackageAssets = readPythonPackageAssets(loaded.bytes);
568
591
  }
569
- const buffer = transferBuffer(runtimeBytes, normalizedRuntimeBytes === deliveryBytes ? loaded.transferOwnership : true);
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. There is no remaining response channel.
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 loadAsset(asset, signal) {
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;