@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/LICENSE +80 -0
- package/README.md +38 -0
- package/index.js +476 -0
- package/package.json +41 -0
- package/provider-browser.mjs +259 -0
- package/provider-common.mjs +91 -0
- package/provider-node.mjs +236 -0
- package/sandbox.js +371 -0
- package/shared-worker.js +216 -0
- package/worker.mjs +344 -0
package/index.js
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
const MAGIC = new Uint8Array([0x48, 0x54, 0x41, 0x30]);
|
|
2
|
+
export const HTA_TAG = Object.freeze({ nil: 0, false: 1, true: 2, i64: 3, string: 4, bytes: 5, keyword: 6, symbol: 7, list: 8, vector: 9, set: 10, map: 11, handle: 12, namespace: 13, var: 14, f64: 15, atom: 16, array: 17, object: 18, character: 19, bigInteger: 20, regex: 22, tuple:23, cons:24, queue:25, orderedMap:26, sortedMap:27, trie:28, orderedSet:29, sortedSet:30, tagged:31, exceptionInfo:32, struct:33, pointer:34, varRef:35, deque:36, priorityMap:37, mapEntry:38 });
|
|
3
|
+
const TAG = HTA_TAG;
|
|
4
|
+
const encoder = new TextEncoder();
|
|
5
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
6
|
+
export const HTA_MAX_FRAME_BYTES = 64 * 1024 * 1024;
|
|
7
|
+
export const HTA_MAX_NESTING_DEPTH = 256;
|
|
8
|
+
export const HTA_BROWSER_WORKER_URL = new URL("./worker.mjs", import.meta.url);
|
|
9
|
+
|
|
10
|
+
export class HtaKeyword { constructor(name) { this.name = name; } }
|
|
11
|
+
export class HtaSymbol { constructor(name) { this.name = name; } }
|
|
12
|
+
export class HtaNamespace { constructor(name) { this.name = name; } toString() { return this.name; } }
|
|
13
|
+
export class HtaCharacter {
|
|
14
|
+
constructor(value) {
|
|
15
|
+
if (typeof value === "number") value = String.fromCodePoint(value);
|
|
16
|
+
if (typeof value !== "string" || [...value].length !== 1) throw new Error("hta/value-malformed: invalid character scalar");
|
|
17
|
+
const codePoint = value.codePointAt(0);
|
|
18
|
+
if (codePoint >= 0xd800 && codePoint <= 0xdfff) throw new Error("hta/value-malformed: invalid character scalar");
|
|
19
|
+
this.value = value;
|
|
20
|
+
}
|
|
21
|
+
get codePoint() { return this.value.codePointAt(0); }
|
|
22
|
+
toString() { return this.value; }
|
|
23
|
+
}
|
|
24
|
+
export class HtaRegex { constructor(source) { if (typeof source !== "string") throw new Error("hta/value-malformed: invalid regex"); this.source = source; } toString() { return `#regex ${this.source}`; } }
|
|
25
|
+
export class HtaVar { constructor(symbol,value=null){this.symbol=symbol;this.value=value;} toString(){return `#'${this.symbol.name}`;} }
|
|
26
|
+
export class HtaPointer { constructor(context,fields){this.context=context;this.fields=fields;} }
|
|
27
|
+
export class HtaAtom { constructor(value){this.value=value;} toString(){return `#atom <${displayHta(this.value)}>`;} }
|
|
28
|
+
export class HtaArray { constructor(values){this.values=values;} toString(){return `(array${this.values.length?` ${this.values.map(displayHta).join(" ")}`:""})`;} }
|
|
29
|
+
export class HtaObject { constructor(entries){this.entries=entries;} toString(){return `(object${this.entries.length?` ${this.entries.map(([key,value])=>`${JSON.stringify(key)} ${displayHta(value)}`).join(" ")}`:""})`;} }
|
|
30
|
+
export class HtaTuple { constructor(values){this.values=values;} }
|
|
31
|
+
export class HtaMapEntry { constructor(key,value){this.key=key;this.value=value;} toString(){return "["+displayHta(this.key)+" "+displayHta(this.value)+"]";} }
|
|
32
|
+
export class HtaCons { constructor(values){this.values=values;} }
|
|
33
|
+
export class HtaQueue { constructor(values){this.values=values;} }
|
|
34
|
+
export class HtaDeque { constructor(values){this.values=values;} }
|
|
35
|
+
export class HtaOrderedMap { constructor(entries){this.entries=entries;} }
|
|
36
|
+
export class HtaSortedMap { constructor(entries){this.entries=entries;} }
|
|
37
|
+
export class HtaTrie { constructor(entries){this.entries=entries;} }
|
|
38
|
+
export class HtaPriorityMap { constructor(entries){this.entries=entries;} }
|
|
39
|
+
export class HtaOrderedSet { constructor(values){this.values=values;} }
|
|
40
|
+
export class HtaSortedSet { constructor(values){this.values=values;} }
|
|
41
|
+
export class HtaTagged { constructor(tag,value){this.tag=tag;this.value=value;} }
|
|
42
|
+
export class HtaExceptionInfo { constructor(message,data,cause=null,provenance=null){this.message=message;this.data=data;this.cause=cause;this.provenance=provenance ?? new Map([[new HtaKeyword("ex/created-at"),null],[new HtaKeyword("ex/throws"),[]]]);} }
|
|
43
|
+
export class HtaStruct { constructor(name,fields,values){this.name=name;this.fields=fields;this.values=values;} }
|
|
44
|
+
function displayHta(value){if(value===null)return"nil";if(typeof value==="string")return JSON.stringify(value);if(value instanceof HtaKeyword)return`:${value.name}`;if(value instanceof Map)return`{${[...value].map(([key,item])=>`${displayHta(key)} ${displayHta(item)}`).join(" ")}}`;if(Array.isArray(value))return`[${value.map(displayHta).join(" ")}]`;return String(value);}
|
|
45
|
+
export class HtaHandle { constructor(owner,type,id,context=null,displayTag="ht",displayKind="handle"){this.owner=owner;this.type=type;this.id=BigInt(id);this.context=context;this.displayTag=displayTag;this.displayKind=displayKind;this.released=false;} release(){if(this.released)return;this.released=true;if(this.context)this.context.releaseHandle(this);} toString(){return `#${this.displayTag}[:${this.displayKind} ${this.id}]`;} }
|
|
46
|
+
|
|
47
|
+
/** Browser host adapter for the portable Hara promise-provider contract. */
|
|
48
|
+
export class BrowserPromiseProvider {
|
|
49
|
+
constructor(options={}) {
|
|
50
|
+
this.enqueue=options.enqueue ?? (task=>queueMicrotask(task));
|
|
51
|
+
this.schedule=options.schedule ?? ((task,milliseconds)=>setTimeout(task,milliseconds));
|
|
52
|
+
this.cancelSchedule=options.cancelSchedule ?? (timer=>clearTimeout(timer));
|
|
53
|
+
}
|
|
54
|
+
create(executor) {
|
|
55
|
+
let settled=false,rejectPromise=()=>{},cancelAction=()=>{};
|
|
56
|
+
const promise=new Promise((resolve,reject)=>{
|
|
57
|
+
rejectPromise=reject;
|
|
58
|
+
const settle=(callback)=>(value)=>{if(settled)return false;settled=true;callback(value);return true;};
|
|
59
|
+
const onCancel=(action)=>{cancelAction=typeof action==="function"?action:()=>{};};
|
|
60
|
+
try{executor(settle(resolve),settle(reject),onCancel);}catch(error){settle(reject)(error);}
|
|
61
|
+
});
|
|
62
|
+
promise.cancel=()=>{if(settled)return false;cancelAction();settled=true;rejectPromise(new Error("cancelled"));return true;};
|
|
63
|
+
return promise;
|
|
64
|
+
}
|
|
65
|
+
run(task) {
|
|
66
|
+
return this.create((resolve,reject,onCancel)=>{
|
|
67
|
+
let cancelled=false;onCancel(()=>{cancelled=true;});
|
|
68
|
+
this.enqueue(()=>{if(cancelled)return;try{resolve(task());}catch(error){reject(error);}});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
delay(milliseconds,task) {
|
|
72
|
+
return this.create((resolve,reject,onCancel)=>{
|
|
73
|
+
const timer=this.schedule(()=>{try{resolve(task());}catch(error){reject(error);}},milliseconds);
|
|
74
|
+
onCancel(()=>this.cancelSchedule(timer));
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
all(values) { return this.create((resolve,reject)=>Promise.all(values).then(resolve,reject)); }
|
|
78
|
+
then(source,callback) { return this.create((resolve,reject)=>Promise.resolve(source).then(callback).then(resolve,reject)); }
|
|
79
|
+
catch(source,callback) { return this.create((resolve,reject)=>Promise.resolve(source).catch(callback).then(resolve,reject)); }
|
|
80
|
+
finally(source,callback) { return this.create((resolve,reject)=>Promise.resolve(source).finally(callback).then(resolve,reject)); }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function encodeHta(value) {
|
|
84
|
+
const output = [...MAGIC];
|
|
85
|
+
writeValue(output, value, 0);
|
|
86
|
+
if(output.length>HTA_MAX_FRAME_BYTES)throw new Error("hta/value-too-large: frame exceeds 64 MiB");
|
|
87
|
+
return Uint8Array.from(output);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function decodeHta(input) {
|
|
91
|
+
const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
92
|
+
if(bytes.length>HTA_MAX_FRAME_BYTES)throw new Error("hta/value-too-large: frame exceeds 64 MiB");
|
|
93
|
+
if (bytes.length < 4 || !MAGIC.every((byte, index) => bytes[index] === byte)) {
|
|
94
|
+
throw new Error("hta/value-malformed: invalid HTA0 header");
|
|
95
|
+
}
|
|
96
|
+
const reader = new Reader(bytes, 4);
|
|
97
|
+
const value = reader.value(0);
|
|
98
|
+
if (reader.cursor !== bytes.length) throw new Error("hta/value-malformed: trailing bytes");
|
|
99
|
+
const canonical = encodeHta(value);
|
|
100
|
+
if (canonical.length !== bytes.length || canonical.some((byte, index) => byte !== bytes[index])) {
|
|
101
|
+
throw new Error("hta/value-noncanonical: frame bytes are not canonical");
|
|
102
|
+
}
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function parseHtaManifest(source) {
|
|
107
|
+
const value = parseEdnData(source, "hta/manifest-malformed");
|
|
108
|
+
if (!(value instanceof Map)) throw new Error("hta/manifest-malformed: expected one EDN map");
|
|
109
|
+
if ([...value.keys()].some(key => !(key instanceof HtaKeyword) || !MANIFEST_FIELDS.has(key.name))) throw new Error("hta/manifest-malformed: unknown manifest field");
|
|
110
|
+
const root = manifestField(value,"root"), namespace = manifestField(value,"namespace"), identity = manifestField(value,"identity"), version = manifestField(value,"version"), providerValue = manifestField(value,"provider"), module = manifestField(value,"module"), abiValue = manifestField(value,"abi");
|
|
111
|
+
if (root !== undefined && !validPackagePath(root)) throw new Error("hta/manifest-malformed: invalid root");
|
|
112
|
+
if (typeof namespace !== "string" || !/^[a-z][a-z0-9-]*(\.[a-z0-9][a-z0-9-]*)+$/.test(namespace)) throw new Error("hta/manifest-malformed: invalid namespace");
|
|
113
|
+
if (identity !== undefined && (typeof identity !== "string" || !/^[a-z][a-z0-9-]*\/[a-z][a-z0-9-]*(\.[a-z0-9][a-z0-9-]*)*$/.test(identity))) throw new Error("hta/manifest-malformed: invalid identity");
|
|
114
|
+
if (typeof version !== "string" || !version.length) throw new Error("hta/manifest-malformed: invalid version");
|
|
115
|
+
if (!(providerValue instanceof HtaKeyword) || !["wasm","hta"].includes(providerValue.name)) throw new Error("hta/manifest-malformed: provider must be :wasm or :hta");
|
|
116
|
+
if (!(abiValue instanceof HtaKeyword)) throw new Error("hta/manifest-malformed: abi must be a keyword");
|
|
117
|
+
const provider=providerValue.name,abi=abiValue.name;
|
|
118
|
+
const targetsValue = manifestField(value,"targets");
|
|
119
|
+
const targets = {};
|
|
120
|
+
if (targetsValue !== undefined && !(targetsValue instanceof Map)) throw new Error("hta/manifest-malformed: targets must be a map");
|
|
121
|
+
for (const [host,spec] of targetsValue ?? []) {
|
|
122
|
+
const hostName = host instanceof HtaKeyword ? host.name : undefined;
|
|
123
|
+
if (!(hostName === "node" || hostName === "browser") || !(spec instanceof Map)) throw new Error("hta/manifest-malformed: invalid target");
|
|
124
|
+
if ([...spec.keys()].some(key => !(key instanceof HtaKeyword) || !["provider", "runtime"].includes(key.name))) {
|
|
125
|
+
throw new Error(`hta/manifest-malformed: invalid ${hostName} target`);
|
|
126
|
+
}
|
|
127
|
+
const targetProvider = manifestField(spec,"provider"), runtime = manifestField(spec,"runtime");
|
|
128
|
+
const expectedRuntime = hostName === "node" ? "process" : "web-worker";
|
|
129
|
+
if (!validPackagePath(targetProvider,".mjs") || !(runtime instanceof HtaKeyword) || runtime.name !== expectedRuntime) throw new Error(`hta/manifest-malformed: invalid ${hostName} target`);
|
|
130
|
+
targets[hostName] = Object.freeze({provider:targetProvider,runtime:runtime.name});
|
|
131
|
+
}
|
|
132
|
+
let browserTarget;
|
|
133
|
+
if (provider === "wasm") {
|
|
134
|
+
if (targetsValue !== undefined) throw new Error("hta/manifest-malformed: WASM providers cannot declare :targets");
|
|
135
|
+
if (!validPackagePath(module,".wasm")) throw new Error("hta/manifest-malformed: invalid module");
|
|
136
|
+
} else {
|
|
137
|
+
if (module !== undefined || abi !== "hta.v1" || !targets.browser) throw new Error("hta/manifest-malformed: HTA targets require :abi :hta.v1 without :module and a browser web-worker target");
|
|
138
|
+
browserTarget=targets.browser;
|
|
139
|
+
}
|
|
140
|
+
const assetsValue=manifestField(value,"assets"),assets=[],seenAssets=new Set();
|
|
141
|
+
if (assetsValue !== undefined) {
|
|
142
|
+
if (!Array.isArray(assetsValue) || assetsValue.some(asset=>!validPackagePath(asset))) throw new Error("hta/manifest-malformed: invalid assets");
|
|
143
|
+
for (const asset of assetsValue) {
|
|
144
|
+
if (seenAssets.has(asset)) throw new Error(`hta/manifest-malformed: duplicate asset ${asset}`);
|
|
145
|
+
seenAssets.add(asset);
|
|
146
|
+
assets.push(asset);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const handleTags = {}, handleReleases = {}, handles = manifestField(value,"handles");
|
|
150
|
+
if (handles !== undefined) {
|
|
151
|
+
if (!(handles instanceof Map)) throw new Error("hta/manifest-malformed: handles must be a map");
|
|
152
|
+
for (const [type,spec] of handles) {
|
|
153
|
+
const tag = spec instanceof Map ? manifestField(spec,"tag") : undefined;
|
|
154
|
+
if (typeof type !== "string" || !/^[a-z][a-z0-9-]*$/.test(type) || !(tag instanceof HtaSymbol) || !/^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)*$/.test(tag.name)) throw new Error("hta/manifest-malformed: invalid handle tag");
|
|
155
|
+
handleTags[type] = tag.name;
|
|
156
|
+
const release = spec instanceof Map ? manifestField(spec,"release") : undefined;
|
|
157
|
+
if (release !== undefined && (typeof release !== "string" || !release.length)) {
|
|
158
|
+
throw new Error("hta/manifest-malformed: invalid handle release");
|
|
159
|
+
}
|
|
160
|
+
if (release !== undefined) handleReleases[type] = release;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const exportsValue = manifestField(value,"exports"), exports = [], exportSpecs = {}, operations = {};
|
|
164
|
+
const exportArity = {};
|
|
165
|
+
if (!(exportsValue instanceof Map) || exportsValue.size === 0) throw new Error("hta/manifest-malformed: exports must be a non-empty map");
|
|
166
|
+
for (const [name,spec] of exportsValue) {
|
|
167
|
+
if (typeof name !== "string" || !name.length || !(spec instanceof Map)) throw new Error("hta/manifest-malformed: invalid export");
|
|
168
|
+
const args = manifestField(spec,"args"), returns = manifestField(spec,"returns"), asynchronous = manifestField(spec,"async");
|
|
169
|
+
if (!Array.isArray(args) || args.some(arg => !(arg instanceof HtaKeyword))) throw new Error("hta/manifest-malformed: invalid export args");
|
|
170
|
+
if (!(returns instanceof HtaKeyword) && !(Array.isArray(returns) && returns.every(item => item instanceof HtaKeyword))) throw new Error("hta/manifest-malformed: invalid export returns");
|
|
171
|
+
if (asynchronous !== undefined && typeof asynchronous !== "boolean") throw new Error("hta/manifest-malformed: export async must be boolean");
|
|
172
|
+
exports.push(name);
|
|
173
|
+
exportArity[name] = args.length;
|
|
174
|
+
exportSpecs[name] = Object.freeze({args:Object.freeze([...args]),returns,async:asynchronous ?? false});
|
|
175
|
+
const rawExport = manifestField(spec,"wasm/export");
|
|
176
|
+
if (rawExport !== undefined && (typeof rawExport !== "string" || !rawExport.length)) throw new Error("hta/manifest-malformed: invalid export wasm/export");
|
|
177
|
+
const operation = manifestField(spec,"operation");
|
|
178
|
+
if (operation !== undefined) {
|
|
179
|
+
if (typeof operation !== "string" || !operation.length) throw new Error("hta/manifest-malformed: invalid export operation");
|
|
180
|
+
operations[name] = operation;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const callbacksValue = manifestField(value,"callbacks"), callbacks = {};
|
|
184
|
+
if (callbacksValue !== undefined) {
|
|
185
|
+
if (!(callbacksValue instanceof Map)) throw new Error("hta/manifest-malformed: callbacks must be a map");
|
|
186
|
+
for (const [name,spec] of callbacksValue) {
|
|
187
|
+
if (typeof name !== "string" || !name.length || !(spec instanceof Map)) {
|
|
188
|
+
throw new Error("hta/manifest-malformed: invalid callback");
|
|
189
|
+
}
|
|
190
|
+
const args = manifestField(spec,"args"), returns = manifestField(spec,"returns"), reentrant = manifestField(spec,"reentrant");
|
|
191
|
+
if (!Array.isArray(args) || args.some(arg => !(arg instanceof HtaKeyword)) ||
|
|
192
|
+
!(returns instanceof HtaKeyword) || (reentrant !== undefined && reentrant !== false)) {
|
|
193
|
+
throw new Error("hta/manifest-malformed: invalid callback");
|
|
194
|
+
}
|
|
195
|
+
callbacks[name] = Object.freeze({args:Object.freeze([...args]),returns});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const hostCallsValue = manifestField(value,"host-calls"), hostCalls = {}, hostCallCapabilities = {};
|
|
199
|
+
if (hostCallsValue !== undefined) {
|
|
200
|
+
if (!(hostCallsValue instanceof Map)) throw new Error("hta/manifest-malformed: host-calls must be a map");
|
|
201
|
+
for (const [service,declared] of hostCallsValue) {
|
|
202
|
+
let methods = declared;
|
|
203
|
+
let capabilities = [];
|
|
204
|
+
if (methods instanceof Map) {
|
|
205
|
+
const declaredMethods = manifestField(methods,"methods");
|
|
206
|
+
const declaredCapabilities = manifestField(methods,"capabilities");
|
|
207
|
+
if (!Array.isArray(declaredMethods)) throw new Error("hta/manifest-malformed: host-call methods must be a vector");
|
|
208
|
+
if (declaredCapabilities !== undefined &&
|
|
209
|
+
(!Array.isArray(declaredCapabilities) || declaredCapabilities.some(capability => !(capability instanceof HtaKeyword)))) {
|
|
210
|
+
throw new Error("hta/manifest-malformed: host-call capabilities must be keywords");
|
|
211
|
+
}
|
|
212
|
+
methods = declaredMethods;
|
|
213
|
+
capabilities = declaredCapabilities?.map(capability => capability.name) ?? [];
|
|
214
|
+
}
|
|
215
|
+
if (typeof service !== "string" || !/^[a-z][a-z0-9.-]*$/.test(service) || !Array.isArray(methods) ||
|
|
216
|
+
methods.some(method => typeof method !== "string" || !/^[a-z][a-z0-9-]*$/.test(method))) {
|
|
217
|
+
throw new Error("hta/manifest-malformed: invalid host-call");
|
|
218
|
+
}
|
|
219
|
+
hostCalls[service] = Object.freeze([...methods]);
|
|
220
|
+
for (const method of methods) hostCallCapabilities[`${service}/${method}`] = Object.freeze([...capabilities]);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const capabilitiesValue = manifestField(value,"capabilities"), capabilities = [];
|
|
224
|
+
if (capabilitiesValue !== undefined) {
|
|
225
|
+
if (!Array.isArray(capabilitiesValue) || capabilitiesValue.some(capability => !(capability instanceof HtaKeyword))) {
|
|
226
|
+
throw new Error("hta/manifest-malformed: capabilities must be keywords");
|
|
227
|
+
}
|
|
228
|
+
capabilities.push(...capabilitiesValue.map(capability => capability.name));
|
|
229
|
+
}
|
|
230
|
+
return Object.freeze({
|
|
231
|
+
root,namespace,identity,version,provider,module,abi,targets:Object.freeze(targets),browserTarget,assets:Object.freeze(assets),
|
|
232
|
+
handleTags:Object.freeze(handleTags),handleReleases:Object.freeze(handleReleases),exports:Object.freeze(exports),
|
|
233
|
+
callbacks:Object.freeze(callbacks),
|
|
234
|
+
exportArity:Object.freeze(exportArity),exportSpecs:Object.freeze(exportSpecs),operations:Object.freeze(operations),capabilities:Object.freeze(capabilities),
|
|
235
|
+
hostCalls:Object.freeze(hostCalls),hostCallCapabilities:Object.freeze(hostCallCapabilities)
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Strict data-only EDN subset shared by extension and package manifests. */
|
|
240
|
+
export function parseEdnData(source, errorCode = "edn/data-malformed") {
|
|
241
|
+
const reader = new ManifestReader(source, errorCode);
|
|
242
|
+
const value = reader.value();
|
|
243
|
+
reader.space();
|
|
244
|
+
if (reader.cursor !== source.length) throw new Error(`${errorCode}: trailing input`);
|
|
245
|
+
return value;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function validPackagePath(value,suffix) {
|
|
249
|
+
return typeof value === "string" && value.length>0 && !value.startsWith("/") && !value.includes("\\") && !value.includes("\0") && !value.includes(":") && !value.split("/").some(part => part === "" || part === "." || part === "..") && (!suffix || value.endsWith(suffix));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const MANIFEST_FIELDS = new Set(["root","namespace","identity","version","provider","module","abi","exports","capabilities","host-calls","callbacks","handles","targets","assets"]);
|
|
253
|
+
|
|
254
|
+
export async function loadHtaExtension({worker,workerFactory,workerUrl,descriptor,descriptorUrl,packageUrl,moduleBytes,libraryBytes,providerUrl,hostCalls={},capabilities=[],instrumentation=false,onProviderEvent}) {
|
|
255
|
+
if (descriptor === undefined) {
|
|
256
|
+
if (!descriptorUrl) throw new Error("hta/manifest-missing: descriptor or descriptorUrl is required");
|
|
257
|
+
const response = await fetch(descriptorUrl);
|
|
258
|
+
if (!response.ok) throw new Error(`hta/manifest-load-failed: ${response.status}`);
|
|
259
|
+
descriptor = await response.text();
|
|
260
|
+
}
|
|
261
|
+
const manifest = parseHtaManifest(descriptor);
|
|
262
|
+
const base = packageUrl ?? descriptorUrl;
|
|
263
|
+
let moduleUrl;
|
|
264
|
+
let libraryUrl;
|
|
265
|
+
if (manifest.provider === "wasm" && moduleBytes === undefined) {
|
|
266
|
+
if (!base) throw new Error("hta/manifest-missing: packageUrl is required with inline descriptors");
|
|
267
|
+
moduleUrl = new URL(manifest.module,base).toString();
|
|
268
|
+
}
|
|
269
|
+
if (manifest.provider === "wasm" && libraryBytes === undefined) {
|
|
270
|
+
const library = manifest.assets.find(asset => asset.endsWith(".wasm") && asset !== manifest.module);
|
|
271
|
+
if (library) {
|
|
272
|
+
if (!base) throw new Error("hta/manifest-missing: packageUrl is required with inline descriptors");
|
|
273
|
+
libraryUrl = new URL(library,base).toString();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (manifest.provider === "hta" && providerUrl === undefined) {
|
|
277
|
+
if (!base) throw new Error("hta/manifest-missing: packageUrl is required with inline descriptors");
|
|
278
|
+
providerUrl = new URL(manifest.browserTarget.provider,base).toString();
|
|
279
|
+
}
|
|
280
|
+
if (manifest.provider === "hta" && !worker) {
|
|
281
|
+
if (!workerFactory && typeof Worker !== "function") throw new Error("hta/worker-missing: worker factory is required");
|
|
282
|
+
workerFactory ??= (url, options) => new Worker(url, options);
|
|
283
|
+
worker = workerFactory(workerUrl ?? HTA_BROWSER_WORKER_URL,{type:"module",name:`hara-${manifest.namespace}`});
|
|
284
|
+
}
|
|
285
|
+
if (!worker) throw new Error("hta/worker-missing: worker is required for WASM providers");
|
|
286
|
+
const context = new HtaContext({
|
|
287
|
+
worker,moduleUrl,moduleBytes,libraryUrl,libraryBytes,providerUrl,hostCalls,capabilities,handleTags:manifest.handleTags,
|
|
288
|
+
manifest,instrumentation,onProviderEvent
|
|
289
|
+
});
|
|
290
|
+
return context;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function manifestField(map,name) { for (const [key,value] of map) if (key instanceof HtaKeyword && key.name===name) return value; }
|
|
294
|
+
|
|
295
|
+
class ManifestReader {
|
|
296
|
+
constructor(source,errorCode="hta/manifest-malformed"){this.source=source;this.cursor=0;this.errorCode=errorCode;}
|
|
297
|
+
error(message){return new Error(`${this.errorCode}: ${message}`);}
|
|
298
|
+
space(){while(this.cursor<this.source.length){const ch=this.source[this.cursor];if(/[\s,]/.test(ch)){this.cursor++;continue;}if(ch===';'){while(this.cursor<this.source.length&&this.source[this.cursor]!=='\n')this.cursor++;continue;}break;}}
|
|
299
|
+
value(){this.space();const ch=this.source[this.cursor++];if(ch===undefined)throw this.error("unexpected EOF");if(ch==='{')return this.map();if(ch==='[')return this.vector();if(ch==='\"')return this.string();if(ch===':')return new HtaKeyword(this.token());this.cursor--;const token=this.token();if(token==='nil')return null;if(token==='true')return true;if(token==='false')return false;if(/^-?[0-9]+$/.test(token))return Number(token);return new HtaSymbol(token);}
|
|
300
|
+
map(){const result=new Map(),keys=new Set();for(;;){this.space();if(this.source[this.cursor]==='}'){this.cursor++;return result;}const key=this.value(),identity=displayHta(key);if(keys.has(identity))throw this.error("duplicate map key");keys.add(identity);this.space();if(this.source[this.cursor]==='}')throw this.error("map value missing");result.set(key,this.value());}}
|
|
301
|
+
vector(){const result=[];for(;;){this.space();if(this.source[this.cursor]===']'){this.cursor++;return result;}result.push(this.value());}}
|
|
302
|
+
string(){let result='';while(this.cursor<this.source.length){const ch=this.source[this.cursor++];if(ch==='\"')return result;if(ch==='\\'){const escaped=this.source[this.cursor++];if(escaped==='u'){const code=this.source.slice(this.cursor,this.cursor+4);if(!/^[0-9a-fA-F]{4}$/.test(code))throw this.error("invalid unicode escape");result+=String.fromCharCode(parseInt(code,16));this.cursor+=4;}else{const escapes={n:'\n',r:'\r',t:'\t',b:'\b',f:'\f','\"':'\"','\\':'\\'};if(!(escaped in escapes))throw this.error("invalid string escape");result+=escapes[escaped];}}else result+=ch;}throw this.error("unterminated string");}
|
|
303
|
+
token(){this.space();const start=this.cursor;while(this.cursor<this.source.length&&!/[\s,{}\[\]\"]/ .test(this.source[this.cursor]))this.cursor++;if(start===this.cursor)throw this.error("invalid token");return this.source.slice(start,this.cursor);}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function writeValue(output, value, depth=0) {
|
|
307
|
+
if(depth>HTA_MAX_NESTING_DEPTH)throw new Error("hta/value-too-deep: nesting exceeds 256");
|
|
308
|
+
if (value === null || value === undefined) output.push(TAG.nil);
|
|
309
|
+
else if (value === false) output.push(TAG.false);
|
|
310
|
+
else if (value === true) output.push(TAG.true);
|
|
311
|
+
else if (typeof value === "bigint") {
|
|
312
|
+
if (value >= -(1n << 63n) && value < (1n << 63n)) {
|
|
313
|
+
output.push(TAG.i64); writeI64(output, value);
|
|
314
|
+
} else {
|
|
315
|
+
output.push(TAG.bigInteger); writeBytes(output, encoder.encode(value.toString()));
|
|
316
|
+
}
|
|
317
|
+
} else if (Number.isSafeInteger(value) && !Object.is(value, -0)) {
|
|
318
|
+
output.push(TAG.i64); writeI64(output, BigInt(value));
|
|
319
|
+
} else if (typeof value === "number") {
|
|
320
|
+
if (!Number.isFinite(value)) throw new Error("hta/non-finite number");
|
|
321
|
+
output.push(TAG.f64); writeF64(output, value);
|
|
322
|
+
} else if (typeof value === "string") { output.push(TAG.string); writeBytes(output, encoder.encode(value)); }
|
|
323
|
+
else if (value instanceof Uint8Array) { output.push(TAG.bytes); writeBytes(output, value); }
|
|
324
|
+
else if (value instanceof HtaKeyword) { output.push(TAG.keyword); writeBytes(output, encoder.encode(value.name)); }
|
|
325
|
+
else if (value instanceof HtaSymbol) { output.push(TAG.symbol); writeBytes(output, encoder.encode(value.name)); }
|
|
326
|
+
else if (value instanceof HtaNamespace) { output.push(TAG.namespace); writeBytes(output, encoder.encode(value.name)); }
|
|
327
|
+
else if (value instanceof HtaCharacter) { output.push(TAG.character); writeU32(output, value.codePoint); }
|
|
328
|
+
else if (value instanceof HtaRegex) { output.push(TAG.regex); writeBytes(output, encoder.encode(value.source)); }
|
|
329
|
+
else if (value instanceof HtaVar) {
|
|
330
|
+
if (!(value.symbol instanceof HtaSymbol) || !value.symbol.name.includes("/")) throw new Error("hta/value-malformed: Var references require a qualified symbol");
|
|
331
|
+
output.push(TAG.varRef); writeValue(output,value.symbol,depth+1);
|
|
332
|
+
}
|
|
333
|
+
else if (value instanceof HtaPointer) {
|
|
334
|
+
if (!(value.context instanceof HtaKeyword) || !(value.fields instanceof Map)) throw new Error("hta/value-malformed: invalid pointer");
|
|
335
|
+
output.push(TAG.pointer); writeValue(output,value.context,depth+1); writeValue(output,value.fields,depth+1);
|
|
336
|
+
}
|
|
337
|
+
else if (value instanceof HtaAtom) { output.push(TAG.atom); writeValue(output,value.value,depth+1); }
|
|
338
|
+
else if (value instanceof HtaArray) { output.push(TAG.array); writeSequence(output,value.values,depth); }
|
|
339
|
+
else if (value instanceof HtaObject) { output.push(TAG.object); writeU32(output,value.entries.length);for(const [key,item] of value.entries){writeValue(output,key,depth+1);writeValue(output,item,depth+1);} }
|
|
340
|
+
else if (value instanceof HtaMapEntry) { output.push(TAG.mapEntry); writeSequence(output,[value.key,value.value],depth); }
|
|
341
|
+
else if (value instanceof HtaTuple) { output.push(TAG.tuple); writeSequence(output,value.values,depth); }
|
|
342
|
+
else if (value instanceof HtaCons) { output.push(TAG.cons); writeSequence(output,value.values,depth); }
|
|
343
|
+
else if (value instanceof HtaQueue) { output.push(TAG.queue); writeSequence(output,value.values,depth); }
|
|
344
|
+
else if (value instanceof HtaDeque) { output.push(TAG.deque); writeSequence(output,value.values,depth); }
|
|
345
|
+
else if (value instanceof HtaOrderedSet) { output.push(TAG.orderedSet); writeSequence(output,value.values,depth); }
|
|
346
|
+
else if (value instanceof HtaSortedSet) { output.push(TAG.sortedSet); writeSequence(output,value.values,depth); }
|
|
347
|
+
else if (value instanceof HtaOrderedMap || value instanceof HtaSortedMap || value instanceof HtaTrie || value instanceof HtaPriorityMap) { output.push(value instanceof HtaOrderedMap?TAG.orderedMap:value instanceof HtaSortedMap?TAG.sortedMap:value instanceof HtaTrie?TAG.trie:TAG.priorityMap);writeU32(output,value.entries.length);for(const [key,item] of value.entries){writeValue(output,key,depth+1);writeValue(output,item,depth+1);} }
|
|
348
|
+
else if (value instanceof HtaTagged) { output.push(TAG.tagged);writeValue(output,value.tag,depth+1);writeValue(output,value.value,depth+1); }
|
|
349
|
+
else if (value instanceof HtaExceptionInfo) { output.push(TAG.exceptionInfo);writeValue(output,value.message,depth+1);writeValue(output,value.data,depth+1);writeValue(output,value.cause,depth+1);writeValue(output,value.provenance,depth+1); }
|
|
350
|
+
else if (value instanceof HtaStruct) { output.push(TAG.struct);writeValue(output,value.name,depth+1);writeValue(output,value.fields,depth+1);writeValue(output,value.values,depth+1); }
|
|
351
|
+
else if (value instanceof HtaHandle) { if(value.released)throw new Error("hta/handle-released");output.push(TAG.handle);writeBytes(output,encoder.encode(value.owner));writeBytes(output,encoder.encode(value.type));writeI64(output,value.id); }
|
|
352
|
+
else if (Array.isArray(value)) { output.push(TAG.vector); writeSequence(output, value, depth); }
|
|
353
|
+
else if (value instanceof Set) { output.push(TAG.set); writeCanonical(output, [...value], depth); }
|
|
354
|
+
else if (value instanceof Map) {
|
|
355
|
+
const entries = [...value].map(([key, item]) => [bare(key,depth+1), bare(item,depth+1)]).sort((a, b) => compare(a[0], b[0]));
|
|
356
|
+
output.push(TAG.map); writeU32(output, entries.length);
|
|
357
|
+
for (const [key, item] of entries) { appendBytes(output,key); appendBytes(output,item); }
|
|
358
|
+
} else throw new Error(`hta/value-unsupported: ${Object.prototype.toString.call(value)}`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function bare(value,depth) { const output = []; writeValue(output, value, depth); return output; }
|
|
362
|
+
function writeSequence(output, values, depth) { writeU32(output, values.length); for (const value of values) writeValue(output, value, depth+1); }
|
|
363
|
+
function writeCanonical(output, values, depth) { const encoded = values.map(value=>bare(value,depth+1)).sort(compare); writeU32(output, encoded.length); for (const value of encoded) appendBytes(output,value); }
|
|
364
|
+
function compare(left, right) { for (let i=0;i<Math.min(left.length,right.length);i++) if(left[i]!==right[i]) return left[i]-right[i]; return left.length-right.length; }
|
|
365
|
+
function writeBytes(output, bytes) { writeU32(output, bytes.length); appendBytes(output,bytes); }
|
|
366
|
+
function appendBytes(output,bytes){if(output.length+bytes.length>HTA_MAX_FRAME_BYTES)throw new Error("hta/value-too-large: frame exceeds 64 MiB");for(let offset=0;offset<bytes.length;offset++)output.push(bytes[offset]);}
|
|
367
|
+
function writeU32(output, value) { if(value<0||value>0xffff_ffff)throw new Error("hta/value-too-large"); output.push(value>>>24,(value>>>16)&255,(value>>>8)&255,value&255); }
|
|
368
|
+
function writeI64(output, value) { const normalized=BigInt.asUintN(64,value); for(let shift=56n;shift>=0n;shift-=8n)output.push(Number((normalized>>shift)&255n)); }
|
|
369
|
+
function writeF64(output, value) { const bytes=new Uint8Array(8);new DataView(bytes.buffer).setFloat64(0,value,false);output.push(...bytes); }
|
|
370
|
+
function canonicalInteger(value) {
|
|
371
|
+
return value>=BigInt(Number.MIN_SAFE_INTEGER)&&value<=BigInt(Number.MAX_SAFE_INTEGER)?Number(value):value;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
class Reader {
|
|
375
|
+
constructor(bytes, cursor) { this.bytes=bytes; this.cursor=cursor; }
|
|
376
|
+
take(size) { const end=this.cursor+size; if(end>this.bytes.length)throw new Error("hta/value-malformed: truncated value"); const value=this.bytes.subarray(this.cursor,end);this.cursor=end;return value; }
|
|
377
|
+
u32() { const value=this.take(4); return ((value[0]*0x1000000)+(value[1]<<16)+(value[2]<<8)+value[3])>>>0; }
|
|
378
|
+
data() { return this.take(this.u32()); }
|
|
379
|
+
sequence(depth) { const size=this.u32();if(size>this.bytes.length-this.cursor)throw new Error("hta/value-malformed: impossible sequence length");const result=[]; for(let i=0;i<size;i++)result.push(this.value(depth+1)); return result; }
|
|
380
|
+
value(depth=0) {
|
|
381
|
+
if(depth>HTA_MAX_NESTING_DEPTH)throw new Error("hta/value-too-deep: nesting exceeds 256");
|
|
382
|
+
const tag=this.take(1)[0];
|
|
383
|
+
if(tag===TAG.nil)return null;if(tag===TAG.false)return false;if(tag===TAG.true)return true;
|
|
384
|
+
if(tag===TAG.i64){const bytes=this.take(8);let value=0n;for(const byte of bytes)value=(value<<8n)|BigInt(byte);value=BigInt.asIntN(64,value);return canonicalInteger(value);}
|
|
385
|
+
if(tag===TAG.bigInteger){const text=decoder.decode(this.data());if(!/^-?(0|[1-9][0-9]*)$/.test(text)||text==="-0")throw new Error("hta/value-malformed: invalid big integer");return canonicalInteger(BigInt(text));}
|
|
386
|
+
if(tag===TAG.f64){const bytes=this.take(8);const value=new DataView(bytes.buffer,bytes.byteOffset,8).getFloat64(0,false);if(!Number.isFinite(value))throw new Error("hta/non-finite number");return value;}
|
|
387
|
+
if(tag===TAG.string)return decoder.decode(this.data());if(tag===TAG.bytes)return this.data().slice();
|
|
388
|
+
if(tag===TAG.keyword)return new HtaKeyword(decoder.decode(this.data()));if(tag===TAG.symbol)return new HtaSymbol(decoder.decode(this.data()));
|
|
389
|
+
if(tag===TAG.namespace)return new HtaNamespace(decoder.decode(this.data()));
|
|
390
|
+
if(tag===TAG.character){const codePoint=this.u32();if(codePoint>0x10ffff||(codePoint>=0xd800&&codePoint<=0xdfff))throw new Error("hta/value-malformed: invalid character scalar");return new HtaCharacter(codePoint);}
|
|
391
|
+
if(tag===TAG.regex)return new HtaRegex(decoder.decode(this.data()));
|
|
392
|
+
if(tag===TAG.list||tag===TAG.vector)return this.sequence(depth);if(tag===TAG.set)return new Set(this.sequence(depth));
|
|
393
|
+
if(tag===TAG.tuple)return new HtaTuple(this.sequence(depth));if(tag===TAG.mapEntry){const values=this.sequence(depth);if(values.length!==2)throw new Error("hta/value-malformed: map entry must contain two values");return new HtaMapEntry(values[0],values[1]);}if(tag===TAG.cons)return new HtaCons(this.sequence(depth));if(tag===TAG.queue)return new HtaQueue(this.sequence(depth));if(tag===TAG.deque)return new HtaDeque(this.sequence(depth));
|
|
394
|
+
if(tag===TAG.orderedSet)return new HtaOrderedSet(this.sequence(depth));if(tag===TAG.sortedSet)return new HtaSortedSet(this.sequence(depth));
|
|
395
|
+
if(tag===TAG.map){const size=this.u32();if(size>(this.bytes.length-this.cursor)/2)throw new Error("hta/value-malformed: impossible map length");const result=new Map();for(let i=0;i<size;i++)result.set(this.value(depth+1),this.value(depth+1));return result;}
|
|
396
|
+
if(tag===TAG.orderedMap||tag===TAG.sortedMap||tag===TAG.trie||tag===TAG.priorityMap){const size=this.u32();if(size>(this.bytes.length-this.cursor)/2)throw new Error("hta/value-malformed: impossible map length");const entries=[];for(let i=0;i<size;i++)entries.push([this.value(depth+1),this.value(depth+1)]);return tag===TAG.orderedMap?new HtaOrderedMap(entries):tag===TAG.sortedMap?new HtaSortedMap(entries):tag===TAG.trie?new HtaTrie(entries):new HtaPriorityMap(entries);}
|
|
397
|
+
if(tag===TAG.var)throw new Error("hta/value-malformed: legacy var tag is not supported; use var-ref");
|
|
398
|
+
if(tag===TAG.varRef){const symbol=this.value(depth+1);if(!(symbol instanceof HtaSymbol)||!symbol.name.includes("/"))throw new Error("hta/value-malformed: invalid Var reference");return new HtaVar(symbol);}
|
|
399
|
+
if(tag===TAG.atom)return new HtaAtom(this.value(depth+1));
|
|
400
|
+
if(tag===TAG.array)return new HtaArray(this.sequence(depth));
|
|
401
|
+
if(tag===TAG.object){const size=this.u32();if(size>(this.bytes.length-this.cursor)/2)throw new Error("hta/value-malformed: impossible object length");const entries=[];for(let i=0;i<size;i++){const key=this.value(depth+1);if(typeof key!=="string")throw new Error("hta/value-malformed: invalid object key");entries.push([key,this.value(depth+1)]);}return new HtaObject(entries);}
|
|
402
|
+
if(tag===TAG.handle){const owner=decoder.decode(this.data()),type=decoder.decode(this.data()),bytes=this.take(8);let id=0n;for(const byte of bytes)id=(id<<8n)|BigInt(byte);return new HtaHandle(owner,type,id);}
|
|
403
|
+
if(tag===TAG.tagged){const tagValue=this.value(depth+1);if(!(tagValue instanceof HtaSymbol))throw new Error("hta/value-malformed: invalid tagged literal tag");return new HtaTagged(tagValue,this.value(depth+1));}
|
|
404
|
+
if(tag===TAG.exceptionInfo){const message=this.value(depth+1);if(typeof message!=="string")throw new Error("hta/value-malformed: invalid exception message");return new HtaExceptionInfo(message,this.value(depth+1),this.value(depth+1),this.value(depth+1));}
|
|
405
|
+
if(tag===TAG.struct){const name=this.value(depth+1),fields=this.value(depth+1),values=this.value(depth+1);if(typeof name!=="string"||!Array.isArray(fields)||!fields.every(field=>typeof field==="string")||!Array.isArray(values)||fields.length!==values.length)throw new Error("hta/value-malformed: invalid struct");return new HtaStruct(name,fields,values);}
|
|
406
|
+
if(tag===TAG.pointer){const context=this.value(depth+1),fields=this.value(depth+1);if(!(context instanceof HtaKeyword)||!(fields instanceof Map))throw new Error("hta/value-malformed: invalid pointer");return new HtaPointer(context,fields);}
|
|
407
|
+
throw new Error(`hta/value-malformed: unknown value tag ${tag}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export class HtaContext {
|
|
412
|
+
constructor({ worker, moduleUrl, moduleBytes, libraryUrl, libraryBytes, providerUrl, hostCalls = {}, capabilities = [], filesystemHost = hostCalls.filesystemHost ?? null, handleTags = {}, promiseProvider = new BrowserPromiseProvider(), kernelId = null, manifest = null, instrumentation = false, onProviderEvent = null }) {
|
|
413
|
+
this.worker=worker;this.hostCalls=hostCalls;this.filesystemHost=filesystemHost;this.handleTags=handleTags;this.promiseProvider=promiseProvider;this.kernelId=kernelId;this.manifest=manifest;this.instrumentation=instrumentation===true||typeof onProviderEvent === "function";this.onProviderEvent=typeof onProviderEvent === "function"?onProviderEvent:null;this.allowedExports=manifest ? new Set(manifest.exports) : null;this.operations=manifest?.operations ?? Object.create(null);this.allowedHostCalls=manifest ? new Set(Object.entries(manifest.hostCalls).flatMap(([service,methods])=>methods.map(method=>`${service}/${method}`))) : null;this.hostCallCapabilities=manifest?.hostCallCapabilities ?? Object.create(null);this.capabilities=new Set(capabilities);this.hostCallsInFlight=new Map();this.handles=new Set();this.next=1;this.pending=new Map();this.sessions=new Map();this.mounts=new Set();this.closed=false;this.closePromise=null;this.workerClosed=new Promise(resolve=>{this.resolveWorkerClosed=resolve;});
|
|
414
|
+
if (manifest?.capabilities.some(capability=>!capabilities.includes(capability)) ||
|
|
415
|
+
Object.values(this.hostCallCapabilities).flat().some(capability=>!this.capabilities.has(capability))) {
|
|
416
|
+
throw new Error(`hta/capability-denied: ${manifest?.namespace ?? "HTA"}`);
|
|
417
|
+
}
|
|
418
|
+
this.ready=new Promise((resolve,reject)=>{this.readyResolve=resolve;this.readyReject=reject;});
|
|
419
|
+
this.ready.catch(()=>{});
|
|
420
|
+
worker.addEventListener("message", event=>this.message(event.data));
|
|
421
|
+
worker.addEventListener("error", error=>this.fail(error));
|
|
422
|
+
worker.postMessage({type:"init",backend:providerUrl ? "provider" : "wasm",providerUrl,moduleUrl,moduleBytes,libraryUrl,libraryBytes,instrumentation:this.instrumentation});
|
|
423
|
+
}
|
|
424
|
+
call(target, args=[]) { let id=null,cancelled=false;
|
|
425
|
+
return this.promiseProvider.create((resolve,reject,onCancel)=>{
|
|
426
|
+
onCancel(()=>{cancelled=true;if(id!==null){this.pending.delete(id);this.worker.postMessage({type:"cancel",id});id=null;}});
|
|
427
|
+
this.ready.then(()=>{if(cancelled)return;if(this.closed)throw new Error("hta/context-closed");if(this.allowedExports && !this.allowedExports.has(target))throw new Error(`hta/export-denied: ${target}`);validateHandles(args,this);id=this.next++;this.pending.set(id,{resolve,reject});this.worker.postMessage({type:"call",id,frame:encodeHta([this.operations[target]??target,args])});}).catch(reject);
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
releaseHandle(handle){if(handle.context!==this)throw new Error("hta/handle-owner-mismatch");const key=handleKey(handle);if(!this.handles.delete(key))throw new Error("hta/handle-stale");if(this.closed)return;const wireHandle=new HtaHandle(handle.owner,handle.type,handle.id);this.worker.postMessage({type:"release",frame:encodeHta(wireHandle)});}
|
|
431
|
+
async createSession(name){await this.call("session/create",[name]);return this.session(name);}
|
|
432
|
+
async createFilesystem(descriptor={provider:"memory"}){
|
|
433
|
+
if(!descriptor||typeof descriptor!=="object")throw new Error("filesystem/descriptor-invalid");
|
|
434
|
+
if(!this.filesystemHost)throw new Error("filesystem/host-unavailable");
|
|
435
|
+
const wire=new Map(Object.entries(descriptor));
|
|
436
|
+
const mountId=await this.call("filesystem/create",[wire]);
|
|
437
|
+
try{await this.filesystemHost.register(this,mountId,descriptor);this.mounts.add(mountId);return mountId;}
|
|
438
|
+
catch(error){await this.call("filesystem/close",[mountId]).catch(()=>{});throw error;}
|
|
439
|
+
}
|
|
440
|
+
async filesystemInfo(mountId){return this.call("filesystem/info",[mountId]);}
|
|
441
|
+
async closeFilesystem(mountId){const result=await this.call("filesystem/close",[mountId]);await this.filesystemHost.close(this,mountId);this.mounts.delete(mountId);return result;}
|
|
442
|
+
session(name="ROOT"){let session=this.sessions.get(name);if(!session){session=new HtaSession(this,name);this.sessions.set(name,session);}return session;}
|
|
443
|
+
listSessions(){return this.call("session/list",[]);}
|
|
444
|
+
async message(message) {
|
|
445
|
+
try {
|
|
446
|
+
if(message.type==="ready"){this.readyResolve();return;}if(message.type==="closed"){this.resolveWorkerClosed();return;}if(message.type==="provider-event"){try{this.onProviderEvent?.(message.event);}catch{}return;}if(message.type==="fatal"){this.fail(new Error(message.error?.message??"HTA worker failed"));return;}
|
|
447
|
+
if(message.type==="result"){const pending=this.pending.get(message.id);if(!pending)return;this.pending.delete(message.id);try{const value=bindHandles(decodeHta(message.frame),this);message.ok?pending.resolve(value):pending.reject(errorFrom(value));}catch(error){pending.reject(error);}return;}
|
|
448
|
+
if(message.type==="host-cancel"){for(const item of message.calls ?? [])this.hostCallsInFlight.get(item.call)?.controller.abort(new Error("cancelled"));return;}
|
|
449
|
+
if(message.type==="host-call"){const key=`${message.service}/${message.method}`,handler=this.hostCalls[key],sessionId=message.session??"ROOT";if(this.hostCallsInFlight.has(message.call))return;const controller=new AbortController();this.hostCallsInFlight.set(message.call,{controller});try{if(this.allowedHostCalls && !this.allowedHostCalls.has(key))throw new Error(`hta/host-call-denied: ${key}`);if ((this.hostCallCapabilities[key] ?? []).some(capability=>!this.capabilities.has(capability))) throw new Error(`hta/capability-denied: ${key}`);if(!handler)throw new Error(`hta/host-call-denied: ${key}`);const argumentsValue=bindHandles(decodeHta(message.frame),this);validateHandles(argumentsValue,this);const value=await handler.call({context:this.session(sessionId),kernelContext:this,kernelId:this.kernelId??null,sessionId,mountId:message.mount??null,task:message.task,signal:controller.signal},...argumentsValue);if(!this.closed)this.worker.postMessage({type:"delivery",call:message.call,ok:true,frame:encodeHta(value)});}catch(error){if(!this.closed)this.worker.postMessage({type:"delivery",call:message.call,ok:false,frame:encodeHta(errorValue(error))});}finally{this.hostCallsInFlight.delete(message.call);}}
|
|
450
|
+
} catch(error) { this.fail(error); }
|
|
451
|
+
}
|
|
452
|
+
fail(error){if(this.closed)return;this.closed=true;this.resolveWorkerClosed();this.readyReject(error);for(const pending of this.pending.values())pending.reject(error);this.pending.clear();for(const {controller} of this.hostCallsInFlight.values())controller.abort(error);this.hostCallsInFlight.clear();this.worker.postMessage({type:"close"});this.worker.terminate();}
|
|
453
|
+
close(){if(this.closePromise)return this.closePromise;const error=new Error("hta/context-closed");if(!this.closed){this.closed=true;this.readyReject(error);for(const pending of this.pending.values())pending.reject(error);this.pending.clear();}for(const {controller} of this.hostCallsInFlight.values())controller.abort(error);this.hostCallsInFlight.clear();for(const key of this.handles){const [owner,type,id]=key.split("\u0000");this.worker.postMessage({type:"release",frame:encodeHta(new HtaHandle(owner,type,BigInt(id)))});}this.handles.clear();const closes=[...this.mounts].map(mountId=>this.filesystemHost?.close(this,mountId).catch(()=>{}));this.mounts.clear();this.worker.postMessage({type:"close"});let timeout;const timeoutPromise=new Promise(resolve=>{timeout=setTimeout(resolve,1000);});const workerClose=Promise.race([this.workerClosed,timeoutPromise]).finally(()=>{clearTimeout(timeout);this.worker.terminate();});this.closePromise=Promise.all([...closes,workerClose]);return this.closePromise;}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export class HtaSession {
|
|
457
|
+
constructor(context,name){if(typeof name!=="string"||!name.length)throw new Error("INVALID_SESSION_NAME");this.context=context;this.name=name;}
|
|
458
|
+
call(target,args=[]){if(target==="eval")return this.eval(args[0]);if(target==="eval-vm")return this.evalVm(args[0]);if(target==="eval-bound")return this.evalBound(args[0],args[1]);if(target==="complete")return this.complete(args[0]);return this.context.call(target,args);}
|
|
459
|
+
eval(source){return this.context.call("session/eval",[this.name,source]);}
|
|
460
|
+
evalVm(source){return this.context.call("session/eval-vm",[this.name,source]);}
|
|
461
|
+
prepareVm(source){return this.context.call("session/prepare-vm",[this.name,source]);}
|
|
462
|
+
invokeVm(program){return this.context.call("session/invoke-vm",[this.name,program]);}
|
|
463
|
+
evalBound(source,bindings=[]){return this.context.call("session/eval-bound",[this.name,source,bindings]);}
|
|
464
|
+
complete(prefix){return this.context.call("session/complete",[this.name,prefix]);}
|
|
465
|
+
info(){return this.context.call("session/info",[this.name]);}
|
|
466
|
+
async attachFilesystem(mountId){return this.context.call("session/attach-filesystem",[this.name,mountId]);}
|
|
467
|
+
async detachFilesystem(){return this.context.call("session/detach-filesystem",[this.name]);}
|
|
468
|
+
async close(){const result=await this.context.call("session/close",[this.name]);this.context.sessions.delete(this.name);return result;}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function handleKey(handle){return `${handle.owner}\u0000${handle.type}\u0000${handle.id}`;}
|
|
472
|
+
function bindHandles(value,context){if(value instanceof HtaHandle){const tag=context.handleTags[value.type];if(context.manifest && Object.keys(context.handleTags).length && !tag)throw new Error(`hta/handle-type-denied: ${value.type}`);if(context.manifest && tag && ![context.manifest.namespace,context.manifest.identity,tag].includes(value.owner))throw new Error(`hta/handle-owner-mismatch: ${value.owner}`);value.context=context;if(tag){value.displayTag=tag;value.displayKind=value.type;}context.handles.add(handleKey(value));return value;}walkHandles(value,item=>bindHandles(item,context));return value;}
|
|
473
|
+
function validateHandles(value,context){if(value instanceof HtaHandle){if(value.released)throw new Error("hta/handle-released");if(value.context!==context)throw new Error("hta/handle-owner-mismatch");if(!context.handles.has(handleKey(value)))throw new Error("hta/handle-stale");return;}walkHandles(value,item=>validateHandles(item,context));}
|
|
474
|
+
function walkHandles(value,visit){if(Array.isArray(value)){value.forEach(visit);}else if(value instanceof Set){for(const item of value)visit(item);}else if(value instanceof Map){for(const [key,item]of value){visit(key);visit(item);}}else if(value instanceof HtaMapEntry){visit(value.key);visit(value.value);}else if(value instanceof HtaTagged)visit(value.value);else if(value instanceof HtaPointer){visit(value.context);visit(value.fields);}else if(value instanceof HtaStruct){visit(value.fields);visit(value.values);}else if(value instanceof HtaObject){for(const [key,item]of value.entries){visit(key);visit(item);}}else if(value instanceof HtaArray||value instanceof HtaTuple||value instanceof HtaCons||value instanceof HtaQueue||value instanceof HtaDeque||value instanceof HtaOrderedSet||value instanceof HtaSortedSet){value.values.forEach(visit);}else if(value instanceof HtaOrderedMap||value instanceof HtaSortedMap||value instanceof HtaTrie||value instanceof HtaPriorityMap){for(const [key,item]of value.entries){visit(key);visit(item);}}else if(value instanceof HtaVar){visit(value.symbol);}else if(value instanceof HtaAtom){visit(value.value);}else if(value instanceof HtaExceptionInfo){visit(value.message);visit(value.data);visit(value.cause);visit(value.provenance);}}
|
|
475
|
+
function errorValue(error){const code=typeof error?.code === "string" && /^[a-z][a-z0-9-]*(\/[a-z][a-z0-9-]*)+$/.test(error.code)?error.code:"host/error";return new Map([[new HtaKeyword("code"),new HtaKeyword(code)],[new HtaKeyword("message"),String(error?.message??error)],[new HtaKeyword("origin"),new HtaKeyword("browser")],[new HtaKeyword("retryable"),error?.retryable===true]]);}
|
|
476
|
+
function errorFrom(value){if(value instanceof Error)return value;if(value instanceof Map){let message="HTA request failed",code;for(const[key,item]of value)if(key instanceof HtaKeyword&&key.name==="message")message=String(item);else if(key instanceof HtaKeyword&&key.name==="code")code=item instanceof HtaKeyword?item.name:String(item);const error=new Error(message);error.code=code;error.data=value;return error;}return new Error(String(value));}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hara-lang/hta",
|
|
3
|
+
"version": "0.1.9",
|
|
4
|
+
"description": "HTA codecs, browser hosts, and provider transports for Hara",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/hara-lang/hara-native.git",
|
|
10
|
+
"directory": "core/rust/web/packages/hta"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/hara-lang/hara-native/tree/main/core/rust/web/packages/hta#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/hara-lang/hara-native/issues"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./index.js",
|
|
21
|
+
"./provider/browser": "./provider-browser.mjs",
|
|
22
|
+
"./provider/node": "./provider-node.mjs",
|
|
23
|
+
"./sandbox": "./sandbox.js",
|
|
24
|
+
"./shared-worker": "./shared-worker.js",
|
|
25
|
+
"./worker": "./worker.mjs"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"index.js",
|
|
29
|
+
"provider-browser.mjs",
|
|
30
|
+
"provider-common.mjs",
|
|
31
|
+
"provider-node.mjs",
|
|
32
|
+
"sandbox.js",
|
|
33
|
+
"shared-worker.js",
|
|
34
|
+
"worker.mjs",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public",
|
|
39
|
+
"registry": "https://registry.npmjs.org"
|
|
40
|
+
}
|
|
41
|
+
}
|