arcane-os 0.69.0 → 0.71.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 +28 -0
- package/README.md +1 -1
- package/browser-runtime/ai/browser-decisions.mjs +46 -2
- package/browser-runtime/ai/browser-speech-artifacts.mjs +58 -62
- package/browser-runtime/ai/browser-speech-providers.mjs +3 -0
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +76 -164
- package/browser-runtime/ai/dbopfs-model-parts.mjs +344 -0
- package/browser-runtime/ai/dbopfs-model-resources.mjs +190 -0
- package/browser-runtime/ai/decision-runtime.mjs +17 -1
- package/browser-runtime/ai/decision-worker.mjs +21 -1
- package/browser-runtime/ai/model-resource-bridge.mjs +231 -0
- package/browser-runtime/ai/speech-worker-client.mjs +68 -28
- package/browser-runtime/ai/speech-worker-runtime.mjs +212 -97
- package/browser-runtime/core/contracts.mjs +1 -1
- package/docs/reference/README.md +2 -0
- package/docs/reference/ai/browser-decisions.md +57 -3
- package/docs/reference/ai/browser-speech.md +45 -17
- package/docs/reference/ai/browser-wasm.md +28 -20
- package/docs/reference/core-native-packaging.md +13 -0
- package/docs/reference/core-runtime.md +9 -0
- package/docs/reference/core-shared-host.md +190 -0
- package/docs/reference/inventory/package-api.json +3 -3
- package/docs/reference/inventory/runtime-components.json +2 -2
- package/docs/reference/inventory/runtime-modules.json +6 -6
- package/docs/reference/runtime-components.md +35 -1
- package/docs/reference/runtime-modules.md +97 -7
- package/docs/reference/sdk-api.md +69 -31
- package/package.json +2 -2
- package/runtime/arcane/components/theme-switcher.html +4 -4
- package/runtime/arcane/modules/AI.js +2 -18
- package/runtime/arcane/modules/AIProviderRuntime.js +77 -3
- package/runtime/arcane/modules/DBOPFS.js +180 -26
- package/src/core/host.mjs +2 -0
- package/src/core/runtime.mjs +9 -4
- package/src/core/services/packaged-web.mjs +8 -1
- package/src/core/shared-host.mjs +500 -0
- package/src/native/portable-layout.mjs +66 -34
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import Is from '../dependencies/strong-type/index.js';
|
|
2
|
+
|
|
3
|
+
const is = new Is(false);
|
|
4
|
+
|
|
5
|
+
function errorRecord(value, seen = new Map()) {
|
|
6
|
+
if (!is.error(value)) return value;
|
|
7
|
+
if (seen.has(value)) return seen.get(value);
|
|
8
|
+
const record = {name: value.name, message: value.message, stack: value.stack};
|
|
9
|
+
seen.set(value, record);
|
|
10
|
+
for (const key of Object.getOwnPropertyNames(value)) {
|
|
11
|
+
if (key === 'cause') record[key] = errorRecord(value[key], seen);
|
|
12
|
+
else if (key === 'errors' && is.array(value[key])) {
|
|
13
|
+
record[key] = value[key].map(function recordError(error) {
|
|
14
|
+
return errorRecord(error, seen);
|
|
15
|
+
});
|
|
16
|
+
} else record[key] = value[key];
|
|
17
|
+
}
|
|
18
|
+
return record;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function reviveError(value, seen = new Map()) {
|
|
22
|
+
if (!value || !is.string(value.name) || !is.string(value.message)) return value;
|
|
23
|
+
if (seen.has(value)) return seen.get(value);
|
|
24
|
+
const error = new Error(value.message);
|
|
25
|
+
seen.set(value, error);
|
|
26
|
+
for (const [key, field] of Object.entries(value)) {
|
|
27
|
+
let item = field;
|
|
28
|
+
if (key === 'cause') item = reviveError(field, seen);
|
|
29
|
+
if (key === 'errors' && is.array(field)) {
|
|
30
|
+
item = field.map(function reviveNestedError(nested) {
|
|
31
|
+
return reviveError(nested, seen);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
Object.defineProperty(error, key, {value: item, enumerable: true, configurable: true, writable: true});
|
|
35
|
+
}
|
|
36
|
+
return error;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function abortReason(signal) {
|
|
40
|
+
return signal?.reason ?? new DOMException('The model resource operation was cancelled.', 'AbortError');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The store stays at its owner; only requests and complete Blob handles cross. */
|
|
44
|
+
export function createModelResourceHost({fetchResource, send, onError}) {
|
|
45
|
+
const operations = new Map();
|
|
46
|
+
const transportFailures = [];
|
|
47
|
+
let closed = false;
|
|
48
|
+
let closing;
|
|
49
|
+
function receive(message) {
|
|
50
|
+
if (message?.arcaneModelResource !== true) return false;
|
|
51
|
+
const {resourceId, op} = message;
|
|
52
|
+
if (op === 'cancel') {
|
|
53
|
+
operations.get(resourceId)?.controller.abort(reviveError(message.reason));
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
if (op !== 'fetch' || closed) return true;
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
const operation = {controller};
|
|
59
|
+
operations.set(resourceId, operation);
|
|
60
|
+
async function runResourceFetch() {
|
|
61
|
+
try {
|
|
62
|
+
if (controller.signal.aborted) throw controller.signal.reason;
|
|
63
|
+
const result = await fetchResource(message.request.url, {
|
|
64
|
+
...message.request.options,
|
|
65
|
+
signal: controller.signal,
|
|
66
|
+
onProgress: function reportResourceProgress(progress) {
|
|
67
|
+
if (!closed) send({arcaneModelResource: true, resourceId, op: 'progress', progress});
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
if (!closed) send({arcaneModelResource: true, resourceId, op: 'result', result});
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (!closed) {
|
|
73
|
+
try {
|
|
74
|
+
send({arcaneModelResource: true, resourceId, op: 'error', error: errorRecord(error)});
|
|
75
|
+
} catch (transportError) {
|
|
76
|
+
throw new AggregateError([error, transportError], 'Model resource operation and error delivery failed.');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else if (error !== controller.signal.reason && error?.name !== 'AbortError') throw error;
|
|
80
|
+
} finally {
|
|
81
|
+
operations.delete(resourceId);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Publish ownership before invoking a store that can synchronously
|
|
85
|
+
// report progress and trigger reentrant Worker shutdown.
|
|
86
|
+
operation.promise = Promise.resolve().then(runResourceFetch);
|
|
87
|
+
// Keep transport failures observable at close without an unhandled
|
|
88
|
+
// rejection while the caller's Worker lifecycle is still active.
|
|
89
|
+
operation.promise.catch(function observeResourceFailure(error) {
|
|
90
|
+
transportFailures.push(error);
|
|
91
|
+
if (!closed) {
|
|
92
|
+
try {
|
|
93
|
+
if (onError) onError(error);
|
|
94
|
+
else globalThis.console?.error('Model resource transport failed.', error);
|
|
95
|
+
} catch (handlerError) {
|
|
96
|
+
const failure = new AggregateError([error, handlerError], 'Model resource transport and its failure handler failed.');
|
|
97
|
+
transportFailures.push(failure);
|
|
98
|
+
globalThis.console?.error('Model resource failure handler failed.', failure);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
function close(reason = abortReason()) {
|
|
105
|
+
if (closing) return closing;
|
|
106
|
+
closed = true;
|
|
107
|
+
let resolveClose;
|
|
108
|
+
let rejectClose;
|
|
109
|
+
closing = new Promise(function retainResourceClose(resolve, reject) {
|
|
110
|
+
resolveClose = resolve;
|
|
111
|
+
rejectClose = reject;
|
|
112
|
+
});
|
|
113
|
+
const pending = [...operations.values()];
|
|
114
|
+
for (const operation of pending) operation.controller.abort(reason);
|
|
115
|
+
Promise.allSettled(pending.map(function resourceSettlement(operation) {
|
|
116
|
+
return operation.promise;
|
|
117
|
+
})).then(function finishResourceClose() {
|
|
118
|
+
if (transportFailures.length) {
|
|
119
|
+
rejectClose(new AggregateError(transportFailures, 'Unable to settle model resource transport.'));
|
|
120
|
+
} else resolveClose();
|
|
121
|
+
});
|
|
122
|
+
return closing;
|
|
123
|
+
}
|
|
124
|
+
return {receive, close};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function createModelResourceClient({send}) {
|
|
128
|
+
const pending = new Map();
|
|
129
|
+
let nextId = 1;
|
|
130
|
+
let closed = false;
|
|
131
|
+
let closeReason;
|
|
132
|
+
function receive(message) {
|
|
133
|
+
if (message?.arcaneModelResource !== true) return false;
|
|
134
|
+
const operation = pending.get(message.resourceId);
|
|
135
|
+
if (!operation) return true;
|
|
136
|
+
if (message.op === 'progress') {
|
|
137
|
+
try {
|
|
138
|
+
operation.onProgress?.(message.progress);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
operation.cancel(error);
|
|
141
|
+
}
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
pending.delete(message.resourceId);
|
|
145
|
+
operation.release();
|
|
146
|
+
if (operation.cancelled) operation.reject(operation.reason);
|
|
147
|
+
else if (message.op === 'error') operation.reject(reviveError(message.error));
|
|
148
|
+
else operation.resolve(message.result);
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
async function fetchResource(input, {onProgress, ...options} = {}) {
|
|
152
|
+
if (closed) throw closeReason;
|
|
153
|
+
const request = new Request(input, options);
|
|
154
|
+
const signal = request.signal;
|
|
155
|
+
if (signal.aborted) throw abortReason(signal);
|
|
156
|
+
const requestOptions = {
|
|
157
|
+
method: request.method,
|
|
158
|
+
headers: [...request.headers],
|
|
159
|
+
mode: request.mode,
|
|
160
|
+
credentials: request.credentials,
|
|
161
|
+
cache: request.cache,
|
|
162
|
+
redirect: request.redirect,
|
|
163
|
+
referrer: request.referrer,
|
|
164
|
+
referrerPolicy: request.referrerPolicy,
|
|
165
|
+
integrity: request.integrity,
|
|
166
|
+
keepalive: request.keepalive
|
|
167
|
+
};
|
|
168
|
+
if (request.body) requestOptions.body = await request.blob();
|
|
169
|
+
if (closed) throw closeReason;
|
|
170
|
+
if (signal.aborted) throw abortReason(signal);
|
|
171
|
+
const resourceId = nextId++;
|
|
172
|
+
return new Promise(function requestModelResource(resolve, reject) {
|
|
173
|
+
const operation = {resolve, reject, onProgress, cancelled: false};
|
|
174
|
+
function cancel(reason) {
|
|
175
|
+
if (operation.cancelled) return;
|
|
176
|
+
operation.cancelled = true;
|
|
177
|
+
operation.reason = reason;
|
|
178
|
+
try {
|
|
179
|
+
send({arcaneModelResource: true, resourceId, op: 'cancel', reason: errorRecord(reason)});
|
|
180
|
+
} catch (error) {
|
|
181
|
+
pending.delete(resourceId);
|
|
182
|
+
release();
|
|
183
|
+
reject(new AggregateError([reason, error], 'Unable to cancel the model resource transport.'));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function cancelFromSignal() {
|
|
187
|
+
cancel(abortReason(signal));
|
|
188
|
+
}
|
|
189
|
+
function release() {
|
|
190
|
+
signal.removeEventListener('abort', cancelFromSignal);
|
|
191
|
+
}
|
|
192
|
+
operation.cancel = cancel;
|
|
193
|
+
operation.release = release;
|
|
194
|
+
pending.set(resourceId, operation);
|
|
195
|
+
signal.addEventListener('abort', cancelFromSignal, {once: true});
|
|
196
|
+
try {
|
|
197
|
+
send({arcaneModelResource: true, resourceId, op: 'fetch',
|
|
198
|
+
request: {url: request.url, options: requestOptions}});
|
|
199
|
+
} catch (error) {
|
|
200
|
+
pending.delete(resourceId);
|
|
201
|
+
release();
|
|
202
|
+
reject(error);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
async function fetch(input, options) {
|
|
207
|
+
const request = new Request(input, options);
|
|
208
|
+
const resource = await fetchResource(request, {onProgress: options?.onProgress});
|
|
209
|
+
const response = resource.status === 0 ? Response.error() : new Response(
|
|
210
|
+
request.method === 'HEAD' || [204, 205, 304].includes(resource.status) ? null : resource.file,
|
|
211
|
+
{status: resource.status, statusText: resource.statusText, headers: resource.headers}
|
|
212
|
+
);
|
|
213
|
+
Object.defineProperties(response, {
|
|
214
|
+
url: {value: resource.url, configurable: true},
|
|
215
|
+
redirected: {value: resource.redirected, configurable: true}
|
|
216
|
+
});
|
|
217
|
+
return response;
|
|
218
|
+
}
|
|
219
|
+
function close(reason = abortReason()) {
|
|
220
|
+
if (closed) return;
|
|
221
|
+
closed = true;
|
|
222
|
+
closeReason = reason;
|
|
223
|
+
for (const operation of pending.values()) {
|
|
224
|
+
operation.cancel(reason);
|
|
225
|
+
operation.release();
|
|
226
|
+
operation.reject(reason);
|
|
227
|
+
}
|
|
228
|
+
pending.clear();
|
|
229
|
+
}
|
|
230
|
+
return {receive, fetchResource, fetch, close};
|
|
231
|
+
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
SPEECH_WORKER_PROTOCOL,
|
|
6
6
|
} from "./speech-worker-runtime.mjs";
|
|
7
7
|
import { arcaneLogging } from "../logging.mjs";
|
|
8
|
+
import { createModelResourceHost } from './model-resource-bridge.mjs';
|
|
8
9
|
|
|
9
10
|
const is = new Is(false);
|
|
10
11
|
|
|
@@ -47,6 +48,10 @@ function abortError(signal, role, op) {
|
|
|
47
48
|
);
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
function reportSpeechTerminationFailure(error) {
|
|
52
|
+
arcaneLogging.error('Speech Worker termination failed.', error);
|
|
53
|
+
}
|
|
54
|
+
|
|
50
55
|
function validateWorker(worker) {
|
|
51
56
|
if (!worker || !is.function(worker.postMessage) || !is.function(worker.terminate)) {
|
|
52
57
|
throw new TypeError("The packaged speech worker did not create a Worker.");
|
|
@@ -72,8 +77,11 @@ class SpeechWorkerClient {
|
|
|
72
77
|
#listeners = [];
|
|
73
78
|
#terminated = false;
|
|
74
79
|
#onTermination;
|
|
80
|
+
#fetchResource;
|
|
81
|
+
#resourceHost = null;
|
|
82
|
+
#terminationPromise = null;
|
|
75
83
|
|
|
76
|
-
constructor({ role, onTermination = () => undefined } = {}) {
|
|
84
|
+
constructor({ role, fetchResource = null, onTermination = () => undefined } = {}) {
|
|
77
85
|
if (role !== "stt" && role !== "tts") {
|
|
78
86
|
throw new TypeError('SpeechWorkerClient role must be "stt" or "tts".');
|
|
79
87
|
}
|
|
@@ -89,6 +97,7 @@ class SpeechWorkerClient {
|
|
|
89
97
|
name: role === "stt" ? "arcane-whisper-stt" : "arcane-kokoro-tts",
|
|
90
98
|
});
|
|
91
99
|
this.#onTermination = onTermination;
|
|
100
|
+
this.#fetchResource = fetchResource;
|
|
92
101
|
WORKER_CLIENTS.add(this);
|
|
93
102
|
this.#trace("created", { workerUrl: workerUrl.href });
|
|
94
103
|
}
|
|
@@ -131,7 +140,25 @@ class SpeechWorkerClient {
|
|
|
131
140
|
);
|
|
132
141
|
}
|
|
133
142
|
const worker = validateWorker(this.#createWorker());
|
|
143
|
+
const client = this;
|
|
134
144
|
this.#worker = worker;
|
|
145
|
+
if (this.#fetchResource) {
|
|
146
|
+
this.#resourceHost = createModelResourceHost({
|
|
147
|
+
fetchResource: this.#fetchResource,
|
|
148
|
+
send: function sendSpeechResource(message) {
|
|
149
|
+
worker.postMessage(message);
|
|
150
|
+
},
|
|
151
|
+
onError: function speechResourceTransportFailed(error) {
|
|
152
|
+
const failure = clientError(
|
|
153
|
+
'ARCANE_AI_WORKER_MESSAGE_ERROR',
|
|
154
|
+
'Unable to deliver a resource to the speech Worker.',
|
|
155
|
+
error,
|
|
156
|
+
`${client.#role}-worker-message-rejected`,
|
|
157
|
+
);
|
|
158
|
+
void client.terminate(failure, { intentional: false }).catch(reportSpeechTerminationFailure);
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
}
|
|
135
162
|
this.#trace("started");
|
|
136
163
|
this.#listen(worker, "message", (event) => {
|
|
137
164
|
this.#handleMessage(event.data);
|
|
@@ -142,7 +169,7 @@ class SpeechWorkerClient {
|
|
|
142
169
|
"The speech Worker returned an unreadable message.",
|
|
143
170
|
event,
|
|
144
171
|
`${this.#role}-worker-message-rejected`,
|
|
145
|
-
), { intentional: false }).catch(
|
|
172
|
+
), { intentional: false }).catch(reportSpeechTerminationFailure);
|
|
146
173
|
});
|
|
147
174
|
this.#listen(worker, "error", (event) => {
|
|
148
175
|
void this.terminate(clientError(
|
|
@@ -150,12 +177,13 @@ class SpeechWorkerClient {
|
|
|
150
177
|
"The speech Worker crashed.",
|
|
151
178
|
event,
|
|
152
179
|
`${this.#role}-worker-crashed`,
|
|
153
|
-
), { intentional: false }).catch(
|
|
180
|
+
), { intentional: false }).catch(reportSpeechTerminationFailure);
|
|
154
181
|
});
|
|
155
182
|
return worker;
|
|
156
183
|
}
|
|
157
184
|
|
|
158
185
|
#handleMessage(message) {
|
|
186
|
+
if (this.#resourceHost?.receive(message)) return;
|
|
159
187
|
this.#trace("response", { message });
|
|
160
188
|
if (message?.protocol !== SPEECH_WORKER_PROTOCOL) {
|
|
161
189
|
void this.terminate(clientError(
|
|
@@ -163,12 +191,12 @@ class SpeechWorkerClient {
|
|
|
163
191
|
"The speech Worker protocol did not match the SDK.",
|
|
164
192
|
undefined,
|
|
165
193
|
`${this.#role}-worker-protocol-mismatch`,
|
|
166
|
-
), { intentional: false }).catch(
|
|
194
|
+
), { intentional: false }).catch(reportSpeechTerminationFailure);
|
|
167
195
|
return;
|
|
168
196
|
}
|
|
169
197
|
if (is.safeInteger(message.id) && message.type === 'progress') {
|
|
170
198
|
const pending = this.#pending.get(message.id);
|
|
171
|
-
|
|
199
|
+
pending?.onProgress?.(message.progress);
|
|
172
200
|
return;
|
|
173
201
|
}
|
|
174
202
|
if (!is.safeInteger(message.id) || !is.boolean(message.ok)) {
|
|
@@ -177,7 +205,7 @@ class SpeechWorkerClient {
|
|
|
177
205
|
"The speech Worker response envelope shape was rejected.",
|
|
178
206
|
undefined,
|
|
179
207
|
`${this.#role}-worker-response-envelope-shape-rejected`,
|
|
180
|
-
), { intentional: false }).catch(
|
|
208
|
+
), { intentional: false }).catch(reportSpeechTerminationFailure);
|
|
181
209
|
return;
|
|
182
210
|
}
|
|
183
211
|
const pending = this.#pending.get(message.id);
|
|
@@ -201,7 +229,7 @@ class SpeechWorkerClient {
|
|
|
201
229
|
`${this.#role}-worker-error-envelope-rejected`,
|
|
202
230
|
);
|
|
203
231
|
pending.reject(failure);
|
|
204
|
-
void this.terminate(failure, { intentional: false }).catch(
|
|
232
|
+
void this.terminate(failure, { intentional: false }).catch(reportSpeechTerminationFailure);
|
|
205
233
|
return;
|
|
206
234
|
}
|
|
207
235
|
pending.reject(clientError(
|
|
@@ -272,20 +300,14 @@ class SpeechWorkerClient {
|
|
|
272
300
|
error,
|
|
273
301
|
`${operationSubject(client.#role, op)}-message-rejected`,
|
|
274
302
|
);
|
|
275
|
-
void client.terminate(failure, { intentional: false }).catch(
|
|
276
|
-
function ignoreCancellationTerminationFailure() {
|
|
277
|
-
return undefined;
|
|
278
|
-
},
|
|
279
|
-
);
|
|
303
|
+
void client.terminate(failure, { intentional: false }).catch(reportSpeechTerminationFailure);
|
|
280
304
|
}
|
|
281
305
|
return;
|
|
282
306
|
}
|
|
283
307
|
void client.terminate(
|
|
284
308
|
abortError(signal, client.#role, op),
|
|
285
309
|
{ intentional: true },
|
|
286
|
-
).catch(
|
|
287
|
-
return undefined;
|
|
288
|
-
});
|
|
310
|
+
).catch(reportSpeechTerminationFailure);
|
|
289
311
|
}
|
|
290
312
|
signal?.addEventListener?.("abort", onAbort, { once: true });
|
|
291
313
|
client.#pending.set(
|
|
@@ -313,11 +335,7 @@ class SpeechWorkerClient {
|
|
|
313
335
|
`${operationSubject(client.#role, op)}-message-rejected`,
|
|
314
336
|
);
|
|
315
337
|
reject(failure);
|
|
316
|
-
void client.terminate(failure, { intentional: false }).catch(
|
|
317
|
-
function ignoreRequestTerminationFailure() {
|
|
318
|
-
return undefined;
|
|
319
|
-
},
|
|
320
|
-
);
|
|
338
|
+
void client.terminate(failure, { intentional: false }).catch(reportSpeechTerminationFailure);
|
|
321
339
|
}
|
|
322
340
|
});
|
|
323
341
|
}
|
|
@@ -327,7 +345,7 @@ class SpeechWorkerClient {
|
|
|
327
345
|
if (!is.boolean(intentional)) {
|
|
328
346
|
throw new TypeError("Speech Worker termination intent must be a boolean.");
|
|
329
347
|
}
|
|
330
|
-
if (this.#
|
|
348
|
+
if (this.#terminationPromise) return this.#terminationPromise;
|
|
331
349
|
const terminationReason = is.error(reason) && WORKER_CLIENT_ERRORS.has(reason)
|
|
332
350
|
? reason
|
|
333
351
|
: clientError(
|
|
@@ -349,15 +367,37 @@ class SpeechWorkerClient {
|
|
|
349
367
|
const pendingOperations = [...this.#pending.values()];
|
|
350
368
|
for (const pending of pendingOperations) pending.cleanup();
|
|
351
369
|
this.#pending.clear();
|
|
370
|
+
const resourceHost = this.#resourceHost;
|
|
371
|
+
this.#resourceHost = null;
|
|
372
|
+
this.#transport = null;
|
|
373
|
+
const client = this;
|
|
374
|
+
const failures = [];
|
|
375
|
+
this.#terminationPromise = Promise.resolve().then(async function terminateSpeechResources() {
|
|
376
|
+
const settlements = await Promise.allSettled([
|
|
377
|
+
Promise.resolve().then(function closeSpeechResources() {
|
|
378
|
+
return resourceHost?.close(terminationReason);
|
|
379
|
+
}),
|
|
380
|
+
Promise.resolve().then(function terminateSpeechWorker() {
|
|
381
|
+
return worker?.terminate();
|
|
382
|
+
}),
|
|
383
|
+
]);
|
|
384
|
+
for (const settlement of settlements) {
|
|
385
|
+
if (settlement.status === 'rejected') failures.push(settlement.reason);
|
|
386
|
+
}
|
|
387
|
+
for (const pending of pendingOperations) pending.reject(terminationReason);
|
|
388
|
+
client.#trace('terminated', { reason: terminationReason, intentional });
|
|
389
|
+
if (failures.length) {
|
|
390
|
+
throw new AggregateError(failures, 'Unable to complete speech Worker termination.', { cause: terminationReason });
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
// Revoke provider readiness immediately; reentrant pool cleanup joins the
|
|
394
|
+
// already-owned promise before releasing prepared files.
|
|
352
395
|
try {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
} finally {
|
|
357
|
-
for (const pending of pendingOperations) pending.reject(terminationReason);
|
|
358
|
-
this.#trace("terminated", { reason: terminationReason, intentional });
|
|
359
|
-
this.#onTermination(completeValue({ reason: terminationReason, intentional }));
|
|
396
|
+
this.#onTermination(completeValue({ reason: terminationReason, intentional }));
|
|
397
|
+
} catch (error) {
|
|
398
|
+
failures.push(error);
|
|
360
399
|
}
|
|
400
|
+
return this.#terminationPromise;
|
|
361
401
|
}
|
|
362
402
|
}
|
|
363
403
|
|