arcane-os 0.2.3 → 0.3.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 +30 -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 +3242 -391
- package/browser-runtime/ai/browser-speech-providers.mjs +1209 -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 +249 -32
- package/browser-runtime/ai/speech-worker-runtime.mjs +2322 -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 +18 -20
- package/docs/reference/ai/browser-speech-package-authority.json +835 -0
- package/docs/reference/ai/browser-speech.md +1205 -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 +185 -33
- package/docs/reference/runtime-components.md +192 -80
- package/docs/reference/runtime-modules.md +589 -70
- package/docs/reference/sdk-api.md +1017 -24
- 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 +2117 -130
- package/runtime/arcane/modules/AIProviderRuntime.js +756 -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 +130 -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-runtime.mjs +804 -22
- 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
|
+
"Strict 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,67 @@ 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
|
+
const strictGraphLoad = graphLoad
|
|
298
|
+
&& payload?.configuration?.security?.secure === true;
|
|
299
|
+
if (strictGraphLoad) {
|
|
300
|
+
initialPrivatePort = this.#establishPrivateTransport();
|
|
301
|
+
} else if (this.#transportMode === null) {
|
|
302
|
+
this.#transportMode = "worker-global-message";
|
|
303
|
+
this.#transport = worker;
|
|
304
|
+
}
|
|
305
|
+
if (op === "load"
|
|
306
|
+
&& !strictGraphLoad
|
|
307
|
+
&& this.#transportMode === "private-message-port") {
|
|
308
|
+
throw clientError(
|
|
309
|
+
"ARCANE_AI_ADAPTER_PROTOCOL_MISMATCH",
|
|
310
|
+
"A private strict artifact graph Worker cannot load an ordinary runtime descriptor.",
|
|
311
|
+
undefined,
|
|
312
|
+
`${this.#role}-worker-runtime-transport-mode-mismatch`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
129
315
|
} catch (error) {
|
|
130
316
|
return Promise.reject(error);
|
|
131
317
|
}
|
|
@@ -140,17 +326,28 @@ class SpeechWorkerClient {
|
|
|
140
326
|
};
|
|
141
327
|
const onAbort = () => {
|
|
142
328
|
if (!this.#pending.has(id)) return;
|
|
143
|
-
void this.terminate(
|
|
329
|
+
void this.terminate(
|
|
330
|
+
abortError(signal, this.#role, op),
|
|
331
|
+
{ intentional: true },
|
|
332
|
+
).catch(() => undefined);
|
|
144
333
|
};
|
|
145
334
|
signal?.addEventListener?.("abort", onAbort, { once: true });
|
|
146
|
-
this.#pending.set(id, { resolve, reject, progress, cleanup });
|
|
335
|
+
this.#pending.set(id, { resolve, reject, progress, cleanup, op });
|
|
336
|
+
const message = {
|
|
337
|
+
protocol: SPEECH_WORKER_PROTOCOL,
|
|
338
|
+
id,
|
|
339
|
+
op,
|
|
340
|
+
payload,
|
|
341
|
+
};
|
|
342
|
+
const transfers = collectSpeechTransferables(payload);
|
|
343
|
+
let destination = this.#transport;
|
|
344
|
+
if (initialPrivatePort) {
|
|
345
|
+
message.privatePort = initialPrivatePort;
|
|
346
|
+
transfers.push(initialPrivatePort);
|
|
347
|
+
destination = worker;
|
|
348
|
+
}
|
|
147
349
|
try {
|
|
148
|
-
|
|
149
|
-
protocol: SPEECH_WORKER_PROTOCOL,
|
|
150
|
-
id,
|
|
151
|
-
op,
|
|
152
|
-
payload,
|
|
153
|
-
}, collectSpeechTransferables(payload));
|
|
350
|
+
destination.postMessage(message, transfers);
|
|
154
351
|
} catch (error) {
|
|
155
352
|
this.#pending.delete(id);
|
|
156
353
|
cleanup();
|
|
@@ -158,21 +355,27 @@ class SpeechWorkerClient {
|
|
|
158
355
|
"ARCANE_AI_WORKER_MESSAGE_ERROR",
|
|
159
356
|
"Unable to send an operation to the speech Worker.",
|
|
160
357
|
error,
|
|
358
|
+
`${operationSubject(this.#role, op)}-message-rejected`,
|
|
161
359
|
);
|
|
162
360
|
reject(failure);
|
|
163
|
-
void this.terminate(failure, { intentional: false });
|
|
361
|
+
void this.terminate(failure, { intentional: false }).catch(() => undefined);
|
|
164
362
|
}
|
|
165
363
|
});
|
|
166
364
|
}
|
|
167
365
|
|
|
168
|
-
async terminate(reason =
|
|
169
|
-
"ARCANE_AI_OPERATION_SUPERSEDED",
|
|
170
|
-
"The speech Worker was terminated.",
|
|
171
|
-
), { intentional = true } = {}) {
|
|
366
|
+
async terminate(reason = null, { intentional = true } = {}) {
|
|
172
367
|
if (typeof intentional !== "boolean") {
|
|
173
368
|
throw new TypeError("Speech Worker termination intent must be a boolean.");
|
|
174
369
|
}
|
|
175
370
|
if (this.#terminated) return;
|
|
371
|
+
const terminationReason = reason instanceof Error && WORKER_CLIENT_ERRORS.has(reason)
|
|
372
|
+
? reason
|
|
373
|
+
: clientError(
|
|
374
|
+
"ARCANE_AI_OPERATION_SUPERSEDED",
|
|
375
|
+
"The speech Worker was terminated.",
|
|
376
|
+
reason instanceof Error ? reason : undefined,
|
|
377
|
+
`${this.#role}-worker-terminated`,
|
|
378
|
+
);
|
|
176
379
|
this.#terminated = true;
|
|
177
380
|
const worker = this.#worker;
|
|
178
381
|
this.#worker = null;
|
|
@@ -184,16 +387,26 @@ class SpeechWorkerClient {
|
|
|
184
387
|
}
|
|
185
388
|
}
|
|
186
389
|
const pendingOperations = [...this.#pending.values()];
|
|
187
|
-
for (const pending of pendingOperations)
|
|
188
|
-
pending.cleanup();
|
|
189
|
-
}
|
|
390
|
+
for (const pending of pendingOperations) pending.cleanup();
|
|
190
391
|
this.#pending.clear();
|
|
191
392
|
try {
|
|
393
|
+
for (const port of [
|
|
394
|
+
this.#privatePorts?.clientPort,
|
|
395
|
+
this.#privatePorts?.workerPort,
|
|
396
|
+
]) {
|
|
397
|
+
try {
|
|
398
|
+
port?.close();
|
|
399
|
+
} catch {
|
|
400
|
+
// Worker termination remains the authoritative cancellation boundary.
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
this.#privatePorts = null;
|
|
404
|
+
this.#transport = null;
|
|
192
405
|
const termination = worker?.terminate();
|
|
193
406
|
if (termination && typeof termination.then === "function") await termination;
|
|
194
407
|
} finally {
|
|
195
|
-
for (const pending of pendingOperations) pending.reject(
|
|
196
|
-
this.#onTermination(Object.freeze({ reason, intentional }));
|
|
408
|
+
for (const pending of pendingOperations) pending.reject(terminationReason);
|
|
409
|
+
this.#onTermination(Object.freeze({ reason: terminationReason, intentional }));
|
|
197
410
|
}
|
|
198
411
|
}
|
|
199
412
|
}
|
|
@@ -205,3 +418,7 @@ export function createSpeechWorkerClient(options) {
|
|
|
205
418
|
export function isSpeechWorkerClient(value) {
|
|
206
419
|
return WORKER_CLIENTS.has(value);
|
|
207
420
|
}
|
|
421
|
+
|
|
422
|
+
export function isSpeechWorkerClientError(value) {
|
|
423
|
+
return value instanceof Error && WORKER_CLIENT_ERRORS.has(value);
|
|
424
|
+
}
|