arcane-os 0.69.0 → 0.70.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 +20 -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/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/runtime-modules.json +3 -3
- package/docs/reference/runtime-modules.md +97 -7
- package/docs/reference/sdk-api.md +41 -30
- package/package.json +2 -2
- 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,344 @@
|
|
|
1
|
+
import Is from '../dependencies/strong-type/index.js';
|
|
2
|
+
|
|
3
|
+
const is = new Is(false);
|
|
4
|
+
const owners = new WeakMap();
|
|
5
|
+
// Physical OPFS framing only; every input chunk is written in full. This is
|
|
6
|
+
// neither a model limit nor public progress, and avoids a file per network read.
|
|
7
|
+
const PART_FRAME = 8 * 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
export function withDbopfsModelLock(dbopfs, tableName, name, operation, {signal} = {}) {
|
|
10
|
+
let tables = owners.get(dbopfs);
|
|
11
|
+
if (!tables) owners.set(dbopfs, tables = new Map());
|
|
12
|
+
let entries = tables.get(tableName);
|
|
13
|
+
if (!entries) tables.set(tableName, entries = new Map());
|
|
14
|
+
const preceding = entries.get(name) ?? Promise.resolve();
|
|
15
|
+
let acquired = false;
|
|
16
|
+
function runOwnedOperation() {
|
|
17
|
+
throwIfModelAborted(signal);
|
|
18
|
+
acquired = true;
|
|
19
|
+
return operation();
|
|
20
|
+
}
|
|
21
|
+
async function acquireStorageOwner() {
|
|
22
|
+
throwIfModelAborted(signal);
|
|
23
|
+
if (dbopfs.readyPromise) await dbopfs.readyPromise;
|
|
24
|
+
throwIfModelAborted(signal);
|
|
25
|
+
const locks = dbopfs.lockManager ?? globalThis.navigator?.locks;
|
|
26
|
+
if (!is.function(locks?.request)) return runOwnedOperation();
|
|
27
|
+
const lockName = `arcane-model-storage:${JSON.stringify([dbopfs.storagePath ?? '', tableName, name])}`;
|
|
28
|
+
return locks.request(lockName, signal ? {mode: 'exclusive', signal} : {mode: 'exclusive'}, runOwnedOperation);
|
|
29
|
+
}
|
|
30
|
+
const current = preceding.then(acquireStorageOwner, acquireStorageOwner);
|
|
31
|
+
entries.set(name, current);
|
|
32
|
+
function releaseOwner() {
|
|
33
|
+
if (entries.get(name) === current) entries.delete(name);
|
|
34
|
+
if (entries.size === 0) tables.delete(tableName);
|
|
35
|
+
}
|
|
36
|
+
current.then(releaseOwner, releaseOwner);
|
|
37
|
+
if (!signal) return current;
|
|
38
|
+
// A cancelled waiter can leave promptly, while its inert queue link stays
|
|
39
|
+
// behind the preceding owner. Once acquired, durable cleanup must settle.
|
|
40
|
+
return new Promise(function observeStorageWait(resolve, reject) {
|
|
41
|
+
function cancelWait() {
|
|
42
|
+
if (!acquired) {
|
|
43
|
+
try {
|
|
44
|
+
throwIfModelAborted(signal);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
reject(error);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
signal.addEventListener('abort', cancelWait, {once: true});
|
|
51
|
+
if (signal.aborted) cancelWait();
|
|
52
|
+
current.then(function ownedOperationFinished(value) {
|
|
53
|
+
signal.removeEventListener('abort', cancelWait);
|
|
54
|
+
resolve(value);
|
|
55
|
+
}, function ownedOperationFailed(error) {
|
|
56
|
+
signal.removeEventListener('abort', cancelWait);
|
|
57
|
+
reject(error);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function throwIfModelAborted(signal) {
|
|
63
|
+
if (signal?.aborted) {
|
|
64
|
+
throw signal.reason ?? new DOMException('The model resource operation was cancelled.', 'AbortError');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function missingFile(error) {
|
|
69
|
+
return error?.name === 'NotFoundError' || error?.code === 'ENOENT';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Own the reader until its cancellation and pending read have settled. */
|
|
73
|
+
export async function* readModelChunks(body, signal) {
|
|
74
|
+
throwIfModelAborted(signal);
|
|
75
|
+
if (body === null || body === undefined) return;
|
|
76
|
+
if (is.function(body.stream)) body = body.stream();
|
|
77
|
+
if (!is.function(body.getReader)) {
|
|
78
|
+
if (body instanceof ArrayBuffer || ArrayBuffer.isView(body) || is.string(body)) {
|
|
79
|
+
body = new Blob([body]).stream();
|
|
80
|
+
} else {
|
|
81
|
+
for await (const chunk of body) {
|
|
82
|
+
throwIfModelAborted(signal);
|
|
83
|
+
yield chunk;
|
|
84
|
+
}
|
|
85
|
+
throwIfModelAborted(signal);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const reader = body.getReader();
|
|
90
|
+
let ended = false;
|
|
91
|
+
let cancellation;
|
|
92
|
+
function cancelReader() {
|
|
93
|
+
cancellation ??= Promise.resolve(reader.cancel(signal?.reason));
|
|
94
|
+
// Observe immediately; cleanup below retains and reports any failure.
|
|
95
|
+
cancellation.catch(function observeCancellation() {});
|
|
96
|
+
}
|
|
97
|
+
signal?.addEventListener('abort', cancelReader, {once: true});
|
|
98
|
+
try {
|
|
99
|
+
if (signal?.aborted) cancelReader();
|
|
100
|
+
while (true) {
|
|
101
|
+
throwIfModelAborted(signal);
|
|
102
|
+
const {value, done} = await reader.read();
|
|
103
|
+
throwIfModelAborted(signal);
|
|
104
|
+
if (done) {
|
|
105
|
+
ended = true;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
yield value;
|
|
109
|
+
}
|
|
110
|
+
} finally {
|
|
111
|
+
signal?.removeEventListener('abort', cancelReader);
|
|
112
|
+
try {
|
|
113
|
+
if (!ended) cancelReader();
|
|
114
|
+
await cancellation;
|
|
115
|
+
} finally {
|
|
116
|
+
reader.releaseLock();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Shared private storage for complete resources and resumable closed parts. */
|
|
122
|
+
export function createDbopfsModelPartStore({dbopfs, tableName}) {
|
|
123
|
+
let directoryPromise;
|
|
124
|
+
async function directory() {
|
|
125
|
+
if (dbopfs.readyPromise) await dbopfs.readyPromise;
|
|
126
|
+
directoryPromise ??= Promise.resolve(dbopfs.getTableHandle(tableName));
|
|
127
|
+
return directoryPromise;
|
|
128
|
+
}
|
|
129
|
+
async function file(name) {
|
|
130
|
+
try {
|
|
131
|
+
const handle = await (await directory()).getFileHandle(name, {create: false});
|
|
132
|
+
return await handle.getFile();
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (missingFile(error)) return null;
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async function removeFile(name) {
|
|
139
|
+
try {
|
|
140
|
+
await (await directory()).removeEntry(name);
|
|
141
|
+
return true;
|
|
142
|
+
} catch (error) {
|
|
143
|
+
if (missingFile(error)) return false;
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function manifestName(name) {
|
|
148
|
+
return `${name}.arcane-parts.json`;
|
|
149
|
+
}
|
|
150
|
+
async function readState(name) {
|
|
151
|
+
const record = await file(manifestName(name));
|
|
152
|
+
return record ? JSON.parse(await record.text()) : null;
|
|
153
|
+
}
|
|
154
|
+
async function saveState(name, record) {
|
|
155
|
+
const target = manifestName(name);
|
|
156
|
+
const existing = await file(target);
|
|
157
|
+
let writer;
|
|
158
|
+
try {
|
|
159
|
+
const handle = await (await directory()).getFileHandle(target, {create: true});
|
|
160
|
+
writer = await handle.createWritable();
|
|
161
|
+
await writer.write(JSON.stringify(record));
|
|
162
|
+
await writer.close();
|
|
163
|
+
} catch (error) {
|
|
164
|
+
const failures = [error];
|
|
165
|
+
try {
|
|
166
|
+
await writer?.abort(error);
|
|
167
|
+
} catch (cleanupError) {
|
|
168
|
+
failures.push(cleanupError);
|
|
169
|
+
}
|
|
170
|
+
if (!existing) {
|
|
171
|
+
try {
|
|
172
|
+
await removeFile(target);
|
|
173
|
+
} catch (cleanupError) {
|
|
174
|
+
failures.push(cleanupError);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (failures.length > 1) throw new AggregateError(failures, 'Unable to settle the model part index.');
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async function assemble(record, signal) {
|
|
182
|
+
const parts = [];
|
|
183
|
+
for (const name of record.parts) {
|
|
184
|
+
throwIfModelAborted(signal);
|
|
185
|
+
const part = await file(name);
|
|
186
|
+
if (!part) return null;
|
|
187
|
+
parts.push(part);
|
|
188
|
+
}
|
|
189
|
+
throwIfModelAborted(signal);
|
|
190
|
+
return new Blob(parts);
|
|
191
|
+
}
|
|
192
|
+
async function clear(name, signal) {
|
|
193
|
+
throwIfModelAborted(signal);
|
|
194
|
+
const record = await readState(name);
|
|
195
|
+
if (!record) return false;
|
|
196
|
+
for (const part of record.parts) {
|
|
197
|
+
throwIfModelAborted(signal);
|
|
198
|
+
await removeFile(part);
|
|
199
|
+
}
|
|
200
|
+
await removeFile(manifestName(name));
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
function locked(name, operation, signal) {
|
|
204
|
+
return withDbopfsModelLock(dbopfs, tableName, `parts:${name}`, operation, {signal});
|
|
205
|
+
}
|
|
206
|
+
function state(name, {signal} = {}) {
|
|
207
|
+
return locked(name, async function readModelPartState() {
|
|
208
|
+
throwIfModelAborted(signal);
|
|
209
|
+
return readState(name);
|
|
210
|
+
}, signal);
|
|
211
|
+
}
|
|
212
|
+
function read(name, {signal} = {}) {
|
|
213
|
+
return locked(name, async function readCompleteModelParts() {
|
|
214
|
+
throwIfModelAborted(signal);
|
|
215
|
+
const record = await readState(name);
|
|
216
|
+
return record?.complete ? assemble(record, signal) : null;
|
|
217
|
+
}, signal);
|
|
218
|
+
}
|
|
219
|
+
function readPartial(name, {signal} = {}) {
|
|
220
|
+
return locked(name, async function readClosedModelParts() {
|
|
221
|
+
throwIfModelAborted(signal);
|
|
222
|
+
const record = await readState(name);
|
|
223
|
+
return record ? assemble(record, signal) : null;
|
|
224
|
+
}, signal);
|
|
225
|
+
}
|
|
226
|
+
function remove(name, {signal} = {}) {
|
|
227
|
+
return locked(name, async function removeModelParts() {
|
|
228
|
+
return clear(name, signal);
|
|
229
|
+
}, signal);
|
|
230
|
+
}
|
|
231
|
+
function write(name, body, {signal, onProgress, append = false} = {}) {
|
|
232
|
+
return locked(name, async function writeModelParts() {
|
|
233
|
+
throwIfModelAborted(signal);
|
|
234
|
+
let record = append ? await readState(name) : null;
|
|
235
|
+
if (!record) {
|
|
236
|
+
await clear(name, signal);
|
|
237
|
+
record = {parts: [], complete: false};
|
|
238
|
+
} else {
|
|
239
|
+
record.complete = false;
|
|
240
|
+
}
|
|
241
|
+
await saveState(name, record);
|
|
242
|
+
let writer = null;
|
|
243
|
+
let openName;
|
|
244
|
+
let framed = 0;
|
|
245
|
+
function report(complete = false) {
|
|
246
|
+
onProgress?.({phase: 'download', completed: record.parts.length,
|
|
247
|
+
total: complete ? record.parts.length : null, unit: 'shards'});
|
|
248
|
+
}
|
|
249
|
+
async function closePart() {
|
|
250
|
+
await writer.close();
|
|
251
|
+
writer = null;
|
|
252
|
+
record.parts.push(openName);
|
|
253
|
+
await saveState(name, record);
|
|
254
|
+
framed = 0;
|
|
255
|
+
report();
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
report();
|
|
259
|
+
for await (const value of readModelChunks(body, signal)) {
|
|
260
|
+
let chunk;
|
|
261
|
+
if (is.string(value)) chunk = new TextEncoder().encode(value);
|
|
262
|
+
else if (value instanceof ArrayBuffer) chunk = new Uint8Array(value);
|
|
263
|
+
else if (ArrayBuffer.isView(value)) {
|
|
264
|
+
chunk = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
265
|
+
} else {
|
|
266
|
+
throw new TypeError('Model stream chunks must be strings, ArrayBuffers or typed arrays.');
|
|
267
|
+
}
|
|
268
|
+
let offset = 0;
|
|
269
|
+
while (offset < chunk.byteLength) {
|
|
270
|
+
throwIfModelAborted(signal);
|
|
271
|
+
if (!writer) {
|
|
272
|
+
openName = `${name}.arcane-part-${record.parts.length}`;
|
|
273
|
+
const handle = await (await directory()).getFileHandle(openName, {create: true});
|
|
274
|
+
writer = await handle.createWritable();
|
|
275
|
+
}
|
|
276
|
+
const end = Math.min(chunk.byteLength, offset + PART_FRAME - framed);
|
|
277
|
+
await writer.write(chunk.subarray(offset, end));
|
|
278
|
+
framed += end - offset;
|
|
279
|
+
offset = end;
|
|
280
|
+
if (framed === PART_FRAME) await closePart();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
throwIfModelAborted(signal);
|
|
284
|
+
if (writer) await closePart();
|
|
285
|
+
throwIfModelAborted(signal);
|
|
286
|
+
record.complete = true;
|
|
287
|
+
await saveState(name, record);
|
|
288
|
+
report(true);
|
|
289
|
+
return await assemble(record, signal);
|
|
290
|
+
} catch (error) {
|
|
291
|
+
if (writer) {
|
|
292
|
+
const failures = [error];
|
|
293
|
+
try {
|
|
294
|
+
await writer.abort(error);
|
|
295
|
+
} catch (cleanupError) {
|
|
296
|
+
failures.push(cleanupError);
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
await removeFile(openName);
|
|
300
|
+
} catch (cleanupError) {
|
|
301
|
+
failures.push(cleanupError);
|
|
302
|
+
}
|
|
303
|
+
if (failures.length > 1) {
|
|
304
|
+
throw new AggregateError(failures, 'Unable to settle the interrupted model part.');
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
309
|
+
}, signal);
|
|
310
|
+
}
|
|
311
|
+
// The HTTP Range owner already selected one physical part and owns its
|
|
312
|
+
// protocol framing. Keep its established filename and complete-file reads.
|
|
313
|
+
function writePart(name, body, {signal} = {}) {
|
|
314
|
+
return locked(name, async function writeNamedModelPart() {
|
|
315
|
+
throwIfModelAborted(signal);
|
|
316
|
+
const handle = await (await directory()).getFileHandle(name, {create: true});
|
|
317
|
+
const writer = await handle.createWritable();
|
|
318
|
+
let closed = false;
|
|
319
|
+
try {
|
|
320
|
+
for await (const chunk of readModelChunks(body, signal)) await writer.write(chunk);
|
|
321
|
+
throwIfModelAborted(signal);
|
|
322
|
+
await writer.close();
|
|
323
|
+
closed = true;
|
|
324
|
+
return await handle.getFile();
|
|
325
|
+
} catch (error) {
|
|
326
|
+
if (closed) throw error;
|
|
327
|
+
const failures = [error];
|
|
328
|
+
try {
|
|
329
|
+
await writer.abort(error);
|
|
330
|
+
} catch (cleanupError) {
|
|
331
|
+
failures.push(cleanupError);
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
await removeFile(name);
|
|
335
|
+
} catch (cleanupError) {
|
|
336
|
+
failures.push(cleanupError);
|
|
337
|
+
}
|
|
338
|
+
if (failures.length > 1) throw new AggregateError(failures, 'Unable to settle the model range part.');
|
|
339
|
+
throw error;
|
|
340
|
+
}
|
|
341
|
+
}, signal);
|
|
342
|
+
}
|
|
343
|
+
return {read, write, remove, state, readPartial, writePart};
|
|
344
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import {createDbopfsModelPartStore, readModelChunks, throwIfModelAborted, withDbopfsModelLock} from './dbopfs-model-parts.mjs';
|
|
2
|
+
|
|
3
|
+
const INDEX_NAME = 'arcane-model-resources.json';
|
|
4
|
+
|
|
5
|
+
/** URL/request semantics identify resources; OPFS names are ordinary index IDs. */
|
|
6
|
+
export function createDbopfsResourceStore({dbopfs, tableName, fetchImpl = globalThis.fetch?.bind(globalThis)}) {
|
|
7
|
+
const parts = createDbopfsModelPartStore({dbopfs, tableName});
|
|
8
|
+
// Credential-bearing request selection stays transient. Its durable entry
|
|
9
|
+
// has an ordinary index name and response metadata, never request headers.
|
|
10
|
+
const privateEntries = new Map();
|
|
11
|
+
let directoryPromise;
|
|
12
|
+
async function directory() {
|
|
13
|
+
if (dbopfs.readyPromise) await dbopfs.readyPromise;
|
|
14
|
+
directoryPromise ??= Promise.resolve(dbopfs.getTableHandle(tableName));
|
|
15
|
+
return directoryPromise;
|
|
16
|
+
}
|
|
17
|
+
async function readIndex() {
|
|
18
|
+
try {
|
|
19
|
+
const handle = await (await directory()).getFileHandle(INDEX_NAME, {create: false});
|
|
20
|
+
return JSON.parse(await (await handle.getFile()).text());
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (error?.name === 'NotFoundError' || error?.code === 'ENOENT') return {entries: []};
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function writeIndex(index) {
|
|
27
|
+
const table = await directory();
|
|
28
|
+
let existing;
|
|
29
|
+
try {
|
|
30
|
+
existing = await table.getFileHandle(INDEX_NAME, {create: false});
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error?.name !== 'NotFoundError' && error?.code !== 'ENOENT') throw error;
|
|
33
|
+
}
|
|
34
|
+
let writer;
|
|
35
|
+
try {
|
|
36
|
+
const handle = existing ?? await table.getFileHandle(INDEX_NAME, {create: true});
|
|
37
|
+
writer = await handle.createWritable();
|
|
38
|
+
await writer.write(JSON.stringify(index));
|
|
39
|
+
await writer.close();
|
|
40
|
+
} catch (error) {
|
|
41
|
+
const failures = [error];
|
|
42
|
+
try {
|
|
43
|
+
await writer?.abort(error);
|
|
44
|
+
} catch (cleanupError) {
|
|
45
|
+
failures.push(cleanupError);
|
|
46
|
+
}
|
|
47
|
+
if (!existing) {
|
|
48
|
+
try {
|
|
49
|
+
await table.removeEntry(INDEX_NAME);
|
|
50
|
+
} catch (cleanupError) {
|
|
51
|
+
if (cleanupError?.name !== 'NotFoundError' && cleanupError?.code !== 'ENOENT') failures.push(cleanupError);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (failures.length > 1) throw new AggregateError(failures, 'Unable to settle the model resource index.');
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function entryFor(key, metadata, privateRequest, signal) {
|
|
59
|
+
return withDbopfsModelLock(dbopfs, tableName, 'resource-index', async function updateResourceIndex() {
|
|
60
|
+
const index = await readIndex();
|
|
61
|
+
let entry = index.entries.find(function sameResource(candidate) {
|
|
62
|
+
return privateRequest ? candidate.name === privateEntries.get(key) : candidate.key === key;
|
|
63
|
+
});
|
|
64
|
+
let changed = false;
|
|
65
|
+
if (!entry) {
|
|
66
|
+
entry = {name: `arcane-resource-${index.entries.length}`};
|
|
67
|
+
if (!privateRequest) entry.key = key;
|
|
68
|
+
index.entries.push(entry);
|
|
69
|
+
changed = true;
|
|
70
|
+
}
|
|
71
|
+
if (metadata !== undefined) {
|
|
72
|
+
entry.metadata = metadata;
|
|
73
|
+
changed = true;
|
|
74
|
+
}
|
|
75
|
+
if (changed) await writeIndex(index);
|
|
76
|
+
if (privateRequest) privateEntries.set(key, entry.name);
|
|
77
|
+
return entry;
|
|
78
|
+
}, {signal});
|
|
79
|
+
}
|
|
80
|
+
async function fetchResource(input, {onProgress, ...options} = {}) {
|
|
81
|
+
const request = new Request(input, options);
|
|
82
|
+
const signal = request.signal;
|
|
83
|
+
const key = JSON.stringify([request.method, request.url, request.credentials, [...request.headers]]);
|
|
84
|
+
const privateRequest = request.headers.has('authorization') || request.headers.has('cookie');
|
|
85
|
+
function reportResourceProgress(progress) {
|
|
86
|
+
onProgress?.({...progress, url: request.url});
|
|
87
|
+
}
|
|
88
|
+
throwIfModelAborted(signal);
|
|
89
|
+
const selected = await entryFor(key, undefined, privateRequest, signal);
|
|
90
|
+
return withDbopfsModelLock(dbopfs, tableName, `resource:${selected.name}`, async function fetchStoredResource() {
|
|
91
|
+
throwIfModelAborted(signal);
|
|
92
|
+
const entry = await entryFor(key, undefined, privateRequest, signal);
|
|
93
|
+
const reusable = request.method === 'GET' && !request.headers.has('range')
|
|
94
|
+
&& !['reload', 'no-cache', 'no-store'].includes(request.cache);
|
|
95
|
+
if (reusable && entry.metadata?.status === 200) {
|
|
96
|
+
const file = await parts.read(entry.name, {signal});
|
|
97
|
+
if (file) {
|
|
98
|
+
const state = await parts.state(entry.name, {signal});
|
|
99
|
+
onProgress?.({phase: 'download', completed: state.parts.length,
|
|
100
|
+
total: state.parts.length, unit: 'shards', cached: true, url: request.url});
|
|
101
|
+
return {...entry.metadata, file};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
let response;
|
|
105
|
+
let append = false;
|
|
106
|
+
let metadata;
|
|
107
|
+
let remaining;
|
|
108
|
+
// Offset/Content-Range exist only at the HTTP resume boundary, never
|
|
109
|
+
// in resource identity, model admission, or application progress.
|
|
110
|
+
const previousHeaders = new Headers(entry.metadata?.headers);
|
|
111
|
+
const encoding = previousHeaders.get('content-encoding');
|
|
112
|
+
const partial = reusable && entry.metadata?.status === 200
|
|
113
|
+
&& (!encoding || encoding === 'identity')
|
|
114
|
+
? await parts.readPartial(entry.name, {signal}) : null;
|
|
115
|
+
try {
|
|
116
|
+
if (partial?.size) {
|
|
117
|
+
const headers = new Headers(request.headers);
|
|
118
|
+
headers.set('Range', `bytes=${partial.size}-`);
|
|
119
|
+
const validator = previousHeaders.get('etag') ?? previousHeaders.get('last-modified');
|
|
120
|
+
if (validator) headers.set('If-Range', validator);
|
|
121
|
+
response = await fetchImpl(new Request(request, {headers}));
|
|
122
|
+
const range = /^bytes (\d+)-(\d+)\/(\d+)$/u.exec(response.headers.get('content-range') ?? '');
|
|
123
|
+
if (response.status === 206 && range && Number(range[1]) === partial.size
|
|
124
|
+
&& Number(range[2]) >= Number(range[1])
|
|
125
|
+
&& Number(range[2]) + 1 === Number(range[3])) {
|
|
126
|
+
append = true;
|
|
127
|
+
remaining = Number(range[2]) - Number(range[1]) + 1;
|
|
128
|
+
// The completed response is the original full resource,
|
|
129
|
+
// not the resumed transport fragment delivered as a 206.
|
|
130
|
+
metadata = {...entry.metadata};
|
|
131
|
+
} else if (response.status === 206 || response.status === 416) {
|
|
132
|
+
await response.body?.cancel();
|
|
133
|
+
response = undefined;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
response ??= await fetchImpl(request);
|
|
137
|
+
throwIfModelAborted(signal);
|
|
138
|
+
metadata ??= {
|
|
139
|
+
status: response.status,
|
|
140
|
+
statusText: response.statusText,
|
|
141
|
+
headers: [...response.headers],
|
|
142
|
+
url: response.url || request.url,
|
|
143
|
+
redirected: response.redirected
|
|
144
|
+
};
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
// A transient server error must not erase a resumable model.
|
|
147
|
+
// Keep its complete response separate and preserve its status.
|
|
148
|
+
const file = await parts.write(`${entry.name}.response`, response.body, {
|
|
149
|
+
signal, onProgress: reportResourceProgress
|
|
150
|
+
});
|
|
151
|
+
return {...metadata, file};
|
|
152
|
+
}
|
|
153
|
+
if (!append) await parts.remove(entry.name, {signal});
|
|
154
|
+
await entryFor(key, metadata, privateRequest, signal);
|
|
155
|
+
let body = response.body;
|
|
156
|
+
if (append) {
|
|
157
|
+
const responseBody = body;
|
|
158
|
+
async function* resumedResponse() {
|
|
159
|
+
let received = 0;
|
|
160
|
+
for await (const chunk of readModelChunks(responseBody, signal)) {
|
|
161
|
+
received += chunk.byteLength;
|
|
162
|
+
if (received > remaining) throw new Error('The model server exceeded its HTTP Content-Range.');
|
|
163
|
+
yield chunk;
|
|
164
|
+
}
|
|
165
|
+
if (received !== remaining) throw new Error('The model server ended before its HTTP Content-Range.');
|
|
166
|
+
}
|
|
167
|
+
body = resumedResponse();
|
|
168
|
+
}
|
|
169
|
+
const file = await parts.write(entry.name, body, {
|
|
170
|
+
signal,
|
|
171
|
+
append,
|
|
172
|
+
onProgress: reportResourceProgress
|
|
173
|
+
});
|
|
174
|
+
return {...metadata, file};
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (response?.body && !response.body.locked) {
|
|
177
|
+
try {
|
|
178
|
+
await response.body.cancel(error);
|
|
179
|
+
} catch (cleanupError) {
|
|
180
|
+
if (cleanupError !== error) {
|
|
181
|
+
throw new AggregateError([error, cleanupError], 'Unable to settle the model resource response.');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
}, {signal});
|
|
188
|
+
}
|
|
189
|
+
return {fetchResource};
|
|
190
|
+
}
|
|
@@ -195,13 +195,29 @@ export function decodeDecisionOutputs(rows, outputs) {
|
|
|
195
195
|
return {decisions, outputs};
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
export async function loadDecisionRuntime(configuration, report) {
|
|
198
|
+
export async function loadDecisionRuntime(configuration, report, resourceFetch = null) {
|
|
199
199
|
const {family, model: repository, revision = 'main', device = 'webgpu',
|
|
200
200
|
dtype = family === 'laya' ? 'fp16' : 'fp32', runtime} = configuration;
|
|
201
201
|
report(
|
|
202
202
|
{phase: 'loading-runtime'}
|
|
203
203
|
);
|
|
204
204
|
const namespace = await import(runtime.moduleUrl);
|
|
205
|
+
if (resourceFetch) {
|
|
206
|
+
// Dedicated Worker environment: all actual tokenizer/config/graph and
|
|
207
|
+
// external-data requests use the selected SDK store. A cache hook alone
|
|
208
|
+
// can be caught and ignored by the upstream loader.
|
|
209
|
+
namespace.env.fetch = function fetchDecisionResource(input, options) {
|
|
210
|
+
return resourceFetch(input, {
|
|
211
|
+
...options,
|
|
212
|
+
onProgress: function reportDecisionResource(progress) {
|
|
213
|
+
report(progress);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
};
|
|
217
|
+
namespace.env.useBrowserCache = false;
|
|
218
|
+
namespace.env.useCustomCache = false;
|
|
219
|
+
namespace.env.useFSCache = false;
|
|
220
|
+
}
|
|
205
221
|
if (runtime.wasmPaths !== undefined) {
|
|
206
222
|
namespace.env.backends.onnx.wasm.wasmPaths = runtime.wasmPaths;
|
|
207
223
|
}
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import Is from '../dependencies/strong-type/index.js';
|
|
2
2
|
import {loadDecisionRuntime} from './decision-runtime.mjs';
|
|
3
|
+
import {createModelResourceClient} from './model-resource-bridge.mjs';
|
|
3
4
|
|
|
4
5
|
const is = new Is(false);
|
|
5
6
|
let engine;
|
|
7
|
+
const nativeFetch = globalThis.fetch.bind(globalThis);
|
|
8
|
+
const resources = createModelResourceClient(
|
|
9
|
+
{
|
|
10
|
+
send: function sendModelResource(message) {
|
|
11
|
+
globalThis.postMessage(message);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
);
|
|
6
15
|
|
|
7
16
|
function errorRecord(error, seen = new Map()) {
|
|
8
17
|
if (!is.error(error)) return error;
|
|
@@ -26,16 +35,27 @@ function errorRecord(error, seen = new Map()) {
|
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
async function handleDecisionOperation(event) {
|
|
38
|
+
if (resources.receive(event.data)) return;
|
|
29
39
|
const {id, op, payload} = event.data;
|
|
30
40
|
try {
|
|
31
41
|
if (op === 'load') {
|
|
42
|
+
if (event.data.storedResources) {
|
|
43
|
+
// Cover native ONNX support-file fetches as well as env.fetch.
|
|
44
|
+
// Native module imports remain the browser module loader's job.
|
|
45
|
+
globalThis.fetch = function fetchDecisionSupport(input, options) {
|
|
46
|
+
const url = new URL(is.string(input) ? input : input.url ?? input.href, globalThis.location.href);
|
|
47
|
+
if (url.protocol === 'blob:' || url.protocol === 'data:') return nativeFetch(input, options);
|
|
48
|
+
return resources.fetch(input, options);
|
|
49
|
+
};
|
|
50
|
+
}
|
|
32
51
|
engine = await loadDecisionRuntime(
|
|
33
52
|
payload,
|
|
34
53
|
function reportDecisionProgress(progress) {
|
|
35
54
|
globalThis.postMessage(
|
|
36
55
|
{id, progress}
|
|
37
56
|
);
|
|
38
|
-
}
|
|
57
|
+
},
|
|
58
|
+
event.data.storedResources ? resources.fetch : null
|
|
39
59
|
);
|
|
40
60
|
globalThis.postMessage(
|
|
41
61
|
{id, result: {loaded: true}}
|