@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/LICENSE +661 -0
- package/README.md +73 -2
- package/dist/kupcad.wasm +0 -0
- package/package.json +43 -3
- package/src/client.js +291 -0
- package/src/errors.js +25 -0
- package/src/files.js +33 -0
- package/src/index.js +6 -0
- package/src/kupcad.js +260 -0
- package/src/load.js +36 -0
- package/src/read-file.browser.js +12 -0
- package/src/read-file.node.js +11 -0
- package/src/serve.js +84 -0
- package/src/testing.js +39 -0
- package/src/types.d.ts +141 -0
- package/src/wasi.js +127 -0
- package/src/worker.js +8 -0
package/README.md
CHANGED
|
@@ -1,3 +1,74 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @kupcad/wasm
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The KupCAD core compiled to WebAssembly: preview, build, check, format and read the parameters of
|
|
4
|
+
a script, in a page or in Node, with no server. The shapes it gives back are
|
|
5
|
+
[@kupcad/protocol](../kupcad-protocol)'s.
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import { createKupcadWorker } from '@kupcad/wasm';
|
|
9
|
+
|
|
10
|
+
const kupcad = createKupcadWorker(); // the core in a Web Worker: a long build doesn't freeze the page
|
|
11
|
+
|
|
12
|
+
const { header, model } = await kupcad.preview(source, { params: { width: 40 } });
|
|
13
|
+
// model: { kind: '3d', glb, parts } | { kind: '2d', svg } | { kind: 'none' }
|
|
14
|
+
// header.error: why the run failed, where, and the parameter whose value was refused
|
|
15
|
+
|
|
16
|
+
const stl = await kupcad.build(source, { format: 'stl' }); // Uint8Array
|
|
17
|
+
const findings = await kupcad.check(source);
|
|
18
|
+
const formatted = await kupcad.format(source);
|
|
19
|
+
const schema = await kupcad.params(source); // the parameter panel's controls, without a run
|
|
20
|
+
// The files the script reads, by the paths it names: a module it imports, data (an STL, a JSON)
|
|
21
|
+
await kupcad.build(source, { files: { 'lib.kup': libSource, 'plate.dxf': bytes } });
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## In a worker, or in this thread
|
|
25
|
+
|
|
26
|
+
* `createKupcadWorker(options)` runs the core in a module Web Worker. Calls run one at a time,
|
|
27
|
+
the next waiting its turn. A call's `signal` (an `AbortSignal`) drops it while it waits, and
|
|
28
|
+
stops the worker while it runs: a script can't be stopped inside the module, so the worker is
|
|
29
|
+
ended and a new one starts with the next call. A worker whose core stopped (a trap) is replaced
|
|
30
|
+
the same way. A
|
|
31
|
+
call that can't be sent (the worker doesn't start, or its arguments can't be posted to one) is
|
|
32
|
+
refused with why, and the next goes. `terminate()` ends it for good.
|
|
33
|
+
* `createKupcad(options)` instantiates the core in this thread: the same calls, for Node, tests
|
|
34
|
+
and a worker of the host's own. `Kupcad.fromExports(exports)` takes a module the host
|
|
35
|
+
instantiated itself.
|
|
36
|
+
|
|
37
|
+
Both take `wasm` (the module: a URL, its bytes or a compiled `WebAssembly.Module`, and for
|
|
38
|
+
`createKupcad` a fetch `Response` too, which can't be posted to a worker; by default the
|
|
39
|
+
`kupcad.wasm` this package ships) and `log` (where the module's log goes, a line at a time; by
|
|
40
|
+
default `console.debug`). A worker is given a relative URL resolved against the page, as the
|
|
41
|
+
page means it.
|
|
42
|
+
|
|
43
|
+
## What a call can fail with
|
|
44
|
+
|
|
45
|
+
* `KupcadError`: the core refused, with its message: a script that doesn't parse
|
|
46
|
+
(`1:6: Expected 'r_paren' ...`) or run (`RuntimeError: boom`), values it refused
|
|
47
|
+
(`parameters: ...`), a format it doesn't write. A preview doesn't throw for these: its frame
|
|
48
|
+
says why.
|
|
49
|
+
* `KupcadCrashError`: the module stopped (a trap, or an exit), or the worker failed (its script
|
|
50
|
+
didn't load, or it sent what couldn't be read). A worker is replaced on the next call; a module
|
|
51
|
+
of `createKupcad` is made again.
|
|
52
|
+
* `ProtocolError` (@kupcad/protocol's): the module speaks a protocol version this package doesn't
|
|
53
|
+
read.
|
|
54
|
+
|
|
55
|
+
## Inside
|
|
56
|
+
|
|
57
|
+
The module asks its host for WASI. A browser has none and Node's would give it a file system, so
|
|
58
|
+
the package answers it itself (`src/wasi.js`): the host's clocks and randomness, the log, and no
|
|
59
|
+
files. A script reads the files its call brings (`files`, by the paths the script names them):
|
|
60
|
+
they are packed into one buffer (`src/files.js`, as `core/src/vfs/packed.zig` reads it) and
|
|
61
|
+
kept in an in-memory file system of that call alone, so no script sees another's files and a
|
|
62
|
+
script's exports go nowhere. A build runs under the core's interactive limits (instructions,
|
|
63
|
+
memory), as a preview does: a runaway script stops with an error instead of hanging the worker. Every string and buffer is copied in and freed after the call;
|
|
64
|
+
every result is copied out and freed. `pnpm build` copies the module from the core's
|
|
65
|
+
build (`pnpm build:core:wasm` at the root) into `dist/`, which the package ships; its version is
|
|
66
|
+
the core's, which the tests hold it to.
|
|
67
|
+
|
|
68
|
+
The threaded build of the core (`-Dwasm_threads`) isn't shipped yet: it needs a host that runs
|
|
69
|
+
its threads in workers and pages served cross-origin isolated.
|
|
70
|
+
|
|
71
|
+
`@kupcad/wasm/types` has the API's types (`KupcadApi`, the options, the worker's messages).
|
|
72
|
+
`@kupcad/wasm/testing` is for the tests of a package that shows what the core makes: the real
|
|
73
|
+
module, made once a test process (`testCore`), and `modelOf` and `schemaOf` of a script, a
|
|
74
|
+
script that fails failing the test.
|
package/dist/kupcad.wasm
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kupcad/wasm",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "The KupCAD core compiled to WebAssembly: preview, build, check, format and read the parameters of a script in the browser or Node, directly or in a Web Worker.",
|
|
5
|
+
"license": "AGPL-3.0-only",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/kupcad/kupcad.git",
|
|
9
|
+
"directory": "packages/kupcad-wasm"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src",
|
|
13
|
+
"!src/**/*.test.js",
|
|
14
|
+
"dist/kupcad.wasm"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"imports": {
|
|
19
|
+
"#read-file": {
|
|
20
|
+
"node": "./src/read-file.node.js",
|
|
21
|
+
"default": "./src/read-file.browser.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"exports": {
|
|
25
|
+
".": "./src/index.js",
|
|
26
|
+
"./types": {
|
|
27
|
+
"types": "./src/types.d.ts"
|
|
28
|
+
},
|
|
29
|
+
"./testing": "./src/testing.js"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@kupcad/protocol": "0.1.1"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^26.6.5",
|
|
39
|
+
"vitest": "^5.0.5"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "node scripts/copy-wasm.js",
|
|
43
|
+
"check": "tsc -p jsconfig.json",
|
|
44
|
+
"test": "vitest run"
|
|
45
|
+
}
|
|
6
46
|
}
|
package/src/client.js
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// The core in a Web Worker, with createKupcad's API: a long build runs there and the page goes
|
|
2
|
+
// on. Calls run one at a time, the next waiting its turn. A call's AbortSignal drops it while
|
|
3
|
+
// it waits, and stops the worker while it runs: the worker is replaced on the next call, as
|
|
4
|
+
// one that crashed is.
|
|
5
|
+
|
|
6
|
+
import { ProtocolError } from '@kupcad/protocol';
|
|
7
|
+
import { KupcadCrashError, KupcadError } from './errors.js';
|
|
8
|
+
|
|
9
|
+
/** @import { Finding, Preview, UiSchema, VersionInfo } from '@kupcad/protocol/types' */
|
|
10
|
+
/** @import { BuildOptions, CallOptions, FromWorker, KupcadApi, Method, PreviewOptions, SentError, ToWorker, WorkerLike, WorkerOptions, WorkerWasmSource } from './types.js' */
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {object} Call
|
|
14
|
+
* @property {number} id
|
|
15
|
+
* @property {Method} method
|
|
16
|
+
* @property {unknown[]} args
|
|
17
|
+
* @property {(value: any) => void} resolve
|
|
18
|
+
* @property {(reason: unknown) => void} reject
|
|
19
|
+
* @property {AbortSignal | undefined} signal
|
|
20
|
+
* @property {() => void} onAbort
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** @type {() => WorkerLike} */
|
|
24
|
+
const moduleWorker = () => new Worker(new URL('./worker.js', import.meta.url), { type: 'module' });
|
|
25
|
+
/** Why a worker failed when its error says nothing: its script didn't load, most often. */
|
|
26
|
+
const SILENT_FAILURE = 'it failed without a message (its script may not have loaded)';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The core in a Web Worker.
|
|
30
|
+
* @param {WorkerOptions} [options]
|
|
31
|
+
* @returns {KupcadWorker}
|
|
32
|
+
*/
|
|
33
|
+
export function createKupcadWorker(options = {}) {
|
|
34
|
+
return new KupcadWorker(options);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** @implements {KupcadApi} */
|
|
38
|
+
export class KupcadWorker {
|
|
39
|
+
/** @type {() => WorkerLike} */
|
|
40
|
+
#spawn;
|
|
41
|
+
/** The module, as it can be posted (a URL as its href). */
|
|
42
|
+
#wasm;
|
|
43
|
+
/** @type {(line: string) => void} */
|
|
44
|
+
#log;
|
|
45
|
+
/** @type {WorkerLike | null} */
|
|
46
|
+
#worker = null;
|
|
47
|
+
/** @type {Call[]} */
|
|
48
|
+
#queue = [];
|
|
49
|
+
/** @type {Call | null} */
|
|
50
|
+
#running = null;
|
|
51
|
+
#nextId = 1;
|
|
52
|
+
/** @type {Error | null} */
|
|
53
|
+
#terminated = null;
|
|
54
|
+
|
|
55
|
+
/** @param {WorkerOptions} options */
|
|
56
|
+
constructor(options) {
|
|
57
|
+
this.#spawn = options.worker ?? moduleWorker;
|
|
58
|
+
this.#wasm = postable(options.wasm);
|
|
59
|
+
this.#log = options.log ?? ((line) => console.debug(line));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {string} source
|
|
64
|
+
* @param {PreviewOptions} [options]
|
|
65
|
+
* @returns {Promise<Preview>}
|
|
66
|
+
*/
|
|
67
|
+
preview(source, { signal, ...options } = {}) {
|
|
68
|
+
return this.#call('preview', [source, options], signal);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {string} source
|
|
73
|
+
* @param {BuildOptions} [options]
|
|
74
|
+
* @returns {Promise<Uint8Array>}
|
|
75
|
+
*/
|
|
76
|
+
build(source, { signal, ...options } = {}) {
|
|
77
|
+
return this.#call('build', [source, options], signal);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {string} source
|
|
82
|
+
* @param {CallOptions} [options]
|
|
83
|
+
* @returns {Promise<Finding[]>}
|
|
84
|
+
*/
|
|
85
|
+
check(source, { signal } = {}) {
|
|
86
|
+
return this.#call('check', [source], signal);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {string} source
|
|
91
|
+
* @param {CallOptions} [options]
|
|
92
|
+
* @returns {Promise<string>}
|
|
93
|
+
*/
|
|
94
|
+
format(source, { signal } = {}) {
|
|
95
|
+
return this.#call('format', [source], signal);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @param {string} source
|
|
100
|
+
* @param {CallOptions} [options]
|
|
101
|
+
* @returns {Promise<UiSchema>}
|
|
102
|
+
*/
|
|
103
|
+
params(source, { signal } = {}) {
|
|
104
|
+
return this.#call('params', [source], signal);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** @returns {Promise<VersionInfo>} */
|
|
108
|
+
version() {
|
|
109
|
+
return this.#call('version', []);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Stops the worker for good: the calls waiting, and any made after, are refused. */
|
|
113
|
+
terminate() {
|
|
114
|
+
this.#terminated = new Error('the kupcad worker was terminated');
|
|
115
|
+
this.#stopWorker();
|
|
116
|
+
for (const call of [...(this.#running ? [this.#running] : []), ...this.#queue]) {
|
|
117
|
+
this.#settle(call, () => call.reject(this.#terminated));
|
|
118
|
+
}
|
|
119
|
+
this.#running = null;
|
|
120
|
+
this.#queue = [];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {Method} method
|
|
125
|
+
* @param {unknown[]} args
|
|
126
|
+
* @param {AbortSignal} [signal]
|
|
127
|
+
* @returns {Promise<any>}
|
|
128
|
+
*/
|
|
129
|
+
#call(method, args, signal) {
|
|
130
|
+
if (this.#terminated) return Promise.reject(this.#terminated);
|
|
131
|
+
if (signal?.aborted) return Promise.reject(signal.reason);
|
|
132
|
+
return new Promise((resolve, reject) => {
|
|
133
|
+
/** @type {Call} */
|
|
134
|
+
const call = {
|
|
135
|
+
id: this.#nextId++,
|
|
136
|
+
method,
|
|
137
|
+
args,
|
|
138
|
+
resolve,
|
|
139
|
+
reject,
|
|
140
|
+
signal,
|
|
141
|
+
onAbort: () => this.#abort(call),
|
|
142
|
+
};
|
|
143
|
+
signal?.addEventListener('abort', call.onAbort, { once: true });
|
|
144
|
+
this.#queue.push(call);
|
|
145
|
+
this.#pump();
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Sends the next call when none runs, starting a worker if none does. One that can't be
|
|
151
|
+
* sent (no worker starts, or its arguments can't be posted) is refused, and the next goes:
|
|
152
|
+
* it was taken for running, and every call after waited for good.
|
|
153
|
+
*/
|
|
154
|
+
#pump() {
|
|
155
|
+
while (!this.#running) {
|
|
156
|
+
const next = this.#queue.shift();
|
|
157
|
+
if (!next) return;
|
|
158
|
+
try {
|
|
159
|
+
this.#ensureWorker().postMessage({
|
|
160
|
+
type: 'call',
|
|
161
|
+
id: next.id,
|
|
162
|
+
method: next.method,
|
|
163
|
+
args: next.args,
|
|
164
|
+
});
|
|
165
|
+
this.#running = next;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
this.#settle(next, () => next.reject(error));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** @returns {WorkerLike} */
|
|
173
|
+
#ensureWorker() {
|
|
174
|
+
if (this.#worker) return this.#worker;
|
|
175
|
+
const worker = this.#spawn();
|
|
176
|
+
// A worker stopped may still have sent: only the running one's messages count
|
|
177
|
+
worker.addEventListener('message', ({ data }) => {
|
|
178
|
+
if (worker === this.#worker) this.#receive(data);
|
|
179
|
+
});
|
|
180
|
+
worker.addEventListener('error', (event) => {
|
|
181
|
+
event.preventDefault();
|
|
182
|
+
if (worker === this.#worker) {
|
|
183
|
+
this.#fail(
|
|
184
|
+
new KupcadCrashError(`the kupcad worker failed: ${event.message || SILENT_FAILURE}`),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
// A reply that couldn't be read would leave its call waiting for good
|
|
189
|
+
worker.addEventListener('messageerror', () => {
|
|
190
|
+
if (worker === this.#worker) {
|
|
191
|
+
this.#fail(new KupcadCrashError("the kupcad worker sent a message that couldn't be read"));
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
try {
|
|
195
|
+
worker.postMessage({ type: 'init', wasm: this.#wasm });
|
|
196
|
+
} catch (error) {
|
|
197
|
+
worker.terminate();
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
this.#worker = worker;
|
|
201
|
+
return worker;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** @param {FromWorker} message */
|
|
205
|
+
#receive(message) {
|
|
206
|
+
if (message.type === 'log') {
|
|
207
|
+
this.#log(message.line);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const call = this.#running;
|
|
211
|
+
if (!call || call.id !== message.id) return;
|
|
212
|
+
this.#running = null;
|
|
213
|
+
if (message.ok) {
|
|
214
|
+
this.#settle(call, () => call.resolve(message.value));
|
|
215
|
+
} else {
|
|
216
|
+
// A core that can't go on is replaced on the next call
|
|
217
|
+
if (message.error.fatal) this.#stopWorker();
|
|
218
|
+
this.#settle(call, () => call.reject(revive(message.error)));
|
|
219
|
+
}
|
|
220
|
+
this.#pump();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Refuses the running call with `error`, the worker gone with it.
|
|
225
|
+
* @param {Error} error
|
|
226
|
+
*/
|
|
227
|
+
#fail(error) {
|
|
228
|
+
this.#stopWorker();
|
|
229
|
+
const call = this.#running;
|
|
230
|
+
this.#running = null;
|
|
231
|
+
if (call) this.#settle(call, () => call.reject(error));
|
|
232
|
+
this.#pump();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** @param {Call} call */
|
|
236
|
+
#abort(call) {
|
|
237
|
+
const reason = call.signal?.reason;
|
|
238
|
+
if (call === this.#running) {
|
|
239
|
+
// A call can't be stopped inside the module: the worker running it is
|
|
240
|
+
this.#stopWorker();
|
|
241
|
+
this.#running = null;
|
|
242
|
+
this.#settle(call, () => call.reject(reason));
|
|
243
|
+
this.#pump();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
this.#queue = this.#queue.filter((queued) => queued !== call);
|
|
247
|
+
this.#settle(call, () => call.reject(reason));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* @param {Call} call
|
|
252
|
+
* @param {() => void} done
|
|
253
|
+
*/
|
|
254
|
+
#settle(call, done) {
|
|
255
|
+
call.signal?.removeEventListener('abort', call.onAbort);
|
|
256
|
+
done();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
#stopWorker() {
|
|
260
|
+
const worker = this.#worker;
|
|
261
|
+
if (!worker) return;
|
|
262
|
+
this.#worker = null;
|
|
263
|
+
worker.terminate();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* The module as the worker is given it: a URL as its href, a relative one resolved against the
|
|
269
|
+
* page (the worker would resolve it against its own script's).
|
|
270
|
+
* @param {WorkerWasmSource | undefined} wasm
|
|
271
|
+
* @returns {Extract<ToWorker, { type: 'init' }>['wasm']}
|
|
272
|
+
*/
|
|
273
|
+
function postable(wasm) {
|
|
274
|
+
if (wasm instanceof URL) return wasm.href;
|
|
275
|
+
if (typeof wasm === 'string' && globalThis.location) return new URL(wasm, location.href).href;
|
|
276
|
+
return wasm;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* A failure from the worker, as the error class it was there.
|
|
281
|
+
* @param {SentError} error
|
|
282
|
+
* @returns {Error}
|
|
283
|
+
*/
|
|
284
|
+
function revive({ name, message }) {
|
|
285
|
+
if (name === 'KupcadError') return new KupcadError(message);
|
|
286
|
+
if (name === 'KupcadCrashError') return new KupcadCrashError(message);
|
|
287
|
+
if (name === 'ProtocolError') return new ProtocolError(message);
|
|
288
|
+
const error = new Error(message);
|
|
289
|
+
error.name = name;
|
|
290
|
+
return error;
|
|
291
|
+
}
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// What a call can fail with besides a JavaScript error.
|
|
2
|
+
|
|
3
|
+
/** The core refused: a script that doesn't parse or run, values it refused, a format it lacks. */
|
|
4
|
+
export class KupcadError extends Error {
|
|
5
|
+
/** @param {string} message the core's, as get_last_error gives it */
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'KupcadError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The module stopped (a trap, or an exit): its instance can't be used again. A worker's is
|
|
14
|
+
* replaced on the next call; a module's own is made anew with `createKupcad`.
|
|
15
|
+
*/
|
|
16
|
+
export class KupcadCrashError extends Error {
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} message
|
|
19
|
+
* @param {{ cause?: unknown }} [options]
|
|
20
|
+
*/
|
|
21
|
+
constructor(message, options) {
|
|
22
|
+
super(message, options);
|
|
23
|
+
this.name = 'KupcadCrashError';
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/files.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// The files a call gives the core, packed into one buffer as the core reads them
|
|
2
|
+
// (core/src/vfs/packed.zig): each file its path, then its content, each with its length first
|
|
3
|
+
// (u32, little-endian), one after another. One buffer crosses into the module's memory, however
|
|
4
|
+
// many files; the core keeps them for that call alone.
|
|
5
|
+
|
|
6
|
+
/** A length, as written before a path and before a content. */
|
|
7
|
+
const LEN_BYTES = 4;
|
|
8
|
+
|
|
9
|
+
const encoder = new TextEncoder();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `files` packed: paths as the script names them (`import { } from "./lib.kup"` reads
|
|
13
|
+
* `lib.kup`; `import_json("data/size.json")` reads `data/size.json`) to their contents.
|
|
14
|
+
* @param {Record<string, Uint8Array | string>} files
|
|
15
|
+
* @returns {Uint8Array}
|
|
16
|
+
*/
|
|
17
|
+
export function packFiles(files) {
|
|
18
|
+
const fields = Object.entries(files).flatMap(([path, content]) => [
|
|
19
|
+
encoder.encode(path),
|
|
20
|
+
typeof content === 'string' ? encoder.encode(content) : content,
|
|
21
|
+
]);
|
|
22
|
+
const packed = new Uint8Array(
|
|
23
|
+
fields.reduce((total, field) => total + LEN_BYTES + field.byteLength, 0),
|
|
24
|
+
);
|
|
25
|
+
const view = new DataView(packed.buffer);
|
|
26
|
+
let at = 0;
|
|
27
|
+
for (const field of fields) {
|
|
28
|
+
view.setUint32(at, field.byteLength, true);
|
|
29
|
+
packed.set(field, at + LEN_BYTES);
|
|
30
|
+
at += LEN_BYTES + field.byteLength;
|
|
31
|
+
}
|
|
32
|
+
return packed;
|
|
33
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @kupcad/wasm: the core compiled to WebAssembly, in this thread (createKupcad) or in a Web
|
|
2
|
+
// Worker (createKupcadWorker), with one API.
|
|
3
|
+
|
|
4
|
+
export { createKupcad, Kupcad } from './kupcad.js';
|
|
5
|
+
export { createKupcadWorker } from './client.js';
|
|
6
|
+
export { KupcadCrashError, KupcadError } from './errors.js';
|