arcane-os 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +8 -8
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +29 -22
- package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +203 -0
- package/browser-runtime/ai/browser-kokoro-worker.mjs +11 -2
- package/browser-runtime/ai/browser-speech-artifacts.mjs +3230 -397
- package/browser-runtime/ai/browser-speech-providers.mjs +1141 -157
- package/browser-runtime/ai/browser-speech.mjs +2 -0
- package/browser-runtime/ai/browser-whisper-worker.mjs +11 -2
- package/browser-runtime/ai/model-controller.mjs +285 -95
- package/browser-runtime/ai/speech-worker-client.mjs +247 -32
- package/browser-runtime/ai/speech-worker-runtime.mjs +2310 -167
- package/browser-runtime/event-manager.mjs +1097 -1
- package/docs/architecture.md +2 -2
- package/docs/event-manager.md +155 -27
- package/docs/reference/README.md +20 -20
- package/docs/reference/ai/browser-speech-package-authority.json +835 -0
- package/docs/reference/ai/browser-speech.md +1162 -246
- package/docs/reference/ai/browser-wasm.md +18 -7
- package/docs/reference/availability-and-normalization.md +2 -2
- package/docs/reference/behavioral-testing.md +28 -5
- package/docs/reference/cli.md +115 -7
- package/docs/reference/core/arcane-ai-contracts.md +1 -1
- package/docs/reference/event-manager.md +577 -32
- package/docs/reference/inventory/package-api.json +478 -2
- package/docs/reference/inventory/runtime-components.json +86 -37
- package/docs/reference/inventory/runtime-modules.json +126 -49
- package/docs/reference/mail.md +316 -0
- package/docs/reference/protocols.md +143 -33
- package/docs/reference/runtime-components.md +192 -80
- package/docs/reference/runtime-modules.md +584 -70
- package/docs/reference/sdk-api.md +1014 -25
- package/package.json +5 -4
- package/runtime/ARCANE_RUNTIME_RELEASE.json +145 -140
- package/runtime/arcane/components/app-bar.html +34 -13
- package/runtime/arcane/components/assistant-panel.html +110 -57
- package/runtime/arcane/components/calculator.html +7 -4
- package/runtime/arcane/components/chart.html +58 -17
- package/runtime/arcane/components/chat.html +606 -136
- package/runtime/arcane/components/conversation-view.html +13 -6
- package/runtime/arcane/components/dashboard-config.html +96 -59
- package/runtime/arcane/components/data-maintenance.html +69 -14
- package/runtime/arcane/components/data-view.html +53 -7
- package/runtime/arcane/components/directory-picker.html +118 -32
- package/runtime/arcane/components/document-inspector.html +47 -10
- package/runtime/arcane/components/file-drop.html +81 -35
- package/runtime/arcane/components/file-inspector.html +72 -22
- package/runtime/arcane/components/file-manager.html +374 -79
- package/runtime/arcane/components/integration-settings.html +12 -5
- package/runtime/arcane/components/local-ai-status.html +48 -19
- package/runtime/arcane/components/markdown-document.html +161 -68
- package/runtime/arcane/components/markdown-editor.html +110 -33
- package/runtime/arcane/components/media-embed.html +8 -5
- package/runtime/arcane/components/modal.html +15 -5
- package/runtime/arcane/components/output-panel.html +28 -23
- package/runtime/arcane/components/preferences-form.html +22 -4
- package/runtime/arcane/components/record-timeline.html +18 -2
- package/runtime/arcane/components/relationship-board.html +23 -3
- package/runtime/arcane/components/screen-capture.html +10 -4
- package/runtime/arcane/components/source-code-viewer.html +76 -9
- package/runtime/arcane/components/source-explanation.html +23 -3
- package/runtime/arcane/components/speech.html +453 -131
- package/runtime/arcane/components/summary-strip.html +22 -11
- package/runtime/arcane/components/table.html +39 -21
- package/runtime/arcane/components/task-progress.html +79 -21
- package/runtime/arcane/components/terminal-workspace.html +7 -4
- package/runtime/arcane/components/theme-editor.html +7 -3
- package/runtime/arcane/components/unified-inbox.html +9 -4
- package/runtime/arcane/components/voice-transcription.html +244 -72
- package/runtime/arcane/components/weather-widget.html +5 -3
- package/runtime/arcane/components/web-navigator.html +48 -8
- package/runtime/arcane/entities/Chat.js +1 -1
- package/runtime/arcane/entities/User.js +110 -23
- package/runtime/arcane/modules/AI.js +2109 -130
- package/runtime/arcane/modules/AIProviderRuntime.js +720 -13
- package/runtime/arcane/modules/AIRuntimeState.js +109 -52
- package/runtime/arcane/modules/ApiModelDatabase.js +390 -17
- package/runtime/arcane/modules/BrowserTestSuite.js +205 -28
- package/runtime/arcane/modules/CalculatorEngine.js +63 -3
- package/runtime/arcane/modules/CommunicationAppController.js +588 -28
- package/runtime/arcane/modules/CommunicationHub.js +590 -10
- package/runtime/arcane/modules/ComponentContracts.js +235 -37
- package/runtime/arcane/modules/ConversationTimebox.js +152 -33
- package/runtime/arcane/modules/DBLS.js +40 -7
- package/runtime/arcane/modules/DBOPFS.js +35 -11
- package/runtime/arcane/modules/DataMaintenance.js +12 -2
- package/runtime/arcane/modules/Errors.js +65 -7
- package/runtime/arcane/modules/HTMLImport.js +198 -14
- package/runtime/arcane/modules/LocalAIReadinessController.js +208 -29
- package/runtime/arcane/modules/Mail.js +738 -115
- package/runtime/arcane/modules/MailOutbox.mjs +1395 -0
- package/runtime/arcane/modules/MailTransport.mjs +197 -39
- package/runtime/arcane/modules/Ollama.js +36 -1
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +583 -7
- package/runtime/arcane/modules/PreferenceStore.js +367 -33
- package/runtime/arcane/modules/RecordReviewStore.js +322 -23
- package/runtime/arcane/modules/ScreenCapture.js +1397 -15
- package/runtime/arcane/modules/SpeechPlayback.js +438 -41
- package/runtime/arcane/modules/TerminalClient.js +277 -12
- package/runtime/arcane/modules/ThemeBootstrap.js +80 -6
- package/runtime/arcane/modules/ThemeManager.js +39 -7
- package/runtime/arcane/modules/TimeGuard.js +131 -20
- package/runtime/arcane/modules/WaitForComponent.js +386 -33
- package/schemas/arcane-lock.schema.json +2 -2
- package/src/cli/main.mjs +435 -9
- package/src/event-manager.mjs +1097 -1
- package/src/import-map.mjs +21 -3
- package/src/index.mjs +13 -0
- package/src/installed-sdk-runtime.mjs +112 -0
- package/src/mail-api.mjs +22 -0
- package/src/mail-credentials.mjs +667 -0
- package/src/mail-server.mjs +1769 -0
- package/src/mail.mjs +261 -0
- package/src/sdk-browser-runtime.mjs +85 -41
- package/src/testing-loader.mjs +7 -0
- package/src/toolchain.mjs +3 -0
- package/src/workspace.mjs +1 -1
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
2
|
collectSpeechTransferables,
|
|
3
|
+
normalizeSpeechWorkerErrorEnvelope,
|
|
3
4
|
SPEECH_WORKER_PROTOCOL,
|
|
4
5
|
} from "./speech-worker-runtime.mjs";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
const ARTIFACT_GRAPH_MODULE_GRAPH =
|
|
8
|
+
"browser-speech-authenticated-artifact-graph";
|
|
9
|
+
const PUBLIC_WORKER_OPERATIONS = new Set([
|
|
10
|
+
"load",
|
|
11
|
+
"use",
|
|
12
|
+
"status",
|
|
13
|
+
"unload",
|
|
14
|
+
"dispose",
|
|
15
|
+
]);
|
|
16
|
+
const WORKER_CLIENT_ERRORS = new WeakSet();
|
|
17
|
+
|
|
18
|
+
function clientError(code, message, cause, reason) {
|
|
7
19
|
const error = cause === undefined
|
|
8
20
|
? new Error(message)
|
|
9
21
|
: new Error(message, { cause });
|
|
@@ -11,17 +23,43 @@ function clientError(code, message, cause) {
|
|
|
11
23
|
? "AbortError"
|
|
12
24
|
: "ArcaneSpeechWorkerError";
|
|
13
25
|
error.code = code;
|
|
26
|
+
if (typeof reason === "string" && reason) error.reason = reason;
|
|
27
|
+
WORKER_CLIENT_ERRORS.add(error);
|
|
14
28
|
return error;
|
|
15
29
|
}
|
|
16
30
|
|
|
17
|
-
function
|
|
31
|
+
function operationSubject(role, op) {
|
|
32
|
+
if (op === "use") return role === "stt" ? "stt-transcription" : "tts-synthesis";
|
|
33
|
+
return `${role}-${op}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function abortError(signal, role, op) {
|
|
18
37
|
return clientError(
|
|
19
38
|
"ARCANE_AI_REQUEST_ABORTED",
|
|
20
39
|
"The speech worker operation was cancelled.",
|
|
21
40
|
signal?.reason,
|
|
41
|
+
`${operationSubject(role, op)}-cancelled`,
|
|
22
42
|
);
|
|
23
43
|
}
|
|
24
44
|
|
|
45
|
+
function validProgress(role, progress) {
|
|
46
|
+
if (!progress || typeof progress !== "object" || Array.isArray(progress)) return false;
|
|
47
|
+
const keys = Object.keys(progress).sort().join(",");
|
|
48
|
+
if (keys !== "completed,heartbeat,phase,total,unit") return false;
|
|
49
|
+
const phases = new Set([
|
|
50
|
+
`${role}-runtime-import-started`,
|
|
51
|
+
`${role}-model-load-started`,
|
|
52
|
+
`${role}-model-load-progress`,
|
|
53
|
+
`${role}-provider-ready`,
|
|
54
|
+
]);
|
|
55
|
+
return phases.has(progress.phase)
|
|
56
|
+
&& Number.isFinite(progress.completed)
|
|
57
|
+
&& progress.completed >= 0
|
|
58
|
+
&& (progress.total === null || (Number.isFinite(progress.total) && progress.total >= 0))
|
|
59
|
+
&& (progress.unit === "bytes" || progress.unit === "items")
|
|
60
|
+
&& progress.heartbeat === true;
|
|
61
|
+
}
|
|
62
|
+
|
|
25
63
|
function validateWorker(worker) {
|
|
26
64
|
if (!worker || typeof worker.postMessage !== "function" || typeof worker.terminate !== "function") {
|
|
27
65
|
throw new TypeError("The packaged speech worker did not create a Worker.");
|
|
@@ -29,6 +67,23 @@ function validateWorker(worker) {
|
|
|
29
67
|
return worker;
|
|
30
68
|
}
|
|
31
69
|
|
|
70
|
+
function validateMessagePort(port) {
|
|
71
|
+
if (
|
|
72
|
+
!port
|
|
73
|
+
|| typeof port.postMessage !== "function"
|
|
74
|
+
|| typeof port.addEventListener !== "function"
|
|
75
|
+
|| typeof port.close !== "function"
|
|
76
|
+
) {
|
|
77
|
+
throw clientError(
|
|
78
|
+
"ARCANE_AI_WORKER_MESSAGE_ERROR",
|
|
79
|
+
"The browser did not create a usable private speech Worker MessagePort.",
|
|
80
|
+
undefined,
|
|
81
|
+
"artifact-graph-private-message-port-unavailable",
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return port;
|
|
85
|
+
}
|
|
86
|
+
|
|
32
87
|
const WORKER_CLIENTS = new WeakSet();
|
|
33
88
|
|
|
34
89
|
/**
|
|
@@ -36,8 +91,12 @@ const WORKER_CLIENTS = new WeakSet();
|
|
|
36
91
|
* the only reliable preemption boundary for the third-party WASM engines.
|
|
37
92
|
*/
|
|
38
93
|
class SpeechWorkerClient {
|
|
94
|
+
#role;
|
|
39
95
|
#createWorker;
|
|
40
96
|
#worker = null;
|
|
97
|
+
#transport = null;
|
|
98
|
+
#transportMode = null;
|
|
99
|
+
#privatePorts = null;
|
|
41
100
|
#pending = new Map();
|
|
42
101
|
#nextId = 1;
|
|
43
102
|
#listeners = [];
|
|
@@ -54,6 +113,7 @@ class SpeechWorkerClient {
|
|
|
54
113
|
const workerUrl = role === "stt"
|
|
55
114
|
? new URL("./browser-whisper-worker.mjs", import.meta.url)
|
|
56
115
|
: new URL("./browser-kokoro-worker.mjs", import.meta.url);
|
|
116
|
+
this.#role = role;
|
|
57
117
|
this.#createWorker = () => new Worker(workerUrl, {
|
|
58
118
|
type: "module",
|
|
59
119
|
name: role === "stt" ? "arcane-whisper-stt" : "arcane-kokoro-tts",
|
|
@@ -62,46 +122,125 @@ class SpeechWorkerClient {
|
|
|
62
122
|
WORKER_CLIENTS.add(this);
|
|
63
123
|
}
|
|
64
124
|
|
|
65
|
-
#listen(
|
|
66
|
-
|
|
67
|
-
this.#listeners.push(() =>
|
|
125
|
+
#listen(target, type, listener) {
|
|
126
|
+
target.addEventListener(type, listener);
|
|
127
|
+
this.#listeners.push(() => target.removeEventListener(type, listener));
|
|
68
128
|
}
|
|
69
129
|
|
|
70
130
|
#start() {
|
|
71
131
|
if (this.#worker) return this.#worker;
|
|
72
132
|
if (this.#terminated) {
|
|
73
|
-
throw clientError(
|
|
133
|
+
throw clientError(
|
|
134
|
+
"ARCANE_AI_OPERATION_SUPERSEDED",
|
|
135
|
+
"The speech Worker was already terminated.",
|
|
136
|
+
undefined,
|
|
137
|
+
`${this.#role}-worker-already-terminated`,
|
|
138
|
+
);
|
|
74
139
|
}
|
|
75
140
|
const worker = validateWorker(this.#createWorker());
|
|
76
141
|
this.#worker = worker;
|
|
77
|
-
this.#listen(worker, "message", (event) =>
|
|
142
|
+
this.#listen(worker, "message", (event) => {
|
|
143
|
+
if (this.#transportMode === "private-message-port") return;
|
|
144
|
+
this.#handleMessage(event.data);
|
|
145
|
+
});
|
|
78
146
|
this.#listen(worker, "messageerror", (event) => {
|
|
79
147
|
void this.terminate(clientError(
|
|
80
148
|
"ARCANE_AI_WORKER_MESSAGE_ERROR",
|
|
81
149
|
"The speech Worker returned an unreadable message.",
|
|
82
150
|
event,
|
|
83
|
-
|
|
151
|
+
`${this.#role}-worker-message-rejected`,
|
|
152
|
+
), { intentional: false }).catch(() => undefined);
|
|
84
153
|
});
|
|
85
154
|
this.#listen(worker, "error", (event) => {
|
|
86
155
|
void this.terminate(clientError(
|
|
87
156
|
"ARCANE_AI_WORKER_CRASHED",
|
|
88
157
|
"The speech Worker crashed.",
|
|
89
158
|
event,
|
|
90
|
-
|
|
159
|
+
`${this.#role}-worker-crashed`,
|
|
160
|
+
), { intentional: false }).catch(() => undefined);
|
|
91
161
|
});
|
|
92
162
|
return worker;
|
|
93
163
|
}
|
|
94
164
|
|
|
165
|
+
#establishPrivateTransport() {
|
|
166
|
+
if (this.#transportMode === "private-message-port") return null;
|
|
167
|
+
if (this.#transportMode !== null) {
|
|
168
|
+
throw clientError(
|
|
169
|
+
"ARCANE_AI_ADAPTER_PROTOCOL_MISMATCH",
|
|
170
|
+
"The speech Worker already uses the legacy global message transport.",
|
|
171
|
+
undefined,
|
|
172
|
+
"artifact-graph-private-message-port-established-too-late",
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
const MessageChannelConstructor = globalThis.MessageChannel;
|
|
176
|
+
if (typeof MessageChannelConstructor !== "function") {
|
|
177
|
+
throw clientError(
|
|
178
|
+
"ARCANE_AI_WORKER_MESSAGE_ERROR",
|
|
179
|
+
"Authenticated artifact graph loading requires MessageChannel.",
|
|
180
|
+
undefined,
|
|
181
|
+
"artifact-graph-private-message-channel-unavailable",
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
const channel = new MessageChannelConstructor();
|
|
185
|
+
const clientPort = validateMessagePort(channel.port1);
|
|
186
|
+
const workerPort = validateMessagePort(channel.port2);
|
|
187
|
+
this.#privatePorts = Object.freeze({ clientPort, workerPort });
|
|
188
|
+
this.#transport = clientPort;
|
|
189
|
+
this.#transportMode = "private-message-port";
|
|
190
|
+
this.#listen(clientPort, "message", (event) => this.#handleMessage(event.data));
|
|
191
|
+
this.#listen(clientPort, "messageerror", (event) => {
|
|
192
|
+
void this.terminate(clientError(
|
|
193
|
+
"ARCANE_AI_WORKER_MESSAGE_ERROR",
|
|
194
|
+
"The private speech Worker MessagePort returned an unreadable message.",
|
|
195
|
+
event,
|
|
196
|
+
`${this.#role}-worker-private-message-rejected`,
|
|
197
|
+
), { intentional: false }).catch(() => undefined);
|
|
198
|
+
});
|
|
199
|
+
clientPort.start?.();
|
|
200
|
+
return workerPort;
|
|
201
|
+
}
|
|
202
|
+
|
|
95
203
|
#handleMessage(message) {
|
|
96
204
|
if (message?.protocol !== SPEECH_WORKER_PROTOCOL) {
|
|
97
205
|
void this.terminate(clientError(
|
|
98
206
|
"ARCANE_AI_ADAPTER_PROTOCOL_MISMATCH",
|
|
99
207
|
"The speech Worker protocol did not match the SDK.",
|
|
100
|
-
|
|
208
|
+
undefined,
|
|
209
|
+
`${this.#role}-worker-protocol-mismatch`,
|
|
210
|
+
), { intentional: false }).catch(() => undefined);
|
|
101
211
|
return;
|
|
102
212
|
}
|
|
103
213
|
if (message.event === "progress") {
|
|
104
|
-
this.#pending.get(message.requestId)
|
|
214
|
+
const pending = this.#pending.get(message.requestId);
|
|
215
|
+
if (!pending) return;
|
|
216
|
+
if (!validProgress(this.#role, message.progress)) {
|
|
217
|
+
void this.terminate(clientError(
|
|
218
|
+
"ARCANE_AI_WORKER_MESSAGE_ERROR",
|
|
219
|
+
"The speech Worker progress envelope was rejected.",
|
|
220
|
+
undefined,
|
|
221
|
+
`${this.#role}-worker-progress-envelope-rejected`,
|
|
222
|
+
), { intentional: false }).catch(() => undefined);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
pending.progress(message.progress);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
void this.terminate(clientError(
|
|
229
|
+
"ARCANE_AI_PROGRESS_CALLBACK_THREW",
|
|
230
|
+
"The speech load progress callback threw an exception.",
|
|
231
|
+
error,
|
|
232
|
+
`${this.#role}-load-progress-callback-threw`,
|
|
233
|
+
), { intentional: false }).catch(() => undefined);
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (!Number.isSafeInteger(message.id) || typeof message.ok !== "boolean") {
|
|
238
|
+
void this.terminate(clientError(
|
|
239
|
+
"ARCANE_AI_WORKER_MESSAGE_ERROR",
|
|
240
|
+
"The speech Worker response envelope shape was rejected.",
|
|
241
|
+
undefined,
|
|
242
|
+
`${this.#role}-worker-response-envelope-shape-rejected`,
|
|
243
|
+
), { intentional: false }).catch(() => undefined);
|
|
105
244
|
return;
|
|
106
245
|
}
|
|
107
246
|
const pending = this.#pending.get(message.id);
|
|
@@ -112,20 +251,65 @@ class SpeechWorkerClient {
|
|
|
112
251
|
pending.resolve(message.result);
|
|
113
252
|
return;
|
|
114
253
|
}
|
|
254
|
+
const admitted = normalizeSpeechWorkerErrorEnvelope(
|
|
255
|
+
message.error,
|
|
256
|
+
this.#role,
|
|
257
|
+
pending.op,
|
|
258
|
+
);
|
|
259
|
+
if (!admitted) {
|
|
260
|
+
const failure = clientError(
|
|
261
|
+
"ARCANE_AI_WORKER_MESSAGE_ERROR",
|
|
262
|
+
"The speech Worker error envelope was rejected.",
|
|
263
|
+
undefined,
|
|
264
|
+
`${this.#role}-worker-error-envelope-rejected`,
|
|
265
|
+
);
|
|
266
|
+
pending.reject(failure);
|
|
267
|
+
void this.terminate(failure, { intentional: false }).catch(() => undefined);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
115
270
|
pending.reject(clientError(
|
|
116
|
-
|
|
117
|
-
|
|
271
|
+
admitted.code,
|
|
272
|
+
admitted.message,
|
|
273
|
+
undefined,
|
|
274
|
+
admitted.reason,
|
|
118
275
|
));
|
|
119
276
|
}
|
|
120
277
|
|
|
121
278
|
request(op, payload, { signal = null, progress = () => undefined } = {}) {
|
|
122
|
-
if (
|
|
279
|
+
if (!PUBLIC_WORKER_OPERATIONS.has(op)) {
|
|
280
|
+
return Promise.reject(clientError(
|
|
281
|
+
"ARCANE_AI_INVALID_REQUEST",
|
|
282
|
+
"The speech worker operation is not part of its protocol.",
|
|
283
|
+
undefined,
|
|
284
|
+
`${this.#role}-worker-operation-unknown`,
|
|
285
|
+
));
|
|
286
|
+
}
|
|
287
|
+
if (signal?.aborted) return Promise.reject(abortError(signal, this.#role, op));
|
|
123
288
|
if (typeof progress !== "function") {
|
|
124
289
|
return Promise.reject(new TypeError("Speech Worker progress must be a function."));
|
|
125
290
|
}
|
|
126
291
|
let worker;
|
|
292
|
+
let initialPrivatePort = null;
|
|
127
293
|
try {
|
|
128
294
|
worker = this.#start();
|
|
295
|
+
const graphLoad = op === "load"
|
|
296
|
+
&& payload?.configuration?.runtime?.moduleGraph === ARTIFACT_GRAPH_MODULE_GRAPH;
|
|
297
|
+
if (graphLoad) {
|
|
298
|
+
initialPrivatePort = this.#establishPrivateTransport();
|
|
299
|
+
} else if (this.#transportMode === null) {
|
|
300
|
+
this.#transportMode = "worker-global-message";
|
|
301
|
+
this.#transport = worker;
|
|
302
|
+
}
|
|
303
|
+
if (op === "load"
|
|
304
|
+
&& !graphLoad
|
|
305
|
+
&& this.#transportMode === "private-message-port") {
|
|
306
|
+
throw clientError(
|
|
307
|
+
"ARCANE_AI_ADAPTER_PROTOCOL_MISMATCH",
|
|
308
|
+
"A private artifact graph Worker cannot load a legacy runtime descriptor.",
|
|
309
|
+
undefined,
|
|
310
|
+
`${this.#role}-worker-runtime-transport-mode-mismatch`,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
129
313
|
} catch (error) {
|
|
130
314
|
return Promise.reject(error);
|
|
131
315
|
}
|
|
@@ -140,17 +324,28 @@ class SpeechWorkerClient {
|
|
|
140
324
|
};
|
|
141
325
|
const onAbort = () => {
|
|
142
326
|
if (!this.#pending.has(id)) return;
|
|
143
|
-
void this.terminate(
|
|
327
|
+
void this.terminate(
|
|
328
|
+
abortError(signal, this.#role, op),
|
|
329
|
+
{ intentional: true },
|
|
330
|
+
).catch(() => undefined);
|
|
144
331
|
};
|
|
145
332
|
signal?.addEventListener?.("abort", onAbort, { once: true });
|
|
146
|
-
this.#pending.set(id, { resolve, reject, progress, cleanup });
|
|
333
|
+
this.#pending.set(id, { resolve, reject, progress, cleanup, op });
|
|
334
|
+
const message = {
|
|
335
|
+
protocol: SPEECH_WORKER_PROTOCOL,
|
|
336
|
+
id,
|
|
337
|
+
op,
|
|
338
|
+
payload,
|
|
339
|
+
};
|
|
340
|
+
const transfers = collectSpeechTransferables(payload);
|
|
341
|
+
let destination = this.#transport;
|
|
342
|
+
if (initialPrivatePort) {
|
|
343
|
+
message.privatePort = initialPrivatePort;
|
|
344
|
+
transfers.push(initialPrivatePort);
|
|
345
|
+
destination = worker;
|
|
346
|
+
}
|
|
147
347
|
try {
|
|
148
|
-
|
|
149
|
-
protocol: SPEECH_WORKER_PROTOCOL,
|
|
150
|
-
id,
|
|
151
|
-
op,
|
|
152
|
-
payload,
|
|
153
|
-
}, collectSpeechTransferables(payload));
|
|
348
|
+
destination.postMessage(message, transfers);
|
|
154
349
|
} catch (error) {
|
|
155
350
|
this.#pending.delete(id);
|
|
156
351
|
cleanup();
|
|
@@ -158,21 +353,27 @@ class SpeechWorkerClient {
|
|
|
158
353
|
"ARCANE_AI_WORKER_MESSAGE_ERROR",
|
|
159
354
|
"Unable to send an operation to the speech Worker.",
|
|
160
355
|
error,
|
|
356
|
+
`${operationSubject(this.#role, op)}-message-rejected`,
|
|
161
357
|
);
|
|
162
358
|
reject(failure);
|
|
163
|
-
void this.terminate(failure, { intentional: false });
|
|
359
|
+
void this.terminate(failure, { intentional: false }).catch(() => undefined);
|
|
164
360
|
}
|
|
165
361
|
});
|
|
166
362
|
}
|
|
167
363
|
|
|
168
|
-
async terminate(reason =
|
|
169
|
-
"ARCANE_AI_OPERATION_SUPERSEDED",
|
|
170
|
-
"The speech Worker was terminated.",
|
|
171
|
-
), { intentional = true } = {}) {
|
|
364
|
+
async terminate(reason = null, { intentional = true } = {}) {
|
|
172
365
|
if (typeof intentional !== "boolean") {
|
|
173
366
|
throw new TypeError("Speech Worker termination intent must be a boolean.");
|
|
174
367
|
}
|
|
175
368
|
if (this.#terminated) return;
|
|
369
|
+
const terminationReason = reason instanceof Error && WORKER_CLIENT_ERRORS.has(reason)
|
|
370
|
+
? reason
|
|
371
|
+
: clientError(
|
|
372
|
+
"ARCANE_AI_OPERATION_SUPERSEDED",
|
|
373
|
+
"The speech Worker was terminated.",
|
|
374
|
+
reason instanceof Error ? reason : undefined,
|
|
375
|
+
`${this.#role}-worker-terminated`,
|
|
376
|
+
);
|
|
176
377
|
this.#terminated = true;
|
|
177
378
|
const worker = this.#worker;
|
|
178
379
|
this.#worker = null;
|
|
@@ -184,16 +385,26 @@ class SpeechWorkerClient {
|
|
|
184
385
|
}
|
|
185
386
|
}
|
|
186
387
|
const pendingOperations = [...this.#pending.values()];
|
|
187
|
-
for (const pending of pendingOperations)
|
|
188
|
-
pending.cleanup();
|
|
189
|
-
}
|
|
388
|
+
for (const pending of pendingOperations) pending.cleanup();
|
|
190
389
|
this.#pending.clear();
|
|
191
390
|
try {
|
|
391
|
+
for (const port of [
|
|
392
|
+
this.#privatePorts?.clientPort,
|
|
393
|
+
this.#privatePorts?.workerPort,
|
|
394
|
+
]) {
|
|
395
|
+
try {
|
|
396
|
+
port?.close();
|
|
397
|
+
} catch {
|
|
398
|
+
// Worker termination remains the authoritative cancellation boundary.
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
this.#privatePorts = null;
|
|
402
|
+
this.#transport = null;
|
|
192
403
|
const termination = worker?.terminate();
|
|
193
404
|
if (termination && typeof termination.then === "function") await termination;
|
|
194
405
|
} finally {
|
|
195
|
-
for (const pending of pendingOperations) pending.reject(
|
|
196
|
-
this.#onTermination(Object.freeze({ reason, intentional }));
|
|
406
|
+
for (const pending of pendingOperations) pending.reject(terminationReason);
|
|
407
|
+
this.#onTermination(Object.freeze({ reason: terminationReason, intentional }));
|
|
197
408
|
}
|
|
198
409
|
}
|
|
199
410
|
}
|
|
@@ -205,3 +416,7 @@ export function createSpeechWorkerClient(options) {
|
|
|
205
416
|
export function isSpeechWorkerClient(value) {
|
|
206
417
|
return WORKER_CLIENTS.has(value);
|
|
207
418
|
}
|
|
419
|
+
|
|
420
|
+
export function isSpeechWorkerClientError(value) {
|
|
421
|
+
return value instanceof Error && WORKER_CLIENT_ERRORS.has(value);
|
|
422
|
+
}
|