wasm-idle 1.1.0 → 1.1.2

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 (145) hide show
  1. package/README.md +140 -37
  2. package/dist/playground/assetBridge.js +106 -31
  3. package/dist/playground/assets.d.ts +2 -2
  4. package/dist/playground/c3.js +22 -0
  5. package/dist/playground/c3Memory.d.ts +19 -0
  6. package/dist/playground/c3Memory.js +117 -0
  7. package/dist/playground/cachedClang.js +143 -34
  8. package/dist/playground/clangAssetIntegrity.js +6 -6
  9. package/dist/playground/clangWorkerProtocol.d.ts +12 -1
  10. package/dist/playground/cobol.d.ts +3 -0
  11. package/dist/playground/cobol.js +10 -1
  12. package/dist/playground/commonlispAssets.d.ts +2 -2
  13. package/dist/playground/compiledArtifactCache.d.ts +3 -1
  14. package/dist/playground/compiledArtifactCache.js +31 -5
  15. package/dist/playground/container/ContainerExplorer.svelte +307 -0
  16. package/dist/playground/container/ContainerExplorer.svelte.d.ts +14 -0
  17. package/dist/playground/container/binding.d.ts +26 -0
  18. package/dist/playground/container/binding.js +247 -0
  19. package/dist/playground/container/clang-runtime.d.ts +38 -0
  20. package/dist/playground/container/clang-runtime.js +343 -0
  21. package/dist/playground/container/client.d.ts +15 -0
  22. package/dist/playground/container/client.js +189 -0
  23. package/dist/playground/container/explorer.d.ts +31 -0
  24. package/dist/playground/container/explorer.js +112 -0
  25. package/dist/playground/container/owner.worker.js +390 -0
  26. package/dist/playground/container/protocol.d.ts +73 -0
  27. package/dist/playground/container/protocol.js +28 -0
  28. package/dist/playground/container/release.d.ts +31 -0
  29. package/dist/playground/container/release.js +612 -0
  30. package/dist/playground/container/runtime-adapter.d.ts +73 -0
  31. package/dist/playground/container/runtime-adapter.js +474 -0
  32. package/dist/playground/container/shell-control.d.ts +49 -0
  33. package/dist/playground/container/shell-control.js +303 -0
  34. package/dist/playground/container/workspace.d.ts +8 -0
  35. package/dist/playground/container/workspace.js +29 -0
  36. package/dist/playground/d.d.ts +6 -1
  37. package/dist/playground/d.js +83 -5
  38. package/dist/playground/dotnet.js +6 -8
  39. package/dist/playground/fortran.d.ts +3 -0
  40. package/dist/playground/fortran.js +10 -1
  41. package/dist/playground/go.js +14 -3
  42. package/dist/playground/goDebugInstrumentation.js +184 -54
  43. package/dist/playground/java.js +3 -2
  44. package/dist/playground/javaRuntimeErrors.d.ts +9 -0
  45. package/dist/playground/javaRuntimeErrors.js +60 -0
  46. package/dist/playground/lean4Assets.d.ts +4 -4
  47. package/dist/playground/lfortran.js +10 -0
  48. package/dist/playground/lfortranAssets.d.ts +2 -2
  49. package/dist/playground/nim.js +22 -0
  50. package/dist/playground/objectivec.d.ts +3 -0
  51. package/dist/playground/objectivec.js +11 -2
  52. package/dist/playground/options.d.ts +2 -0
  53. package/dist/playground/python.js +8 -0
  54. package/dist/playground/runtimeAssetCache.d.ts +1 -32
  55. package/dist/playground/runtimeAssetCache.js +1 -193
  56. package/dist/playground/runtimeSession.d.ts +2 -2
  57. package/dist/playground/runtimeSession.js +11 -11
  58. package/dist/playground/rust.d.ts +6 -0
  59. package/dist/playground/rust.js +103 -6
  60. package/dist/playground/staticStdinRing.d.ts +2 -1
  61. package/dist/playground/staticStdinRing.js +21 -10
  62. package/dist/playground/staticWorkerRuntime.d.ts +15 -1
  63. package/dist/playground/staticWorkerRuntime.js +119 -17
  64. package/dist/playground/tinygo.d.ts +3 -0
  65. package/dist/playground/tinygo.js +81 -6
  66. package/dist/playground/v.d.ts +3 -0
  67. package/dist/playground/v.js +10 -1
  68. package/dist/playground/wasmAwkVersion.d.ts +13 -13
  69. package/dist/playground/wasmAwkVersion.js +4 -4
  70. package/dist/playground/wasmBqnVersion.d.ts +2 -2
  71. package/dist/playground/wasmBqnVersion.js +2 -2
  72. package/dist/playground/wasmC3Version.d.ts +2 -2
  73. package/dist/playground/wasmC3Version.js +2 -2
  74. package/dist/playground/wasmClojureScriptVersion.d.ts +2 -2
  75. package/dist/playground/wasmClojureScriptVersion.js +2 -2
  76. package/dist/playground/wasmCommonLispVersion.d.ts +2 -2
  77. package/dist/playground/wasmCommonLispVersion.js +2 -2
  78. package/dist/playground/wasmDIntegrity.d.ts +5 -5
  79. package/dist/playground/wasmDIntegrity.js +5 -5
  80. package/dist/playground/wasmDVersion.d.ts +16 -1
  81. package/dist/playground/wasmDVersion.js +16 -1
  82. package/dist/playground/wasmDebugVersion.d.ts +1 -1
  83. package/dist/playground/wasmDebugVersion.js +1 -1
  84. package/dist/playground/wasmForthVersion.d.ts +2 -2
  85. package/dist/playground/wasmForthVersion.js +2 -2
  86. package/dist/playground/wasmGleamVersion.d.ts +2 -2
  87. package/dist/playground/wasmGleamVersion.js +2 -2
  88. package/dist/playground/wasmGrainVersion.d.ts +2 -2
  89. package/dist/playground/wasmGrainVersion.js +2 -2
  90. package/dist/playground/wasmJVersion.d.ts +2 -2
  91. package/dist/playground/wasmJVersion.js +2 -2
  92. package/dist/playground/wasmJanetVersion.js +2 -2
  93. package/dist/playground/wasmJuliaVersion.js +2 -2
  94. package/dist/playground/wasmLean4Version.d.ts +5 -5
  95. package/dist/playground/wasmLean4Version.js +5 -5
  96. package/dist/playground/wasmLfortranVersion.d.ts +2 -2
  97. package/dist/playground/wasmLfortranVersion.js +2 -2
  98. package/dist/playground/wasmNimVersion.d.ts +9 -9
  99. package/dist/playground/wasmNimVersion.js +6 -6
  100. package/dist/playground/wasmOcamlVersion.d.ts +2 -2
  101. package/dist/playground/wasmOcamlVersion.js +1 -1
  102. package/dist/playground/wasmPascalVersion.js +2 -2
  103. package/dist/playground/wasmPerlVersion.js +2 -2
  104. package/dist/playground/wasmPrologVersion.d.ts +2 -2
  105. package/dist/playground/wasmPrologVersion.js +2 -2
  106. package/dist/playground/wasmReScriptVersion.d.ts +2 -2
  107. package/dist/playground/wasmReScriptVersion.js +2 -2
  108. package/dist/playground/wasmRustVersion.d.ts +8 -8
  109. package/dist/playground/wasmRustVersion.js +7 -7
  110. package/dist/playground/wasmTclVersion.d.ts +4 -4
  111. package/dist/playground/wasmTclVersion.js +2 -2
  112. package/dist/playground/wasmTinyGoVersion.d.ts +14 -14
  113. package/dist/playground/wasmTinyGoVersion.js +14 -14
  114. package/dist/playground/wasmTypeScriptVersion.d.ts +5 -5
  115. package/dist/playground/wasmTypeScriptVersion.js +5 -5
  116. package/dist/playground/worker/cachedClangCompile.js +52 -4
  117. package/dist/playground/worker/cachedClangExecute.js +6 -5
  118. package/dist/playground/worker/clang.js +32 -10
  119. package/dist/playground/worker/cobol.js +2 -2
  120. package/dist/playground/worker/d.js +12 -5
  121. package/dist/playground/worker/fortran.js +2 -2
  122. package/dist/playground/worker/go.js +16 -5
  123. package/dist/playground/worker/haskell.js +10 -7
  124. package/dist/playground/worker/java.js +75 -10
  125. package/dist/playground/worker/lua.js +2 -3
  126. package/dist/playground/worker/php.js +15 -9
  127. package/dist/playground/worker/python.js +46 -9
  128. package/dist/playground/worker/pythonExecution.js +17 -1
  129. package/dist/playground/worker/pythonWorkspaceFiles.d.ts +6 -0
  130. package/dist/playground/worker/pythonWorkspaceFiles.js +26 -0
  131. package/dist/playground/worker/pythonWorkspaceImports.d.ts +4 -0
  132. package/dist/playground/worker/pythonWorkspaceImports.js +129 -0
  133. package/dist/playground/worker/r.js +13 -6
  134. package/dist/playground/worker/rust.js +34 -2
  135. package/dist/playground/worker/sqlite.js +4 -3
  136. package/dist/playground/worker/tinygo.js +11 -3
  137. package/dist/playground/worker/typescript.js +2 -3
  138. package/dist/playground/worker/v.js +2 -2
  139. package/dist/playground/worker/wat.js +2 -3
  140. package/dist/playground/worker/zig.js +20 -6
  141. package/dist/playground/workerSession.d.ts +1 -28
  142. package/dist/playground/workerSession.js +1 -164
  143. package/package.json +20 -13
  144. package/dist/playground/worker/python.runtime.cases.js +0 -374
  145. /package/dist/playground/{worker/python.runtime.cases.d.ts → container/owner.worker.d.ts} +0 -0
@@ -1,3 +1,12 @@
1
+ function snapshotArtifact(artifact) {
2
+ const { bytes, wasm, ...metadata } = artifact;
3
+ return {
4
+ ...structuredClone(metadata),
5
+ // Compiled modules are immutable. Byte views must not retain an oversized backing buffer.
6
+ wasm,
7
+ bytes: ArrayBuffer.isView(bytes) ? Uint8Array.from(bytes) : bytes.slice(0)
8
+ };
9
+ }
1
10
  /** Only immutable compiler output lives here, never a worker, instance, or filesystem. */
2
11
  export class CompiledArtifactCache {
3
12
  entries = new Map();
@@ -7,6 +16,10 @@ export class CompiledArtifactCache {
7
16
  maxEntries;
8
17
  maxBytes;
9
18
  constructor(maxEntries = 8, maxBytes = 32 * 1024 * 1024) {
19
+ if (!Number.isSafeInteger(maxEntries) || maxEntries < 0)
20
+ throw new TypeError('Invalid compiled artifact cache maxEntries');
21
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 0)
22
+ throw new TypeError('Invalid compiled artifact cache maxBytes');
10
23
  this.maxEntries = maxEntries;
11
24
  this.maxBytes = maxBytes;
12
25
  }
@@ -19,17 +32,29 @@ export class CompiledArtifactCache {
19
32
  this.hits++;
20
33
  this.entries.delete(key);
21
34
  this.entries.set(key, entry);
22
- return entry.artifact;
35
+ return snapshotArtifact(entry.artifact);
36
+ }
37
+ getDiagnostics(key) {
38
+ return this.entries.get(key)?.diagnostics.map((diagnostic) => ({ ...diagnostic })) ?? [];
23
39
  }
24
- set(key, artifact) {
25
- const size = artifact.bytes.byteLength + key.length * 2;
40
+ set(key, artifact, diagnostics = []) {
41
+ // A zero budget disables retention without disabling compilation.
42
+ if (this.maxEntries === 0 || this.maxBytes === 0)
43
+ return;
44
+ const size = artifact.bytes.byteLength +
45
+ key.length * 2 +
46
+ (diagnostics.length ? JSON.stringify(diagnostics).length * 2 : 0);
26
47
  if (size > this.maxBytes)
27
48
  return;
28
49
  const old = this.entries.get(key);
29
50
  if (old)
30
51
  this.bytes -= old.size;
31
52
  this.entries.delete(key);
32
- this.entries.set(key, { artifact, size });
53
+ this.entries.set(key, {
54
+ artifact: snapshotArtifact(artifact),
55
+ diagnostics: diagnostics.map((diagnostic) => ({ ...diagnostic })),
56
+ size
57
+ });
33
58
  this.bytes += size;
34
59
  while (this.entries.size > this.maxEntries || this.bytes > this.maxBytes) {
35
60
  const oldest = this.entries.keys().next().value;
@@ -52,7 +77,7 @@ export class CompiledArtifactCache {
52
77
  }
53
78
  export function clangCompileKey(runtimeBaseUrl, maxAssetBytes, request, languageSysroots = false) {
54
79
  return JSON.stringify([
55
- 'wasm-idle/clang-cache-v2',
80
+ 'wasm-idle/clang-cache-v3',
56
81
  runtimeBaseUrl,
57
82
  maxAssetBytes,
58
83
  languageSysroots,
@@ -62,6 +87,7 @@ export function clangCompileKey(runtimeBaseUrl, maxAssetBytes, request, language
62
87
  request.compileArgs,
63
88
  request.cVersion,
64
89
  request.cppVersion,
90
+ request.maxWasmMemoryBytes,
65
91
  [...(request.workspaceFiles || [])].sort((a, b) => a.path.localeCompare(b.path))
66
92
  ]);
67
93
  }
@@ -0,0 +1,307 @@
1
+ <script lang="ts">
2
+ import type { ContainerWorkspaceEntry } from './protocol';
3
+ import {
4
+ buildExplorerModel,
5
+ explorerBreadcrumbs,
6
+ EXPLORER_PAGE_SIZE,
7
+ resolveExplorerDirectory,
8
+ visibleExplorerRows
9
+ } from './explorer';
10
+
11
+ interface Props {
12
+ entries: readonly ContainerWorkspaceEntry[];
13
+ selectedPath?: string;
14
+ initialDirectory?: string;
15
+ busy?: boolean;
16
+ onOpenFile: (entry: ContainerWorkspaceEntry) => void;
17
+ }
18
+
19
+ let {
20
+ entries,
21
+ selectedPath,
22
+ initialDirectory = '',
23
+ busy = false,
24
+ onOpenFile
25
+ }: Props = $props();
26
+ let requestedDirectory = $state('');
27
+ let expanded = $state(new Set<string>());
28
+ let offset = $state(0);
29
+ const model = $derived(buildExplorerModel(entries));
30
+ const directory = $derived(resolveExplorerDirectory(model, requestedDirectory));
31
+ const breadcrumbs = $derived(explorerBreadcrumbs(directory));
32
+ const visible = $derived(visibleExplorerRows(model, directory, expanded, offset));
33
+
34
+ $effect(() => {
35
+ requestedDirectory = initialDirectory;
36
+ expanded = new Set();
37
+ offset = 0;
38
+ });
39
+
40
+ function enterDirectory(path: string) {
41
+ requestedDirectory = resolveExplorerDirectory(model, path);
42
+ offset = 0;
43
+ }
44
+
45
+ function toggleDirectory(path: string) {
46
+ const next = new Set(expanded);
47
+ if (next.has(path)) next.delete(path);
48
+ else next.add(path);
49
+ expanded = next;
50
+ offset = 0;
51
+ }
52
+ </script>
53
+
54
+ <div class="container-explorer">
55
+ <nav aria-label="Workspace folder" class="breadcrumbs">
56
+ <button
57
+ type="button"
58
+ class="parent-button"
59
+ aria-label="Go to parent folder"
60
+ title="Go to parent folder"
61
+ disabled={!directory}
62
+ onclick={() => enterDirectory(model.entries.get(directory)?.parent ?? '')}>↑</button
63
+ >
64
+ <ol>
65
+ {#each breadcrumbs as crumb (crumb.path)}
66
+ <li>
67
+ <button
68
+ type="button"
69
+ title={crumb.path ? `/workspace/${crumb.path}` : '/workspace'}
70
+ aria-current={crumb.path === directory ? 'location' : undefined}
71
+ onclick={() => enterDirectory(crumb.path)}>{crumb.name}</button
72
+ >
73
+ </li>
74
+ {/each}
75
+ </ol>
76
+ </nav>
77
+ <div class="files">
78
+ {#if visible.rows.length === 0}
79
+ <p class="empty">This folder is empty.</p>
80
+ {:else}
81
+ <ul aria-label="Workspace files">
82
+ {#each visible.rows as row (row.entry.path)}
83
+ {@const entry = row.entry}
84
+ <li style:padding-left={`${Math.min(row.depth, 16) * 12}px`}>
85
+ {#if entry.type === 'directory'}
86
+ <button
87
+ type="button"
88
+ class="toggle"
89
+ aria-label={`${row.expanded ? 'Collapse' : 'Expand'} /workspace/${entry.path}`}
90
+ aria-expanded={row.expanded}
91
+ onclick={() => toggleDirectory(entry.path)}
92
+ >{row.expanded ? '▾' : '▸'}</button
93
+ >
94
+ <button
95
+ type="button"
96
+ class="entry directory"
97
+ data-container-directory={entry.path}
98
+ aria-label={`Open folder /workspace/${entry.path}`}
99
+ title={`/workspace/${entry.path}/`}
100
+ onclick={() => enterDirectory(entry.path)}
101
+ >
102
+ <span class="material-symbols-outlined" aria-hidden="true"
103
+ >folder</span
104
+ >
105
+ <span>{entry.name}</span>
106
+ </button>
107
+ {:else if entry.type === 'file'}
108
+ <button
109
+ type="button"
110
+ class="entry file"
111
+ class:active={entry.path === selectedPath}
112
+ data-container-path={entry.path}
113
+ aria-current={entry.path === selectedPath ? 'true' : undefined}
114
+ title={`/workspace/${entry.path} (${entry.size} bytes)`}
115
+ disabled={busy}
116
+ onclick={() => onOpenFile(entry)}
117
+ >
118
+ <span class="material-symbols-outlined" aria-hidden="true"
119
+ >draft</span
120
+ >
121
+ <span>{entry.name}</span>
122
+ </button>
123
+ {:else}
124
+ <div
125
+ class="entry symlink"
126
+ title={`/workspace/${entry.path} (symbolic link; target is not followed)`}
127
+ >
128
+ <span class="material-symbols-outlined" aria-hidden="true"
129
+ >link</span
130
+ >
131
+ <span>{entry.name}</span>
132
+ <small>link</small>
133
+ </div>
134
+ {/if}
135
+ </li>
136
+ {/each}
137
+ </ul>
138
+ {/if}
139
+ </div>
140
+ {#if visible.offset > 0 || visible.remaining > 0}
141
+ <div class="pagination">
142
+ {#if visible.offset > 0}<button
143
+ type="button"
144
+ onclick={() => (offset = visible.offset - EXPLORER_PAGE_SIZE)}
145
+ >Previous files</button
146
+ >{/if}
147
+ <span aria-live="polite"
148
+ >{visible.offset + 1}–{visible.offset + visible.rows.length} of {visible.total}</span
149
+ >
150
+ {#if visible.remaining > 0}<button
151
+ type="button"
152
+ onclick={() => (offset = visible.offset + EXPLORER_PAGE_SIZE)}
153
+ >Show more ({visible.remaining} remaining)</button
154
+ >{/if}
155
+ </div>
156
+ {/if}
157
+ </div>
158
+
159
+ <style>
160
+ .container-explorer {
161
+ display: flex;
162
+ flex-direction: column;
163
+ min-height: 36px;
164
+ overflow: hidden;
165
+ }
166
+ .breadcrumbs {
167
+ display: flex;
168
+ align-items: flex-start;
169
+ gap: 4px;
170
+ padding: 4px 0;
171
+ border-bottom: 1px solid var(--line-soft, #e5e7eb);
172
+ }
173
+ .breadcrumbs ol {
174
+ display: flex;
175
+ flex-wrap: wrap;
176
+ align-items: center;
177
+ gap: 2px;
178
+ min-width: 0;
179
+ margin: 0;
180
+ padding: 0;
181
+ list-style: none;
182
+ }
183
+ .breadcrumbs li {
184
+ display: flex;
185
+ align-items: center;
186
+ min-width: 0;
187
+ }
188
+ .breadcrumbs li + li::before {
189
+ content: '/';
190
+ color: var(--text-4, #888);
191
+ font-size: 10px;
192
+ }
193
+ button {
194
+ border: 0;
195
+ border-radius: 6px;
196
+ background: transparent;
197
+ color: var(--text-2, #535c6d);
198
+ cursor: pointer;
199
+ font: inherit;
200
+ }
201
+ button:disabled {
202
+ cursor: not-allowed;
203
+ opacity: 0.6;
204
+ }
205
+ button:hover:enabled,
206
+ button.active {
207
+ background: var(--primary-soft, #eaf2ff);
208
+ color: var(--primary-strong, #2563eb);
209
+ }
210
+ button:focus-visible {
211
+ outline: 2px solid var(--primary, #3182f6);
212
+ outline-offset: -2px;
213
+ }
214
+ .breadcrumbs button {
215
+ max-width: 200px;
216
+ padding: 4px 5px;
217
+ overflow-wrap: anywhere;
218
+ text-align: left;
219
+ font: 10px var(--mono, monospace);
220
+ }
221
+ .breadcrumbs button[aria-current] {
222
+ color: var(--text, #1f2937);
223
+ }
224
+ .breadcrumbs .parent-button {
225
+ flex: 0 0 26px;
226
+ text-align: center;
227
+ font-size: 15px;
228
+ }
229
+ .files {
230
+ min-height: 36px;
231
+ overflow: auto;
232
+ }
233
+ .files ul {
234
+ margin: 0;
235
+ padding: 4px 0;
236
+ list-style: none;
237
+ }
238
+ .files li {
239
+ display: flex;
240
+ align-items: center;
241
+ box-sizing: border-box;
242
+ min-width: 0;
243
+ }
244
+ .entry {
245
+ display: flex;
246
+ align-items: center;
247
+ flex: 1 1 auto;
248
+ gap: 6px;
249
+ min-width: 0;
250
+ min-height: 30px;
251
+ padding: 5px 8px;
252
+ text-align: left;
253
+ font: 11px var(--mono, monospace);
254
+ color: var(--text-2, #535c6d);
255
+ }
256
+ .entry > span:last-child {
257
+ min-width: 0;
258
+ overflow: hidden;
259
+ text-overflow: ellipsis;
260
+ white-space: nowrap;
261
+ }
262
+ .entry.file,
263
+ .entry.symlink {
264
+ margin-left: 24px;
265
+ }
266
+ .toggle {
267
+ flex: 0 0 24px;
268
+ width: 24px;
269
+ min-height: 30px;
270
+ padding: 0;
271
+ font-size: 13px;
272
+ }
273
+ .material-symbols-outlined {
274
+ flex: 0 0 auto;
275
+ font-size: 16px;
276
+ }
277
+ .symlink {
278
+ color: var(--text-3, #7a8393);
279
+ }
280
+ .symlink span:nth-child(2) {
281
+ overflow: hidden;
282
+ text-overflow: ellipsis;
283
+ white-space: nowrap;
284
+ }
285
+ .symlink small {
286
+ margin-left: auto;
287
+ font-size: 9px;
288
+ }
289
+ .empty {
290
+ margin: 8px;
291
+ color: var(--text-3, #7a8393);
292
+ font-size: 11px;
293
+ }
294
+ .pagination {
295
+ display: flex;
296
+ flex-wrap: wrap;
297
+ align-items: center;
298
+ gap: 5px;
299
+ border-top: 1px solid var(--line-soft, #e5e7eb);
300
+ padding: 5px 0;
301
+ color: var(--text-3, #7a8393);
302
+ font-size: 10px;
303
+ }
304
+ .pagination button {
305
+ padding: 5px;
306
+ }
307
+ </style>
@@ -0,0 +1,14 @@
1
+ import { SvelteComponentTyped } from "svelte";
2
+ declare const __propDef: {
3
+ props: Record<string, never>;
4
+ events: {
5
+ [evt: string]: CustomEvent<any>;
6
+ };
7
+ slots: {};
8
+ };
9
+ export type ContainerExplorerProps = typeof __propDef.props;
10
+ export type ContainerExplorerEvents = typeof __propDef.events;
11
+ export type ContainerExplorerSlots = typeof __propDef.slots;
12
+ export default class ContainerExplorer extends SvelteComponentTyped<ContainerExplorerProps, ContainerExplorerEvents, ContainerExplorerSlots> {
13
+ }
14
+ export {};
@@ -0,0 +1,26 @@
1
+ import type { PlaygroundBinding, SandboxRuntimeAssets } from '../sandbox';
2
+ import type { TerminalShellBinding } from '@wasm-idle/terminal';
3
+ import type { WorkspaceFile as ContainerWorkspaceFile } from '@wasm-idle/core';
4
+ import { type ContainerWorkerClient } from './client';
5
+ import type { ContainerWorkerEvent, ContainerWorkspaceEntry } from './protocol';
6
+ type BindingOptions = {
7
+ applicationRootURL: string;
8
+ workspaceId: string;
9
+ getLanguage?: () => string;
10
+ /** Deterministic transport contract tests only. */
11
+ clientFactory?: () => ContainerWorkerClient;
12
+ };
13
+ export interface ContainerPlaygroundBinding extends PlaygroundBinding {
14
+ readonly shell: TerminalShellBinding;
15
+ inspect(): Promise<unknown>;
16
+ setShellDirectory(language: string): Promise<void>;
17
+ syncWorkspace(language: string, files: readonly ContainerWorkspaceFile[]): Promise<void>;
18
+ readWorkspace(language: string): Promise<ContainerWorkspaceFile[]>;
19
+ listWorkspaceFiles(): Promise<ContainerWorkspaceEntry[]>;
20
+ publishedWorkspace(language: string): ContainerWorkspaceFile[];
21
+ acceptWorkspace(language: string, files: readonly ContainerWorkspaceFile[]): void;
22
+ onWorkspaceChange(listener: (event: ContainerWorkerEvent) => void): () => void;
23
+ }
24
+ /** Existing Terminal/Debug controls use this facade; every execution belongs to the owner Container. */
25
+ export declare function createContainerPlaygroundBinding(runtimeAssets: SandboxRuntimeAssets, options: BindingOptions): ContainerPlaygroundBinding;
26
+ export {};
@@ -0,0 +1,247 @@
1
+ import { createContainerWorkerClient } from './client';
2
+ const copyFiles = (files) => files.map((file) => ({
3
+ path: file.path,
4
+ content: typeof file.content === 'string' ? file.content : Uint8Array.from(file.content)
5
+ }));
6
+ const bytes = (content) => typeof content === 'string' ? new TextEncoder().encode(content) : content;
7
+ const equal = (left, right) => {
8
+ const a = bytes(left), b = bytes(right);
9
+ return a.length === b.length && a.every((byte, index) => byte === b[index]);
10
+ };
11
+ function sameFiles(left, right) {
12
+ return (left.length === right.length &&
13
+ left.every((file) => {
14
+ const other = right.find((candidate) => candidate.path === file.path);
15
+ return other && equal(file.content, other.content);
16
+ }));
17
+ }
18
+ /** Existing Terminal/Debug controls use this facade; every execution belongs to the owner Container. */
19
+ export function createContainerPlaygroundBinding(runtimeAssets, options) {
20
+ let client, disposed = false;
21
+ const sandboxes = new Map();
22
+ const published = new Map();
23
+ const changes = new Set();
24
+ const queues = new Map();
25
+ let shellOutput;
26
+ let shellError;
27
+ let shellConnection;
28
+ let requestedShellLanguage;
29
+ const decoders = new Map();
30
+ const decoder = (key) => {
31
+ let value = decoders.get(key);
32
+ if (!value)
33
+ decoders.set(key, (value = new TextDecoder()));
34
+ return value;
35
+ };
36
+ const connection = () => {
37
+ if (disposed)
38
+ throw new Error('Container playground has been disposed');
39
+ if (client)
40
+ return client;
41
+ client =
42
+ options.clientFactory?.() ??
43
+ createContainerWorkerClient({
44
+ runtimeAssets,
45
+ applicationRootURL: options.applicationRootURL,
46
+ workspaceId: options.workspaceId
47
+ });
48
+ client.onEvent((event) => {
49
+ if (event.name === 'workspace') {
50
+ for (const listener of changes)
51
+ listener(event);
52
+ return;
53
+ }
54
+ if (event.name === 'shell-output') {
55
+ const data = event.data;
56
+ if (data.error)
57
+ shellError?.(new Error(data.error));
58
+ if (data.bytes)
59
+ shellOutput?.(decoder(`shell:${data.stream}`).decode(data.bytes, { stream: true }));
60
+ return;
61
+ }
62
+ const sandbox = event.language && sandboxes.get(event.language);
63
+ if (!sandbox)
64
+ return;
65
+ if (event.name === 'output')
66
+ sandbox.output?.(typeof event.data === 'string'
67
+ ? event.data
68
+ : decoder(event.language).decode(event.data, {
69
+ stream: true
70
+ }));
71
+ if (event.name === 'debug')
72
+ sandbox.ondebug?.(event.data);
73
+ if (event.name === 'diagnostic')
74
+ sandbox.oncompilerdiagnostic?.(event.data);
75
+ if (event.name === 'image')
76
+ sandbox.image?.(event.data);
77
+ });
78
+ return client;
79
+ };
80
+ const syncWorkspace = (language, files) => {
81
+ const snapshot = copyFiles(files);
82
+ const previous = queues.get(language) ?? Promise.resolve();
83
+ const next = previous
84
+ .catch(() => { })
85
+ .then(async () => {
86
+ const baseline = published.get(language);
87
+ if (baseline && sameFiles(baseline, snapshot))
88
+ return;
89
+ await connection().call('syncWorkspace', [
90
+ language,
91
+ snapshot,
92
+ baseline && copyFiles(baseline)
93
+ ]);
94
+ published.set(language, copyFiles(snapshot));
95
+ });
96
+ queues.set(language, next);
97
+ return next;
98
+ };
99
+ const shell = {
100
+ connect(output, error) {
101
+ shellOutput = output;
102
+ shellError = error;
103
+ return (shellConnection ??= (async () => {
104
+ const language = requestedShellLanguage ?? options.getLanguage?.() ?? 'CPP';
105
+ await queues.get(language)?.catch(() => { });
106
+ await connection().call('shellStart', [
107
+ { cwd: `/workspace/${language}`, env: { PS1: '\\w $ ' } }
108
+ ]);
109
+ })());
110
+ },
111
+ async write(text) {
112
+ await shellConnection;
113
+ await connection().call('shellWrite', [text]);
114
+ },
115
+ async eof() {
116
+ await shellConnection;
117
+ await connection().call('shellEof');
118
+ },
119
+ async interrupt() {
120
+ await shellConnection;
121
+ await connection().call('shellInterrupt');
122
+ },
123
+ async dispose() {
124
+ if (client && !disposed)
125
+ await client.call('shellDispose');
126
+ }
127
+ };
128
+ const binding = {
129
+ async setShellDirectory(language) {
130
+ if (!/^[A-Z][A-Z0-9_]*$/.test(language))
131
+ throw new TypeError('Invalid shell language');
132
+ requestedShellLanguage = language;
133
+ // Selecting a language before Terminal mounts must not start a temporary shell.
134
+ if (!shellConnection)
135
+ return;
136
+ await shellConnection;
137
+ await connection().call('shellSetDirectory', [language]);
138
+ },
139
+ runtimeAssets,
140
+ terminalProps: undefined,
141
+ shell,
142
+ async load(language) {
143
+ let sandbox = sandboxes.get(language);
144
+ if (sandbox)
145
+ return sandbox;
146
+ const invoke = (method, args = [], execution, progress) => connection().call(method, [language, ...args], {
147
+ signal: execution?.signal,
148
+ progress
149
+ });
150
+ const record = {
151
+ runtimeAssets,
152
+ elapse: 0,
153
+ async load(code, log = true, args = [], execution = {}, progress) {
154
+ await queues.get(language);
155
+ await invoke('load', [code, log, args, execution], execution, progress);
156
+ },
157
+ async run(code, prepare, log = true, progress, args = [], execution = {}) {
158
+ await queues.get(language);
159
+ const started = performance.now();
160
+ try {
161
+ return await invoke(prepare ? 'prepare' : 'run', [code, log, args, execution], execution, progress);
162
+ }
163
+ finally {
164
+ record.elapse = Math.round(performance.now() - started);
165
+ }
166
+ },
167
+ async clear() {
168
+ await invoke('clear');
169
+ },
170
+ async restart() {
171
+ await invoke('restart');
172
+ },
173
+ async terminate() {
174
+ await invoke('stop');
175
+ },
176
+ async kill() {
177
+ await invoke('stop');
178
+ },
179
+ async dispose() {
180
+ if (client && !disposed)
181
+ await invoke('clear');
182
+ },
183
+ write(text) {
184
+ void invoke('write', [text]).catch((error) => shellError?.(error));
185
+ },
186
+ eof() {
187
+ void invoke('eof').catch((error) => shellError?.(error));
188
+ }
189
+ };
190
+ for (const method of [
191
+ 'debugCommand',
192
+ 'debugPause',
193
+ 'setBreakpoints',
194
+ 'debugEvaluate',
195
+ 'debugVariables',
196
+ 'debugScopes',
197
+ 'debugFrameScopes',
198
+ 'debugFrameName',
199
+ 'debugReadMemory',
200
+ 'debugWriteMemory',
201
+ 'debugDataBreakpointInfo',
202
+ 'debugSetDataBreakpoints'
203
+ ]) {
204
+ record[method] = (...args) => invoke(method, args);
205
+ }
206
+ sandboxes.set(language, record);
207
+ return record;
208
+ },
209
+ // Owner and command index can be ready without fetching compiler bodies at page boot.
210
+ async prewarm() {
211
+ connection();
212
+ return true;
213
+ },
214
+ async setPrewarmEnabled() { },
215
+ async cancelPrewarm() { },
216
+ syncWorkspace,
217
+ async readWorkspace(language) {
218
+ await queues.get(language)?.catch(() => { });
219
+ return await connection().call('readWorkspace', [language]);
220
+ },
221
+ async listWorkspaceFiles() {
222
+ return await connection().call('listWorkspaceFiles');
223
+ },
224
+ publishedWorkspace(language) {
225
+ return copyFiles(published.get(language) ?? []);
226
+ },
227
+ acceptWorkspace(language, files) {
228
+ published.set(language, copyFiles(files));
229
+ },
230
+ onWorkspaceChange(listener) {
231
+ changes.add(listener);
232
+ return () => changes.delete(listener);
233
+ },
234
+ async inspect() {
235
+ return await connection().call('inspect');
236
+ },
237
+ async dispose() {
238
+ if (disposed)
239
+ return;
240
+ disposed = true;
241
+ changes.clear();
242
+ await client?.dispose();
243
+ }
244
+ };
245
+ binding.terminalProps = { playground: binding, runtimeAssets };
246
+ return binding;
247
+ }