@kupcad/wasm 0.0.0-stage → 0.1.1

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/src/kupcad.js ADDED
@@ -0,0 +1,260 @@
1
+ // The core in this thread: the module instantiated once, its exports given a typed face. Every
2
+ // string and buffer goes in through `wasm_alloc` and is freed after the call; every result is
3
+ // copied out and freed (`wasm_free`), and an export's null result is the core's refusal, its
4
+ // message read from `get_last_error`.
5
+
6
+ import { isCompatible, PROTOCOL_VERSION, ProtocolError, readFrame } from '@kupcad/protocol';
7
+ import { KupcadCrashError, KupcadError } from './errors.js';
8
+ import { packFiles } from './files.js';
9
+ import { loadModule } from './load.js';
10
+ import { createWasi, WasiExit } from './wasi.js';
11
+
12
+ /** @import { Finding, Preview, UiSchema, VersionInfo } from '@kupcad/protocol/types' */
13
+ /** @import { BuildOptions, CallOptions, CreateOptions, KupcadApi, KupcadExports, PreviewOptions } from './types.js' */
14
+
15
+ /** The module this package ships, built by core/build.zig and copied in by `pnpm build`. */
16
+ const DEFAULT_WASM = new URL('../dist/kupcad.wasm', import.meta.url);
17
+ /** A `usize` of wasm32: the slot an export writes its result's length to. */
18
+ const USIZE_BYTES = 4;
19
+ /** The longest message get_last_error holds (core/src/wasm.zig's MAX_ERROR_MSG_LEN). */
20
+ const MAX_ERROR_BYTES = 1024;
21
+ const DEFAULT_FORMAT = 'glb';
22
+ /** What a call without files gives the core. */
23
+ const NO_FILES = new Uint8Array();
24
+
25
+ const encoder = new TextEncoder();
26
+ const decoder = new TextDecoder();
27
+
28
+ /**
29
+ * A wasm32 pointer an export returned: a u32 that JavaScript gets as an i32, so an address
30
+ * past 2 GiB comes negative, as does an empty block's (0xFFFFFFFF, which `wasm_alloc(0)` gives).
31
+ * @param {number} ptr
32
+ */
33
+ const address = (ptr) => ptr >>> 0;
34
+
35
+ /**
36
+ * The core, instantiated in this thread. A long build blocks it: a page runs the core in a
37
+ * worker (`createKupcadWorker`).
38
+ * @param {CreateOptions} [options]
39
+ * @returns {Promise<Kupcad>}
40
+ * @throws {ProtocolError} when the module speaks a protocol version this package doesn't read
41
+ */
42
+ export async function createKupcad(options = {}) {
43
+ const module = await loadModule(options.wasm ?? DEFAULT_WASM);
44
+ const wasi = createWasi(options.log ?? ((line) => console.debug(line)));
45
+ const instance = await WebAssembly.instantiate(module, {
46
+ wasi_snapshot_preview1: wasi.imports(module),
47
+ });
48
+ const exports = /** @type {KupcadExports} */ (/** @type {unknown} */ (instance.exports));
49
+ wasi.bind(exports.memory);
50
+ // oxlint-disable-next-line no-underscore-dangle -- the WASI reactor's initializer, by that name
51
+ exports._initialize();
52
+ return Kupcad.fromExports(exports);
53
+ }
54
+
55
+ /** @implements {KupcadApi} */
56
+ export class Kupcad {
57
+ /**
58
+ * The core over a module a host instantiated itself (its own WASI, its own memory), once it
59
+ * is initialized.
60
+ * @param {KupcadExports} exports
61
+ * @returns {Promise<Kupcad>}
62
+ * @throws {ProtocolError} when the module speaks a protocol version this package doesn't read
63
+ */
64
+ static async fromExports(exports) {
65
+ const kupcad = new Kupcad(exports);
66
+ const { version, protocolVersion } = await kupcad.version();
67
+ if (!isCompatible(protocolVersion)) {
68
+ throw new ProtocolError(
69
+ `kupcad.wasm ${version} speaks protocol version ${protocolVersion}; this package reads version ${PROTOCOL_VERSION}`,
70
+ );
71
+ }
72
+ return kupcad;
73
+ }
74
+
75
+ /** @type {KupcadExports} */
76
+ #exports;
77
+ /** Where every export writes its result's length. */
78
+ #outLen;
79
+ /** Why the module can't be used again, once it stopped. @type {KupcadCrashError | null} */
80
+ #crashed = null;
81
+
82
+ /** @param {KupcadExports} exports an instantiated, initialized kupcad.wasm's */
83
+ constructor(exports) {
84
+ this.#exports = exports;
85
+ this.#outLen = address(exports.wasm_alloc(USIZE_BYTES));
86
+ if (this.#outLen === 0) throw new KupcadError('kupcad.wasm has no memory to start in');
87
+ }
88
+
89
+ /** The module's linear memory, in bytes: it grows as builds need, and stays grown. */
90
+ get memoryBytes() {
91
+ return this.#exports.memory.buffer.byteLength;
92
+ }
93
+
94
+ /**
95
+ * @param {string} source
96
+ * @param {PreviewOptions} [options]
97
+ * @returns {Promise<Preview>}
98
+ */
99
+ async preview(source, options = {}) {
100
+ options.signal?.throwIfAborted();
101
+ const params = options.params ? JSON.stringify(options.params) : '';
102
+ const files = options.files ? packFiles(options.files) : NO_FILES;
103
+ return this.#run(() =>
104
+ readFrame(
105
+ this.#withInputs([source, params, files], ([s, p, f]) =>
106
+ this.#take(
107
+ this.#exports.preview_wasm(s.ptr, s.len, p.ptr, p.len, f.ptr, f.len, this.#outLen),
108
+ ),
109
+ ),
110
+ ),
111
+ );
112
+ }
113
+
114
+ /**
115
+ * @param {string} source
116
+ * @param {BuildOptions} [options]
117
+ * @returns {Promise<Uint8Array>}
118
+ */
119
+ async build(source, options = {}) {
120
+ options.signal?.throwIfAborted();
121
+ const params = options.params ? JSON.stringify(options.params) : '';
122
+ const files = options.files ? packFiles(options.files) : NO_FILES;
123
+ const format = options.format ?? DEFAULT_FORMAT;
124
+ const draco = options.draco ? 1 : 0;
125
+ return this.#run(() =>
126
+ this.#withInputs([source, params, files, format], ([s, p, fs, f]) =>
127
+ this.#take(
128
+ this.#exports.build_model_wasm(
129
+ s.ptr,
130
+ s.len,
131
+ p.ptr,
132
+ p.len,
133
+ fs.ptr,
134
+ fs.len,
135
+ f.ptr,
136
+ f.len,
137
+ draco,
138
+ this.#outLen,
139
+ ),
140
+ ),
141
+ ),
142
+ );
143
+ }
144
+
145
+ /**
146
+ * @param {string} source
147
+ * @param {CallOptions} [options]
148
+ * @returns {Promise<Finding[]>}
149
+ */
150
+ async check(source, options = {}) {
151
+ options.signal?.throwIfAborted();
152
+ return this.#run(() => JSON.parse(this.#text(this.#exports.check_code_wasm, source)));
153
+ }
154
+
155
+ /**
156
+ * @param {string} source
157
+ * @param {CallOptions} [options]
158
+ * @returns {Promise<string>}
159
+ */
160
+ async format(source, options = {}) {
161
+ options.signal?.throwIfAborted();
162
+ return this.#run(() => this.#text(this.#exports.format_code_wasm, source));
163
+ }
164
+
165
+ /**
166
+ * @param {string} source
167
+ * @param {CallOptions} [options]
168
+ * @returns {Promise<UiSchema>}
169
+ */
170
+ async params(source, options = {}) {
171
+ options.signal?.throwIfAborted();
172
+ return this.#run(() => JSON.parse(this.#text(this.#exports.extract_params_wasm, source)));
173
+ }
174
+
175
+ /** @returns {Promise<VersionInfo>} */
176
+ async version() {
177
+ return this.#run(() =>
178
+ JSON.parse(decoder.decode(this.#take(this.#exports.version_wasm(this.#outLen)))),
179
+ );
180
+ }
181
+
182
+ /**
183
+ * Runs `work` unless the module stopped; a trap or an exit in it stops the module for good.
184
+ * @template T
185
+ * @param {() => T} work
186
+ * @returns {T}
187
+ */
188
+ #run(work) {
189
+ if (this.#crashed) throw this.#crashed;
190
+ try {
191
+ return work();
192
+ } catch (err) {
193
+ if (!(err instanceof WebAssembly.RuntimeError || err instanceof WasiExit)) throw err;
194
+ this.#crashed = new KupcadCrashError(`kupcad.wasm stopped: ${err.message}`, { cause: err });
195
+ throw this.#crashed;
196
+ }
197
+ }
198
+
199
+ /**
200
+ * The text an export of one source gives (check, format, params).
201
+ * @param {(source: number, sourceLen: number, outLen: number) => number} exported
202
+ * @param {string} source
203
+ * @returns {string}
204
+ */
205
+ #text(exported, source) {
206
+ return decoder.decode(
207
+ this.#withInputs([source], ([s]) => this.#take(exported(s.ptr, s.len, this.#outLen))),
208
+ );
209
+ }
210
+
211
+ /**
212
+ * Copies `inputs` into the module's memory for `call`, and frees them after.
213
+ * @template T
214
+ * @param {(string | Uint8Array)[]} inputs
215
+ * @param {(placed: { ptr: number, len: number }[]) => T} call
216
+ * @returns {T}
217
+ */
218
+ #withInputs(inputs, call) {
219
+ /** @type {{ ptr: number, len: number }[]} */
220
+ const placed = [];
221
+ try {
222
+ for (const input of inputs) {
223
+ const bytes = typeof input === 'string' ? encoder.encode(input) : input;
224
+ const ptr = address(this.#exports.wasm_alloc(bytes.byteLength));
225
+ if (ptr === 0)
226
+ throw new KupcadError(`kupcad.wasm has no room for ${bytes.byteLength} bytes`);
227
+ placed.push({ ptr, len: bytes.byteLength });
228
+ if (bytes.byteLength > 0) {
229
+ new Uint8Array(this.#exports.memory.buffer, ptr, bytes.byteLength).set(bytes);
230
+ }
231
+ }
232
+ return call(placed);
233
+ } finally {
234
+ for (const { ptr, len } of placed) this.#exports.wasm_free(ptr, len);
235
+ }
236
+ }
237
+
238
+ /**
239
+ * An export's result, copied out of the module's memory and freed there; its null is the
240
+ * core's refusal.
241
+ * @param {number} result
242
+ * @returns {Uint8Array}
243
+ */
244
+ #take(result) {
245
+ const ptr = address(result);
246
+ if (ptr === 0) throw new KupcadError(this.#lastError());
247
+ const len = new DataView(this.#exports.memory.buffer).getUint32(this.#outLen, true);
248
+ const bytes =
249
+ len > 0 ? new Uint8Array(this.#exports.memory.buffer, ptr, len).slice() : new Uint8Array();
250
+ this.#exports.wasm_free(ptr, len);
251
+ return bytes;
252
+ }
253
+
254
+ #lastError() {
255
+ const at = address(this.#exports.get_last_error());
256
+ const bytes = new Uint8Array(this.#exports.memory.buffer, at, MAX_ERROR_BYTES);
257
+ const end = bytes.indexOf(0);
258
+ return decoder.decode(bytes.subarray(0, end === -1 ? MAX_ERROR_BYTES : end));
259
+ }
260
+ }
package/src/load.js ADDED
@@ -0,0 +1,36 @@
1
+ // The compiled module from what a host gives: a compiled WebAssembly.Module, its bytes, a
2
+ // fetch Response, or a URL (a `file:` one read from the disk in Node).
3
+
4
+ import { readFile } from '#read-file';
5
+
6
+ /** @import { WasmSource } from './types.js' */
7
+
8
+ /**
9
+ * @param {WasmSource} source
10
+ * @returns {Promise<WebAssembly.Module>}
11
+ */
12
+ export async function loadModule(source) {
13
+ if (source instanceof WebAssembly.Module) return source;
14
+ if (source instanceof ArrayBuffer || ArrayBuffer.isView(source))
15
+ return WebAssembly.compile(source);
16
+ if (source instanceof Response) return compileResponse(source);
17
+ const url = new URL(source, globalThis.location?.href);
18
+ if (url.protocol === 'file:') return WebAssembly.compile(await readFile(url));
19
+ return compileResponse(await fetch(url));
20
+ }
21
+
22
+ /**
23
+ * Compiles while it downloads when the server says it is wasm; compileStreaming refuses
24
+ * another content type, so then the bytes are compiled once they are all there.
25
+ * @param {Response} response
26
+ * @returns {Promise<WebAssembly.Module>}
27
+ */
28
+ async function compileResponse(response) {
29
+ if (!response.ok) {
30
+ throw new Error(`kupcad.wasm: ${response.status} ${response.statusText} from ${response.url}`);
31
+ }
32
+ if (response.headers.get('content-type')?.startsWith('application/wasm')) {
33
+ return WebAssembly.compileStreaming(response);
34
+ }
35
+ return WebAssembly.compile(await response.arrayBuffer());
36
+ }
@@ -0,0 +1,12 @@
1
+ // A browser can't read a `file:` URL (#read-file); a page gives the module by an http(s) URL,
2
+ // its bytes or a compiled WebAssembly.Module.
3
+
4
+ /**
5
+ * @param {URL} url
6
+ * @returns {Promise<Uint8Array<ArrayBuffer>>}
7
+ */
8
+ export async function readFile(url) {
9
+ throw new Error(
10
+ `kupcad.wasm: a browser can't read ${url.href}; give the module by URL or as bytes`,
11
+ );
12
+ }
@@ -0,0 +1,11 @@
1
+ // Node's reader of a `file:` URL, for a module loaded from the disk (#read-file).
2
+
3
+ import { readFile as readFromDisk } from 'node:fs/promises';
4
+
5
+ /**
6
+ * @param {URL} url
7
+ * @returns {Promise<Uint8Array<ArrayBuffer>>}
8
+ */
9
+ export async function readFile(url) {
10
+ return readFromDisk(url);
11
+ }
package/src/serve.js ADDED
@@ -0,0 +1,84 @@
1
+ // The worker's side: the core instantiated in the worker, answering the client's calls on
2
+ // `port` in the order they come. A result's buffer is transferred, not copied.
3
+
4
+ import { createKupcad } from './kupcad.js';
5
+ import { KupcadCrashError } from './errors.js';
6
+
7
+ /** @import { Preview } from '@kupcad/protocol/types' */
8
+ /** @import { CreateOptions, FromWorker, KupcadApi, Method, PortLike, SentError, ToWorker } from './types.js' */
9
+
10
+ /** @type {Record<Method, (kupcad: KupcadApi, args: any[]) => Promise<unknown>>} */
11
+ const CALLS = {
12
+ preview: (kupcad, [source, options]) => kupcad.preview(source, options),
13
+ build: (kupcad, [source, options]) => kupcad.build(source, options),
14
+ check: (kupcad, [source]) => kupcad.check(source),
15
+ format: (kupcad, [source]) => kupcad.format(source),
16
+ params: (kupcad, [source]) => kupcad.params(source),
17
+ version: (kupcad) => kupcad.version(),
18
+ };
19
+
20
+ /**
21
+ * Serves the core on `port`: an `init` message makes it (`create`, the module given), and each
22
+ * `call` is answered with a `reply`. The core's log goes as `log` messages.
23
+ * @param {PortLike} port
24
+ * @param {(options: CreateOptions) => Promise<KupcadApi>} [create]
25
+ */
26
+ export function serve(port, create = createKupcad) {
27
+ /** @type {Promise<KupcadApi> | null} */
28
+ let kupcad = null;
29
+ /** @param {FromWorker} message @param {Transferable[]} [transfer] */
30
+ const send = (message, transfer = []) => port.postMessage(message, transfer);
31
+
32
+ /** @param {Extract<ToWorker, { type: 'init' }>} init */
33
+ const start = ({ wasm }) => create({ wasm, log: (line) => send({ type: 'log', line }) });
34
+
35
+ /** @param {Extract<ToWorker, { type: 'call' }>} call */
36
+ const answer = async ({ id, method, args }) => {
37
+ /** @type {KupcadApi} */
38
+ let ready;
39
+ try {
40
+ if (!kupcad) throw new Error('the kupcad worker was called before it was given its module');
41
+ ready = await kupcad;
42
+ } catch (err) {
43
+ send({ type: 'reply', id, ok: false, error: sent(err, true) });
44
+ return;
45
+ }
46
+ try {
47
+ const value = await CALLS[method](ready, args);
48
+ send({ type: 'reply', id, ok: true, value }, transferOf(value));
49
+ } catch (err) {
50
+ send({ type: 'reply', id, ok: false, error: sent(err, err instanceof KupcadCrashError) });
51
+ }
52
+ };
53
+
54
+ port.addEventListener('message', ({ data }) => {
55
+ if (data.type === 'init') {
56
+ kupcad = start(data);
57
+ // A core that didn't start is told to each call, which awaits it
58
+ kupcad.catch(() => {});
59
+ return;
60
+ }
61
+ void answer(data);
62
+ });
63
+ }
64
+
65
+ /**
66
+ * @param {unknown} err
67
+ * @param {boolean} fatal
68
+ * @returns {SentError}
69
+ */
70
+ function sent(err, fatal) {
71
+ if (err instanceof Error) return { name: err.name, message: err.message, fatal };
72
+ return { name: 'Error', message: String(err), fatal };
73
+ }
74
+
75
+ /**
76
+ * The buffers a result holds, to transfer: a build's bytes, a preview's GLB.
77
+ * @param {unknown} value
78
+ * @returns {Transferable[]}
79
+ */
80
+ function transferOf(value) {
81
+ if (value instanceof Uint8Array) return [value.buffer];
82
+ const model = /** @type {Partial<Preview> | null} */ (value)?.model;
83
+ return model?.kind === '3d' ? [model.glb.buffer] : [];
84
+ }
package/src/testing.js ADDED
@@ -0,0 +1,39 @@
1
+ // For the tests of a package that shows what the core makes: the real module, made once a test
2
+ // process and silent, and what it gives of a script. A script that fails fails the test.
3
+
4
+ import { createKupcad } from './kupcad.js';
5
+
6
+ /** @import { Model, UiSchema } from '@kupcad/protocol/types' */
7
+ /** @import { Kupcad } from './kupcad.js' */
8
+
9
+ /** @type {Promise<Kupcad> | null} */
10
+ let core = null;
11
+
12
+ /**
13
+ * The core, made on first use, its log dropped.
14
+ * @returns {Promise<Kupcad>}
15
+ */
16
+ export function testCore() {
17
+ core ??= createKupcad({ log: () => {} });
18
+ return core;
19
+ }
20
+
21
+ /**
22
+ * The model the core's preview of `source` shows.
23
+ * @param {string} source
24
+ * @returns {Promise<Model>}
25
+ */
26
+ export async function modelOf(source) {
27
+ const { header, model } = await (await testCore()).preview(source);
28
+ if (header.error) throw new Error(`the test's script failed: ${header.error.message}`);
29
+ return model;
30
+ }
31
+
32
+ /**
33
+ * The schema of `source`'s parameters.
34
+ * @param {string} source
35
+ * @returns {Promise<UiSchema>}
36
+ */
37
+ export async function schemaOf(source) {
38
+ return (await testCore()).params(source);
39
+ }
package/src/types.d.ts ADDED
@@ -0,0 +1,141 @@
1
+ // The shapes of @kupcad/wasm's API. What the core gives back is @kupcad/protocol's.
2
+
3
+ import type {
4
+ ExportFormat,
5
+ Finding,
6
+ ParamValues,
7
+ Preview,
8
+ UiSchema,
9
+ VersionInfo,
10
+ } from '@kupcad/protocol/types';
11
+
12
+ /** Where the module comes from: compiled, its bytes, a fetch Response, or a URL. */
13
+ export type WasmSource = WebAssembly.Module | BufferSource | Response | URL | string;
14
+
15
+ /** What a call may be stopped by. */
16
+ export interface CallOptions {
17
+ signal?: AbortSignal;
18
+ }
19
+
20
+ export interface PreviewOptions extends CallOptions {
21
+ /** The parameters' values; those not given take their defaults. */
22
+ params?: ParamValues;
23
+ /**
24
+ * The files the script reads, by the paths it names them by (`import { } from "./lib.kup"`
25
+ * reads `lib.kup`, `import_json("data/size.json")` reads `data/size.json`): a module it
26
+ * imports, data it reads. The core keeps them for this call alone.
27
+ */
28
+ files?: Record<string, Uint8Array | string>;
29
+ }
30
+
31
+ export interface BuildOptions extends PreviewOptions {
32
+ /** Default 'glb'. */
33
+ format?: ExportFormat;
34
+ /** Draco-compressed meshes, for glb and gltf. */
35
+ draco?: boolean;
36
+ }
37
+
38
+ export interface CreateOptions {
39
+ /** The module; by default the kupcad.wasm this package ships. */
40
+ wasm?: WasmSource;
41
+ /** Where the module's log goes, a line at a time (a failure is thrown too); by default console.debug. */
42
+ log?: (line: string) => void;
43
+ }
44
+
45
+ /** What both `createKupcad` and `createKupcadWorker` give. */
46
+ export interface KupcadApi {
47
+ /** The script's preview frame: a run that fails, or values refused, is a frame saying why. */
48
+ preview(source: string, options?: PreviewOptions): Promise<Preview>;
49
+ /** The script's model written as `format`; a KupcadError says why it couldn't be. */
50
+ build(source: string, options?: BuildOptions): Promise<Uint8Array>;
51
+ /** `kupcad check`'s findings, syntax errors among them. */
52
+ check(source: string, options?: CallOptions): Promise<Finding[]>;
53
+ /** The source as `kupcad fmt` writes it; a KupcadError for one that doesn't parse. */
54
+ format(source: string, options?: CallOptions): Promise<string>;
55
+ /** The parameters' schema, as `kupcad doc` gives it, without running the script. */
56
+ params(source: string, options?: CallOptions): Promise<UiSchema>;
57
+ /** What the module says of itself. */
58
+ version(): Promise<VersionInfo>;
59
+ }
60
+
61
+ /** The exports of kupcad.wasm (core/src/wasm.zig). Pointers and lengths are byte offsets. */
62
+ export interface KupcadExports {
63
+ memory: WebAssembly.Memory;
64
+ _initialize(): void;
65
+ wasm_alloc(len: number): number;
66
+ wasm_free(ptr: number, len: number): void;
67
+ get_last_error(): number;
68
+ preview_wasm(
69
+ source: number,
70
+ sourceLen: number,
71
+ params: number,
72
+ paramsLen: number,
73
+ files: number,
74
+ filesLen: number,
75
+ outLen: number,
76
+ ): number;
77
+ build_model_wasm(
78
+ source: number,
79
+ sourceLen: number,
80
+ params: number,
81
+ paramsLen: number,
82
+ files: number,
83
+ filesLen: number,
84
+ format: number,
85
+ formatLen: number,
86
+ draco: number,
87
+ outLen: number,
88
+ ): number;
89
+ check_code_wasm(source: number, sourceLen: number, outLen: number): number;
90
+ format_code_wasm(source: number, sourceLen: number, outLen: number): number;
91
+ extract_params_wasm(source: number, sourceLen: number, outLen: number): number;
92
+ version_wasm(outLen: number): number;
93
+ }
94
+
95
+ /** Where a worker's module comes from: what can be posted to it (not a Response). */
96
+ export type WorkerWasmSource = Exclude<WasmSource, Response>;
97
+
98
+ export interface WorkerOptions extends Omit<CreateOptions, 'wasm'> {
99
+ /** The module; by default the kupcad.wasm this package ships. A relative URL is the page's. */
100
+ wasm?: WorkerWasmSource;
101
+ /** Starts a worker running src/worker.js; by default a module Web Worker. */
102
+ worker?: () => WorkerLike;
103
+ }
104
+
105
+ /** What a client calls the worker's core with: the methods of KupcadApi. */
106
+ export type Method = 'preview' | 'build' | 'check' | 'format' | 'params' | 'version';
107
+
108
+ /** A failure, as it crosses to the client: `fatal` when the worker's core can't go on. */
109
+ export interface SentError {
110
+ name: string;
111
+ message: string;
112
+ fatal: boolean;
113
+ }
114
+
115
+ export type ToWorker =
116
+ | {
117
+ type: 'init';
118
+ /** A URL as its href. */
119
+ wasm: Exclude<WorkerWasmSource, URL> | undefined;
120
+ }
121
+ | { type: 'call'; id: number; method: Method; args: unknown[] };
122
+
123
+ export type FromWorker =
124
+ | { type: 'reply'; id: number; ok: true; value: unknown }
125
+ | { type: 'reply'; id: number; ok: false; error: SentError }
126
+ | { type: 'log'; line: string };
127
+
128
+ /** The client's end of a worker: a Web Worker, or a stand-in in tests. */
129
+ export interface WorkerLike {
130
+ postMessage(message: ToWorker): void;
131
+ addEventListener(type: 'message', listener: (event: MessageEvent<FromWorker>) => void): void;
132
+ addEventListener(type: 'error', listener: (event: ErrorEvent) => void): void;
133
+ addEventListener(type: 'messageerror', listener: (event: MessageEvent) => void): void;
134
+ terminate(): void;
135
+ }
136
+
137
+ /** The worker's end: its global scope, or a MessagePort in tests. */
138
+ export interface PortLike {
139
+ postMessage(message: FromWorker, transfer?: Transferable[]): void;
140
+ addEventListener(type: 'message', listener: (event: MessageEvent<ToWorker>) => void): void;
141
+ }