@hara-lang/hta 0.1.9

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/sandbox.js ADDED
@@ -0,0 +1,371 @@
1
+ import { HtaContext, HtaHandle, HtaKeyword, HtaMapEntry, HtaStruct } from "./index.js";
2
+
3
+ export const BROWSER_WASM_SANDBOX_PROTOCOL = "hara.browser-wasm-sandbox/0-alpha";
4
+ export const MCP_PURE_PROFILE = "hara.mcp-pure/0-alpha";
5
+ export const SANDBOX_EVAL_TARGET = "sandbox/eval";
6
+
7
+ export const DEFAULT_BROWSER_SANDBOX_LIMITS = Object.freeze({
8
+ sourceBytes: 65_536,
9
+ outputBytes: 1_048_576,
10
+ wallMs: 5_000,
11
+ });
12
+
13
+ export const MAX_BROWSER_SANDBOX_LIMITS = Object.freeze({
14
+ sourceBytes: 1_048_576,
15
+ outputBytes: 4_194_304,
16
+ wallMs: 30_000,
17
+ });
18
+
19
+ const REQUEST_KEYS = new Set(["operation", "source", "limits"]);
20
+ const LIMIT_KEYS = new Set(["sourceBytes", "outputBytes", "wallMs"]);
21
+ const TEXT_ENCODER = new TextEncoder();
22
+ const MAX_TRANSFER_DEPTH = 32;
23
+ const MAX_TRANSFER_ITEMS = 65_536;
24
+
25
+ function fail(code, message) {
26
+ const error = new Error(message);
27
+ error.code = code;
28
+ return error;
29
+ }
30
+
31
+ function exactObject(value, keys, label) {
32
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
33
+ throw fail("sandbox/request-invalid", `${label} must be an object`);
34
+ }
35
+ for (const key of Object.keys(value)) {
36
+ if (!keys.has(key)) {
37
+ throw fail("sandbox/request-invalid", `${label} contains unknown field ${key}`);
38
+ }
39
+ }
40
+ return value;
41
+ }
42
+
43
+ function positiveInteger(value, label, maximum) {
44
+ if (!Number.isSafeInteger(value) || value <= 0 || value > maximum) {
45
+ throw fail(
46
+ "sandbox/limit-invalid",
47
+ `${label} must be a positive integer no greater than ${maximum}`,
48
+ );
49
+ }
50
+ return value;
51
+ }
52
+
53
+ function byteLength(value) {
54
+ return TEXT_ENCODER.encode(value).byteLength;
55
+ }
56
+
57
+ function validateSource(source, maximum) {
58
+ if (typeof source !== "string" || source.length === 0) {
59
+ throw fail("sandbox/request-invalid", "sandbox.eval requires non-empty source");
60
+ }
61
+ const bytes = byteLength(source);
62
+ if (bytes > maximum) {
63
+ throw fail(
64
+ "sandbox/source-limit",
65
+ `source is ${bytes} bytes, above the sandbox maximum ${maximum}`,
66
+ );
67
+ }
68
+ return source;
69
+ }
70
+
71
+ function validateLimits(value) {
72
+ if (value === undefined) return { ...DEFAULT_BROWSER_SANDBOX_LIMITS };
73
+ const limits = exactObject(value, LIMIT_KEYS, "sandbox limits");
74
+ return {
75
+ sourceBytes:
76
+ limits.sourceBytes === undefined
77
+ ? DEFAULT_BROWSER_SANDBOX_LIMITS.sourceBytes
78
+ : positiveInteger(
79
+ limits.sourceBytes,
80
+ "limits.sourceBytes",
81
+ MAX_BROWSER_SANDBOX_LIMITS.sourceBytes,
82
+ ),
83
+ outputBytes:
84
+ limits.outputBytes === undefined
85
+ ? DEFAULT_BROWSER_SANDBOX_LIMITS.outputBytes
86
+ : positiveInteger(
87
+ limits.outputBytes,
88
+ "limits.outputBytes",
89
+ MAX_BROWSER_SANDBOX_LIMITS.outputBytes,
90
+ ),
91
+ wallMs:
92
+ limits.wallMs === undefined
93
+ ? DEFAULT_BROWSER_SANDBOX_LIMITS.wallMs
94
+ : positiveInteger(limits.wallMs, "limits.wallMs", MAX_BROWSER_SANDBOX_LIMITS.wallMs),
95
+ };
96
+ }
97
+
98
+ export function validateBrowserSandboxRequest(value) {
99
+ const request = exactObject(value, REQUEST_KEYS, "sandbox request");
100
+ if (request.operation !== "sandbox.eval") {
101
+ throw fail(
102
+ "sandbox/capability-unsupported",
103
+ `browser sandbox operation is not available: ${String(request.operation)}`,
104
+ );
105
+ }
106
+ const limits = validateLimits(request.limits);
107
+ return Object.freeze({
108
+ operation: request.operation,
109
+ source: validateSource(request.source, limits.sourceBytes),
110
+ limits: Object.freeze(limits),
111
+ });
112
+ }
113
+
114
+ function projectMap(value, state, depth) {
115
+ const projected = {};
116
+ const entries = [...value.entries()];
117
+ state.items += entries.length;
118
+ if (state.items > MAX_TRANSFER_ITEMS) {
119
+ throw fail("sandbox/result-limit", "sandbox result contains too many values");
120
+ }
121
+ for (const [key, item] of entries) {
122
+ let name;
123
+ if (typeof key === "string") name = key;
124
+ else if (key instanceof HtaKeyword) name = `:${key.name}`;
125
+ else {
126
+ throw fail(
127
+ "sandbox/result-non-transferable",
128
+ "sandbox map keys must be strings or keywords",
129
+ );
130
+ }
131
+ if (Object.prototype.hasOwnProperty.call(projected, name)) {
132
+ throw fail("sandbox/result-non-transferable", `sandbox map key collision: ${name}`);
133
+ }
134
+ Object.defineProperty(projected, name, {
135
+ value: projectValue(item, state, depth + 1),
136
+ enumerable: true,
137
+ configurable: true,
138
+ writable: true,
139
+ });
140
+ }
141
+ return projected;
142
+ }
143
+
144
+ function projectValue(value, state, depth) {
145
+ if (depth > MAX_TRANSFER_DEPTH) {
146
+ throw fail("sandbox/result-limit", "sandbox result exceeds maximum nesting depth");
147
+ }
148
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
149
+ if (typeof value === "number") {
150
+ if (!Number.isFinite(value)) {
151
+ throw fail("sandbox/result-non-transferable", "sandbox result contains a non-finite number");
152
+ }
153
+ return value;
154
+ }
155
+ if (typeof value === "bigint") {
156
+ const number = Number(value);
157
+ if (!Number.isSafeInteger(number)) {
158
+ throw fail("sandbox/result-non-transferable", "sandbox integer exceeds transfer-safe range");
159
+ }
160
+ return number;
161
+ }
162
+ if (value instanceof HtaKeyword) return `:${value.name}`;
163
+ if (value instanceof HtaMapEntry) {
164
+ return [
165
+ projectValue(value.key, state, depth + 1),
166
+ projectValue(value.value, state, depth + 1),
167
+ ];
168
+ }
169
+ if (value instanceof HtaHandle || value instanceof HtaStruct) {
170
+ throw fail(
171
+ "sandbox/result-non-transferable",
172
+ "sandbox result contains a live runtime value",
173
+ );
174
+ }
175
+ if (Array.isArray(value)) {
176
+ state.items += value.length;
177
+ if (state.items > MAX_TRANSFER_ITEMS) {
178
+ throw fail("sandbox/result-limit", "sandbox result contains too many values");
179
+ }
180
+ return value.map((item) => projectValue(item, state, depth + 1));
181
+ }
182
+ if (value instanceof Map) return projectMap(value, state, depth);
183
+ throw fail(
184
+ "sandbox/result-non-transferable",
185
+ `sandbox result type is not transfer-safe: ${Object.prototype.toString.call(value)}`,
186
+ );
187
+ }
188
+
189
+ export function projectBrowserSandboxValue(value, outputBytes) {
190
+ const json = projectValue(value, { items: 1 }, 0);
191
+ const text = typeof json === "string" ? json : JSON.stringify(json);
192
+ const bytes = byteLength(text);
193
+ if (bytes > outputBytes) {
194
+ throw fail(
195
+ "sandbox/output-limit",
196
+ `sandbox result is ${bytes} bytes, above the requested maximum ${outputBytes}`,
197
+ );
198
+ }
199
+ return Object.freeze({ text, json });
200
+ }
201
+
202
+ function defaultWorkerFactory(workerUrl) {
203
+ if (typeof Worker !== "function") {
204
+ throw fail("sandbox/worker-unavailable", "Worker is unavailable in this environment");
205
+ }
206
+ return new Worker(workerUrl, { type: "module", name: "hara-browser-sandbox" });
207
+ }
208
+
209
+ function defaultContextFactory(options) {
210
+ return new HtaContext(options);
211
+ }
212
+
213
+ export class BrowserWasmSandbox {
214
+ constructor(options = {}) {
215
+ const allowed = new Set([
216
+ "workerUrl",
217
+ "moduleUrl",
218
+ "moduleBytes",
219
+ "workerFactory",
220
+ "contextFactory",
221
+ "setTimer",
222
+ "clearTimer",
223
+ ]);
224
+ exactObject(options, allowed, "BrowserWasmSandbox options");
225
+ if (
226
+ !(typeof options.workerUrl === "string" || options.workerUrl instanceof URL) ||
227
+ String(options.workerUrl).length === 0
228
+ ) {
229
+ throw fail("sandbox/config-invalid", "workerUrl must be a non-empty URL or string");
230
+ }
231
+ if ((options.moduleUrl === undefined) === (options.moduleBytes === undefined)) {
232
+ throw fail(
233
+ "sandbox/config-invalid",
234
+ "exactly one of moduleUrl or moduleBytes must be supplied",
235
+ );
236
+ }
237
+ if (
238
+ options.moduleUrl !== undefined &&
239
+ !(typeof options.moduleUrl === "string" || options.moduleUrl instanceof URL)
240
+ ) {
241
+ throw fail("sandbox/config-invalid", "moduleUrl must be a URL or string");
242
+ }
243
+ if (
244
+ options.moduleBytes !== undefined &&
245
+ !(options.moduleBytes instanceof Uint8Array || options.moduleBytes instanceof ArrayBuffer)
246
+ ) {
247
+ throw fail("sandbox/config-invalid", "moduleBytes must be an ArrayBuffer or Uint8Array");
248
+ }
249
+ this.workerUrl = options.workerUrl;
250
+ this.moduleUrl = options.moduleUrl;
251
+ this.moduleBytes = options.moduleBytes;
252
+ this.workerFactory = options.workerFactory ?? defaultWorkerFactory;
253
+ this.contextFactory = options.contextFactory ?? defaultContextFactory;
254
+ this.setTimer = options.setTimer ?? ((callback, milliseconds) => setTimeout(callback, milliseconds));
255
+ this.clearTimer = options.clearTimer ?? ((timer) => clearTimeout(timer));
256
+ this.state = "new";
257
+ this.worker = null;
258
+ this.context = null;
259
+ this.activePromise = null;
260
+ }
261
+
262
+ snapshot() {
263
+ return Object.freeze({
264
+ protocol: BROWSER_WASM_SANDBOX_PROTOCOL,
265
+ profile: MCP_PURE_PROFILE,
266
+ state: this.state,
267
+ reusable: false,
268
+ hostCalls: false,
269
+ filesystem: false,
270
+ });
271
+ }
272
+
273
+ cancel() {
274
+ if (this.state !== "running" || !this.activePromise) return false;
275
+ const active = this.activePromise;
276
+ this.activePromise = null;
277
+ const cancelled = active.cancel?.() === true;
278
+ this.state = "cancelling";
279
+ if (!cancelled) this.close();
280
+ return cancelled;
281
+ }
282
+
283
+ close() {
284
+ if (this.state === "closed") return;
285
+ this.activePromise?.cancel?.();
286
+ this.activePromise = null;
287
+ const context = this.context;
288
+ const worker = this.worker;
289
+ this.context = null;
290
+ this.worker = null;
291
+ try {
292
+ context?.close?.();
293
+ } finally {
294
+ worker?.terminate?.();
295
+ this.state = "closed";
296
+ }
297
+ }
298
+
299
+ async run(value, options = {}) {
300
+ exactObject(options, new Set(["signal"]), "sandbox run options");
301
+ if (this.state !== "new") {
302
+ throw fail("sandbox/not-reusable", "a browser sandbox instance can execute exactly once");
303
+ }
304
+ const request = validateBrowserSandboxRequest(value);
305
+ const signal = options.signal;
306
+ if (
307
+ signal !== undefined &&
308
+ !(
309
+ typeof signal === "object" &&
310
+ typeof signal.aborted === "boolean" &&
311
+ typeof signal.addEventListener === "function" &&
312
+ typeof signal.removeEventListener === "function"
313
+ )
314
+ ) {
315
+ throw fail("sandbox/request-invalid", "signal must be an AbortSignal-compatible object");
316
+ }
317
+ if (signal?.aborted) {
318
+ this.state = "closed";
319
+ throw fail("sandbox/cancelled", "sandbox request was cancelled before start");
320
+ }
321
+
322
+ let timer;
323
+ let timedOut = false;
324
+ const onAbort = () => this.cancel();
325
+ this.state = "starting";
326
+ try {
327
+ this.worker = this.workerFactory(this.workerUrl);
328
+ this.context = this.contextFactory({
329
+ worker: this.worker,
330
+ moduleUrl: this.moduleUrl,
331
+ moduleBytes: this.moduleBytes,
332
+ hostCalls: Object.freeze({}),
333
+ filesystemHost: null,
334
+ kernelId: null,
335
+ });
336
+ signal?.addEventListener("abort", onAbort, { once: true });
337
+ timer = this.setTimer(() => {
338
+ timedOut = true;
339
+ if (!this.cancel()) this.close();
340
+ }, request.limits.wallMs);
341
+ if (signal?.aborted) throw fail("sandbox/cancelled", "sandbox request was cancelled");
342
+ await this.context.ready;
343
+ if (timedOut) throw fail("sandbox/timed-out", "sandbox wall-time limit expired");
344
+ this.state = "running";
345
+ const call = this.context.call(SANDBOX_EVAL_TARGET, [request.source]);
346
+ this.activePromise = call;
347
+ const result = await call;
348
+ if (timedOut) throw fail("sandbox/timed-out", "sandbox wall-time limit expired");
349
+ if (signal?.aborted) throw fail("sandbox/cancelled", "sandbox request was cancelled");
350
+ const projection = projectBrowserSandboxValue(result, request.limits.outputBytes);
351
+ this.state = "completed";
352
+ return Object.freeze({
353
+ protocol: BROWSER_WASM_SANDBOX_PROTOCOL,
354
+ profile: MCP_PURE_PROFILE,
355
+ status: "completed",
356
+ value: projection,
357
+ cleanup: "completed",
358
+ });
359
+ } catch (error) {
360
+ if (timedOut) throw fail("sandbox/timed-out", "sandbox wall-time limit expired");
361
+ if (signal?.aborted || error?.message === "cancelled") {
362
+ throw fail("sandbox/cancelled", "sandbox request was cancelled");
363
+ }
364
+ throw error;
365
+ } finally {
366
+ if (timer !== undefined) this.clearTimer(timer);
367
+ signal?.removeEventListener("abort", onAbort);
368
+ this.close();
369
+ }
370
+ }
371
+ }
@@ -0,0 +1,216 @@
1
+ import { decodeHta, encodeHta, HTA_MAX_FRAME_BYTES } from "./index.js";
2
+ import { createBrowserProvider } from "./provider-browser.mjs";
3
+
4
+ // One raw HTA instance shared by every same-origin tab. Each MessagePort owns
5
+ // its request IDs and host calls, while the task table associates kernel work
6
+ // with the port that initiated it.
7
+ let instance;
8
+ let boot;
9
+ const clients = new Map();
10
+ const providers = new Map();
11
+ const tasks = new Map();
12
+ const hostCalls = new Map();
13
+
14
+ self.onconnect = (event) => {
15
+ const port = event.ports[0];
16
+ clients.set(port, new Map());
17
+ port.addEventListener("message", (message) => receive(port, message.data));
18
+ port.start();
19
+ };
20
+
21
+ async function receive(port, message) {
22
+ try {
23
+ if (message.type === "init") {
24
+ if (message.backend === "provider") {
25
+ await initializeProvider(port, message);
26
+ port.postMessage({ type: "ready" });
27
+ return;
28
+ }
29
+ boot ??= instantiate(message);
30
+ await boot;
31
+ port.postMessage({ type: "ready" });
32
+ } else if (providers.has(port)) {
33
+ const provider = providers.get(port);
34
+ await provider.handle(message);
35
+ if (message.type === "close") {
36
+ providers.delete(port);
37
+ clients.delete(port);
38
+ port.postMessage({ type: "closed" });
39
+ }
40
+ } else if (message.type === "call") {
41
+ await boot;
42
+ const session = requestSession(message.frame);
43
+ const task = Number(callFrame(instance.exports.hta_start, message.frame));
44
+ clients.get(port)?.set(message.id, task);
45
+ tasks.set(task, { port, id: message.id, session });
46
+ pump();
47
+ } else if (message.type === "delivery") {
48
+ await boot;
49
+ if (hostCalls.get(message.call)?.port !== port) return;
50
+ hostCalls.delete(message.call);
51
+ callFrame(instance.exports.hta_deliver, encodeHta([message.call, message.ok ? 0 : 1, decodeHta(message.frame)]));
52
+ pump();
53
+ } else if (message.type === "cancel") {
54
+ const task = clients.get(port)?.get(message.id);
55
+ if (task !== undefined) {
56
+ const calls = [...hostCalls.entries()]
57
+ .filter(([, owner]) => owner.port === port && owner.task === task)
58
+ .map(([call, owner]) => ({ ...owner, call }));
59
+ if (calls.length) port.postMessage({ type: "host-cancel", calls });
60
+ clients.get(port).delete(message.id);
61
+ tasks.delete(task);
62
+ try { instance.exports.hta_cancel(BigInt(task)); }
63
+ finally { instance.exports.hta_drop_task(BigInt(task)); }
64
+ for (const [call, owner] of hostCalls) if (owner.port === port && owner.task === task) hostCalls.delete(call);
65
+ pump();
66
+ }
67
+ } else if (message.type === "release") {
68
+ await boot;
69
+ const status = callFrame(instance.exports.hta_release, message.frame);
70
+ if (Number(status) !== 0) throw new Error(`hta/handle-release-failed: ${status}`);
71
+ pump();
72
+ } else if (message.type === "close") {
73
+ dropClientTasks(port);
74
+ clients.delete(port);
75
+ // HtaContext terminates the client port after the acknowledgement. The
76
+ // acknowledgement must cross the port before the client closes it.
77
+ port.postMessage({ type: "closed" });
78
+ }
79
+ } catch (error) {
80
+ failPort(port, error);
81
+ }
82
+ }
83
+
84
+ async function initializeProvider(port, message) {
85
+ if (providers.has(port)) throw new Error("hta/worker-already-initialized");
86
+ if (typeof message.providerUrl !== "string") throw new Error("hta/provider-missing");
87
+ const providerModule = await import(message.providerUrl);
88
+ const call = providerModule.default ?? providerModule.call ?? providerModule.provider;
89
+ if (typeof call !== "function") throw new Error("hta/provider-invalid");
90
+ const close = providerModule.close ?? providerModule.closeAll;
91
+ if (close !== undefined && typeof close !== "function") {
92
+ throw new Error("hta/provider-close-invalid");
93
+ }
94
+ if (providerModule.release !== undefined && typeof providerModule.release !== "function") {
95
+ throw new Error("hta/provider-release-invalid");
96
+ }
97
+ providers.set(port, createBrowserProvider(call, {
98
+ scope: { postMessage: value => port.postMessage(value) },
99
+ origin: "shared-worker",
100
+ errorCode: message.errorCode,
101
+ close: close === undefined ? undefined : () => close(),
102
+ release: providerModule.release,
103
+ onEvent: message.instrumentation
104
+ ? event => port.postMessage({ type: "provider-event", event })
105
+ : undefined
106
+ }));
107
+ }
108
+
109
+ async function instantiate(message) {
110
+ const bytes = message.moduleBytes ?? await (await fetch(message.moduleUrl)).arrayBuffer();
111
+ instance = (await WebAssembly.instantiate(bytes, {
112
+ env: {
113
+ hara_random_fill(pointer, length) {
114
+ if (!validRange(pointer, length)) return 1;
115
+ crypto.getRandomValues(new Uint8Array(instance.exports.memory.buffer, pointer, length));
116
+ return 0;
117
+ },
118
+ hara_time_ms() { return BigInt(Math.trunc(Date.now())); },
119
+ hara_time_ns() { return BigInt(Math.trunc(performance.now() * 1_000_000)); }
120
+ }
121
+ })).instance;
122
+ for (const name of ["memory", "hta_abi_version", "hta_alloc", "hta_dealloc", "hta_start", "hta_next_event", "hta_deliver", "hta_cancel", "hta_drop_task", "hta_release"]) {
123
+ if (!(name in instance.exports)) throw new Error(`hta/export-missing: ${name}`);
124
+ }
125
+ if (![1, 2, 3, 4].includes(instance.exports.hta_abi_version())) throw new Error("hta/version-unsupported");
126
+ }
127
+
128
+ function callFrame(fn, frame) {
129
+ const bytes = frame instanceof Uint8Array ? frame : new Uint8Array(frame);
130
+ if (bytes.length > HTA_MAX_FRAME_BYTES) throw new Error("hta/value-too-large: frame exceeds 64 MiB");
131
+ const pointer = Number(instance.exports.hta_alloc(bytes.length));
132
+ if (!Number.isSafeInteger(pointer) || !validRange(pointer, bytes.length)) throw new Error("hta/memory-unavailable");
133
+ new Uint8Array(instance.exports.memory.buffer, pointer, bytes.length).set(bytes);
134
+ try { return fn(pointer, bytes.length); } finally { instance.exports.hta_dealloc(pointer, bytes.length); }
135
+ }
136
+
137
+ function next() {
138
+ const packed = instance.exports.hta_next_event();
139
+ if (packed === 0n) return null;
140
+ const pointer = Number(packed >> 32n);
141
+ const size = Number(packed & 0xffff_ffffn);
142
+ if (!Number.isSafeInteger(pointer) || !Number.isSafeInteger(size) || size === 0 ||
143
+ size > HTA_MAX_FRAME_BYTES || !validRange(pointer, size)) {
144
+ if (Number.isSafeInteger(pointer) && Number.isSafeInteger(size) && pointer >= 0 && size >= 0) {
145
+ instance.exports.hta_dealloc(pointer, size);
146
+ }
147
+ throw new Error("hta/event-memory-invalid");
148
+ }
149
+ const frame = new Uint8Array(instance.exports.memory.buffer, pointer, size).slice();
150
+ instance.exports.hta_dealloc(pointer, size);
151
+ return decodeHta(frame);
152
+ }
153
+
154
+ function requestSession(frame) {
155
+ const [target, args] = decodeHta(frame);
156
+ return typeof target === "string" &&
157
+ ["session/eval", "session/eval-vm", "session/prepare-vm", "session/invoke-vm",
158
+ "session/eval-bound", "session/trace-eval", "session/complete"].includes(target) &&
159
+ typeof args?.[0] === "string"
160
+ ? args[0]
161
+ : "ROOT";
162
+ }
163
+
164
+ function pump() {
165
+ for (let event; (event = next()) !== null;) {
166
+ const kind = Number(event[0]);
167
+ if (kind === 0 || kind === 1) {
168
+ const task = Number(event[1]);
169
+ const request = tasks.get(task);
170
+ if (!request) continue;
171
+ tasks.delete(task);
172
+ clients.get(request.port)?.delete(request.id);
173
+ instance.exports.hta_drop_task(BigInt(task));
174
+ request.port.postMessage({ type: "result", id: request.id, ok: kind === 0, frame: encodeHta(event[2]) });
175
+ } else if (kind === 2) {
176
+ const request = tasks.get(Number(event[2]));
177
+ if (request) {
178
+ const call = Number(event[1]);
179
+ hostCalls.set(call, {
180
+ port: request.port,
181
+ task: Number(event[2]),
182
+ session: event[3],
183
+ mount: event[4] ?? null,
184
+ service: event[5],
185
+ method: event[6]
186
+ });
187
+ request.port.postMessage({ type: "host-call", call, task: Number(event[2]), session: event[3], mount: event[4] ?? null, service: event[5], method: event[6], frame: encodeHta(event[7]) });
188
+ }
189
+ } else throw new Error("hta/event-unknown");
190
+ }
191
+ }
192
+
193
+ function validRange(pointer, size) {
194
+ return Number.isSafeInteger(pointer) && Number.isSafeInteger(size) &&
195
+ pointer >= 0 && size >= 0 && pointer + size <= instance.exports.memory.buffer.byteLength;
196
+ }
197
+
198
+ function dropClientTasks(port) {
199
+ const requests = clients.get(port);
200
+ if (!requests) return;
201
+ for (const [id, task] of requests) {
202
+ const calls = [...hostCalls.entries()]
203
+ .filter(([, owner]) => owner.port === port && owner.task === task)
204
+ .map(([call, owner]) => ({ ...owner, call }));
205
+ if (calls.length) port.postMessage({ type: "host-cancel", calls });
206
+ tasks.delete(task);
207
+ try { instance.exports.hta_cancel(BigInt(task)); } finally { instance.exports.hta_drop_task(BigInt(task)); }
208
+ }
209
+ requests.clear();
210
+ for (const [call, owner] of hostCalls) if (owner.port === port) hostCalls.delete(call);
211
+ }
212
+
213
+ function failPort(port, error) {
214
+ dropClientTasks(port);
215
+ port.postMessage({ type: "fatal", error: { message: String(error?.message ?? error) } });
216
+ }