@lix-js/sdk 0.16.1 → 0.17.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 +162 -44
- package/dist/binding-types.d.ts +15 -5
- package/dist/binding.browser.d.ts +2 -0
- package/dist/binding.browser.js +14 -4
- package/dist/binding.node-wasm.d.ts +1 -1
- package/dist/binding.node-wasm.js +5 -3
- package/dist/binding.node.d.ts +2 -0
- package/dist/binding.node.js +34 -8
- package/dist/bundled-plugins/plugin_csv.lixplugin +0 -0
- package/dist/bundled-plugins/plugin_markdown.lixplugin +0 -0
- package/dist/compatibility.d.ts +6 -0
- package/dist/compatibility.js +6 -0
- package/dist/component-host/dispatch.d.ts +12 -0
- package/dist/component-host/dispatch.js +364 -0
- package/dist/component-host/index.d.ts +15 -0
- package/dist/component-host/index.js +84 -0
- package/dist/component-host/instrument.d.ts +6 -0
- package/dist/component-host/instrument.js +260 -0
- package/dist/conversion-provider.d.ts +4 -0
- package/dist/conversion-provider.js +20 -0
- package/dist/hosted-lix.js +1 -1
- package/dist/http-transport.d.ts +25 -0
- package/dist/http-transport.js +162 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/lix.d.ts +14 -9
- package/dist/lix.js +44 -9
- package/dist/migration-binding.browser.d.ts +33 -0
- package/dist/migration-binding.browser.js +46 -0
- package/dist/migration-binding.node.d.ts +6 -0
- package/dist/migration-binding.node.js +5 -0
- package/dist/migration-wasm/lix_js_sdk.d.ts +271 -0
- package/dist/migration-wasm/lix_js_sdk.js +1852 -0
- package/dist/migration-wasm/lix_js_sdk_bg.wasm +4 -0
- package/dist/migration-wasm/lix_js_sdk_bg.wasm.d.ts +101 -0
- package/dist/migration.d.ts +23 -0
- package/dist/migration.js +55 -0
- package/dist/open-lix.js +29 -18
- package/dist/open-progress.d.ts +10 -0
- package/dist/open-progress.js +59 -0
- package/dist/remote/client.d.ts +2 -1
- package/dist/remote/client.js +11 -1
- package/dist/result.d.ts +4 -3
- package/dist/result.js +3 -4
- package/dist/storage-adapter.d.ts +7 -1
- package/dist/storage-ownership.d.ts +5 -0
- package/dist/storage-ownership.js +4 -0
- package/dist/types.d.ts +65 -31
- package/dist/wasm/lix_js_sdk.d.ts +45 -16
- package/dist/wasm/lix_js_sdk.js +214 -60
- package/dist/wasm/lix_js_sdk_bg.wasm +2 -2
- package/dist/wasm/lix_js_sdk_bg.wasm.d.ts +17 -9
- package/dist/worker/client.d.ts +12 -3
- package/dist/worker/client.js +95 -81
- package/dist/worker/durable-local-admission.d.ts +20 -0
- package/dist/worker/durable-local-admission.js +87 -0
- package/dist/worker/entry.shared.browser.d.ts +1 -0
- package/dist/worker/entry.shared.browser.js +211 -0
- package/dist/worker/factory.browser.d.ts +2 -0
- package/dist/worker/factory.browser.js +65 -1
- package/dist/worker/factory.node.d.ts +1 -0
- package/dist/worker/factory.node.js +3 -0
- package/dist/worker/host.d.ts +4 -2
- package/dist/worker/host.js +86 -32
- package/dist/worker/protocol.d.ts +25 -8
- package/dist/worker/protocol.js +25 -7
- package/dist/worker/shared-admission.d.ts +26 -0
- package/dist/worker/shared-admission.js +116 -0
- package/dist/worker/shared-engine.d.ts +34 -0
- package/dist/worker/shared-engine.js +194 -0
- package/package.json +21 -9
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/** One isolated handle registry per engine runtime, shared by Node and browsers. */
|
|
2
|
+
export function createComponentDispatch() {
|
|
3
|
+
const factories = new Map();
|
|
4
|
+
const guests = new Map();
|
|
5
|
+
const pending = new Map();
|
|
6
|
+
const begin = (requestId) => {
|
|
7
|
+
if (requestId == null)
|
|
8
|
+
return undefined;
|
|
9
|
+
const key = String(requestId);
|
|
10
|
+
let entry = pending.get(key);
|
|
11
|
+
if (!entry) {
|
|
12
|
+
entry = { canceled: false, completed: false };
|
|
13
|
+
pending.set(key, entry);
|
|
14
|
+
}
|
|
15
|
+
return { key, entry };
|
|
16
|
+
};
|
|
17
|
+
const complete = (token, dispose) => {
|
|
18
|
+
if (!token)
|
|
19
|
+
return;
|
|
20
|
+
token.entry.completed = true;
|
|
21
|
+
token.entry.dispose = dispose;
|
|
22
|
+
if (token.entry.canceled) {
|
|
23
|
+
dispose();
|
|
24
|
+
pending.delete(token.key);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
let sequence = 0;
|
|
28
|
+
const allocate = () => {
|
|
29
|
+
if (++sequence > Number.MAX_SAFE_INTEGER)
|
|
30
|
+
throw new Error("Component handle space exhausted");
|
|
31
|
+
return sequence;
|
|
32
|
+
};
|
|
33
|
+
return async (request) => {
|
|
34
|
+
const data = JSON.parse(request.data);
|
|
35
|
+
switch (request.operation) {
|
|
36
|
+
case "compile": {
|
|
37
|
+
const token = begin(data.requestId);
|
|
38
|
+
try {
|
|
39
|
+
if (!(request.bytes instanceof Uint8Array))
|
|
40
|
+
throw new Error("Missing component bytes");
|
|
41
|
+
const { compileComponent } = await import("./index.js");
|
|
42
|
+
const factory = await compileComponent(request.bytes, data.limits);
|
|
43
|
+
const id = allocate();
|
|
44
|
+
factories.set(id, factory);
|
|
45
|
+
complete(token, () => {
|
|
46
|
+
factories.delete(id);
|
|
47
|
+
});
|
|
48
|
+
return JSON.stringify({ id });
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
complete(token, () => { });
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
case "instantiate": {
|
|
56
|
+
const token = begin(data.requestId);
|
|
57
|
+
try {
|
|
58
|
+
const factory = factories.get(data.id);
|
|
59
|
+
if (!factory)
|
|
60
|
+
throw new Error("Unknown component factory");
|
|
61
|
+
const resources = resourceTypes();
|
|
62
|
+
const instance = await factory.instantiate({
|
|
63
|
+
...wasiImports(),
|
|
64
|
+
"lix:plugin-v2/host": resources,
|
|
65
|
+
// Previously published v2 components retain their original identity.
|
|
66
|
+
"lix:plugin/host": resources,
|
|
67
|
+
});
|
|
68
|
+
const id = allocate();
|
|
69
|
+
guests.set(id, { instance, resources, busy: false });
|
|
70
|
+
complete(token, () => {
|
|
71
|
+
guests.delete(id);
|
|
72
|
+
});
|
|
73
|
+
return JSON.stringify({ id });
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
complete(token, () => { });
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
case "invoke": {
|
|
81
|
+
const guest = guests.get(data.id);
|
|
82
|
+
if (!guest || guest.busy)
|
|
83
|
+
throw new Error("Unknown or busy component guest");
|
|
84
|
+
if (!request.host)
|
|
85
|
+
throw new Error("Missing component host");
|
|
86
|
+
guest.busy = true;
|
|
87
|
+
const scope = { host: request.host, active: true };
|
|
88
|
+
try {
|
|
89
|
+
guest.instance.setDeadline(Number(data.limits.timeoutMs ?? 5000));
|
|
90
|
+
const { input, output } = data.input;
|
|
91
|
+
const r = guest.resources;
|
|
92
|
+
const wrap = (kind, id) => new r[kind](scope, id);
|
|
93
|
+
let argument;
|
|
94
|
+
let sink;
|
|
95
|
+
let iface;
|
|
96
|
+
if (data.operation === "merge") {
|
|
97
|
+
argument = wrap("ColumnMergeSource", input);
|
|
98
|
+
sink = wrap("ColumnMergeSink", output);
|
|
99
|
+
iface = guest.instance.exports.columnMerger;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
argument = decode(input);
|
|
103
|
+
sink = wrap("Transition", output);
|
|
104
|
+
iface = guest.instance.exports.fileProjection;
|
|
105
|
+
switch (data.operation) {
|
|
106
|
+
case "parse":
|
|
107
|
+
argument.file = wrap("Snapshot", input.file);
|
|
108
|
+
break;
|
|
109
|
+
case "parseChanges":
|
|
110
|
+
argument.before = wrap("Snapshot", input.before);
|
|
111
|
+
argument.rows =
|
|
112
|
+
input.rows == null
|
|
113
|
+
? undefined
|
|
114
|
+
: wrap("RowSource", input.rows);
|
|
115
|
+
break;
|
|
116
|
+
case "serialize":
|
|
117
|
+
argument.rows = wrap("RowSource", input.rows);
|
|
118
|
+
argument.before =
|
|
119
|
+
input.before == null
|
|
120
|
+
? undefined
|
|
121
|
+
: wrap("Snapshot", input.before);
|
|
122
|
+
break;
|
|
123
|
+
case "serializeChanges":
|
|
124
|
+
argument.before = wrap("Snapshot", input.before);
|
|
125
|
+
argument.rowChanges = wrap("RowSource", input.rowChanges);
|
|
126
|
+
break;
|
|
127
|
+
default:
|
|
128
|
+
throw new Error(`Unknown component operation ${data.operation}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const fn = iface?.[data.operation];
|
|
132
|
+
if (typeof fn !== "function")
|
|
133
|
+
throw new Error(`Component does not export ${data.operation}`);
|
|
134
|
+
await fn(argument, sink);
|
|
135
|
+
const memory = JSON.parse(scope.host.call("runtime.memoryHighWater", JSON.stringify({ bytes: String(guest.instance.memoryBytes()) })));
|
|
136
|
+
if (memory.error)
|
|
137
|
+
throw memory.error;
|
|
138
|
+
return "null";
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
const payload = error?.payload;
|
|
142
|
+
if (payload?.tag &&
|
|
143
|
+
["invalid-input", "limit-exceeded", "internal"].includes(payload.tag))
|
|
144
|
+
return JSON.stringify({ pluginError: payload });
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
scope.active = false;
|
|
149
|
+
request.host.free?.();
|
|
150
|
+
guest.busy = false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
case "cancelRequest": {
|
|
154
|
+
const token = begin(data.requestId ?? data.id);
|
|
155
|
+
if (token) {
|
|
156
|
+
token.entry.canceled = true;
|
|
157
|
+
token.entry.dispose?.();
|
|
158
|
+
if (token.entry.completed)
|
|
159
|
+
pending.delete(token.key);
|
|
160
|
+
}
|
|
161
|
+
return "null";
|
|
162
|
+
}
|
|
163
|
+
case "finishRequest":
|
|
164
|
+
pending.delete(String(data.requestId ?? data.id));
|
|
165
|
+
return "null";
|
|
166
|
+
case "disposeFactory":
|
|
167
|
+
factories.delete(data.id);
|
|
168
|
+
return "null";
|
|
169
|
+
case "disposeGuest":
|
|
170
|
+
guests.delete(data.id);
|
|
171
|
+
return "null";
|
|
172
|
+
default:
|
|
173
|
+
throw new Error(`Unknown component dispatch operation ${request.operation}`);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
const u64Fields = new Set([
|
|
178
|
+
"high",
|
|
179
|
+
"offset",
|
|
180
|
+
"deleteLen",
|
|
181
|
+
"totalLen",
|
|
182
|
+
"totalLength",
|
|
183
|
+
"baseLen",
|
|
184
|
+
"aLen",
|
|
185
|
+
"bLen",
|
|
186
|
+
"baseRowLen",
|
|
187
|
+
"aRowLen",
|
|
188
|
+
"bRowLen",
|
|
189
|
+
]);
|
|
190
|
+
const byteFields = new Set(["bytes", "payload", "schemaFingerprint", "insert"]);
|
|
191
|
+
function decode(value, key = "") {
|
|
192
|
+
if (value == null)
|
|
193
|
+
return undefined;
|
|
194
|
+
if (u64Fields.has(key))
|
|
195
|
+
return BigInt(value);
|
|
196
|
+
if (byteFields.has(key))
|
|
197
|
+
return Uint8Array.from(value);
|
|
198
|
+
if (key === "primaryKey" || key === "attachments")
|
|
199
|
+
return value.map((bytes) => Uint8Array.from(bytes));
|
|
200
|
+
if (Array.isArray(value))
|
|
201
|
+
return value.map((item) => decode(item));
|
|
202
|
+
if (typeof value === "object")
|
|
203
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, decode(v, k)]));
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
function encode(_key, value) {
|
|
207
|
+
if (typeof value === "bigint")
|
|
208
|
+
return value.toString();
|
|
209
|
+
if (value instanceof Uint8Array)
|
|
210
|
+
return Array.from(value);
|
|
211
|
+
return value;
|
|
212
|
+
}
|
|
213
|
+
function resourceTypes() {
|
|
214
|
+
class Resource {
|
|
215
|
+
scope;
|
|
216
|
+
id;
|
|
217
|
+
constructor(scope, id) {
|
|
218
|
+
this.scope = scope;
|
|
219
|
+
this.id = id;
|
|
220
|
+
}
|
|
221
|
+
invoke(type, method, names, values) {
|
|
222
|
+
if (!this.scope.active)
|
|
223
|
+
throw new Error("Component resource invocation has ended");
|
|
224
|
+
const args = {
|
|
225
|
+
resource: this.id,
|
|
226
|
+
...Object.fromEntries(names.map((name, i) => [name, values[i]])),
|
|
227
|
+
};
|
|
228
|
+
const result = JSON.parse(this.scope.host.call(`${type}.${method}`, JSON.stringify(args, encode)));
|
|
229
|
+
if (result.error)
|
|
230
|
+
throw result.error;
|
|
231
|
+
if (method === "fileLen")
|
|
232
|
+
return BigInt(result.ok);
|
|
233
|
+
if (method === "readFile" ||
|
|
234
|
+
method === "readRow" ||
|
|
235
|
+
method === "readValue")
|
|
236
|
+
return result.ok == null ? undefined : Uint8Array.from(result.ok);
|
|
237
|
+
return decode(result.ok);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const define = (type, methods) => {
|
|
241
|
+
class Handle extends Resource {
|
|
242
|
+
}
|
|
243
|
+
for (const [method, names] of Object.entries(methods))
|
|
244
|
+
Object.defineProperty(Handle.prototype, method, {
|
|
245
|
+
value: function (...values) {
|
|
246
|
+
return this.invoke(type, method, names, values);
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
Object.defineProperty(Handle.prototype, Symbol.dispose || Symbol.for("dispose"), {
|
|
250
|
+
value: function () {
|
|
251
|
+
if (this.scope.active)
|
|
252
|
+
this.invoke(type, "drop", [], []);
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
return Handle;
|
|
256
|
+
};
|
|
257
|
+
return {
|
|
258
|
+
Snapshot: define("snapshot", {
|
|
259
|
+
fileLen: [],
|
|
260
|
+
readFile: ["offset", "length"],
|
|
261
|
+
readState: ["key", "offset", "maxBytes"],
|
|
262
|
+
}),
|
|
263
|
+
RowSource: define("rowSource", { nextPage: ["maxBytes"] }),
|
|
264
|
+
Transition: define("transition", {
|
|
265
|
+
maxBatchBytes: [],
|
|
266
|
+
putState: ["key", "value"],
|
|
267
|
+
deleteState: ["key"],
|
|
268
|
+
deleteStatePrefix: ["prefix"],
|
|
269
|
+
emitRows: ["page"],
|
|
270
|
+
replaceAllRows: [],
|
|
271
|
+
emitFileEdit: ["edit"],
|
|
272
|
+
beginFileReplacement: ["totalLength"],
|
|
273
|
+
writeFileReplacement: ["chunk"],
|
|
274
|
+
finishFileReplacement: [],
|
|
275
|
+
}),
|
|
276
|
+
ColumnMergeSource: define("columnMergeSource", {
|
|
277
|
+
len: [],
|
|
278
|
+
get: ["index"],
|
|
279
|
+
readValue: ["index", "side", "offset", "length"],
|
|
280
|
+
readRow: ["index", "side", "offset", "length"],
|
|
281
|
+
}),
|
|
282
|
+
ColumnMergeSink: define("columnMergeSink", {
|
|
283
|
+
maxBatchBytes: [],
|
|
284
|
+
useLww: ["ordinal"],
|
|
285
|
+
beginReplace: ["ordinal", "totalLength"],
|
|
286
|
+
writeReplacement: ["chunk"],
|
|
287
|
+
finishReplace: [],
|
|
288
|
+
}),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
/** Closed input, discarded output, and no ambient filesystem/network grants. */
|
|
292
|
+
function wasiImports() {
|
|
293
|
+
class Pollable {
|
|
294
|
+
ready() {
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
block() { }
|
|
298
|
+
}
|
|
299
|
+
class InputStream {
|
|
300
|
+
read() {
|
|
301
|
+
throw { tag: "closed" };
|
|
302
|
+
}
|
|
303
|
+
blockingRead() {
|
|
304
|
+
return this.read();
|
|
305
|
+
}
|
|
306
|
+
subscribe() {
|
|
307
|
+
return new Pollable();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
class OutputStream {
|
|
311
|
+
checkWrite() {
|
|
312
|
+
return 65536n;
|
|
313
|
+
}
|
|
314
|
+
write() { }
|
|
315
|
+
flush() { }
|
|
316
|
+
blockingFlush() { }
|
|
317
|
+
blockingWriteAndFlush() { }
|
|
318
|
+
subscribe() {
|
|
319
|
+
return new Pollable();
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
class TerminalInput {
|
|
323
|
+
}
|
|
324
|
+
class TerminalOutput {
|
|
325
|
+
}
|
|
326
|
+
class IoError {
|
|
327
|
+
toDebugString() {
|
|
328
|
+
return "Component stream error";
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
"wasi:cli/environment": {
|
|
333
|
+
getEnvironment: () => [],
|
|
334
|
+
getArguments: () => [],
|
|
335
|
+
initialCwd: () => undefined,
|
|
336
|
+
},
|
|
337
|
+
"wasi:cli/exit": {
|
|
338
|
+
exit() {
|
|
339
|
+
throw new Error("Component requested process exit");
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
"wasi:cli/stdin": { getStdin: () => new InputStream() },
|
|
343
|
+
"wasi:cli/stdout": { getStdout: () => new OutputStream() },
|
|
344
|
+
"wasi:cli/stderr": { getStderr: () => new OutputStream() },
|
|
345
|
+
"wasi:cli/terminal-input": { TerminalInput },
|
|
346
|
+
"wasi:cli/terminal-output": { TerminalOutput },
|
|
347
|
+
"wasi:cli/terminal-stdin": { getTerminalStdin: () => undefined },
|
|
348
|
+
"wasi:cli/terminal-stdout": { getTerminalStdout: () => undefined },
|
|
349
|
+
"wasi:cli/terminal-stderr": { getTerminalStderr: () => undefined },
|
|
350
|
+
"wasi:clocks/monotonic-clock": {
|
|
351
|
+
now: () => BigInt(Math.floor(performance.now() * 1e6)),
|
|
352
|
+
resolution: () => 1000000n,
|
|
353
|
+
subscribeDuration: () => new Pollable(),
|
|
354
|
+
subscribeInstant: () => new Pollable(),
|
|
355
|
+
},
|
|
356
|
+
"wasi:io/error": { Error: IoError },
|
|
357
|
+
"wasi:io/poll": {
|
|
358
|
+
Pollable,
|
|
359
|
+
poll: (items) => Uint32Array.from(items.map((_, i) => i)),
|
|
360
|
+
},
|
|
361
|
+
"wasi:io/streams": { InputStream, OutputStream },
|
|
362
|
+
"wasi:random/insecure-seed": { insecureSeed: () => [0n, 0n] },
|
|
363
|
+
};
|
|
364
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type ComponentImports = Record<string, unknown>;
|
|
2
|
+
export type ComponentLimits = {
|
|
3
|
+
maxMemoryBytes: string;
|
|
4
|
+
maxFuel?: string | null;
|
|
5
|
+
timeoutMs?: string | null;
|
|
6
|
+
};
|
|
7
|
+
export type ComponentInstance = {
|
|
8
|
+
exports: Record<string, any>;
|
|
9
|
+
memoryBytes(): number;
|
|
10
|
+
setDeadline(milliseconds: number): void;
|
|
11
|
+
};
|
|
12
|
+
/** Compile once, instantiate separately for every engine actor. */
|
|
13
|
+
export declare function compileComponent(bytes: Uint8Array, limits: ComponentLimits): Promise<{
|
|
14
|
+
instantiate(imports: ComponentImports): Promise<ComponentInstance>;
|
|
15
|
+
}>;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { $init, generate } from "@bytecodealliance/jco-transpile/component";
|
|
2
|
+
import { coreMemoryCount, coreTableCount, instrumentCore, TICK_MODULE, } from "./instrument.js";
|
|
3
|
+
/** Compile once, instantiate separately for every engine actor. */
|
|
4
|
+
export async function compileComponent(bytes, limits) {
|
|
5
|
+
if (limits.maxFuel != null)
|
|
6
|
+
throw new Error("Instruction fuel limits are unsupported by the JavaScript Component host");
|
|
7
|
+
await $init;
|
|
8
|
+
const output = generate(bytes, {
|
|
9
|
+
name: "plugin",
|
|
10
|
+
instantiation: { tag: "async" },
|
|
11
|
+
noTypescript: true,
|
|
12
|
+
noNodejsCompat: true,
|
|
13
|
+
base64Cutoff: 0,
|
|
14
|
+
});
|
|
15
|
+
const cores = output.files.filter(([name]) => name.endsWith(".wasm"));
|
|
16
|
+
const memoryCount = cores.reduce((total, [, core]) => total + coreMemoryCount(core), 0);
|
|
17
|
+
if (memoryCount > 1)
|
|
18
|
+
throw new Error("Multiple component memories are unsupported");
|
|
19
|
+
if (cores.reduce((total, [, core]) => total + coreTableCount(core), 0) > 8)
|
|
20
|
+
throw new Error("Component table count exceeds limit");
|
|
21
|
+
const modules = new Map();
|
|
22
|
+
const tableCounts = new Map();
|
|
23
|
+
for (const [name, core] of cores) {
|
|
24
|
+
const compiled = await WebAssembly.compile(instrumentCore(core, Math.floor(Number(limits.maxMemoryBytes) / Math.max(1, memoryCount))));
|
|
25
|
+
if (WebAssembly.Module.imports(compiled).some((item) => item.kind === "memory"))
|
|
26
|
+
throw new Error("Imported component memories are unsupported");
|
|
27
|
+
modules.set(name, compiled);
|
|
28
|
+
tableCounts.set(compiled, coreTableCount(core));
|
|
29
|
+
}
|
|
30
|
+
const js = output.files.find(([name]) => name === "plugin.js")?.[1];
|
|
31
|
+
if (!js)
|
|
32
|
+
throw new Error("Component transpiler did not emit JavaScript bindings");
|
|
33
|
+
const url = `data:text/javascript;charset=utf-8,${encodeURIComponent(new TextDecoder().decode(js))}`;
|
|
34
|
+
const binding = await import(/* @vite-ignore */ url);
|
|
35
|
+
return {
|
|
36
|
+
async instantiate(imports) {
|
|
37
|
+
const memories = [];
|
|
38
|
+
let tableCount = 0;
|
|
39
|
+
let instanceCount = 0;
|
|
40
|
+
let deadline = performance.now() + Number(limits.timeoutMs ?? 5000);
|
|
41
|
+
const exports = await binding.instantiate((name) => {
|
|
42
|
+
const module = modules.get(name);
|
|
43
|
+
if (!module)
|
|
44
|
+
throw new Error(`Unknown component core module ${name}`);
|
|
45
|
+
return module;
|
|
46
|
+
}, imports, (module, coreImports) => {
|
|
47
|
+
// Match Wasmtime's StoreLimitsBuilder::instances limit, including
|
|
48
|
+
// memoryless/tableless modules and repeated instances of one module.
|
|
49
|
+
if (++instanceCount > 64)
|
|
50
|
+
throw new Error("Component core instance count exceeds limit");
|
|
51
|
+
if (memories.length &&
|
|
52
|
+
WebAssembly.Module.exports(module).some((item) => item.name === "__lix_runtime_memory"))
|
|
53
|
+
throw new Error("Multiple component memory instances are unsupported");
|
|
54
|
+
tableCount += tableCounts.get(module) ?? 0;
|
|
55
|
+
if (tableCount > 8)
|
|
56
|
+
throw new Error("Component table instance count exceeds limit");
|
|
57
|
+
const instance = new WebAssembly.Instance(module, {
|
|
58
|
+
...coreImports,
|
|
59
|
+
[TICK_MODULE]: {
|
|
60
|
+
tick() {
|
|
61
|
+
if (performance.now() >= deadline)
|
|
62
|
+
throw new Error("Component execution deadline exceeded");
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
const memory = instance.exports.__lix_runtime_memory;
|
|
67
|
+
if (memory instanceof WebAssembly.Memory)
|
|
68
|
+
memories.push(memory);
|
|
69
|
+
return instance;
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
exports,
|
|
73
|
+
memoryBytes: () => memories.reduce((total, memory) => total + memory.buffer.byteLength, 0),
|
|
74
|
+
setDeadline(milliseconds) {
|
|
75
|
+
if (!Number.isFinite(milliseconds) || milliseconds <= 0)
|
|
76
|
+
throw new Error("Invalid component deadline");
|
|
77
|
+
deadline =
|
|
78
|
+
performance.now() +
|
|
79
|
+
Math.min(milliseconds, Number(limits.timeoutMs ?? milliseconds));
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const TICK_MODULE = "lix:runtime/deadline";
|
|
2
|
+
/** Add checks on every cycle (function/loop entry) and cap memory before compilation. */
|
|
3
|
+
export declare function instrumentCore(bytes: Uint8Array, maxMemoryBytes: number): Uint8Array;
|
|
4
|
+
/** Count defined memories to apportion the component's aggregate ceiling. */
|
|
5
|
+
export declare function coreMemoryCount(bytes: Uint8Array): number;
|
|
6
|
+
export declare function coreTableCount(bytes: Uint8Array): number;
|