@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
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { decodeHta, encodeHta, HtaKeyword } from "./index.js";
|
|
2
|
+
import {
|
|
3
|
+
createProviderLifecycle,
|
|
4
|
+
HTA_PROVIDER_EVENT,
|
|
5
|
+
providerError,
|
|
6
|
+
providerErrorCode,
|
|
7
|
+
toHta
|
|
8
|
+
} from "./provider-common.mjs";
|
|
9
|
+
|
|
10
|
+
function errorFrom(value) {
|
|
11
|
+
if (value instanceof Error) return value;
|
|
12
|
+
if (value instanceof Map) {
|
|
13
|
+
let code = "host/error";
|
|
14
|
+
let message = "HTA host call failed";
|
|
15
|
+
let data = value;
|
|
16
|
+
for (const [key, item] of value) {
|
|
17
|
+
const name = key instanceof HtaKeyword ? key.name : String(key);
|
|
18
|
+
if (name === "code") code = item instanceof HtaKeyword ? item.name : String(item);
|
|
19
|
+
if (name === "message") message = String(item);
|
|
20
|
+
}
|
|
21
|
+
const error = new Error(message);
|
|
22
|
+
error.code = code;
|
|
23
|
+
error.data = data;
|
|
24
|
+
return error;
|
|
25
|
+
}
|
|
26
|
+
return new Error(String(value));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Creates the provider side of the HTA transport.
|
|
31
|
+
*
|
|
32
|
+
* The provider receives a third argument with a cancellable signal and a
|
|
33
|
+
* manifest-authorized host-call bridge. Ordinary providers that only accept
|
|
34
|
+
* `(operation, args)` remain source compatible.
|
|
35
|
+
*/
|
|
36
|
+
export function createBrowserProvider(call, options = {}) {
|
|
37
|
+
const scope = options.scope ?? self;
|
|
38
|
+
const lifecycle = createProviderLifecycle({
|
|
39
|
+
origin: options.origin ?? "browser",
|
|
40
|
+
onEvent: options.onEvent
|
|
41
|
+
});
|
|
42
|
+
const cancelled = new Set();
|
|
43
|
+
const calls = new Map();
|
|
44
|
+
const hostCalls = new Map();
|
|
45
|
+
const releases = new Set();
|
|
46
|
+
let nextHostCall = 0;
|
|
47
|
+
let closing = false;
|
|
48
|
+
let closed = false;
|
|
49
|
+
|
|
50
|
+
lifecycle.emit(HTA_PROVIDER_EVENT.START);
|
|
51
|
+
|
|
52
|
+
function rejectHostCalls(error) {
|
|
53
|
+
for (const pending of hostCalls.values()) pending.reject(error);
|
|
54
|
+
hostCalls.clear();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function hostCall(service, method, args = [], metadata = {}) {
|
|
58
|
+
if (closed) return Promise.reject(new Error("hta/provider-closed"));
|
|
59
|
+
const id = ++nextHostCall;
|
|
60
|
+
const eventFields = {
|
|
61
|
+
...(metadata.request === undefined ? {} : { request: Number(metadata.request) }),
|
|
62
|
+
...(metadata.task === undefined ? {} : { task: Number(metadata.task) }),
|
|
63
|
+
call: id,
|
|
64
|
+
service: String(service),
|
|
65
|
+
method: String(method)
|
|
66
|
+
};
|
|
67
|
+
const signal = Object.hasOwn(metadata, "signal") ? metadata.signal : undefined;
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
let abort;
|
|
70
|
+
const cleanup = () => signal?.removeEventListener?.("abort", abort);
|
|
71
|
+
abort = () => {
|
|
72
|
+
if (!hostCalls.delete(id)) return;
|
|
73
|
+
cleanup();
|
|
74
|
+
const error = new Error("hta/host-call-cancelled");
|
|
75
|
+
error.code = "hta/host-call-cancelled";
|
|
76
|
+
lifecycle.emit(HTA_PROVIDER_EVENT.HOST_CALL, {
|
|
77
|
+
...eventFields,
|
|
78
|
+
status: "error",
|
|
79
|
+
code: error.code
|
|
80
|
+
});
|
|
81
|
+
reject(error);
|
|
82
|
+
};
|
|
83
|
+
hostCalls.set(id, {
|
|
84
|
+
eventFields,
|
|
85
|
+
resolve(value) {
|
|
86
|
+
cleanup();
|
|
87
|
+
resolve(value);
|
|
88
|
+
},
|
|
89
|
+
reject(error) {
|
|
90
|
+
cleanup();
|
|
91
|
+
reject(error);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
if (signal?.aborted) {
|
|
95
|
+
abort();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
signal?.addEventListener?.("abort", abort, { once: true });
|
|
99
|
+
scope.postMessage({
|
|
100
|
+
type: "host-call",
|
|
101
|
+
call: id,
|
|
102
|
+
service: String(service),
|
|
103
|
+
method: String(method),
|
|
104
|
+
session: metadata.session,
|
|
105
|
+
mount: metadata.mount,
|
|
106
|
+
task: metadata.task,
|
|
107
|
+
frame: encodeHta(toHta(args))
|
|
108
|
+
});
|
|
109
|
+
lifecycle.emit(HTA_PROVIDER_EVENT.HOST_CALL, { ...eventFields, status: "enter" });
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function closeProvider() {
|
|
114
|
+
if (closing) return;
|
|
115
|
+
closing = true;
|
|
116
|
+
const error = new Error("hta/provider-closed");
|
|
117
|
+
for (const controller of calls.values()) controller.abort(error);
|
|
118
|
+
calls.clear();
|
|
119
|
+
let failure = null;
|
|
120
|
+
try {
|
|
121
|
+
await Promise.all([...releases]);
|
|
122
|
+
} catch (releaseError) {
|
|
123
|
+
failure = releaseError;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
await options.close?.();
|
|
127
|
+
} catch (closeError) {
|
|
128
|
+
failure ??= closeError;
|
|
129
|
+
} finally {
|
|
130
|
+
rejectHostCalls(error);
|
|
131
|
+
closed = true;
|
|
132
|
+
if (failure) {
|
|
133
|
+
lifecycle.emit(HTA_PROVIDER_EVENT.FAILURE, {
|
|
134
|
+
status: "error",
|
|
135
|
+
code: providerErrorCode(failure, options.errorCode)
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
lifecycle.shutdown({
|
|
139
|
+
status: failure === null ? "ok" : "error",
|
|
140
|
+
...(failure === null
|
|
141
|
+
? {}
|
|
142
|
+
: { code: providerErrorCode(failure, options.errorCode) })
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
if (failure) {
|
|
146
|
+
scope.postMessage({
|
|
147
|
+
type: "fatal",
|
|
148
|
+
error: { message: String(failure?.message ?? failure) }
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function handle(message) {
|
|
154
|
+
try {
|
|
155
|
+
if (message.type === "delivery") {
|
|
156
|
+
const pending = hostCalls.get(message.call);
|
|
157
|
+
if (!pending) return;
|
|
158
|
+
hostCalls.delete(message.call);
|
|
159
|
+
try {
|
|
160
|
+
const value = decodeHta(message.frame);
|
|
161
|
+
const error = message.ok ? null : errorFrom(value);
|
|
162
|
+
lifecycle.emit(HTA_PROVIDER_EVENT.HOST_CALL, {
|
|
163
|
+
...pending.eventFields,
|
|
164
|
+
status: message.ok ? "ok" : "error",
|
|
165
|
+
...(error ? { code: providerErrorCode(error, options.errorCode) } : {})
|
|
166
|
+
});
|
|
167
|
+
message.ok ? pending.resolve(value) : pending.reject(error);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
pending.reject(error);
|
|
170
|
+
}
|
|
171
|
+
} else if (message.type === "release") {
|
|
172
|
+
if (typeof options.release !== "function") {
|
|
173
|
+
throw new Error("hta/handle-release-unsupported");
|
|
174
|
+
}
|
|
175
|
+
const release = Promise.resolve().then(() => options.release(decodeHta(message.frame)));
|
|
176
|
+
releases.add(release);
|
|
177
|
+
try {
|
|
178
|
+
await release;
|
|
179
|
+
lifecycle.emit(HTA_PROVIDER_EVENT.RELEASE, { status: "ok" });
|
|
180
|
+
} catch (error) {
|
|
181
|
+
lifecycle.emit(HTA_PROVIDER_EVENT.RELEASE, {
|
|
182
|
+
status: "error",
|
|
183
|
+
code: providerErrorCode(error, options.errorCode)
|
|
184
|
+
});
|
|
185
|
+
throw error;
|
|
186
|
+
} finally {
|
|
187
|
+
releases.delete(release);
|
|
188
|
+
}
|
|
189
|
+
} else if (message.type === "cancel") {
|
|
190
|
+
const controller = calls.get(message.id);
|
|
191
|
+
if (controller) {
|
|
192
|
+
cancelled.add(message.id);
|
|
193
|
+
controller.abort(new Error("cancelled"));
|
|
194
|
+
lifecycle.emit(HTA_PROVIDER_EVENT.CANCEL, { request: Number(message.id) });
|
|
195
|
+
}
|
|
196
|
+
} else if (message.type === "close") {
|
|
197
|
+
await closeProvider();
|
|
198
|
+
} else if (message.type === "call") {
|
|
199
|
+
if (closing) throw new Error("hta/provider-closed");
|
|
200
|
+
const [operation, args] = decodeHta(message.frame);
|
|
201
|
+
lifecycle.emit(HTA_PROVIDER_EVENT.CALL_ENTER, {
|
|
202
|
+
request: Number(message.id),
|
|
203
|
+
operation: String(operation)
|
|
204
|
+
});
|
|
205
|
+
const controller = new AbortController();
|
|
206
|
+
calls.set(message.id, controller);
|
|
207
|
+
const context = Object.freeze({
|
|
208
|
+
signal: controller.signal,
|
|
209
|
+
hostCall(service, method, values = [], metadata = {}) {
|
|
210
|
+
return hostCall(service, method, values, {
|
|
211
|
+
...metadata,
|
|
212
|
+
task: metadata.task ?? message.id,
|
|
213
|
+
request: metadata.request ?? message.id,
|
|
214
|
+
signal: Object.hasOwn(metadata, "signal") ? metadata.signal : controller.signal
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
try {
|
|
219
|
+
const value = await call(operation, args, context);
|
|
220
|
+
if (!cancelled.has(message.id) && !closing) {
|
|
221
|
+
scope.postMessage({
|
|
222
|
+
type: "result",
|
|
223
|
+
id: message.id,
|
|
224
|
+
ok: true,
|
|
225
|
+
frame: encodeHta(toHta(value))
|
|
226
|
+
});
|
|
227
|
+
lifecycle.emit(HTA_PROVIDER_EVENT.CALL_RETURN, {
|
|
228
|
+
request: Number(message.id),
|
|
229
|
+
operation: String(operation),
|
|
230
|
+
status: "ok"
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if (!cancelled.has(message.id) && !closing) {
|
|
235
|
+
scope.postMessage({
|
|
236
|
+
type: "result",
|
|
237
|
+
id: message.id,
|
|
238
|
+
ok: false,
|
|
239
|
+
frame: encodeHta(providerError(error, "browser", options.errorCode))
|
|
240
|
+
});
|
|
241
|
+
lifecycle.emit(HTA_PROVIDER_EVENT.CALL_ERROR, {
|
|
242
|
+
request: Number(message.id),
|
|
243
|
+
operation: String(operation),
|
|
244
|
+
status: "error",
|
|
245
|
+
code: providerErrorCode(error, options.errorCode)
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
} finally {
|
|
249
|
+
calls.delete(message.id);
|
|
250
|
+
cancelled.delete(message.id);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} catch (error) {
|
|
254
|
+
scope.postMessage({ type: "fatal", error: { message: String(error?.message ?? error) } });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return Object.freeze({ close: closeProvider, handle });
|
|
259
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { HtaKeyword, HtaMapEntry } from "./index.js";
|
|
2
|
+
|
|
3
|
+
export const HTA_PROVIDER_EVENT_SCHEMA = "hara.hta.provider.event/0-alpha";
|
|
4
|
+
|
|
5
|
+
export const HTA_PROVIDER_EVENT = Object.freeze({
|
|
6
|
+
START: "start",
|
|
7
|
+
CALL_ENTER: "call-enter",
|
|
8
|
+
CALL_RETURN: "call-return",
|
|
9
|
+
CALL_ERROR: "call-error",
|
|
10
|
+
HOST_CALL: "host-call",
|
|
11
|
+
CALLBACK: "callback",
|
|
12
|
+
CANCEL: "cancel",
|
|
13
|
+
RELEASE: "release",
|
|
14
|
+
FAILURE: "failure",
|
|
15
|
+
TERMINAL: "terminal",
|
|
16
|
+
SHUTDOWN: "shutdown"
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Creates the host-neutral lifecycle trace used by every HTA provider
|
|
21
|
+
* runner. The trace deliberately contains request/operation identity and
|
|
22
|
+
* terminal status, but never provider values or opaque handle identities.
|
|
23
|
+
*/
|
|
24
|
+
export function createProviderLifecycle(options = {}) {
|
|
25
|
+
const origin = options.origin === undefined ? "provider" : String(options.origin);
|
|
26
|
+
let sequence = 0;
|
|
27
|
+
let closed = false;
|
|
28
|
+
|
|
29
|
+
function emit(event, fields = {}) {
|
|
30
|
+
const record = Object.freeze({
|
|
31
|
+
schema: HTA_PROVIDER_EVENT_SCHEMA,
|
|
32
|
+
sequence: ++sequence,
|
|
33
|
+
origin,
|
|
34
|
+
event,
|
|
35
|
+
...fields
|
|
36
|
+
});
|
|
37
|
+
try {
|
|
38
|
+
options.onEvent?.(record);
|
|
39
|
+
} catch {
|
|
40
|
+
// Instrumentation must not change provider semantics.
|
|
41
|
+
}
|
|
42
|
+
return record;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return Object.freeze({
|
|
46
|
+
emit,
|
|
47
|
+
isClosed() {
|
|
48
|
+
return closed;
|
|
49
|
+
},
|
|
50
|
+
shutdown(fields = {}) {
|
|
51
|
+
if (closed) return null;
|
|
52
|
+
closed = true;
|
|
53
|
+
return emit(HTA_PROVIDER_EVENT.SHUTDOWN, fields);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function providerErrorCode(error, fallback = "provider/error") {
|
|
59
|
+
const message = String(error?.message ?? error);
|
|
60
|
+
const separator = message.indexOf(":");
|
|
61
|
+
return separator > 0 ? message.slice(0, separator) : fallback;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function toHta(value) {
|
|
65
|
+
if (value === null || value === undefined || typeof value !== "object") {
|
|
66
|
+
return value ?? null;
|
|
67
|
+
}
|
|
68
|
+
if (value instanceof HtaKeyword || value instanceof HtaMapEntry || value instanceof Uint8Array) return value;
|
|
69
|
+
if (Array.isArray(value)) return value.map(toHta);
|
|
70
|
+
if (value instanceof Map) {
|
|
71
|
+
const result = new Map();
|
|
72
|
+
for (const [key, item] of value) result.set(key, toHta(item));
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
const result = new Map();
|
|
76
|
+
for (const [key, item] of Object.entries(value)) {
|
|
77
|
+
result.set(new HtaKeyword(key), toHta(item));
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function providerError(error, origin, fallbackCode = "provider/error") {
|
|
83
|
+
const message = String(error?.message ?? error);
|
|
84
|
+
const code = providerErrorCode(error, fallbackCode);
|
|
85
|
+
return new Map([
|
|
86
|
+
[new HtaKeyword("code"), new HtaKeyword(code)],
|
|
87
|
+
[new HtaKeyword("message"), message],
|
|
88
|
+
[new HtaKeyword("origin"), new HtaKeyword(origin)],
|
|
89
|
+
[new HtaKeyword("retryable"), false]
|
|
90
|
+
]);
|
|
91
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { decodeHta, encodeHta } from "./index.js";
|
|
2
|
+
import {
|
|
3
|
+
createProviderLifecycle,
|
|
4
|
+
HTA_PROVIDER_EVENT,
|
|
5
|
+
providerError,
|
|
6
|
+
providerErrorCode,
|
|
7
|
+
toHta
|
|
8
|
+
} from "./provider-common.mjs";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Serves a provider module over the Node process framing used by the Rust
|
|
12
|
+
* loader. The framing is Node-specific; lifecycle, cancellation, release,
|
|
13
|
+
* and provider context semantics are shared with the browser runner.
|
|
14
|
+
*/
|
|
15
|
+
export function serveNodeProvider(call, options = {}) {
|
|
16
|
+
const input = options.input ?? process.stdin;
|
|
17
|
+
const output = options.output ?? process.stdout;
|
|
18
|
+
const exit = options.exit ?? (code => process.exit(code));
|
|
19
|
+
const maxFrameSize = options.maxFrameSize ?? 64 * 1024 * 1024;
|
|
20
|
+
const lifecycle = createProviderLifecycle({
|
|
21
|
+
origin: options.origin ?? "node",
|
|
22
|
+
onEvent: options.onEvent
|
|
23
|
+
});
|
|
24
|
+
const cancelled = new Set();
|
|
25
|
+
const controllers = new Map();
|
|
26
|
+
const inFlight = new Map();
|
|
27
|
+
let nextHostCall = 0;
|
|
28
|
+
let buffered = new Uint8Array();
|
|
29
|
+
let expected = null;
|
|
30
|
+
let closing = false;
|
|
31
|
+
let closePromise = null;
|
|
32
|
+
let exited = false;
|
|
33
|
+
|
|
34
|
+
if (options.redirectConsole !== false) {
|
|
35
|
+
console.log = (...values) => console.error(...values);
|
|
36
|
+
console.info = (...values) => console.error(...values);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
input.on("data", chunk => {
|
|
40
|
+
try {
|
|
41
|
+
const bytes = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
|
|
42
|
+
const next = new Uint8Array(buffered.length + bytes.length);
|
|
43
|
+
next.set(buffered);
|
|
44
|
+
next.set(bytes, buffered.length);
|
|
45
|
+
buffered = next;
|
|
46
|
+
drain();
|
|
47
|
+
} catch (error) {
|
|
48
|
+
reportFatal(error);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
input.on("end", () => {
|
|
52
|
+
void closeProvider().catch(() => {}).finally(finish);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function drain() {
|
|
56
|
+
while (true) {
|
|
57
|
+
if (expected === null) {
|
|
58
|
+
if (buffered.length < 4) return;
|
|
59
|
+
expected = new DataView(buffered.buffer, buffered.byteOffset, 4).getUint32(0, false);
|
|
60
|
+
buffered = buffered.slice(4);
|
|
61
|
+
if (expected === 0 || expected > maxFrameSize) {
|
|
62
|
+
throw new Error("hta/process-frame-size");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (buffered.length < expected) return;
|
|
66
|
+
const frame = buffered.slice(0, expected);
|
|
67
|
+
buffered = buffered.slice(expected);
|
|
68
|
+
expected = null;
|
|
69
|
+
void dispatch(decodeHta(frame)).catch(reportFatal);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function dispatch(frame) {
|
|
74
|
+
const [kind, id, operation, args] = frame;
|
|
75
|
+
if (kind === "handshake") {
|
|
76
|
+
lifecycle.emit(HTA_PROVIDER_EVENT.START, {
|
|
77
|
+
namespace: typeof operation === "string" ? operation : undefined
|
|
78
|
+
});
|
|
79
|
+
write(["ready", 1]);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (kind === "shutdown") {
|
|
83
|
+
await closeProvider();
|
|
84
|
+
finish();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (kind === "cancel") {
|
|
88
|
+
const requestId = Number(id);
|
|
89
|
+
const controller = controllers.get(requestId);
|
|
90
|
+
if (controller) {
|
|
91
|
+
cancelled.add(requestId);
|
|
92
|
+
controller.abort(new Error("cancelled"));
|
|
93
|
+
lifecycle.emit(HTA_PROVIDER_EVENT.CANCEL, { request: requestId });
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (kind === "release") {
|
|
98
|
+
if (typeof options.release !== "function") {
|
|
99
|
+
throw new Error("hta/handle-release-unsupported");
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
await options.release(id);
|
|
103
|
+
lifecycle.emit(HTA_PROVIDER_EVENT.RELEASE, { status: "ok" });
|
|
104
|
+
} catch (error) {
|
|
105
|
+
lifecycle.emit(HTA_PROVIDER_EVENT.RELEASE, {
|
|
106
|
+
status: "error",
|
|
107
|
+
code: providerErrorCode(error, options.errorCode)
|
|
108
|
+
});
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (kind !== "call") throw new Error(`hta/process-event-unknown: ${kind}`);
|
|
114
|
+
if (closing) throw new Error("hta/provider-closed");
|
|
115
|
+
|
|
116
|
+
const requestId = Number(id);
|
|
117
|
+
const controller = new AbortController();
|
|
118
|
+
controllers.set(requestId, controller);
|
|
119
|
+
lifecycle.emit(HTA_PROVIDER_EVENT.CALL_ENTER, {
|
|
120
|
+
request: requestId,
|
|
121
|
+
operation: String(operation)
|
|
122
|
+
});
|
|
123
|
+
const context = Object.freeze({
|
|
124
|
+
signal: controller.signal,
|
|
125
|
+
hostCall(service, method, values = [], metadata = {}) {
|
|
126
|
+
if (typeof options.hostCall !== "function") {
|
|
127
|
+
return Promise.reject(new Error("hta/host-call-unsupported: node provider has no host bridge"));
|
|
128
|
+
}
|
|
129
|
+
const call = ++nextHostCall;
|
|
130
|
+
const fields = {
|
|
131
|
+
request: requestId,
|
|
132
|
+
call,
|
|
133
|
+
service: String(service),
|
|
134
|
+
method: String(method)
|
|
135
|
+
};
|
|
136
|
+
lifecycle.emit(HTA_PROVIDER_EVENT.HOST_CALL, { ...fields, status: "enter" });
|
|
137
|
+
return Promise.resolve()
|
|
138
|
+
.then(() => options.hostCall(service, method, values, {
|
|
139
|
+
...metadata,
|
|
140
|
+
request: requestId,
|
|
141
|
+
call,
|
|
142
|
+
signal: controller.signal
|
|
143
|
+
}))
|
|
144
|
+
.then(value => {
|
|
145
|
+
lifecycle.emit(HTA_PROVIDER_EVENT.HOST_CALL, { ...fields, status: "ok" });
|
|
146
|
+
return value;
|
|
147
|
+
}, error => {
|
|
148
|
+
lifecycle.emit(HTA_PROVIDER_EVENT.HOST_CALL, {
|
|
149
|
+
...fields,
|
|
150
|
+
status: "error",
|
|
151
|
+
code: providerErrorCode(error, options.errorCode)
|
|
152
|
+
});
|
|
153
|
+
throw error;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
const pending = invoke(requestId, operation, args, context, controller);
|
|
158
|
+
inFlight.set(requestId, pending);
|
|
159
|
+
try {
|
|
160
|
+
await pending;
|
|
161
|
+
} finally {
|
|
162
|
+
inFlight.delete(requestId);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function invoke(requestId, operation, args, context, controller) {
|
|
167
|
+
try {
|
|
168
|
+
const value = await call(operation, args, context);
|
|
169
|
+
if (!controller.signal.aborted && !cancelled.has(requestId) && !closing) {
|
|
170
|
+
write(["result", requestId, toHta(value)]);
|
|
171
|
+
lifecycle.emit(HTA_PROVIDER_EVENT.CALL_RETURN, {
|
|
172
|
+
request: requestId,
|
|
173
|
+
operation: String(operation),
|
|
174
|
+
status: "ok"
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (!controller.signal.aborted && !cancelled.has(requestId) && !closing) {
|
|
179
|
+
write(["error", requestId, providerError(error, "node", options.errorCode)]);
|
|
180
|
+
lifecycle.emit(HTA_PROVIDER_EVENT.CALL_ERROR, {
|
|
181
|
+
request: requestId,
|
|
182
|
+
operation: String(operation),
|
|
183
|
+
status: "error",
|
|
184
|
+
code: providerErrorCode(error, options.errorCode)
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
} finally {
|
|
188
|
+
controllers.delete(requestId);
|
|
189
|
+
cancelled.delete(requestId);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function closeProvider() {
|
|
194
|
+
if (closePromise) return closePromise;
|
|
195
|
+
closing = true;
|
|
196
|
+
closePromise = (async () => {
|
|
197
|
+
const error = new Error("hta/provider-closed");
|
|
198
|
+
for (const controller of controllers.values()) controller.abort(error);
|
|
199
|
+
await Promise.allSettled([...inFlight.values()]);
|
|
200
|
+
let failure = null;
|
|
201
|
+
try {
|
|
202
|
+
await options.close?.();
|
|
203
|
+
} catch (closeError) {
|
|
204
|
+
failure = closeError;
|
|
205
|
+
}
|
|
206
|
+
lifecycle.shutdown({
|
|
207
|
+
status: failure === null ? "ok" : "error",
|
|
208
|
+
...(failure === null
|
|
209
|
+
? {}
|
|
210
|
+
: { code: providerErrorCode(failure, options.errorCode) })
|
|
211
|
+
});
|
|
212
|
+
if (failure) throw failure;
|
|
213
|
+
})();
|
|
214
|
+
return closePromise;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function finish() {
|
|
218
|
+
if (exited) return;
|
|
219
|
+
exited = true;
|
|
220
|
+
exit(0);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function reportFatal(error) {
|
|
224
|
+
console.error(String(error?.message ?? error));
|
|
225
|
+
void closeProvider().catch(() => {}).finally(finish);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function write(value) {
|
|
229
|
+
if (closing) return;
|
|
230
|
+
const frame = encodeHta(value);
|
|
231
|
+
const header = new Uint8Array(4);
|
|
232
|
+
new DataView(header.buffer).setUint32(0, frame.length, false);
|
|
233
|
+
output.write(header);
|
|
234
|
+
output.write(frame);
|
|
235
|
+
}
|
|
236
|
+
}
|