arcane-os 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/NOTICE +5 -3
- package/README.md +73 -24
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +67 -18
- package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +16 -5
- package/browser-runtime/ai/browser-kokoro-worker.mjs +3 -0
- package/browser-runtime/ai/browser-speech-artifacts.mjs +1108 -0
- package/browser-runtime/ai/browser-speech-providers.mjs +780 -0
- package/browser-runtime/ai/browser-speech.mjs +9 -0
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +1537 -167
- package/browser-runtime/ai/browser-wasm.mjs +46 -1
- package/browser-runtime/ai/browser-whisper-worker.mjs +3 -0
- package/browser-runtime/ai/browser-wllama-runtime.mjs +677 -132
- package/browser-runtime/ai/model-controller.mjs +138 -12
- package/browser-runtime/ai/speech-worker-client.mjs +207 -0
- package/browser-runtime/ai/speech-worker-runtime.mjs +516 -0
- package/browser-runtime/ai/wllama/index.mjs +389 -0
- package/docs/architecture.md +132 -22
- package/docs/reference/README.md +1 -1
- package/docs/reference/ai/browser-wasm.md +101 -42
- package/docs/reference/availability-and-normalization.md +19 -5
- package/docs/reference/behavioral-testing.md +18 -5
- package/docs/reference/cli.md +2 -2
- package/docs/reference/inventory/package-api.json +14 -14
- package/docs/reference/protocols.md +4 -4
- package/docs/reference/sdk-api.md +68 -38
- package/docs/work-amplification.md +8 -4
- package/package.json +7 -3
- package/runtime/ARCANE_RUNTIME_RELEASE.json +50 -20
- package/runtime/arcane/components/chat.html +551 -62
- package/runtime/arcane/components/speech.html +1113 -265
- package/runtime/arcane/entities/Chat.js +246 -43
- package/runtime/arcane/modules/AI.js +1394 -162
- package/runtime/arcane/modules/AIProviderRuntime.js +2289 -0
- package/runtime/arcane/modules/AIRuntimeState.js +872 -0
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +382 -31
- package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +1106 -0
- package/runtime/arcane/modules/DocumentLexicalSearch.js +292 -0
- package/runtime/arcane/modules/PersistentAIChatSession.js +268 -0
- package/runtime/arcane/modules/StaticDocumentCatalog.js +25 -206
- package/schemas/arcane-lock.schema.json +10 -6
- package/src/cli/main.mjs +14 -2
- package/src/constants.mjs +1 -1
- package/src/dev-server.mjs +273 -26
- package/src/doctor.mjs +1 -3
- package/src/import-map.mjs +193 -84
- package/src/packager/core.mjs +313 -41
- package/src/runtime.mjs +14 -4
- package/src/scaffold.mjs +45 -17
- package/src/sdk-browser-runtime.mjs +28 -75
- package/src/templates/workspace-template.mjs +27 -8
- package/src/toolchain.mjs +13 -2
- package/src/workspace-runtime.mjs +1 -1
- package/src/workspace.mjs +178 -25
|
@@ -1,5 +1,96 @@
|
|
|
1
1
|
export const ARCANE_AI_ADAPTER_PROTOCOL = "arcane-ai-adapter/1";
|
|
2
2
|
|
|
3
|
+
const SECURITY_KEYS = Object.freeze(["secure", "checks"]);
|
|
4
|
+
const SECURITY_CHECK_KEYS = Object.freeze(["byteLength", "sha256"]);
|
|
5
|
+
const EMPTY_SECURITY_CHECKS = Object.freeze({});
|
|
6
|
+
const EMPTY_MODEL_SECURITY = Object.freeze({ checks: EMPTY_SECURITY_CHECKS });
|
|
7
|
+
|
|
8
|
+
function closedSecurityRecord(value, keys, label) {
|
|
9
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
10
|
+
throw new TypeError(`${label} must be a plain object when provided.`);
|
|
11
|
+
}
|
|
12
|
+
const prototype = Object.getPrototypeOf(value);
|
|
13
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
14
|
+
throw new TypeError(`${label} must be a plain object when provided.`);
|
|
15
|
+
}
|
|
16
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
17
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
18
|
+
if (typeof key !== "string" || !keys.includes(key)) {
|
|
19
|
+
throw new TypeError(`${label} contains an unknown ${String(key)} field.`);
|
|
20
|
+
}
|
|
21
|
+
if (descriptors[key].get || descriptors[key].set) {
|
|
22
|
+
throw new TypeError(`${label}.${key} must be a data property.`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return descriptors;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function normalizeModelSecurity(value, label = "security") {
|
|
29
|
+
if (value === undefined) return EMPTY_MODEL_SECURITY;
|
|
30
|
+
const descriptors = closedSecurityRecord(value, SECURITY_KEYS, label);
|
|
31
|
+
const normalized = {};
|
|
32
|
+
if (descriptors.secure?.value !== undefined) {
|
|
33
|
+
if (typeof descriptors.secure.value !== "boolean") {
|
|
34
|
+
throw new TypeError(`${label}.secure must be a boolean when provided.`);
|
|
35
|
+
}
|
|
36
|
+
normalized.secure = descriptors.secure.value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let checks = EMPTY_SECURITY_CHECKS;
|
|
40
|
+
if (descriptors.checks?.value !== undefined) {
|
|
41
|
+
const checkDescriptors = closedSecurityRecord(
|
|
42
|
+
descriptors.checks.value,
|
|
43
|
+
SECURITY_CHECK_KEYS,
|
|
44
|
+
`${label}.checks`,
|
|
45
|
+
);
|
|
46
|
+
const normalizedChecks = {};
|
|
47
|
+
for (const check of SECURITY_CHECK_KEYS) {
|
|
48
|
+
if (checkDescriptors[check]?.value === undefined) continue;
|
|
49
|
+
if (typeof checkDescriptors[check].value !== "boolean") {
|
|
50
|
+
throw new TypeError(`${label}.checks.${check} must be a boolean when provided.`);
|
|
51
|
+
}
|
|
52
|
+
normalizedChecks[check] = checkDescriptors[check].value;
|
|
53
|
+
}
|
|
54
|
+
checks = Object.freeze(normalizedChecks);
|
|
55
|
+
}
|
|
56
|
+
normalized.checks = checks;
|
|
57
|
+
return Object.freeze(normalized);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function resolveModelSecurity({ app, binding, load } = {}) {
|
|
61
|
+
const scopes = [
|
|
62
|
+
normalizeModelSecurity(app, "app security"),
|
|
63
|
+
normalizeModelSecurity(binding, "provider security"),
|
|
64
|
+
normalizeModelSecurity(load, "load security"),
|
|
65
|
+
];
|
|
66
|
+
let secure = false;
|
|
67
|
+
let byteLength;
|
|
68
|
+
let sha256;
|
|
69
|
+
for (const scope of scopes) {
|
|
70
|
+
if (Object.hasOwn(scope, "secure")) secure = scope.secure;
|
|
71
|
+
if (Object.hasOwn(scope.checks, "byteLength")) byteLength = scope.checks.byteLength;
|
|
72
|
+
if (Object.hasOwn(scope.checks, "sha256")) sha256 = scope.checks.sha256;
|
|
73
|
+
}
|
|
74
|
+
return Object.freeze({
|
|
75
|
+
secure,
|
|
76
|
+
checks: Object.freeze({
|
|
77
|
+
byteLength: byteLength ?? secure,
|
|
78
|
+
sha256: sha256 ?? secure,
|
|
79
|
+
}),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function sameModelSecurity(left, right) {
|
|
84
|
+
return left?.checks?.byteLength === right?.checks?.byteLength
|
|
85
|
+
&& left?.checks?.sha256 === right?.checks?.sha256;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function hasModelSecurityOverrides(value) {
|
|
89
|
+
return Object.hasOwn(value, "secure")
|
|
90
|
+
|| Object.hasOwn(value.checks, "byteLength")
|
|
91
|
+
|| Object.hasOwn(value.checks, "sha256");
|
|
92
|
+
}
|
|
93
|
+
|
|
3
94
|
const ERROR_CODES = Object.freeze({
|
|
4
95
|
load: "ARCANE_AI_LOAD_FAILED",
|
|
5
96
|
unload: "ARCANE_AI_UNLOAD_FAILED",
|
|
@@ -133,8 +224,10 @@ function toolRecordFromCompletion(completion) {
|
|
|
133
224
|
export class ModelController {
|
|
134
225
|
#provider;
|
|
135
226
|
#loadPolicy;
|
|
227
|
+
#security;
|
|
136
228
|
#listeners = new Map();
|
|
137
229
|
#loadPromise = null;
|
|
230
|
+
#readyPolicyResolved = false;
|
|
138
231
|
#unloadPromise = null;
|
|
139
232
|
#disposePromise = null;
|
|
140
233
|
#operationGeneration = 0;
|
|
@@ -145,7 +238,7 @@ export class ModelController {
|
|
|
145
238
|
#progress = null;
|
|
146
239
|
#error = null;
|
|
147
240
|
|
|
148
|
-
constructor({ provider, loadPolicy = "on-demand" } = {}) {
|
|
241
|
+
constructor({ provider, loadPolicy = "on-demand", security } = {}) {
|
|
149
242
|
if (!provider || typeof provider !== "object") {
|
|
150
243
|
throw new TypeError("ModelController requires an LLM provider.");
|
|
151
244
|
}
|
|
@@ -164,10 +257,13 @@ export class ModelController {
|
|
|
164
257
|
}
|
|
165
258
|
this.#provider = provider;
|
|
166
259
|
this.#loadPolicy = loadPolicy;
|
|
260
|
+
this.#security = normalizeModelSecurity(security, "app security");
|
|
167
261
|
}
|
|
168
262
|
|
|
169
263
|
status() {
|
|
170
|
-
const providerStatus = providerMethod(this.#provider, "status")?.(
|
|
264
|
+
const providerStatus = providerMethod(this.#provider, "status")?.(
|
|
265
|
+
Object.freeze({ security: this.#security }),
|
|
266
|
+
) ?? {};
|
|
171
267
|
return Object.freeze({
|
|
172
268
|
...providerStatus,
|
|
173
269
|
kind: "llm",
|
|
@@ -221,16 +317,40 @@ export class ModelController {
|
|
|
221
317
|
{ operation: "load" },
|
|
222
318
|
);
|
|
223
319
|
}
|
|
224
|
-
if (state === "ready") return this.status();
|
|
225
|
-
if (this.#loadPromise) return this.#loadPromise;
|
|
226
320
|
const load = providerMethod(this.#provider, "load");
|
|
227
321
|
if (!load) throw new ArcaneAIError("ARCANE_AI_UNAVAILABLE", "The LLM provider cannot load a model.");
|
|
228
322
|
const signal = options.signal ?? null;
|
|
323
|
+
const explicitOperationSecurity = options.security !== undefined;
|
|
324
|
+
const securityMustResolve = explicitOperationSecurity
|
|
325
|
+
|| (
|
|
326
|
+
state === "ready"
|
|
327
|
+
&& !this.#readyPolicyResolved
|
|
328
|
+
&& hasModelSecurityOverrides(this.#security)
|
|
329
|
+
);
|
|
330
|
+
if (state === "ready" && !securityMustResolve) return this.status();
|
|
331
|
+
if (this.#loadPromise) {
|
|
332
|
+
if (!explicitOperationSecurity) return this.#loadPromise;
|
|
333
|
+
try {
|
|
334
|
+
await load(options, Object.freeze({
|
|
335
|
+
protocol: ARCANE_AI_ADAPTER_PROTOCOL,
|
|
336
|
+
kind: "llm",
|
|
337
|
+
operation: "load",
|
|
338
|
+
signal,
|
|
339
|
+
security: this.#security,
|
|
340
|
+
}));
|
|
341
|
+
return this.status();
|
|
342
|
+
} catch (error) {
|
|
343
|
+
throw normalizeArcaneAIError(error, { operation: "load", signal });
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const wasReady = state === "ready";
|
|
229
347
|
const operationGeneration = ++this.#operationGeneration;
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
348
|
+
if (!wasReady) {
|
|
349
|
+
this.#fallbackState = "loading";
|
|
350
|
+
this.#progress = null;
|
|
351
|
+
this.#error = null;
|
|
352
|
+
this.#emit("statechange");
|
|
353
|
+
}
|
|
234
354
|
this.#loadPromise = (async () => {
|
|
235
355
|
try {
|
|
236
356
|
await load(options, Object.freeze({
|
|
@@ -238,6 +358,7 @@ export class ModelController {
|
|
|
238
358
|
kind: "llm",
|
|
239
359
|
operation: "load",
|
|
240
360
|
signal,
|
|
361
|
+
security: this.#security,
|
|
241
362
|
reportProgress: (progress) => {
|
|
242
363
|
if (
|
|
243
364
|
operationGeneration !== this.#operationGeneration
|
|
@@ -253,9 +374,12 @@ export class ModelController {
|
|
|
253
374
|
|| this.#disposing
|
|
254
375
|
|| this.#disposed
|
|
255
376
|
) return this.status();
|
|
256
|
-
this.#
|
|
257
|
-
|
|
258
|
-
|
|
377
|
+
this.#readyPolicyResolved = true;
|
|
378
|
+
if (!wasReady) {
|
|
379
|
+
this.#fallbackState = "ready";
|
|
380
|
+
this.#progress = null;
|
|
381
|
+
this.#emit("statechange");
|
|
382
|
+
}
|
|
259
383
|
return this.status();
|
|
260
384
|
} catch (error) {
|
|
261
385
|
const normalized = normalizeArcaneAIError(error, { operation: "load", signal });
|
|
@@ -263,6 +387,7 @@ export class ModelController {
|
|
|
263
387
|
operationGeneration === this.#operationGeneration
|
|
264
388
|
&& !this.#disposing
|
|
265
389
|
&& !this.#disposed
|
|
390
|
+
&& !wasReady
|
|
266
391
|
) {
|
|
267
392
|
this.#fallbackState = "error";
|
|
268
393
|
this.#error = normalized;
|
|
@@ -286,7 +411,7 @@ export class ModelController {
|
|
|
286
411
|
{ operation: "request" },
|
|
287
412
|
);
|
|
288
413
|
}
|
|
289
|
-
if (state === "ready") return;
|
|
414
|
+
if (state === "ready") return void await this.load(loadOptions);
|
|
290
415
|
if (this.#loadPolicy === "manual") {
|
|
291
416
|
throw new ArcaneAIError(
|
|
292
417
|
"ARCANE_AI_NOT_READY",
|
|
@@ -330,6 +455,7 @@ export class ModelController {
|
|
|
330
455
|
}
|
|
331
456
|
if (operationGeneration === this.#operationGeneration) {
|
|
332
457
|
this.#fallbackState = "unloaded";
|
|
458
|
+
this.#readyPolicyResolved = false;
|
|
333
459
|
this.#progress = null;
|
|
334
460
|
this.#error = null;
|
|
335
461
|
this.#emit("statechange");
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import {
|
|
2
|
+
collectSpeechTransferables,
|
|
3
|
+
SPEECH_WORKER_PROTOCOL,
|
|
4
|
+
} from "./speech-worker-runtime.mjs";
|
|
5
|
+
|
|
6
|
+
function clientError(code, message, cause) {
|
|
7
|
+
const error = cause === undefined
|
|
8
|
+
? new Error(message)
|
|
9
|
+
: new Error(message, { cause });
|
|
10
|
+
error.name = code === "ARCANE_AI_REQUEST_ABORTED"
|
|
11
|
+
? "AbortError"
|
|
12
|
+
: "ArcaneSpeechWorkerError";
|
|
13
|
+
error.code = code;
|
|
14
|
+
return error;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function abortError(signal) {
|
|
18
|
+
return clientError(
|
|
19
|
+
"ARCANE_AI_REQUEST_ABORTED",
|
|
20
|
+
"The speech worker operation was cancelled.",
|
|
21
|
+
signal?.reason,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function validateWorker(worker) {
|
|
26
|
+
if (!worker || typeof worker.postMessage !== "function" || typeof worker.terminate !== "function") {
|
|
27
|
+
throw new TypeError("The packaged speech worker did not create a Worker.");
|
|
28
|
+
}
|
|
29
|
+
return worker;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const WORKER_CLIENTS = new WeakSet();
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Owns exactly one role Worker. Cancellation terminates that Worker, which is
|
|
36
|
+
* the only reliable preemption boundary for the third-party WASM engines.
|
|
37
|
+
*/
|
|
38
|
+
class SpeechWorkerClient {
|
|
39
|
+
#createWorker;
|
|
40
|
+
#worker = null;
|
|
41
|
+
#pending = new Map();
|
|
42
|
+
#nextId = 1;
|
|
43
|
+
#listeners = [];
|
|
44
|
+
#terminated = false;
|
|
45
|
+
#onTermination;
|
|
46
|
+
|
|
47
|
+
constructor({ role, onTermination = () => undefined } = {}) {
|
|
48
|
+
if (role !== "stt" && role !== "tts") {
|
|
49
|
+
throw new TypeError('SpeechWorkerClient role must be "stt" or "tts".');
|
|
50
|
+
}
|
|
51
|
+
if (typeof onTermination !== "function") {
|
|
52
|
+
throw new TypeError("SpeechWorkerClient onTermination must be a function.");
|
|
53
|
+
}
|
|
54
|
+
const workerUrl = role === "stt"
|
|
55
|
+
? new URL("./browser-whisper-worker.mjs", import.meta.url)
|
|
56
|
+
: new URL("./browser-kokoro-worker.mjs", import.meta.url);
|
|
57
|
+
this.#createWorker = () => new Worker(workerUrl, {
|
|
58
|
+
type: "module",
|
|
59
|
+
name: role === "stt" ? "arcane-whisper-stt" : "arcane-kokoro-tts",
|
|
60
|
+
});
|
|
61
|
+
this.#onTermination = onTermination;
|
|
62
|
+
WORKER_CLIENTS.add(this);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#listen(worker, type, listener) {
|
|
66
|
+
worker.addEventListener(type, listener);
|
|
67
|
+
this.#listeners.push(() => worker.removeEventListener(type, listener));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#start() {
|
|
71
|
+
if (this.#worker) return this.#worker;
|
|
72
|
+
if (this.#terminated) {
|
|
73
|
+
throw clientError("ARCANE_AI_OPERATION_SUPERSEDED", "The speech Worker was already terminated.");
|
|
74
|
+
}
|
|
75
|
+
const worker = validateWorker(this.#createWorker());
|
|
76
|
+
this.#worker = worker;
|
|
77
|
+
this.#listen(worker, "message", (event) => this.#handleMessage(event.data));
|
|
78
|
+
this.#listen(worker, "messageerror", (event) => {
|
|
79
|
+
void this.terminate(clientError(
|
|
80
|
+
"ARCANE_AI_WORKER_MESSAGE_ERROR",
|
|
81
|
+
"The speech Worker returned an unreadable message.",
|
|
82
|
+
event,
|
|
83
|
+
), { intentional: false });
|
|
84
|
+
});
|
|
85
|
+
this.#listen(worker, "error", (event) => {
|
|
86
|
+
void this.terminate(clientError(
|
|
87
|
+
"ARCANE_AI_WORKER_CRASHED",
|
|
88
|
+
"The speech Worker crashed.",
|
|
89
|
+
event,
|
|
90
|
+
), { intentional: false });
|
|
91
|
+
});
|
|
92
|
+
return worker;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#handleMessage(message) {
|
|
96
|
+
if (message?.protocol !== SPEECH_WORKER_PROTOCOL) {
|
|
97
|
+
void this.terminate(clientError(
|
|
98
|
+
"ARCANE_AI_ADAPTER_PROTOCOL_MISMATCH",
|
|
99
|
+
"The speech Worker protocol did not match the SDK.",
|
|
100
|
+
), { intentional: false });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (message.event === "progress") {
|
|
104
|
+
this.#pending.get(message.requestId)?.progress(message.progress);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const pending = this.#pending.get(message.id);
|
|
108
|
+
if (!pending) return;
|
|
109
|
+
this.#pending.delete(message.id);
|
|
110
|
+
pending.cleanup();
|
|
111
|
+
if (message.ok === true) {
|
|
112
|
+
pending.resolve(message.result);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
pending.reject(clientError(
|
|
116
|
+
message.error?.code ?? "ARCANE_AI_PROVIDER_REQUEST_FAILED",
|
|
117
|
+
message.error?.message ?? "The speech Worker operation failed.",
|
|
118
|
+
));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
request(op, payload, { signal = null, progress = () => undefined } = {}) {
|
|
122
|
+
if (signal?.aborted) return Promise.reject(abortError(signal));
|
|
123
|
+
if (typeof progress !== "function") {
|
|
124
|
+
return Promise.reject(new TypeError("Speech Worker progress must be a function."));
|
|
125
|
+
}
|
|
126
|
+
let worker;
|
|
127
|
+
try {
|
|
128
|
+
worker = this.#start();
|
|
129
|
+
} catch (error) {
|
|
130
|
+
return Promise.reject(error);
|
|
131
|
+
}
|
|
132
|
+
const id = this.#nextId;
|
|
133
|
+
this.#nextId += 1;
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
let listening = true;
|
|
136
|
+
const cleanup = () => {
|
|
137
|
+
if (!listening) return;
|
|
138
|
+
listening = false;
|
|
139
|
+
signal?.removeEventListener?.("abort", onAbort);
|
|
140
|
+
};
|
|
141
|
+
const onAbort = () => {
|
|
142
|
+
if (!this.#pending.has(id)) return;
|
|
143
|
+
void this.terminate(abortError(signal), { intentional: true });
|
|
144
|
+
};
|
|
145
|
+
signal?.addEventListener?.("abort", onAbort, { once: true });
|
|
146
|
+
this.#pending.set(id, { resolve, reject, progress, cleanup });
|
|
147
|
+
try {
|
|
148
|
+
worker.postMessage({
|
|
149
|
+
protocol: SPEECH_WORKER_PROTOCOL,
|
|
150
|
+
id,
|
|
151
|
+
op,
|
|
152
|
+
payload,
|
|
153
|
+
}, collectSpeechTransferables(payload));
|
|
154
|
+
} catch (error) {
|
|
155
|
+
this.#pending.delete(id);
|
|
156
|
+
cleanup();
|
|
157
|
+
const failure = clientError(
|
|
158
|
+
"ARCANE_AI_WORKER_MESSAGE_ERROR",
|
|
159
|
+
"Unable to send an operation to the speech Worker.",
|
|
160
|
+
error,
|
|
161
|
+
);
|
|
162
|
+
reject(failure);
|
|
163
|
+
void this.terminate(failure, { intentional: false });
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async terminate(reason = clientError(
|
|
169
|
+
"ARCANE_AI_OPERATION_SUPERSEDED",
|
|
170
|
+
"The speech Worker was terminated.",
|
|
171
|
+
), { intentional = true } = {}) {
|
|
172
|
+
if (typeof intentional !== "boolean") {
|
|
173
|
+
throw new TypeError("Speech Worker termination intent must be a boolean.");
|
|
174
|
+
}
|
|
175
|
+
if (this.#terminated) return;
|
|
176
|
+
this.#terminated = true;
|
|
177
|
+
const worker = this.#worker;
|
|
178
|
+
this.#worker = null;
|
|
179
|
+
for (const remove of this.#listeners.splice(0).reverse()) {
|
|
180
|
+
try {
|
|
181
|
+
remove();
|
|
182
|
+
} catch {
|
|
183
|
+
// Listener removal follows Worker isolation and is best effort.
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const pendingOperations = [...this.#pending.values()];
|
|
187
|
+
for (const pending of pendingOperations) {
|
|
188
|
+
pending.cleanup();
|
|
189
|
+
}
|
|
190
|
+
this.#pending.clear();
|
|
191
|
+
try {
|
|
192
|
+
const termination = worker?.terminate();
|
|
193
|
+
if (termination && typeof termination.then === "function") await termination;
|
|
194
|
+
} finally {
|
|
195
|
+
for (const pending of pendingOperations) pending.reject(reason);
|
|
196
|
+
this.#onTermination(Object.freeze({ reason, intentional }));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function createSpeechWorkerClient(options) {
|
|
202
|
+
return new SpeechWorkerClient(options);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function isSpeechWorkerClient(value) {
|
|
206
|
+
return WORKER_CLIENTS.has(value);
|
|
207
|
+
}
|