@swiftbrowser/web 0.1.0
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 +600 -0
- package/app/index.html +125 -0
- package/index.html +56 -0
- package/node/dist/node/apps.d.ts +58 -0
- package/node/dist/node/apps.d.ts.map +1 -0
- package/node/dist/node/apps.js +60 -0
- package/node/dist/node/apps.js.map +1 -0
- package/node/dist/node/config.d.ts +42 -0
- package/node/dist/node/config.d.ts.map +1 -0
- package/node/dist/node/config.js +99 -0
- package/node/dist/node/config.js.map +1 -0
- package/node/dist/node/examples.d.ts +39 -0
- package/node/dist/node/examples.d.ts.map +1 -0
- package/node/dist/node/examples.js +116 -0
- package/node/dist/node/examples.js.map +1 -0
- package/node/dist/node/index.d.ts +47 -0
- package/node/dist/node/index.d.ts.map +1 -0
- package/node/dist/node/index.js +52 -0
- package/node/dist/node/index.js.map +1 -0
- package/node/dist/plugins/app-manifests.d.ts +61 -0
- package/node/dist/plugins/app-manifests.d.ts.map +1 -0
- package/node/dist/plugins/app-manifests.js +168 -0
- package/node/dist/plugins/app-manifests.js.map +1 -0
- package/node/dist/plugins/asset-catalogs.d.ts +119 -0
- package/node/dist/plugins/asset-catalogs.d.ts.map +1 -0
- package/node/dist/plugins/asset-catalogs.js +488 -0
- package/node/dist/plugins/asset-catalogs.js.map +1 -0
- package/node/dist/plugins/bundle-resources.d.ts +49 -0
- package/node/dist/plugins/bundle-resources.d.ts.map +1 -0
- package/node/dist/plugins/bundle-resources.js +172 -0
- package/node/dist/plugins/bundle-resources.js.map +1 -0
- package/node/dist/plugins/swift-wasm.d.ts +33 -0
- package/node/dist/plugins/swift-wasm.d.ts.map +1 -0
- package/node/dist/plugins/swift-wasm.js +241 -0
- package/node/dist/plugins/swift-wasm.js.map +1 -0
- package/node/dist/src/assets.d.ts +60 -0
- package/node/dist/src/assets.d.ts.map +1 -0
- package/node/dist/src/assets.js +54 -0
- package/node/dist/src/assets.js.map +1 -0
- package/node/dist/src/devEvents.d.ts +31 -0
- package/node/dist/src/devEvents.d.ts.map +1 -0
- package/node/dist/src/devEvents.js +9 -0
- package/node/dist/src/devEvents.js.map +1 -0
- package/package.json +56 -0
- package/plugins/app-manifests.ts +192 -0
- package/plugins/asset-catalogs.ts +558 -0
- package/plugins/bundle-resources.ts +195 -0
- package/plugins/swift-wasm.ts +259 -0
- package/public/_headers +23 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/icon.svg +14 -0
- package/public/manifest.webmanifest +14 -0
- package/src/animation.ts +521 -0
- package/src/assets.ts +86 -0
- package/src/chartDom.ts +481 -0
- package/src/devEvents.ts +35 -0
- package/src/deviceMode.ts +80 -0
- package/src/ghost.ts +25 -0
- package/src/interaction.ts +1113 -0
- package/src/landing.css +261 -0
- package/src/landing.ts +141 -0
- package/src/layout/chart.ts +477 -0
- package/src/layout/controls.ts +116 -0
- package/src/layout/engine.ts +3581 -0
- package/src/layout/index.ts +91 -0
- package/src/layout/measure.ts +86 -0
- package/src/layout/text.ts +124 -0
- package/src/layout/types.ts +153 -0
- package/src/layout/typography.ts +188 -0
- package/src/layoutDom.ts +549 -0
- package/src/layoutEvents.ts +110 -0
- package/src/locale.ts +64 -0
- package/src/main.ts +554 -0
- package/src/mock.ts +752 -0
- package/src/nodeQueries.ts +60 -0
- package/src/protocol.ts +1421 -0
- package/src/renderer.ts +2882 -0
- package/src/styles.css +2834 -0
- package/src/styles.ts +373 -0
- package/src/swipe.ts +110 -0
- package/src/symbols.ts +595 -0
- package/src/typography.ts +76 -0
- package/src/vite-env.d.ts +26 -0
- package/src/wasm.ts +400 -0
package/src/wasm.ts
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loads a SwiftBrowser app compiled to Wasm (a WASI command) and bridges its
|
|
3
|
+
* ops buffer / event exports to the renderer. See docs/ops-protocol.md,
|
|
4
|
+
* "Events (renderer → Swift)", "Phase 2 additions" and "Executor" (Phase 4).
|
|
5
|
+
*
|
|
6
|
+
* Phase 4 modules export `sb_run_jobs(now_ms) -> delay_ms`, which runs the
|
|
7
|
+
* cooperative executor (Task, .task, Task.sleep). The host drives it: once
|
|
8
|
+
* after `_start`, after every event, and whenever the returned delay elapses
|
|
9
|
+
* (a single `setTimeout`, clamped to at least 4 ms); the pending ops are
|
|
10
|
+
* read and applied after every call. See `JobScheduler`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { ConsoleStdout, Directory, type Fd, File, OpenFile, PreopenDirectory, WASI, WASIProcExit, type Inode } from '@bjorn3/browser_wasi_shim';
|
|
14
|
+
import { isOp, type ElementId, type Op, type RendererEvent } from './protocol';
|
|
15
|
+
|
|
16
|
+
export type OpsSink = (ops: Op[]) => void;
|
|
17
|
+
|
|
18
|
+
export interface AppHandle {
|
|
19
|
+
/** Forward a tap on element `id` to Swift (`sb_event`) and apply the resulting ops. */
|
|
20
|
+
sendEvent(id: ElementId): void;
|
|
21
|
+
/**
|
|
22
|
+
* Deliver a JSON event through `sb_alloc` + `sb_event_json` and apply the
|
|
23
|
+
* resulting ops. Returns `false` (and does nothing) when the module only
|
|
24
|
+
* speaks the Phase 1 protocol. A `tap` event is routed to `sendEvent`.
|
|
25
|
+
*/
|
|
26
|
+
sendEventJson(event: RendererEvent): boolean;
|
|
27
|
+
/** Whether the module exports `sb_alloc` and `sb_event_json`. */
|
|
28
|
+
readonly supportsJsonEvents: boolean;
|
|
29
|
+
/** Whether a `batch` event may be sent through `sendEventJson` (the Wasm runtime; not the mocks). */
|
|
30
|
+
readonly supportsBatchEvents?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* The current `@State` snapshot (`sb_state_ptr`/`sb_state_len`) as a JSON
|
|
33
|
+
* string, or `null` when the module has no snapshot support or no state.
|
|
34
|
+
*/
|
|
35
|
+
snapshotState(): string | null;
|
|
36
|
+
/**
|
|
37
|
+
* Phase 4: run the module's executor now (`sb_run_jobs(performance.now())`),
|
|
38
|
+
* apply the resulting ops and (re)schedule the next run from the returned
|
|
39
|
+
* delay. Returns that delay in ms (`-1`: nothing pending), or `null` when
|
|
40
|
+
* the module has no executor export.
|
|
41
|
+
*/
|
|
42
|
+
runJobs(): number | null;
|
|
43
|
+
/** Whether the module exports `sb_run_jobs`. */
|
|
44
|
+
readonly supportsJobs: boolean;
|
|
45
|
+
/** Exit code reported by `_start` (0 when `main` returned normally). */
|
|
46
|
+
readonly exitCode: number;
|
|
47
|
+
readonly instance: WebAssembly.Instance;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface LoadAppOptions {
|
|
51
|
+
/** `argv` passed to the WASI command; defaults to `["Counter"]`. */
|
|
52
|
+
args?: string[];
|
|
53
|
+
/** `KEY=VALUE` environment entries; defaults to none. */
|
|
54
|
+
env?: string[];
|
|
55
|
+
/** Log every WASI syscall to the console (the shim's own tracing). */
|
|
56
|
+
debug?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Phase 11: the app's resources, mounted read-only at `/bundle` (path inside
|
|
59
|
+
* the bundle → bytes); `Bundle.main` reads them. Nothing is mounted without it.
|
|
60
|
+
*/
|
|
61
|
+
bundle?: ReadonlyMap<string, Uint8Array>;
|
|
62
|
+
/** Phase 11: receives the app's `UserDefaults` whenever they change (a `defaults` op); the op never reaches `onOps`. */
|
|
63
|
+
onDefaults?: (values: Record<string, unknown>) => void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** An in-memory WASI directory tree from `path → bytes` (paths `/`-separated, no leading slash). */
|
|
67
|
+
export function bundleDirectory(files: ReadonlyMap<string, Uint8Array>): Directory {
|
|
68
|
+
const root = new Directory(new Map<string, Inode>());
|
|
69
|
+
for (const [filePath, bytes] of files) {
|
|
70
|
+
const parts = filePath.split('/').filter((part) => part !== '' && part !== '.' && part !== '..');
|
|
71
|
+
if (parts.length === 0) continue;
|
|
72
|
+
let dir = root;
|
|
73
|
+
for (const part of parts.slice(0, -1)) {
|
|
74
|
+
const existing = dir.contents.get(part);
|
|
75
|
+
let next: Directory;
|
|
76
|
+
if (existing instanceof Directory) {
|
|
77
|
+
next = existing;
|
|
78
|
+
} else {
|
|
79
|
+
next = new Directory(new Map<string, Inode>());
|
|
80
|
+
dir.contents.set(part, next);
|
|
81
|
+
}
|
|
82
|
+
dir = next;
|
|
83
|
+
}
|
|
84
|
+
dir.contents.set(parts[parts.length - 1]!, new File(bytes, { readonly: true }));
|
|
85
|
+
}
|
|
86
|
+
return root;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface SwiftBrowserExports {
|
|
90
|
+
memory: WebAssembly.Memory;
|
|
91
|
+
_start?: () => unknown;
|
|
92
|
+
_initialize?: () => unknown;
|
|
93
|
+
sb_event: (id: number) => void;
|
|
94
|
+
sb_ops_ptr: () => number;
|
|
95
|
+
sb_ops_len: () => number;
|
|
96
|
+
sb_ops_clear: () => void;
|
|
97
|
+
// Phase 2 (optional so Phase 1 modules still load).
|
|
98
|
+
sb_alloc?: (len: number) => number;
|
|
99
|
+
sb_event_json?: (ptr: number, len: number) => void;
|
|
100
|
+
sb_state_ptr?: () => number;
|
|
101
|
+
sb_state_len?: () => number;
|
|
102
|
+
// Phase 4 (optional).
|
|
103
|
+
sb_run_jobs?: (nowMs: number) => number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const WASI_MODULE = 'wasi_snapshot_preview1';
|
|
107
|
+
const REQUIRED_EXPORTS = ['memory', 'sb_event', 'sb_ops_ptr', 'sb_ops_len', 'sb_ops_clear'] as const;
|
|
108
|
+
|
|
109
|
+
export class WasmLoadError extends Error {
|
|
110
|
+
override name = 'WasmLoadError';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** `setTimeout` is clamped to this so a busy executor never spins the main thread. */
|
|
114
|
+
export const MIN_JOB_DELAY_MS = 4;
|
|
115
|
+
|
|
116
|
+
/** The timer surface `JobScheduler` uses (the window's by default; tests stub it). */
|
|
117
|
+
export interface JobTimers {
|
|
118
|
+
now(): number;
|
|
119
|
+
setTimeout(callback: () => void, ms: number): number;
|
|
120
|
+
clearTimeout(handle: number): void;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const defaultTimers: JobTimers = {
|
|
124
|
+
now: () => performance.now(),
|
|
125
|
+
setTimeout: (callback, ms) => window.setTimeout(callback, ms),
|
|
126
|
+
clearTimeout: (handle) => window.clearTimeout(handle),
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Drives a cooperative executor: `run(now)` runs the due jobs and returns the
|
|
131
|
+
* milliseconds until the next timer (`< 0`: none). `kick()` runs it right away
|
|
132
|
+
* and schedules the next run; there is only ever one pending timer, which
|
|
133
|
+
* every `kick` cancels and replaces, so a burst of events never piles up
|
|
134
|
+
* extra wake-ups. `apply()` is called after every run to drain the ops buffer.
|
|
135
|
+
*/
|
|
136
|
+
export class JobScheduler {
|
|
137
|
+
private timer: number | null = null;
|
|
138
|
+
/** How many times `run` was called (for tests and debugging). */
|
|
139
|
+
runs = 0;
|
|
140
|
+
/** The delay the last run returned (`-1` when nothing is pending). */
|
|
141
|
+
lastDelay = -1;
|
|
142
|
+
private disposed = false;
|
|
143
|
+
|
|
144
|
+
constructor(
|
|
145
|
+
private readonly run: (nowMs: number) => number,
|
|
146
|
+
private readonly apply: () => void,
|
|
147
|
+
private readonly timers: JobTimers = defaultTimers,
|
|
148
|
+
private readonly onError: (error: unknown) => void = (error) => console.error('[sb] sb_run_jobs failed', error),
|
|
149
|
+
) {}
|
|
150
|
+
|
|
151
|
+
/** Run the executor now, apply its ops and reschedule. Returns the delay. */
|
|
152
|
+
kick(): number {
|
|
153
|
+
this.cancel();
|
|
154
|
+
if (this.disposed) return -1;
|
|
155
|
+
let delay = -1;
|
|
156
|
+
try {
|
|
157
|
+
const raw = this.run(this.timers.now());
|
|
158
|
+
delay = Number.isFinite(raw) ? raw : -1;
|
|
159
|
+
this.runs += 1;
|
|
160
|
+
this.apply();
|
|
161
|
+
} catch (error) {
|
|
162
|
+
this.onError(error);
|
|
163
|
+
delay = -1;
|
|
164
|
+
}
|
|
165
|
+
this.lastDelay = delay;
|
|
166
|
+
if (delay >= 0 && !this.disposed) {
|
|
167
|
+
this.timer = this.timers.setTimeout(() => {
|
|
168
|
+
this.timer = null;
|
|
169
|
+
this.kick();
|
|
170
|
+
}, Math.max(MIN_JOB_DELAY_MS, delay));
|
|
171
|
+
}
|
|
172
|
+
return delay;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Whether a timer is waiting to run the executor again. */
|
|
176
|
+
get pending(): boolean {
|
|
177
|
+
return this.timer !== null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Cancel the pending timer (the next `kick` schedules again). */
|
|
181
|
+
cancel(): void {
|
|
182
|
+
if (this.timer !== null) {
|
|
183
|
+
this.timers.clearTimeout(this.timer);
|
|
184
|
+
this.timer = null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Cancel and refuse further runs. */
|
|
189
|
+
dispose(): void {
|
|
190
|
+
this.disposed = true;
|
|
191
|
+
this.cancel();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function loadApp(
|
|
196
|
+
url: string,
|
|
197
|
+
onOps: OpsSink,
|
|
198
|
+
options: LoadAppOptions = {},
|
|
199
|
+
): Promise<AppHandle> {
|
|
200
|
+
const module = await compile(url);
|
|
201
|
+
checkImports(module);
|
|
202
|
+
|
|
203
|
+
const fds: Fd[] = [
|
|
204
|
+
new OpenFile(new File([])), // stdin: empty
|
|
205
|
+
ConsoleStdout.lineBuffered((line) => console.log(`[wasm stdout] ${line}`)),
|
|
206
|
+
ConsoleStdout.lineBuffered((line) => console.error(`[wasm stderr] ${line}`)),
|
|
207
|
+
];
|
|
208
|
+
if (options.bundle) {
|
|
209
|
+
// Preopened as fd 3; WASI libc resolves `/bundle/...` paths through it.
|
|
210
|
+
const root = bundleDirectory(options.bundle);
|
|
211
|
+
fds.push(new PreopenDirectory('/bundle', root.contents));
|
|
212
|
+
}
|
|
213
|
+
const wasi = new WASI(options.args ?? ['Counter'], options.env ?? [], fds, { debug: options.debug ?? false });
|
|
214
|
+
|
|
215
|
+
const instance = await WebAssembly.instantiate(module, {
|
|
216
|
+
[WASI_MODULE]: wasi.wasiImport,
|
|
217
|
+
});
|
|
218
|
+
const exports = instance.exports as unknown as SwiftBrowserExports;
|
|
219
|
+
for (const name of REQUIRED_EXPORTS) {
|
|
220
|
+
if (!(name in exports)) {
|
|
221
|
+
throw new WasmLoadError(`${url} does not export \`${name}\`; see docs/ops-protocol.md`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Run `main`. For a WASI command this is `_start`; it returns (or raises
|
|
226
|
+
// `proc_exit`) after `main` has mounted the root view and performed the
|
|
227
|
+
// first render pass into the pending buffer. The instance and its linear
|
|
228
|
+
// memory stay alive afterwards, so later `sb_event` calls are fine.
|
|
229
|
+
// A reactor-style build (`-mexec-model=reactor`) exposes `_initialize`
|
|
230
|
+
// instead; support it too.
|
|
231
|
+
let exitCode = 0;
|
|
232
|
+
if (typeof exports._start === 'function') {
|
|
233
|
+
try {
|
|
234
|
+
exitCode = wasi.start(instance as { exports: { memory: WebAssembly.Memory; _start: () => unknown } });
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (error instanceof WASIProcExit) {
|
|
237
|
+
exitCode = error.code;
|
|
238
|
+
} else {
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
} else if (typeof exports._initialize === 'function') {
|
|
243
|
+
wasi.initialize(instance as { exports: { memory: WebAssembly.Memory; _initialize?: () => unknown } });
|
|
244
|
+
} else {
|
|
245
|
+
throw new WasmLoadError(`${url} exports neither \`_start\` nor \`_initialize\``);
|
|
246
|
+
}
|
|
247
|
+
if (exitCode !== 0) {
|
|
248
|
+
throw new WasmLoadError(`${url} exited with code ${exitCode}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const drain = (): void => {
|
|
252
|
+
let ops = readPendingOps(exports);
|
|
253
|
+
// Clear before applying so a renderer error never re-applies stale ops.
|
|
254
|
+
exports.sb_ops_clear();
|
|
255
|
+
if (ops.some((op) => op.op === 'defaults')) {
|
|
256
|
+
// The last `defaults` op of a batch holds the whole dictionary.
|
|
257
|
+
const latest = ops.filter((op) => op.op === 'defaults').pop();
|
|
258
|
+
if (latest && latest.op === 'defaults') options.onDefaults?.(latest.values);
|
|
259
|
+
ops = ops.filter((op) => op.op !== 'defaults');
|
|
260
|
+
}
|
|
261
|
+
if (ops.length > 0) onOps(ops);
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const supportsJsonEvents =
|
|
265
|
+
typeof exports.sb_alloc === 'function' && typeof exports.sb_event_json === 'function';
|
|
266
|
+
const supportsSnapshot =
|
|
267
|
+
typeof exports.sb_state_ptr === 'function' && typeof exports.sb_state_len === 'function';
|
|
268
|
+
const supportsJobs = typeof exports.sb_run_jobs === 'function';
|
|
269
|
+
|
|
270
|
+
// Phase 4: the executor runs after the first ops and after every event; the
|
|
271
|
+
// scheduler's own timer covers sleeps and clocks in between.
|
|
272
|
+
const scheduler = supportsJobs ? new JobScheduler((now) => exports.sb_run_jobs!(now), drain) : null;
|
|
273
|
+
const runJobs = (): number | null => (scheduler ? scheduler.kick() : null);
|
|
274
|
+
|
|
275
|
+
const sendEvent = (id: ElementId): void => {
|
|
276
|
+
exports.sb_event(id | 0);
|
|
277
|
+
drain();
|
|
278
|
+
runJobs();
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
drain();
|
|
282
|
+
runJobs();
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
instance,
|
|
286
|
+
exitCode,
|
|
287
|
+
supportsJsonEvents,
|
|
288
|
+
supportsBatchEvents: supportsJsonEvents,
|
|
289
|
+
supportsJobs,
|
|
290
|
+
runJobs,
|
|
291
|
+
sendEvent,
|
|
292
|
+
sendEventJson(event: RendererEvent): boolean {
|
|
293
|
+
if (event.type === 'tap') {
|
|
294
|
+
sendEvent(event.id);
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
if (!supportsJsonEvents) return false;
|
|
298
|
+
const bytes = encoder.encode(JSON.stringify(event));
|
|
299
|
+
const ptr = exports.sb_alloc!(bytes.byteLength) >>> 0;
|
|
300
|
+
// `memory.buffer` must be read after `sb_alloc`: allocating may grow
|
|
301
|
+
// linear memory and detach the previous ArrayBuffer.
|
|
302
|
+
const memory = exports.memory.buffer;
|
|
303
|
+
if (ptr + bytes.byteLength > memory.byteLength) {
|
|
304
|
+
throw new WasmLoadError(
|
|
305
|
+
`sb_alloc(${bytes.byteLength}) returned ${ptr}, outside linear memory (${memory.byteLength} bytes)`,
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
new Uint8Array(memory, ptr, bytes.byteLength).set(bytes);
|
|
309
|
+
exports.sb_event_json!(ptr, bytes.byteLength);
|
|
310
|
+
drain();
|
|
311
|
+
runJobs();
|
|
312
|
+
return true;
|
|
313
|
+
},
|
|
314
|
+
snapshotState(): string | null {
|
|
315
|
+
if (!supportsSnapshot) return null;
|
|
316
|
+
// `sb_state_ptr` refreshes the buffer, so call it before `sb_state_len`.
|
|
317
|
+
const ptr = exports.sb_state_ptr!() >>> 0;
|
|
318
|
+
const len = exports.sb_state_len!() >>> 0;
|
|
319
|
+
if (len === 0) return null;
|
|
320
|
+
const memory = exports.memory.buffer;
|
|
321
|
+
if (ptr + len > memory.byteLength) {
|
|
322
|
+
throw new WasmLoadError(
|
|
323
|
+
`sb_state_ptr/len (${ptr}, ${len}) point outside linear memory (${memory.byteLength} bytes)`,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
return decoder.decode(new Uint8Array(memory, ptr, len));
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function compile(url: string): Promise<WebAssembly.Module> {
|
|
332
|
+
const response = await fetch(url);
|
|
333
|
+
if (!response.ok) {
|
|
334
|
+
throw new WasmLoadError(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
|
|
335
|
+
}
|
|
336
|
+
const type = response.headers.get('content-type') ?? '';
|
|
337
|
+
if (typeof WebAssembly.compileStreaming === 'function' && type.includes('application/wasm')) {
|
|
338
|
+
return WebAssembly.compileStreaming(response);
|
|
339
|
+
}
|
|
340
|
+
// Fall back for servers that mislabel the MIME type.
|
|
341
|
+
const bytes = await response.arrayBuffer();
|
|
342
|
+
if (bytes.byteLength < 8 || new DataView(bytes).getUint32(0, true) !== 0x6d736100) {
|
|
343
|
+
throw new WasmLoadError(
|
|
344
|
+
`${url} is not a Wasm binary (${bytes.byteLength} bytes, content-type "${type}"). ` +
|
|
345
|
+
'Has scripts/build-wasm.sh copied the module into Web/public/?',
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
return WebAssembly.compile(bytes);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function checkImports(module: WebAssembly.Module): void {
|
|
352
|
+
const foreign = WebAssembly.Module.imports(module).filter((i) => i.module !== WASI_MODULE);
|
|
353
|
+
if (foreign.length > 0) {
|
|
354
|
+
const list = foreign.map((i) => `${i.module}.${i.name}`).join(', ');
|
|
355
|
+
throw new WasmLoadError(
|
|
356
|
+
`Module imports symbols outside ${WASI_MODULE}: ${list}. ` +
|
|
357
|
+
'The renderer only provides WASI preview1.',
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
363
|
+
const encoder = new TextEncoder();
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Read the pending ops buffer. `memory.buffer` is re-read on every call: the
|
|
367
|
+
* ArrayBuffer is detached whenever linear memory grows.
|
|
368
|
+
*/
|
|
369
|
+
function readPendingOps(exports: SwiftBrowserExports): Op[] {
|
|
370
|
+
const ptr = exports.sb_ops_ptr() >>> 0;
|
|
371
|
+
const len = exports.sb_ops_len() >>> 0;
|
|
372
|
+
if (len === 0) return [];
|
|
373
|
+
const memory = exports.memory.buffer;
|
|
374
|
+
if (ptr + len > memory.byteLength) {
|
|
375
|
+
throw new WasmLoadError(
|
|
376
|
+
`sb_ops_ptr/len (${ptr}, ${len}) point outside linear memory (${memory.byteLength} bytes)`,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
const bytes = new Uint8Array(memory, ptr, len);
|
|
380
|
+
let json: string;
|
|
381
|
+
try {
|
|
382
|
+
json = decoder.decode(bytes);
|
|
383
|
+
} catch (error) {
|
|
384
|
+
throw new WasmLoadError(`Pending ops buffer is not valid UTF-8: ${String(error)}`);
|
|
385
|
+
}
|
|
386
|
+
let parsed: unknown;
|
|
387
|
+
try {
|
|
388
|
+
parsed = JSON.parse(json);
|
|
389
|
+
} catch (error) {
|
|
390
|
+
throw new WasmLoadError(`Pending ops buffer is not valid JSON: ${String(error)}\n${json}`);
|
|
391
|
+
}
|
|
392
|
+
if (!Array.isArray(parsed)) {
|
|
393
|
+
throw new WasmLoadError(`Pending ops buffer must be a JSON array, got ${typeof parsed}`);
|
|
394
|
+
}
|
|
395
|
+
const bad = parsed.find((item) => !isOp(item));
|
|
396
|
+
if (bad !== undefined) {
|
|
397
|
+
throw new WasmLoadError(`Unknown op in pending buffer: ${JSON.stringify(bad)}`);
|
|
398
|
+
}
|
|
399
|
+
return parsed as Op[];
|
|
400
|
+
}
|