arm64js 0.4.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Konstantin Goncharuk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,286 @@
1
+ # ARM64JS SDK
2
+
3
+ [![npm](https://img.shields.io/npm/v/arm64js)](https://www.npmjs.com/package/arm64js)
4
+ [![CI](https://github.com/kooler/arm64js-sdk/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/kooler/arm64js-sdk/actions/workflows/ci.yml)
5
+
6
+ Arm64js is a WebAssembly-based arm64 emulator running in a web browser and allowing you to boot and control a real Linux (Alpine).
7
+
8
+ This SDK helps to: create a VM, set it up and interact with it.
9
+
10
+ The SDK is distributed as an npm package and integrated with the arm64js CDN.
11
+
12
+ ## VM creation lifecycle
13
+
14
+ ### 1. Boot a base image
15
+
16
+ Similarly to Docker, you need to select a base image to boot the initial state of your VM. At the moment, only base images provided by arm64js can be used as they require some customization to work "quicker" when emulated in a web browser. The list of images and their versions is at [arm64js.com/images](https://arm64js.com/images/). Booting the image creates a VM instance:
17
+
18
+ ```js
19
+ import { Arm64JS } from 'arm64js';
20
+ const vm = await Arm64JS.boot('alpine');
21
+ ```
22
+
23
+ ### 2. Set up the VM
24
+
25
+ You would likely want to install some extra packages or copy files into your VM so that it can do something useful. For managing packages, use `apk` as you would in the native Alpine. To execute a command (any command pretty much), the `vm.exec` method is used:
26
+
27
+ ```js
28
+ const { output, exitCode } = await vm.exec('apk add --no-cache curl');
29
+ ```
30
+
31
+ It returns raw `output` and `exitCode` so that you can verify the result.
32
+
33
+ The VM _does not_ have access to the Internet except for the apk registry. To give it your own data, you can use one of the two options:
34
+
35
+ - `vm.writeFile`: copies the specified file into the VM. Changing or deleting the file happens only inside the VM. The file is saved and loaded with the snapshot. Uses VM memory to copy the file, thus is intended mainly for smaller (up to 100 MiB) files.
36
+ - `vm.mount`: read-only, direct mount of the data blob. Doesn't use any VM memory. Not stored in the snapshot. To unmount use `vm.unmount()`.
37
+
38
+ See example below:
39
+
40
+ ```js
41
+ await vm.writeFile('/root/data.csv', file); // creates a physical file in the VM
42
+ await vm.mount({ 'model.bin': bigFile }, '/data'); // read-only, directly mounted blob, does not copy anything into the VM so it can be a large file
43
+ ```
44
+
45
+ To read any file from the VM, use `readFile`:
46
+
47
+ ```js
48
+ const result = await vm.readFile('/root/result.txt'); // a copy out, as a Blob
49
+ ```
50
+
51
+ ### 3. Make a snapshot and save it locally
52
+
53
+ Once you are done with the VM setup, you would likely want to save it so that you don't need to do the same setup again. For that, make a snapshot of the VM. Snapshots are stored in the [OPFS](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system).
54
+
55
+ ```js
56
+ const snap = await vm.snapshot({ name: 'alpine with curl' });
57
+ console.log(snap.id); // this is the id we'd need to store to load the snapshot later
58
+ ```
59
+
60
+ To load from the snapshot, specify its ID when creating the VM:
61
+
62
+ ```js
63
+ const again = await Arm64JS.boot(snap.id);
64
+ ```
65
+
66
+ ## Where the VM runs
67
+
68
+ The VM needs `SharedArrayBuffer` which browsers only give to cross-origin-isolated pages. If the `Cross-Origin-Opener-Policy` (COOP) header is not specified or not sufficient, the SDK will load the VM inside an iframe on the `cdn.arm64js.com` domain.
69
+
70
+ The following decision process is being used:
71
+
72
+ | Your page | What happens |
73
+ | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------- |
74
+ | sends `Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Policy: require-corp` | the VM runs **inline**, in your page's own workers. Works in all modern browsers. |
75
+ | sends no COOP headers (likely) | the VM runs in a hidden **frame** served from the arm64js CDN. Works in Chrome and Edge 137+. |
76
+ | no COOP header and a browser that doesn't support the iframe fallback (Firefox, Safari) | `boot()` rejects with `unsupported-browser`. |
77
+
78
+ Call `Arm64JS.mode()` to see which mode is being used.
79
+
80
+ ### Setting the headers
81
+
82
+ To run the VM inline, send these two headers with the page that runs the VM:
83
+
84
+ ```
85
+ Cross-Origin-Opener-Policy: same-origin
86
+ Cross-Origin-Embedder-Policy: require-corp
87
+ ```
88
+
89
+ We recommend setting them only on the pages that run the VM, not on the whole site, because they change how the browser treats a page:
90
+
91
+ - files from other domains (scripts, images, fonts, iframes) load only if their server allows it with a `Cross-Origin-Resource-Policy` or CORS header (arm64js CDN already does).
92
+ - popups the page opens lose their link to it, which can break for example sign-in or payment popups.
93
+
94
+ Here's an example of how to send them with nginx:
95
+
96
+ ```nginx
97
+ location = /playground.html {
98
+ add_header Cross-Origin-Opener-Policy same-origin;
99
+ add_header Cross-Origin-Embedder-Policy require-corp;
100
+ }
101
+ ```
102
+
103
+ To check the setup, run `await Arm64JS.mode()`, it should return `'inline'`.
104
+
105
+ ### Content-Security-Policy
106
+
107
+ If your page sends a Content-Security-Policy and you would like to use the iframe-based solution, you need to allow the arm64js CDN as an origin: `script-src https://cdn.arm64js.com`, `connect-src https://cdn.arm64js.com`, `worker-src blob:` (the VM's workers are started through a blob), and `frame-src https://cdn.arm64js.com` for the iframe itself.
108
+
109
+ ## Install
110
+
111
+ ```sh
112
+ npm install arm64js
113
+ ```
114
+
115
+ ## API
116
+
117
+ ### `Arm64JS.boot(image | snapshotId, { vcpus?, onProgress? })` → `Vm`
118
+
119
+ A VM can be created either from a base image or your own local snapshot.
120
+
121
+ - `image`: name of an image on the CDN ([all images](https://arm64js.com/images/)): `'alpine'` is the newest version of it, `'alpine:2'` a fixed one, `'alpine@sha256:<hash>'` an exact snapshot.
122
+ - `snapshotId`: a snapshot id (`snap.id`, 64 hex characters) loads one of your own snapshots made previously.
123
+
124
+ Additionally, you can specify an `onProgress` callback to track VM loading phases (`resolve → manifest → start → memory (repeats, with counts) → run → done`) and the number of virtual CPUs the VM will have (more is not always better).
125
+
126
+ ### Listener: `vm.onOutput(cb)` → unsubscribe
127
+
128
+ The raw bytes the guest prints on its console, including escape sequences.
129
+
130
+ ```js
131
+ const decoder = new TextDecoder(); // The output comes as bytes (Uint8Array), TextDecoder turns them into text.
132
+ const stopOutput = vm.onOutput((bytes, info) => {
133
+ if (info.exec) return; // skip commands that we call ourselves
134
+ console.log(decoder.decode(bytes, { stream: true }));
135
+ });
136
+
137
+ // to stop listening:
138
+ stopOutput();
139
+ ```
140
+
141
+ ### `vm.onExit(cb)` → unsubscribe
142
+
143
+ The VM stopped (halted or faulted). Every later call on the VM rejects with `vm-exited`. If the hidden frame running the VM stops answering, `reason` is `'lost'` and later calls reject with `vm-lost`.
144
+
145
+ If the VM has already stopped, the callback is called right away. It is not called for `vm.dispose()`.
146
+
147
+ ```js
148
+ const stopExit = vm.onExit(({ reason }) => console.log(`VM stopped: ${reason}`));
149
+
150
+ // later, to stop listening:
151
+ stopExit();
152
+ ```
153
+
154
+ ### `vm.exec(command, { timeoutMs? })` → `{ output, exitCode, truncated }`
155
+
156
+ Runs `command` in the guest's shell (`sh`), and returns what it printed and its exit status. `command` can be one command or several, one per line. The default timeout is two minutes.
157
+
158
+ It works by typing the command into the guest's serial console and reading the bytes back. There are a few limits:
159
+
160
+ - stdout and stderr are merged into one stream.
161
+ - Each line of the command must be under 4000 bytes.
162
+ - **`output` is capped at 1 MiB**, and past that the head is dropped and `truncated` is `true`. Write to a file and read it back if you need more.
163
+ - A timeout does its best to terminate the command (sends Ctrl-C to the VM's console), but that does not guarantee that the command will stop -- a program that ignores Ctrl-C (like vim) will keep running.
164
+
165
+ Whatever the command does runs as **root** in the guest.
166
+
167
+ ### `vm.write(data)`
168
+
169
+ Send data directly into the VM console, lower level than `vm.exec`, allows sending any keyboard sequences including key presses like Ctrl-C ('\x03') or Enter ('\r').
170
+
171
+ ### `vm.resize(cols, rows)`
172
+
173
+ Set the terminal console size. Mainly useful if you are writing your own terminal implementation.
174
+
175
+ If you want to use a terminal with your VM, it's easier to connect xterm (see "Terminal" section below).
176
+
177
+ ### `vm.writeFile(path, data, { mode?, timeoutMs? })`
178
+
179
+ Copies `data` (a `Blob` or `File`, bytes, or a string) into the VM as the file `path` (an absolute path). An existing file is replaced, a missing folder is created.
180
+
181
+ - `mode` sets the permission bits, e.g. `0o755` for a script.
182
+ - `timeoutMs` will terminate the operation if it doesn't complete within the specified time (for example, if the VM is very busy). Default is two minutes plus one second per MiB.
183
+
184
+ The copy is kept in VM memory, so it has to fit there (`alpine` for example has 1 GiB). If you need to use a larger file, use `vm.mount` instead.
185
+
186
+ **Files become part of the VM, so when a snapshot is taken those files will be present in the snapshot.**
187
+
188
+ ### `vm.readFile(path, { maxBytes?, timeoutMs? })` → `Blob`
189
+
190
+ Copies a file out of the VM. The copy is kept in VM memory, so the file should fit there (default 1 GiB). If it doesn't, or the path is not a file but a directory, or doesn't exist, the read is refused with `read-failed`.
191
+
192
+ Here's an example of how a file can be downloaded from the VM:
193
+
194
+ ```js
195
+ const blob = await vm.readFile('/root/report.pdf');
196
+ const a = document.createElement('a');
197
+ a.href = URL.createObjectURL(blob);
198
+ a.download = 'report.pdf';
199
+ a.click();
200
+ ```
201
+
202
+ ### `vm.mount(files, path)` → `{ path, unmount() }`
203
+
204
+ Mounts files in read-only mode into the VM. Files are not copied, so they do not consume memory. `path` is created if missing. `files` is a record of names to Blobs (`{ 'data.bin': blob }`), or a list of Files (`input.files` or `dataTransfer.files`).
205
+
206
+ A path that is already mounted is refused with `invalid-input`: `unmount` it first, and wait for that to finish.
207
+
208
+ **Mounts are not part of the VM and are not saved in snapshots.** After you boot a snapshot, mount again.
209
+
210
+ ### `vm.unmount(path)`
211
+
212
+ Undoes a `mount`. If nothing is mounted at `path` it does nothing. While a program in the VM still uses the files, it rejects with `unmount-failed`.
213
+
214
+ ### `vm.snapshot({ name?, meta? })` → `SnapshotInfo`
215
+
216
+ Saves the VM in browser storage (OPFS) and keeps it running. Use `meta` to store any extra info you'd like to add to the snapshot (any JSON up to 16 KiB); it is returned in `snapshots.list()`.
217
+
218
+ ### `vm.dispose()`
219
+
220
+ Stops the VM and frees its workers.
221
+
222
+ ### `Arm64JS.snapshots.list()` / `.get(id)` / `.remove(id, { force? })`
223
+
224
+ Lists stored snapshots, newest first: `{ id, name, created, base, engine, vcpus, sizeBytes, meta }`. `remove` is refused while a VM runs from that snapshot, unless `force` is specified.
225
+
226
+ ### `Arm64JS.storage.status()`
227
+
228
+ Returns an object describing the current storage situation: `{ available, reason?, persistent?, quota?, usage?, snapshots, poolBytes }`.
229
+
230
+ Storage can be unavailable when:
231
+
232
+ 1. a page is not a secure context
233
+ 2. a browser without OPFS
234
+ 3. in **frame mode when the browser blocks third-party storage** (for example in a private window).
235
+
236
+ When storage is not available, `snapshot()` rejects with `storage-unavailable`.
237
+
238
+ ### `Arm64JS.engine()` → `'0.4'`
239
+
240
+ The engine version the SDK has been built with. A VM booted from a snapshot can run on another version (based on the version that was running when the snapshot was made). Use `vm.engine` to check which.
241
+
242
+ ### `Arm64JS.shutdown()`
243
+
244
+ Disposes every VM and, in frame mode, the frames.
245
+
246
+ ### Errors
247
+
248
+ Everything rejects with an `Arm64JSError` carrying a `code`: `unsupported-browser`, `protocol-mismatch`, `engine-mismatch`, `image-not-found`, `boot-failed`, `exec-timeout`, `exec-failed`, `vm-exited`, `vm-lost`, `storage-unavailable`, `quota`, `snapshot-failed`, `snapshot-not-found`, `snapshot-in-use`, `snapshot-corrupt`, `invalid-input`, `share-unavailable`, `mount-failed`, `unmount-failed`, `write-failed`, `read-failed`.
249
+
250
+ ## Terminal (optional)
251
+
252
+ A terminal is based on [xterm.js](https://xtermjs.org) and is not included by default; you need to install and wire it separately.
253
+
254
+ ```sh
255
+ npm install @xterm/xterm @xterm/addon-fit
256
+ ```
257
+
258
+ ```js
259
+ import { attachTerminal } from 'arm64js/terminal';
260
+ import '@xterm/xterm/css/xterm.css';
261
+
262
+ const term = attachTerminal(vm, document.getElementById('term'));
263
+ // term.xterm is the xterm instance; term.dispose() removes it and leaves the VM running.
264
+ ```
265
+
266
+ It shows the VM console, sends what is typed to the VM, and sizes itself to the element (pass `{ fit: false }` to keep a fixed size, and `{ xterm: { … } }` for xterm's own options). `vm.exec` runs on the same console but stays out of the terminal; keys pressed while a `vm.exec` command is running are sent once it finishes.
267
+
268
+ ## Networking
269
+
270
+ The VM has an emulated network card, DNS, and can install Alpine packages with `apk` through the CDN's package mirror. It has no general internet access.
271
+
272
+ ## Development
273
+
274
+ ```sh
275
+ npm install
276
+ npm test
277
+ npm run build
278
+ ```
279
+
280
+ `src/version.ts` and `protocol/dist/` are generated. `npm install` creates them; run `npm run stamp` again after changing the version in `package.json`.
281
+
282
+ `protocol/` is the `@arm64js/protocol` package, which the engine uses too. To change it, bump its version and publish it (tag `protocol-v<version>`), update it in the engine and release the engine, then release this package.
283
+
284
+ ## License
285
+
286
+ MIT
package/dist/api.d.ts ADDED
@@ -0,0 +1,32 @@
1
+ import type { BootOptions } from '@arm64js/protocol';
2
+ import { type Arm64JSDeps } from './runtime.js';
3
+ export type { Arm64JSDeps } from './runtime.js';
4
+ /**
5
+ * Build the API over a runtime. Tests pass `deps`; the package's own
6
+ * `Arm64JS` is one of these over the real page.
7
+ */
8
+ export declare function createArm64JS(deps?: Arm64JSDeps): {
9
+ /** Boot a CDN image (`'alpine'`, `'alpine:2'`) or one of this browser's snapshots by id.
10
+ * A snapshot boots on the engine it was saved on when this page's own cannot resume it. */
11
+ boot: (target: string, opts?: BootOptions) => Promise<import("./vm.js").Vm>;
12
+ /** `'inline'` when the VM runs in this page, `'frame'` when in the CDN's frame. */
13
+ mode: () => Promise<import("./hosts.js").HostMode>;
14
+ /** This page's engine, e.g. `'0.11'`. A VM booted from a snapshot may run on
15
+ * another: see `vm.engine`. */
16
+ engine: () => Promise<string>;
17
+ snapshots: {
18
+ list: () => Promise<import("@arm64js/protocol").SnapshotInfo[]>;
19
+ get: (id: string) => Promise<import("@arm64js/protocol").SnapshotInfo | null>;
20
+ /** Delete a snapshot. Refused while a VM runs from it, unless `force`. */
21
+ remove: (id: string, opts?: {
22
+ force?: boolean;
23
+ }) => Promise<void>;
24
+ };
25
+ storage: {
26
+ status: () => Promise<import("@arm64js/protocol").StorageStatus>;
27
+ };
28
+ /** Dispose every VM and, in frame mode, the frame. */
29
+ shutdown: () => Promise<void>;
30
+ version: string;
31
+ };
32
+ export type Arm64JSApi = ReturnType<typeof createArm64JS>;
package/dist/api.js ADDED
@@ -0,0 +1,32 @@
1
+ // The object a page calls: `Arm64JS.boot(...)`, `Arm64JS.snapshots.list()`.
2
+ import { Runtime } from './runtime.js';
3
+ import { VERSION } from './version.js';
4
+ /**
5
+ * Build the API over a runtime. Tests pass `deps`; the package's own
6
+ * `Arm64JS` is one of these over the real page.
7
+ */
8
+ export function createArm64JS(deps = {}) {
9
+ const rt = new Runtime(deps);
10
+ return {
11
+ /** Boot a CDN image (`'alpine'`, `'alpine:2'`) or one of this browser's snapshots by id.
12
+ * A snapshot boots on the engine it was saved on when this page's own cannot resume it. */
13
+ boot: async (target, opts) => rt.boot(target, opts),
14
+ /** `'inline'` when the VM runs in this page, `'frame'` when in the CDN's frame. */
15
+ mode: async () => rt.mode(),
16
+ /** This page's engine, e.g. `'0.11'`. A VM booted from a snapshot may run on
17
+ * another: see `vm.engine`. */
18
+ engine: async () => rt.engine(),
19
+ snapshots: {
20
+ list: async () => rt.listSnapshots(),
21
+ get: async (id) => rt.getSnapshot(id),
22
+ /** Delete a snapshot. Refused while a VM runs from it, unless `force`. */
23
+ remove: async (id, opts) => rt.removeSnapshot(id, opts),
24
+ },
25
+ storage: {
26
+ status: async () => rt.storageStatus(),
27
+ },
28
+ /** Dispose every VM and, in frame mode, the frame. */
29
+ shutdown: async () => rt.shutdown(),
30
+ version: VERSION,
31
+ };
32
+ }
@@ -0,0 +1,2 @@
1
+ /** Await a host call, re-raising its failure as this package's error class. */
2
+ export declare function withSdkErrors<T>(p: Promise<T>): Promise<T>;
package/dist/errors.js ADDED
@@ -0,0 +1,24 @@
1
+ // Errors raised by this package. Every host call a page can reach goes through
2
+ // `withSdkErrors`, so a caller sees one `Arm64JSError` class whichever way the VM runs.
3
+ import { Arm64JSError } from '@arm64js/protocol';
4
+ /** Await a host call, re-raising its failure as this package's error class. */
5
+ export async function withSdkErrors(p) {
6
+ return p.catch(rethrow);
7
+ }
8
+ /**
9
+ * Re-raise a host failure as this package's `Arm64JSError`.
10
+ *
11
+ * Inline, the rejection carries the CDN runtime's own copy of the class, so an
12
+ * `instanceof` against the copy this package exports would be false — while in
13
+ * frame mode, where `RpcClient` rebuilds it locally, it would be true.
14
+ */
15
+ function rethrow(e) {
16
+ const code = e?.code;
17
+ if (e instanceof Arm64JSError) {
18
+ throw e;
19
+ }
20
+ if (code) {
21
+ throw new Arm64JSError(code, e.message, e.data);
22
+ }
23
+ throw new Arm64JSError('boot-failed', String(e?.message ?? e));
24
+ }
@@ -0,0 +1,30 @@
1
+ import { type Host } from '@arm64js/protocol';
2
+ import { RpcClient } from '@arm64js/protocol/rpc-client';
3
+ /**
4
+ * How long the frame gets to say hello before the page gives up on it: a frame
5
+ * that never runs a script (blocked third-party frames, a network failure).
6
+ */
7
+ export declare const FRAME_HANDSHAKE_TIMEOUT_MS = 8000;
8
+ /** The message the developer sees when the browser cannot isolate the frame. */
9
+ export declare const UNSUPPORTED_MESSAGE: string;
10
+ /** Seams for tests: how the iframe is made and how messages arrive. */
11
+ export interface FrameDeps {
12
+ document?: Pick<Document, 'createElement' | 'body'>;
13
+ view?: Pick<Window, 'addEventListener' | 'removeEventListener'>;
14
+ setTimeout?: (cb: () => void, ms: number) => unknown;
15
+ clearTimeout?: (h: unknown) => void;
16
+ makeChannel?: () => {
17
+ port1: MessagePort;
18
+ port2: MessagePort;
19
+ };
20
+ }
21
+ export interface FrameHost {
22
+ host: Host;
23
+ client: RpcClient;
24
+ iframe: HTMLIFrameElement;
25
+ /** Fires once when the frame stops answering. */
26
+ onLost(cb: () => void): () => void;
27
+ dispose(): void;
28
+ }
29
+ /** Mount the frame for `frameUrl`, wait for its handshake, hand it a port. */
30
+ export declare function mountFrameHost(url: string, deps?: FrameDeps): Promise<FrameHost>;
@@ -0,0 +1,89 @@
1
+ // Frame mode: the runtime in a hidden iframe on the CDN, over a MessagePort.
2
+ // The frame document carries Document-Isolation-Policy, so it is cross-origin
3
+ // isolated whatever this page's headers say — the only way a page that cannot
4
+ // set COOP/COEP gets a VM. Its handshake says whether that worked; a browser
5
+ // without the header comes up unisolated and this reports
6
+ // `unsupported-browser` rather than a VM that would never boot.
7
+ import { Arm64JSError, FRAME_MESSAGE_KIND, FRAME_OP_CONNECT, PROTOCOL_VERSION, } from '@arm64js/protocol';
8
+ import { RpcClient } from '@arm64js/protocol/rpc-client';
9
+ /**
10
+ * How long the frame gets to say hello before the page gives up on it: a frame
11
+ * that never runs a script (blocked third-party frames, a network failure).
12
+ */
13
+ export const FRAME_HANDSHAKE_TIMEOUT_MS = 8_000;
14
+ /** The message the developer sees when the browser cannot isolate the frame. */
15
+ export const UNSUPPORTED_MESSAGE = 'arm64js needs SharedArrayBuffer, and this browser cannot isolate the VM frame on its own ' +
16
+ '(Document-Isolation-Policy is Chrome/Edge 137+). To run here, serve your page with ' +
17
+ 'Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp ' +
18
+ 'so that crossOriginIsolated is true; the SDK then runs inline in every browser.';
19
+ /** Mount the frame for `frameUrl`, wait for its handshake, hand it a port. */
20
+ export async function mountFrameHost(url, deps = {}) {
21
+ const doc = deps.document ?? document;
22
+ const view = deps.view ?? window;
23
+ const later = deps.setTimeout ?? ((cb, ms) => setTimeout(cb, ms));
24
+ const cancel = deps.clearTimeout ?? ((h) => clearTimeout(h));
25
+ const origin = new URL(url).origin;
26
+ const iframe = doc.createElement('iframe');
27
+ iframe.src = url;
28
+ iframe.setAttribute('title', 'arm64js');
29
+ iframe.setAttribute('aria-hidden', 'true');
30
+ // In the layout and on screen, never display:none: a hidden frame's timers are
31
+ // throttled by the browser. The VM steps in workers, but the frame's main
32
+ // thread still relays.
33
+ iframe.style.cssText = 'position:fixed;bottom:0;right:0;width:1px;height:1px;border:0;opacity:0;pointer-events:none';
34
+ return new Promise((resolve, reject) => {
35
+ let settled = false;
36
+ const finish = (err, value) => {
37
+ if (settled) {
38
+ return;
39
+ }
40
+ settled = true;
41
+ cancel(timer);
42
+ view.removeEventListener('message', onMessage);
43
+ if (err) {
44
+ iframe.remove();
45
+ reject(err);
46
+ }
47
+ else {
48
+ resolve(value);
49
+ }
50
+ };
51
+ const timer = later(() => finish(new Arm64JSError('unsupported-browser', `the VM frame did not answer within ${FRAME_HANDSHAKE_TIMEOUT_MS / 1000}s. ${UNSUPPORTED_MESSAGE}`)), FRAME_HANDSHAKE_TIMEOUT_MS);
52
+ const onMessage = (e) => {
53
+ // Only this frame, from the origin we loaded it from, saying the one thing
54
+ // we expect. Anything else on the page can post to this window.
55
+ if (e.source !== iframe.contentWindow) {
56
+ return;
57
+ }
58
+ if (e.origin !== origin) {
59
+ return;
60
+ }
61
+ const d = e.data;
62
+ if (!d || d.kind !== FRAME_MESSAGE_KIND || d.v !== 1 || typeof d.isolated !== 'boolean') {
63
+ return;
64
+ }
65
+ if (!d.isolated) {
66
+ return finish(new Arm64JSError('unsupported-browser', UNSUPPORTED_MESSAGE));
67
+ }
68
+ if (d.protocol !== PROTOCOL_VERSION) {
69
+ return finish(new Arm64JSError('protocol-mismatch', `this SDK speaks protocol ${PROTOCOL_VERSION} but engine ${String(d.engine)} speaks ${String(d.protocol)}; update the arm64js package`));
70
+ }
71
+ const { port1, port2 } = (deps.makeChannel ?? (() => new MessageChannel()))();
72
+ const connect = { kind: FRAME_MESSAGE_KIND, v: 1, op: FRAME_OP_CONNECT };
73
+ iframe.contentWindow?.postMessage(connect, origin, [port2]);
74
+ const client = new RpcClient(port1);
75
+ finish(null, {
76
+ host: client,
77
+ client,
78
+ iframe,
79
+ onLost: (cb) => client.onLost(cb),
80
+ dispose() {
81
+ client.close();
82
+ iframe.remove();
83
+ },
84
+ });
85
+ };
86
+ view.addEventListener('message', onMessage);
87
+ doc.body.appendChild(iframe);
88
+ });
89
+ }
@@ -0,0 +1,27 @@
1
+ import { type Host, type RuntimeModule } from '@arm64js/protocol';
2
+ import { type FrameDeps } from './frame-host.js';
3
+ /** `'inline'` when the VM runs in this page, `'frame'` when in the CDN's frame. */
4
+ export type HostMode = 'inline' | 'frame';
5
+ /** A runtime ready to boot VMs. */
6
+ export interface LoadedHost {
7
+ host: Host;
8
+ mode: HostMode;
9
+ engine: string;
10
+ dispose(): void;
11
+ }
12
+ /** Seams for tests; a page never passes these. */
13
+ export interface HostDeps {
14
+ isolated?: boolean;
15
+ importRuntime?: (url: string) => Promise<RuntimeModule>;
16
+ mountFrame?: (url: string) => Promise<{
17
+ host: Host;
18
+ onLost(cb: () => void): () => void;
19
+ dispose(): void;
20
+ }>;
21
+ frameDeps?: FrameDeps;
22
+ }
23
+ /**
24
+ * Load `engine` inline when this page is cross-origin isolated, in a frame
25
+ * otherwise. `onLost` fires if a frame stops answering.
26
+ */
27
+ export declare function loadHost(engine: string, deps: HostDeps, onLost: () => void): Promise<LoadedHost>;
package/dist/hosts.js ADDED
@@ -0,0 +1,46 @@
1
+ // Loads the runtime for one engine, in one of two ways. A cross-origin isolated
2
+ // page imports it from the CDN and runs the VM itself. Any other page gets a
3
+ // hidden CDN frame that runs it.
4
+ import { Arm64JSError, PROTOCOL_VERSION } from '@arm64js/protocol';
5
+ import { mountFrameHost } from './frame-host.js';
6
+ import { frameUrl, runtimeModuleUrl } from './loader.js';
7
+ /**
8
+ * Load `engine` inline when this page is cross-origin isolated, in a frame
9
+ * otherwise. `onLost` fires if a frame stops answering.
10
+ */
11
+ export async function loadHost(engine, deps, onLost) {
12
+ const isolated = deps.isolated ?? Boolean(globalThis.crossOriginIsolated);
13
+ return isolated ? loadInlineHost(engine, deps) : loadFrameHost(engine, deps, onLost);
14
+ }
15
+ async function loadInlineHost(engine, deps) {
16
+ const importer = deps.importRuntime ??
17
+ (async (url) => import(/* @vite-ignore */ /* webpackIgnore: true */ url));
18
+ let mod;
19
+ try {
20
+ mod = await importer(runtimeModuleUrl(engine));
21
+ }
22
+ catch (e) {
23
+ throw new Arm64JSError('boot-failed', `could not load engine ${engine} from the CDN: ${String(e)}`);
24
+ }
25
+ if (mod.protocol !== PROTOCOL_VERSION) {
26
+ throw new Arm64JSError('protocol-mismatch', `this SDK speaks protocol ${PROTOCOL_VERSION} but engine ${engine} speaks ${String(mod.protocol)}; update the arm64js package`);
27
+ }
28
+ return { host: mod.createHost(), mode: 'inline', engine, dispose() { } };
29
+ }
30
+ async function loadFrameHost(engine, deps, onLost) {
31
+ const mounter = deps.mountFrame ?? (async (url) => mountFrameHost(url, deps.frameDeps));
32
+ const frame = await mounter(frameUrl(engine));
33
+ frame.onLost(() => {
34
+ // The next call builds a new frame. Tear this one down first: only its
35
+ // main thread stopped answering, and the workers behind it keep stepping
36
+ // a guest with no handle left.
37
+ onLost();
38
+ try {
39
+ frame.dispose();
40
+ }
41
+ catch {
42
+ // A frame already gone from the document has nothing to remove.
43
+ }
44
+ });
45
+ return { host: frame.host, mode: 'frame', engine, dispose: () => frame.dispose() };
46
+ }
@@ -0,0 +1,10 @@
1
+ import { type Arm64JSApi } from './api.js';
2
+ export type { Arm64JSApi } from './api.js';
3
+ export type { HostMode } from './hosts.js';
4
+ export type { MountSource } from './mount.js';
5
+ export { VERSION as version } from './version.js';
6
+ export { Vm, type FileData, type Mount } from './vm.js';
7
+ export type { Arm64JSErrorCode, BootOptions, BootProgress, ExecOptions, ExecResult, OutputInfo, ReadFileOptions, SnapshotInfo, SnapshotOptions, StorageStatus, VmExit, WriteFileOptions, } from '@arm64js/protocol';
8
+ export { Arm64JSError } from '@arm64js/protocol';
9
+ export declare const Arm64JS: Arm64JSApi;
10
+ export default Arm64JS;
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ import { createArm64JS } from './api.js';
2
+ export { VERSION as version } from './version.js';
3
+ export { Vm } from './vm.js';
4
+ export { Arm64JSError } from '@arm64js/protocol';
5
+ export const Arm64JS = createArm64JS();
6
+ export default Arm64JS;
@@ -0,0 +1,10 @@
1
+ /** The engine this package runs: its own major.minor. */
2
+ export declare function packageEngine(): string;
3
+ /** `'0.11'` as `[0, 11]`, or `null` for anything that is not an exact engine tag. */
4
+ export declare function engineParts(tag: string): [number, number] | null;
5
+ /** Whether engine `a` is newer than engine `b`. False unless both are exact tags. */
6
+ export declare function isNewerEngine(a: string, b: string): boolean;
7
+ /** The immutable runtime directory for an exact `X.Y`, with its trailing slash. */
8
+ export declare function runtimeDir(version: string): string;
9
+ export declare function runtimeModuleUrl(version: string): string;
10
+ export declare function frameUrl(version: string): string;