arcane-os 0.4.2 → 0.5.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.
@@ -15,6 +15,7 @@ const completeValue = (value) => value;
15
15
  const MODEL_MANIFEST_SCHEMA = "arcane.ai.browser-wasm.model.v4";
16
16
  const BROWSER_MODEL_SOURCES = new WeakSet();
17
17
  const BROWSER_MODEL_SOURCE_METADATA = new WeakMap();
18
+ const BROWSER_MODEL_SOURCE_TRANSPORTS = new WeakMap();
18
19
  const MODEL_DESCRIPTOR_METADATA = new WeakMap();
19
20
  const DBOPFS_MODEL_STORES = new WeakSet();
20
21
  const V1_LLM_PROVIDER_ADAPTERS = new WeakMap();
@@ -25,6 +26,11 @@ const WEBGPU_ADAPTER_SELECTION_PROTOCOL = "arcane-ai-webgpu-adapter-selection/1"
25
26
  const CHROME_HIGH_PERFORMANCE_GPU_FLAG_URL =
26
27
  "chrome://flags/#force-high-performance-gpu";
27
28
  const MODEL_LOAD_HEARTBEAT_MS = 5_000;
29
+ const DEFAULT_MODEL_DOWNLOAD_CONCURRENCY = 4;
30
+ const MODEL_DOWNLOAD_PROGRESS_INTERVAL_MS = 250;
31
+ const MODEL_DOWNLOAD_SPEED_WINDOW_MS = 5_000;
32
+ const MODEL_DOWNLOAD_MAX_RANGE_PARTS = 16;
33
+ const MODEL_DOWNLOAD_TARGET_RANGE_BYTES = 128_000_000;
28
34
  const INTEL_VENDOR_ID = 0x8086;
29
35
  const CAPABILITY_POLICY_PROTOCOL = "arcane-ai-browser-capability-policy/1";
30
36
  let highPerformanceGpuNoticeShown = false;
@@ -50,6 +56,78 @@ function normalizationSignal(error, signal) {
50
56
  return error?.code === "ARCANE_AI_WORKER_TERMINATION_UNCONFIRMED" ? null : signal;
51
57
  }
52
58
 
59
+ async function cancelReadableBody(body, reason) {
60
+ try {
61
+ await body?.cancel?.(reason);
62
+ } catch {
63
+ // Cancellation is cleanup; preserve the transfer failure that prompted it.
64
+ }
65
+ }
66
+
67
+ async function cancelOpenedDownload(opened, reason) {
68
+ try {
69
+ await opened?.cancel?.(reason);
70
+ } catch {
71
+ // Cancellation is cleanup; preserve the transfer failure that prompted it.
72
+ }
73
+ }
74
+
75
+ function httpContentRange(value) {
76
+ const match = /^bytes ([0-9]+)-([0-9]+)\/([0-9]+)$/iu.exec(
77
+ String(value ?? "").trim(),
78
+ );
79
+ if (!match) return null;
80
+ const start = Number(match[1]);
81
+ const end = Number(match[2]);
82
+ const total = Number(match[3]);
83
+ if (
84
+ !Number.isSafeInteger(start)
85
+ || !Number.isSafeInteger(end)
86
+ || !Number.isSafeInteger(total)
87
+ || start < 0
88
+ || end < start
89
+ || total <= end
90
+ ) return null;
91
+ return completeValue({ start, end, total });
92
+ }
93
+
94
+ function httpContentLength(value) {
95
+ const length = Number(String(value ?? "").trim());
96
+ return Number.isSafeInteger(length) && length > 0 ? length : null;
97
+ }
98
+
99
+ function downloadConcurrencyValue(value) {
100
+ if (!Number.isSafeInteger(value) || value < 1) {
101
+ throw new RangeError("Model downloadConcurrency must be a positive safe integer.");
102
+ }
103
+ return value;
104
+ }
105
+
106
+ function modelHttpRanges(total) {
107
+ const count = Math.min(
108
+ MODEL_DOWNLOAD_MAX_RANGE_PARTS,
109
+ Math.ceil(total / MODEL_DOWNLOAD_TARGET_RANGE_BYTES),
110
+ total,
111
+ );
112
+ const width = Math.floor(total / count);
113
+ const remainder = total % count;
114
+ const ranges = [];
115
+ let start = 0;
116
+ for (let index = 0; index < count; index += 1) {
117
+ const length = width + (index < remainder ? 1 : 0);
118
+ const end = start + length - 1;
119
+ ranges.push(completeValue({ start, end, total }));
120
+ start = end + 1;
121
+ }
122
+ return completeValue(ranges);
123
+ }
124
+
125
+ function progressClock() {
126
+ return typeof globalThis.performance?.now === "function"
127
+ ? globalThis.performance.now()
128
+ : Date.now();
129
+ }
130
+
53
131
  function modelSourceUrl(value) {
54
132
  let url;
55
133
  try {
@@ -134,6 +212,9 @@ function descriptorFile(value, { fallbackName = null } = {}) {
134
212
  name: descriptorFileName(value, url, fallbackName),
135
213
  url: url.href,
136
214
  };
215
+ if (Number.isSafeInteger(value.bytes) && value.bytes > 0) {
216
+ file.bytes = value.bytes;
217
+ }
137
218
  return completeValue(file);
138
219
  }
139
220
 
@@ -177,11 +258,13 @@ function modelDescriptor(value) {
177
258
  const publicFiles = completeValue(files.map((file) => completeValue({
178
259
  name: file.name,
179
260
  url: file.url,
261
+ ...(file.bytes === undefined ? {} : { bytes: file.bytes }),
180
262
  })));
181
263
  let descriptor;
182
264
  if (legacy) {
183
265
  const [file] = files;
184
266
  descriptor = { id, url: file.url };
267
+ if (file.bytes !== undefined) descriptor.bytes = file.bytes;
185
268
  } else {
186
269
  descriptor = { id, files: publicFiles };
187
270
  }
@@ -276,61 +359,183 @@ export function createBrowserModelSource(descriptor, {
276
359
  } = {}) {
277
360
  const model = modelDescriptor(descriptor);
278
361
  const metadata = MODEL_DESCRIPTOR_METADATA.get(model);
362
+ const rangeRequestUrls = new Array(metadata.files.length).fill(null);
279
363
 
280
- async function open(memberOrOptions = 0, options = {}) {
281
- let memberIndex = memberOrOptions;
282
- if (!Number.isSafeInteger(memberOrOptions)) {
283
- if (metadata.files.length !== 1) {
284
- throw new TypeError("A browser model file index is required for a multi-file source.");
285
- }
286
- memberIndex = 0;
287
- options = memberOrOptions ?? {};
364
+ function selectedMember(memberIndex) {
365
+ if (!Number.isSafeInteger(memberIndex)) {
366
+ throw new TypeError("A browser model file index must be a safe integer.");
288
367
  }
289
368
  if (memberIndex < 0 || memberIndex >= metadata.files.length) {
290
369
  throw new RangeError("Browser model file index is out of range.");
291
370
  }
292
- const member = metadata.files[memberIndex];
293
- const { signal } = options;
371
+ return metadata.files[memberIndex];
372
+ }
373
+
374
+ async function request(memberIndex, { signal, range = null, url = null } = {}) {
375
+ const member = selectedMember(memberIndex);
294
376
  throwIfAborted(signal, "install");
295
377
  const fetchFunction = fetchImpl ?? globalThis.fetch?.bind(globalThis);
296
378
  if (typeof fetchFunction !== "function") {
297
379
  throw fail("ARCANE_AI_MODEL_SOURCE_UNAVAILABLE", "Browser fetch is unavailable.");
298
380
  }
381
+ const requestOptions = {
382
+ cache: "no-store",
383
+ redirect: "follow",
384
+ signal,
385
+ };
386
+ if (range) requestOptions.headers = { Range: `bytes=${range.start}-${range.end}` };
299
387
 
300
388
  let response;
301
389
  try {
302
- response = await fetchFunction(member.url, {
303
- cache: "no-store",
304
- redirect: "follow",
305
- signal,
306
- });
390
+ response = await fetchFunction(url ?? member.url, requestOptions);
307
391
  } catch (error) {
308
392
  if (signal?.aborted || error?.name === "AbortError") throwIfAborted(signal, "install");
309
393
  throw fail("ARCANE_AI_MODEL_DOWNLOAD_FAILED", "The model download failed.", error);
310
394
  }
311
- if (!response?.ok) {
312
- await response?.body?.cancel?.().catch(() => undefined);
313
- throw fail(
314
- "ARCANE_AI_MODEL_DOWNLOAD_FAILED",
315
- `The model server returned HTTP ${response?.status ?? "unknown"}.`,
316
- );
317
- }
318
395
  let finalUrl;
319
396
  try {
320
- finalUrl = new URL(response.url || member.url);
397
+ finalUrl = new URL(response?.url || url || member.url);
321
398
  } catch {
322
399
  finalUrl = null;
323
400
  }
324
- finalUrl ??= new URL(member.url);
401
+ finalUrl ??= new URL(url ?? member.url);
402
+ return completeValue({ member, response, finalUrl: finalUrl.href });
403
+ }
404
+
405
+ async function rejectHttpResponse(download) {
406
+ await cancelReadableBody(download.response?.body);
407
+ throw fail(
408
+ "ARCANE_AI_MODEL_DOWNLOAD_FAILED",
409
+ `The model server returned HTTP ${download.response?.status ?? "unknown"}.`,
410
+ );
411
+ }
412
+
413
+ function openedDownload(download) {
414
+ const { member, response, finalUrl } = download;
325
415
  if (!response.body || typeof response.body.getReader !== "function") {
326
416
  throw fail("ARCANE_AI_MODEL_SOURCE_INVALID", "The model response did not provide a byte stream.");
327
417
  }
418
+ async function cancel(reason) {
419
+ await response.body.cancel(reason);
420
+ }
328
421
  return completeValue({
329
422
  body: response.body,
330
423
  requestedUrl: member.url,
331
- finalUrl: finalUrl.href,
332
- cancel: (reason) => response.body.cancel(reason),
424
+ finalUrl,
425
+ contentLength: httpContentLength(response.headers?.get?.("content-length")),
426
+ cancel,
427
+ });
428
+ }
429
+
430
+ async function open(memberOrOptions = 0, options = {}) {
431
+ let memberIndex = memberOrOptions;
432
+ if (!Number.isSafeInteger(memberOrOptions)) {
433
+ if (metadata.files.length !== 1) {
434
+ throw new TypeError("A browser model file index is required for a multi-file source.");
435
+ }
436
+ memberIndex = 0;
437
+ options = memberOrOptions ?? {};
438
+ }
439
+ const { signal } = options;
440
+ const download = await request(memberIndex, { signal });
441
+ if (!download.response?.ok) await rejectHttpResponse(download);
442
+ return openedDownload(download);
443
+ }
444
+
445
+ async function probeRange(memberIndex, { signal } = {}) {
446
+ const download = await request(memberIndex, {
447
+ signal,
448
+ range: { start: 0, end: 0 },
449
+ });
450
+ if (download.response?.status === 200) {
451
+ if (download.finalUrl !== download.member.url) {
452
+ let redirectedProbe = null;
453
+ try {
454
+ redirectedProbe = await request(memberIndex, {
455
+ signal,
456
+ range: { start: 0, end: 0 },
457
+ url: download.finalUrl,
458
+ });
459
+ if (redirectedProbe.response?.status === 206) {
460
+ const header = redirectedProbe.response.headers?.get?.("content-range");
461
+ const observed = httpContentRange(header);
462
+ const total = observed?.total
463
+ ?? httpContentLength(download.response.headers?.get?.("content-length"))
464
+ ?? selectedMember(memberIndex).bytes
465
+ ?? null;
466
+ if (
467
+ (!header || observed)
468
+ && (!observed || (observed.start === 0 && observed.end === 0))
469
+ && Number.isSafeInteger(total)
470
+ && total > 0
471
+ ) {
472
+ await cancelReadableBody(download.response.body);
473
+ await cancelReadableBody(redirectedProbe.response.body);
474
+ rangeRequestUrls[memberIndex] = redirectedProbe.finalUrl;
475
+ return completeValue({ kind: "supported", total });
476
+ }
477
+ }
478
+ await cancelReadableBody(redirectedProbe.response?.body);
479
+ } catch (error) {
480
+ if (signal?.aborted) {
481
+ await cancelReadableBody(download.response.body, signal.reason);
482
+ throw error;
483
+ }
484
+ await cancelReadableBody(redirectedProbe?.response?.body, error);
485
+ }
486
+ }
487
+ return completeValue({ kind: "complete", opened: openedDownload(download) });
488
+ }
489
+ if (download.response?.status !== 206) await rejectHttpResponse(download);
490
+ const header = download.response.headers?.get?.("content-range");
491
+ const observed = httpContentRange(header);
492
+ await cancelReadableBody(download.response.body);
493
+ if (
494
+ (header && !observed)
495
+ || (observed && (observed.start !== 0 || observed.end !== 0))
496
+ ) {
497
+ return completeValue({ kind: "unsupported" });
498
+ }
499
+ const total = observed?.total ?? selectedMember(memberIndex).bytes ?? null;
500
+ if (total === null) return completeValue({ kind: "unsupported" });
501
+ rangeRequestUrls[memberIndex] = download.finalUrl;
502
+ return completeValue({ kind: "supported", total });
503
+ }
504
+
505
+ async function openRange(memberIndex, { signal, start, end, total } = {}) {
506
+ if (
507
+ !Number.isSafeInteger(start)
508
+ || !Number.isSafeInteger(end)
509
+ || !Number.isSafeInteger(total)
510
+ || start < 0
511
+ || end < start
512
+ || end >= total
513
+ ) {
514
+ throw new RangeError("Browser model HTTP range framing is invalid.");
515
+ }
516
+ const download = await request(memberIndex, {
517
+ signal,
518
+ range: { start, end },
519
+ url: rangeRequestUrls[memberIndex],
333
520
  });
521
+ const header = download.response?.headers?.get?.("content-range");
522
+ const observed = httpContentRange(header);
523
+ if (
524
+ download.response?.status !== 206
525
+ || (header && !observed)
526
+ || (observed && (
527
+ observed.start !== start
528
+ || observed.end !== end
529
+ || observed.total !== total
530
+ ))
531
+ ) {
532
+ await cancelReadableBody(download.response?.body);
533
+ throw fail(
534
+ "ARCANE_AI_MODEL_DOWNLOAD_FAILED",
535
+ "The model server did not preserve the requested HTTP byte range.",
536
+ );
537
+ }
538
+ return openedDownload(download);
334
539
  }
335
540
 
336
541
  const sourceRecord = {
@@ -346,6 +551,7 @@ export function createBrowserModelSource(descriptor, {
346
551
  const source = completeValue(sourceRecord);
347
552
  BROWSER_MODEL_SOURCES.add(source);
348
553
  BROWSER_MODEL_SOURCE_METADATA.set(source, metadata);
554
+ BROWSER_MODEL_SOURCE_TRANSPORTS.set(source, completeValue({ probeRange, openRange }));
349
555
  return source;
350
556
  }
351
557
 
@@ -420,6 +626,34 @@ function storageName(source, { legacy = false } = {}) {
420
626
  });
421
627
  }
422
628
 
629
+ function rangePartName(modelName, range) {
630
+ return `${modelName}.range-${range.start}-${range.end}-of-${range.total}.part`;
631
+ }
632
+
633
+ function rangePartPrefix(modelName) {
634
+ return `${modelName}.range-`;
635
+ }
636
+
637
+ function rangePartDetails(modelName, name) {
638
+ const prefix = rangePartPrefix(modelName);
639
+ if (!name.startsWith(prefix)) return null;
640
+ const match = /^([0-9]+)-([0-9]+)-of-([0-9]+)\.part$/u.exec(name.slice(prefix.length));
641
+ if (!match) return null;
642
+ const start = Number(match[1]);
643
+ const end = Number(match[2]);
644
+ const total = Number(match[3]);
645
+ if (
646
+ !Number.isSafeInteger(start)
647
+ || !Number.isSafeInteger(end)
648
+ || !Number.isSafeInteger(total)
649
+ || start < 0
650
+ || end < start
651
+ || total <= end
652
+ ) return null;
653
+ const range = completeValue({ start, end, total });
654
+ return rangePartName(modelName, range) === name ? range : null;
655
+ }
656
+
423
657
  function securitySnapshot(security) {
424
658
  return security?.secure === true ? { secure: true } : undefined;
425
659
  }
@@ -433,6 +667,7 @@ export function createDbopfsModelStore({
433
667
  dbopfs,
434
668
  tableName = "arcane_ai_browser_models",
435
669
  estimateStorage = null,
670
+ downloadConcurrency = DEFAULT_MODEL_DOWNLOAD_CONCURRENCY,
436
671
  } = {}) {
437
672
  if (!dbopfs || (typeof dbopfs !== "object" && typeof dbopfs !== "function")) {
438
673
  throw new TypeError("createDbopfsModelStore requires an existing DBOPFS instance.");
@@ -446,6 +681,7 @@ export function createDbopfsModelStore({
446
681
  if (estimateStorage !== null && typeof estimateStorage !== "function") {
447
682
  throw new TypeError("estimateStorage must be a function or null.");
448
683
  }
684
+ const workerLimit = downloadConcurrencyValue(downloadConcurrency);
449
685
  let tablePromise = null;
450
686
 
451
687
  async function table() {
@@ -478,38 +714,630 @@ export function createDbopfsModelStore({
478
714
  }
479
715
  }
480
716
 
481
- async function write(name, body, { signal } = {}) {
717
+ function createDownloadProgressReporter(members, onProgress, retainFailure) {
718
+ const memberTotals = members.map((member) => member.bytes ?? null);
719
+ let loadedBytes = 0;
720
+ let completed = 0;
721
+ let activeTransfers = 0;
722
+ let transferMode = members.length === 1 ? "probing" : "files";
723
+ let timer = null;
724
+ let lastPublishedAt = 0;
725
+ let samples = [];
726
+
727
+ function totalBytesValue() {
728
+ if (memberTotals.some((value) => value === null)) return null;
729
+ const totalBytes = memberTotals.reduce((sum, value) => sum + value, 0);
730
+ return Number.isSafeInteger(totalBytes) ? totalBytes : null;
731
+ }
732
+
733
+ function progressRecord(now) {
734
+ samples.push(completeValue({ at: now, loadedBytes }));
735
+ const oldestUsefulTime = now - MODEL_DOWNLOAD_SPEED_WINDOW_MS;
736
+ while (samples.length > 1 && samples[1].at <= oldestUsefulTime) samples.shift();
737
+ const firstSample = samples[0];
738
+ const elapsedSeconds = (now - firstSample.at) / 1_000;
739
+ const sampledBytes = loadedBytes - firstSample.loadedBytes;
740
+ const bytesPerSecond = sampledBytes > 0 && elapsedSeconds > 0
741
+ ? Math.round(sampledBytes / elapsedSeconds)
742
+ : null;
743
+ const totalBytes = totalBytesValue();
744
+ const remainingBytes = totalBytes === null
745
+ ? null
746
+ : Math.max(0, totalBytes - loadedBytes);
747
+ const etaSeconds = remainingBytes === 0
748
+ ? 0
749
+ : bytesPerSecond === null || remainingBytes === null
750
+ ? null
751
+ : Math.ceil(remainingBytes / bytesPerSecond);
752
+ return completeValue({
753
+ phase: "download",
754
+ completed,
755
+ total: members.length,
756
+ unit: "files",
757
+ heartbeat: false,
758
+ loadedBytes,
759
+ totalBytes,
760
+ remainingBytes,
761
+ bytesPerSecond,
762
+ etaSeconds,
763
+ activeTransfers,
764
+ transferLimit: workerLimit,
765
+ transferMode,
766
+ });
767
+ }
768
+
769
+ function stopTimer() {
770
+ if (timer === null) return;
771
+ clearInterval(timer);
772
+ timer = null;
773
+ }
774
+
775
+ function publish({ force = false } = {}) {
776
+ if (onProgress === null) return;
777
+ const now = progressClock();
778
+ if (!force && now - lastPublishedAt < MODEL_DOWNLOAD_PROGRESS_INTERVAL_MS) return;
779
+ onProgress(progressRecord(now));
780
+ lastPublishedAt = now;
781
+ }
782
+
783
+ function publishSafely(options) {
784
+ try {
785
+ publish(options);
786
+ return true;
787
+ } catch (error) {
788
+ stopTimer();
789
+ retainFailure(error);
790
+ return false;
791
+ }
792
+ }
793
+
794
+ function setMemberTotal(memberIndex, totalBytes) {
795
+ if (!Number.isSafeInteger(totalBytes) || totalBytes <= 0) return;
796
+ if (memberTotals[memberIndex] === totalBytes) return;
797
+ memberTotals[memberIndex] = totalBytes;
798
+ publishSafely({ force: true });
799
+ }
800
+
801
+ function addBytes(value) {
802
+ if (!Number.isSafeInteger(value) || value <= 0) return;
803
+ loadedBytes += value;
804
+ publishSafely();
805
+ }
806
+
807
+ function discardBytes(value) {
808
+ if (!Number.isSafeInteger(value) || value <= 0) return;
809
+ loadedBytes = Math.max(0, loadedBytes - value);
810
+ samples = [];
811
+ publishSafely({ force: true });
812
+ }
813
+
814
+ return completeValue({
815
+ addBytes,
816
+ beginTransfer() {
817
+ activeTransfers += 1;
818
+ publishSafely({ force: true });
819
+ },
820
+ completeMember(memberIndex, totalBytes) {
821
+ if (Number.isSafeInteger(totalBytes) && totalBytes > 0) {
822
+ memberTotals[memberIndex] = totalBytes;
823
+ }
824
+ completed += 1;
825
+ publishSafely({ force: true });
826
+ },
827
+ discardBytes,
828
+ dispose: stopTimer,
829
+ endTransfer({ publishProgress = true } = {}) {
830
+ activeTransfers = Math.max(0, activeTransfers - 1);
831
+ if (publishProgress) publishSafely({ force: true });
832
+ },
833
+ finish() {
834
+ publishSafely({ force: true });
835
+ },
836
+ setMemberTotal,
837
+ setMode(value) {
838
+ if (transferMode === value) return;
839
+ transferMode = value;
840
+ publishSafely({ force: true });
841
+ },
842
+ restoreBytes(value) {
843
+ if (!Number.isSafeInteger(value) || value <= 0) return;
844
+ loadedBytes += value;
845
+ samples = [];
846
+ publishSafely({ force: true });
847
+ },
848
+ restoreMember(memberIndex, totalBytes) {
849
+ if (!Number.isSafeInteger(totalBytes) || totalBytes <= 0) return;
850
+ memberTotals[memberIndex] = totalBytes;
851
+ loadedBytes += totalBytes;
852
+ completed += 1;
853
+ samples = [];
854
+ publishSafely({ force: true });
855
+ },
856
+ start() {
857
+ if (!publishSafely({ force: true })) return;
858
+ if (onProgress !== null && timer === null) {
859
+ timer = setInterval(function publishDownloadProgressTick() {
860
+ publishSafely();
861
+ }, MODEL_DOWNLOAD_PROGRESS_INTERVAL_MS);
862
+ }
863
+ },
864
+ });
865
+ }
866
+
867
+ async function write(name, body, {
868
+ signal,
869
+ onChunk = null,
870
+ onDiscard = null,
871
+ } = {}) {
482
872
  const directory = await table();
483
873
  const handle = await directory.getFileHandle(name, { create: true });
484
874
  const writable = await handle.createWritable();
875
+ let written = 0;
485
876
  try {
486
877
  for await (const chunk of byteChunks(body, signal)) {
487
878
  await writable.write(chunk);
879
+ written += chunk.byteLength;
880
+ onChunk?.(chunk.byteLength);
881
+ throwIfAborted(signal, "install");
488
882
  }
489
883
  throwIfAborted(signal, "install");
490
884
  await writable.close();
491
- return undefined;
885
+ return written;
492
886
  } catch (error) {
493
887
  await writable.abort?.(error).catch(() => undefined);
494
888
  await directory.removeEntry(name).catch(() => undefined);
889
+ onDiscard?.(written);
890
+ throw error;
891
+ }
892
+ }
893
+
894
+ async function storedModelFile(name) {
895
+ const modelFile = await file(name);
896
+ if (!modelFile || modelFile.size === 0) {
897
+ throw fail(
898
+ "ARCANE_AI_MODEL_CACHE_REJECTED",
899
+ "A stored model file could not be reopened as a non-empty model.",
900
+ );
901
+ }
902
+ return modelFile;
903
+ }
904
+
905
+ async function writeOpenedModel(name, opened, {
906
+ signal,
907
+ onChunk = null,
908
+ onDiscard = null,
909
+ } = {}) {
910
+ let written = 0;
911
+ try {
912
+ written = await write(name, opened.body, { signal, onChunk, onDiscard });
913
+ return await storedModelFile(name);
914
+ } catch (error) {
915
+ if (written > 0) onDiscard?.(written);
916
+ await cancelOpenedDownload(opened, error);
495
917
  throw error;
496
918
  }
497
919
  }
498
920
 
921
+ async function writeParallelRanges(
922
+ source,
923
+ memberIndex,
924
+ name,
925
+ total,
926
+ {
927
+ signal,
928
+ progress,
929
+ retainInstallFailure = null,
930
+ rangeWorkerLimit = workerLimit,
931
+ } = {},
932
+ ) {
933
+ const transport = BROWSER_MODEL_SOURCE_TRANSPORTS.get(source);
934
+ const ranges = modelHttpRanges(total);
935
+ const partFiles = new Array(ranges.length);
936
+ const pendingRangeIndexes = [];
937
+ for (let rangeIndex = 0; rangeIndex < ranges.length; rangeIndex += 1) {
938
+ throwIfAborted(signal, "install");
939
+ const range = ranges[rangeIndex];
940
+ const partName = rangePartName(name, range);
941
+ const partFile = await file(partName);
942
+ const expected = range.end - range.start + 1;
943
+ if (partFile?.size === expected) {
944
+ partFiles[rangeIndex] = partFile;
945
+ progress?.restoreBytes(expected);
946
+ } else {
947
+ if (partFile) await removeEntry(partName);
948
+ pendingRangeIndexes.push(rangeIndex);
949
+ }
950
+ }
951
+ const linked = linkAbortSignal(signal);
952
+ const downloadSignal = linked.controller.signal;
953
+ let nextPendingIndex = 0;
954
+ let failure = null;
955
+
956
+ function retainFailure(error) {
957
+ if (failure === null) failure = error;
958
+ if (!downloadSignal.aborted) linked.controller.abort(error);
959
+ }
960
+
961
+ async function transferRangeWorker() {
962
+ while (true) {
963
+ const pendingIndex = nextPendingIndex;
964
+ nextPendingIndex += 1;
965
+ if (pendingIndex >= pendingRangeIndexes.length) return;
966
+ const rangeIndex = pendingRangeIndexes[pendingIndex];
967
+ const range = ranges[rangeIndex];
968
+ const partName = rangePartName(name, range);
969
+ let opened = null;
970
+ let writable = null;
971
+ let received = 0;
972
+ progress?.beginTransfer();
973
+ try {
974
+ opened = await transport.openRange(memberIndex, {
975
+ signal: downloadSignal,
976
+ start: range.start,
977
+ end: range.end,
978
+ total: range.total,
979
+ });
980
+ const directory = await table();
981
+ const handle = await directory.getFileHandle(partName, { create: true });
982
+ writable = await handle.createWritable();
983
+ const expected = range.end - range.start + 1;
984
+ for await (const chunk of byteChunks(opened.body, downloadSignal)) {
985
+ const nextReceived = received + chunk.byteLength;
986
+ if (!Number.isSafeInteger(nextReceived) || nextReceived > expected) {
987
+ throw fail(
988
+ "ARCANE_AI_MODEL_DOWNLOAD_FAILED",
989
+ "The model server returned more content than the requested HTTP byte range.",
990
+ );
991
+ }
992
+ await writable.write(chunk);
993
+ received = nextReceived;
994
+ progress?.addBytes(chunk.byteLength);
995
+ throwIfAborted(downloadSignal, "install");
996
+ }
997
+ if (received !== expected) {
998
+ throw fail(
999
+ "ARCANE_AI_MODEL_DOWNLOAD_FAILED",
1000
+ "The model server returned less content than the requested HTTP byte range.",
1001
+ );
1002
+ }
1003
+ throwIfAborted(downloadSignal, "install");
1004
+ await writable.close();
1005
+ writable = null;
1006
+ partFiles[rangeIndex] = await storedModelFile(partName);
1007
+ } catch (error) {
1008
+ retainInstallFailure?.(error);
1009
+ retainFailure(error);
1010
+ await cancelOpenedDownload(opened, error);
1011
+ try {
1012
+ await writable?.abort?.(error);
1013
+ } catch {
1014
+ // The range part is removed below even if its writable already closed.
1015
+ }
1016
+ await removeEntry(partName).catch(() => undefined);
1017
+ progress?.discardBytes(received);
1018
+ throw error;
1019
+ } finally {
1020
+ progress?.endTransfer({ publishProgress: failure === null });
1021
+ }
1022
+ }
1023
+ }
1024
+
1025
+ const workers = [];
1026
+ const workerCount = Math.min(rangeWorkerLimit, pendingRangeIndexes.length);
1027
+ for (let index = 0; index < workerCount; index += 1) {
1028
+ workers.push(transferRangeWorker());
1029
+ }
1030
+ try {
1031
+ const results = await Promise.allSettled(workers);
1032
+ if (failure === null) {
1033
+ for (const result of results) {
1034
+ if (result.status === "rejected") {
1035
+ failure = result.reason;
1036
+ break;
1037
+ }
1038
+ }
1039
+ }
1040
+ if (failure !== null) throw failure;
1041
+ throwIfAborted(downloadSignal, "install");
1042
+ const modelFile = new Blob(partFiles, { type: "application/octet-stream" });
1043
+ await removeStaleRangePartsAfterCompletion(name, ranges);
1044
+ return modelFile;
1045
+ } catch (error) {
1046
+ retainFailure(error);
1047
+ throw failure;
1048
+ } finally {
1049
+ linked.release();
1050
+ }
1051
+ }
1052
+
1053
+ async function installOneFile(
1054
+ source,
1055
+ memberIndex,
1056
+ name,
1057
+ {
1058
+ signal,
1059
+ progress,
1060
+ retainFailure = null,
1061
+ multiFile = false,
1062
+ rangeWorkerLimit = workerLimit,
1063
+ } = {},
1064
+ ) {
1065
+ const transport = BROWSER_MODEL_SOURCE_TRANSPORTS.get(source);
1066
+ if (!transport) return null;
1067
+ if (!multiFile) progress?.setMode("probing");
1068
+ progress?.beginTransfer();
1069
+ let probe;
1070
+ try {
1071
+ probe = await transport.probeRange(memberIndex, { signal });
1072
+ } catch (error) {
1073
+ retainFailure?.(error);
1074
+ progress?.endTransfer({ publishProgress: false });
1075
+ throw error;
1076
+ }
1077
+ if (probe.kind === "complete") {
1078
+ if (!multiFile) progress?.setMode("single");
1079
+ progress?.setMemberTotal(memberIndex, probe.opened.contentLength);
1080
+ try {
1081
+ const modelFile = await writeOpenedModel(name, probe.opened, {
1082
+ signal,
1083
+ onChunk: progress?.addBytes,
1084
+ onDiscard: progress?.discardBytes,
1085
+ });
1086
+ await removeRangePartsAfterWholeFile(name, probe.opened.contentLength);
1087
+ return modelFile;
1088
+ } catch (error) {
1089
+ retainFailure?.(error);
1090
+ throw error;
1091
+ } finally {
1092
+ progress?.endTransfer({ publishProgress: false });
1093
+ }
1094
+ }
1095
+ progress?.endTransfer({ publishProgress: false });
1096
+ if (probe.kind !== "supported") return null;
1097
+ if (!multiFile) progress?.setMode("ranges");
1098
+ progress?.setMemberTotal(memberIndex, probe.total);
1099
+ return writeParallelRanges(source, memberIndex, name, probe.total, {
1100
+ signal,
1101
+ progress,
1102
+ rangeWorkerLimit,
1103
+ retainInstallFailure: retainFailure,
1104
+ });
1105
+ }
1106
+
499
1107
  async function removeNames(names) {
500
- const removed = [await removeEntry(names.manifest)];
1108
+ const removed = [];
501
1109
  for (const entry of names.models) removed.push(await removeEntry(entry.name));
1110
+ removed.push(await removeEntry(names.manifest));
1111
+ return removed.some(Boolean);
1112
+ }
1113
+
1114
+ async function memberRangePartNames(modelName) {
1115
+ const directory = await table();
1116
+ if (typeof directory.entries !== "function") return null;
1117
+ const prefix = rangePartPrefix(modelName);
1118
+ const names = [];
1119
+ for await (const [name] of directory.entries()) {
1120
+ if (name.startsWith(prefix)) names.push(name);
1121
+ }
1122
+ return names;
1123
+ }
1124
+
1125
+ async function removeMemberRangeParts(modelName, {
1126
+ except = null,
1127
+ total = null,
1128
+ } = {}) {
1129
+ const existingNames = await memberRangePartNames(modelName);
1130
+ const names = existingNames ?? (
1131
+ Number.isSafeInteger(total) && total > 0
1132
+ ? modelHttpRanges(total).map((range) => rangePartName(modelName, range))
1133
+ : []
1134
+ );
1135
+ const removed = [];
1136
+ for (const name of names) {
1137
+ if (except?.has(name)) continue;
1138
+ removed.push(await removeEntry(name));
1139
+ }
1140
+ return removed.some(Boolean);
1141
+ }
1142
+
1143
+ async function removeRangePartsAfterWholeFile(modelName, total) {
1144
+ try {
1145
+ await removeMemberRangeParts(modelName, { total });
1146
+ } catch (error) {
1147
+ globalThis.console?.warn?.(
1148
+ "Arcane could not remove superseded browser model range parts.",
1149
+ error,
1150
+ );
1151
+ }
1152
+ }
1153
+
1154
+ async function removeStaleRangePartsAfterCompletion(modelName, ranges) {
1155
+ const keep = new Set(ranges.map((range) => rangePartName(modelName, range)));
1156
+ try {
1157
+ await removeMemberRangeParts(modelName, {
1158
+ except: keep,
1159
+ total: ranges[0]?.total ?? null,
1160
+ });
1161
+ } catch (error) {
1162
+ globalThis.console?.warn?.(
1163
+ "Arcane could not remove superseded browser model range parts.",
1164
+ error,
1165
+ );
1166
+ }
1167
+ }
1168
+
1169
+ async function removeRangePartsBehindWholeFiles(members, names, modelFiles) {
1170
+ const completeMembers = [];
1171
+ for (let memberIndex = 0; memberIndex < modelFiles.length; memberIndex += 1) {
1172
+ if (!modelFiles[memberIndex]) continue;
1173
+ completeMembers.push(completeValue({
1174
+ modelName: names.models[memberIndex].name,
1175
+ total: members[memberIndex].bytes,
1176
+ }));
1177
+ }
1178
+ if (completeMembers.length === 0) return;
1179
+ try {
1180
+ const directory = await table();
1181
+ if (typeof directory.entries === "function") {
1182
+ const prefixes = completeMembers.map(({ modelName }) => rangePartPrefix(modelName));
1183
+ const entries = [];
1184
+ for await (const [name] of directory.entries()) {
1185
+ if (prefixes.some((prefix) => name.startsWith(prefix))) entries.push(name);
1186
+ }
1187
+ for (const name of entries) await removeEntry(name);
1188
+ return;
1189
+ }
1190
+ for (const { modelName, total } of completeMembers) {
1191
+ await removeMemberRangeParts(modelName, { total });
1192
+ }
1193
+ } catch (error) {
1194
+ globalThis.console?.warn?.(
1195
+ "Arcane could not remove superseded browser model range parts.",
1196
+ error,
1197
+ );
1198
+ }
1199
+ }
1200
+
1201
+ async function removeRangeParts(source, names) {
1202
+ const members = sourceMetadata(source).files;
1203
+ const directory = await table();
1204
+ if (typeof directory.entries === "function") {
1205
+ const prefixes = names.models.map((entry) => rangePartPrefix(entry.name));
1206
+ const entries = [];
1207
+ for await (const [name] of directory.entries()) {
1208
+ if (prefixes.some((prefix) => name.startsWith(prefix))) entries.push(name);
1209
+ }
1210
+ const removed = [];
1211
+ for (const name of entries) removed.push(await removeEntry(name));
1212
+ return removed.some(Boolean);
1213
+ }
1214
+ const removed = [];
1215
+ for (let memberIndex = 0; memberIndex < members.length; memberIndex += 1) {
1216
+ const total = members[memberIndex].bytes;
1217
+ if (!Number.isSafeInteger(total) || total <= 0) continue;
1218
+ for (const range of modelHttpRanges(total)) {
1219
+ removed.push(await removeEntry(rangePartName(names.models[memberIndex].name, range)));
1220
+ }
1221
+ }
502
1222
  return removed.some(Boolean);
503
1223
  }
504
1224
 
505
1225
  async function remove(source) {
506
- let removed = await removeNames(storageName(source));
507
- if (sourceMetadata(source).legacy) {
508
- removed = await removeNames(storageName(source, { legacy: true })) || removed;
1226
+ const names = storageName(source);
1227
+ const legacyNames = storageName(source, { legacy: true });
1228
+ let legacyManifest = null;
1229
+ let removeLegacy = sourceMetadata(source).legacy;
1230
+ try {
1231
+ legacyManifest = await legacyManifestForSource(source, legacyNames);
1232
+ removeLegacy ||= Boolean(legacyManifest);
1233
+ } catch (error) {
1234
+ globalThis.console?.warn?.(
1235
+ "Arcane could not inspect the legacy browser model cache during removal.",
1236
+ error,
1237
+ );
1238
+ }
1239
+ let removed = await removeNames(names);
1240
+ removed = await removeRangeParts(source, names) || removed;
1241
+ if (removeLegacy) {
1242
+ removed = await removeNames(
1243
+ legacyStorageNames(source, legacyNames, legacyManifest),
1244
+ ) || removed;
509
1245
  }
510
1246
  return removed;
511
1247
  }
512
1248
 
1249
+ async function legacyManifestForSource(source, legacyNames) {
1250
+ const manifestFile = await file(legacyNames.manifest);
1251
+ if (!manifestFile) return null;
1252
+ let manifest;
1253
+ try {
1254
+ manifest = JSON.parse(await manifestFile.text());
1255
+ } catch {
1256
+ return null;
1257
+ }
1258
+ const model = manifest?.model;
1259
+ return manifest?.complete === true && model?.id === source.id ? manifest : null;
1260
+ }
1261
+
1262
+ function legacyStorageNames(source, names, manifest) {
1263
+ if (!manifest) return names;
1264
+ const safeId = source.id.replace(/[^a-z0-9._-]+/giu, "_");
1265
+ const storedNames = new Set(names.models.map((entry) => entry.name));
1266
+ const candidates = [];
1267
+ if (Array.isArray(manifest.model?.files)) candidates.push(...manifest.model.files);
1268
+ if (manifest.model && typeof manifest.model === "object") candidates.push(manifest.model);
1269
+ if (Array.isArray(manifest.files)) candidates.push(...manifest.files);
1270
+ for (const candidate of candidates) {
1271
+ if (!candidate || typeof candidate !== "object") continue;
1272
+ if (
1273
+ candidate.name === undefined
1274
+ && candidate.url === undefined
1275
+ && candidate.immutableUrl === undefined
1276
+ ) continue;
1277
+ try {
1278
+ const url = modelSourceUrl(candidate.url ?? candidate.immutableUrl)
1279
+ ?? new URL("https://arcane.invalid/model.gguf");
1280
+ const name = descriptorFileName(candidate, url);
1281
+ storedNames.add(`${safeId}--${name}`);
1282
+ } catch {
1283
+ // Keep cleanup limited to valid filenames attributable to this manifest.
1284
+ }
1285
+ }
1286
+ return completeValue({
1287
+ ...names,
1288
+ models: completeValue([...storedNames].map((name) => completeValue({ name }))),
1289
+ });
1290
+ }
1291
+
1292
+ async function legacyNamespaceMatchesSource(source, legacyNames) {
1293
+ const metadata = sourceMetadata(source);
1294
+ if (metadata.legacy) return true;
1295
+ if (metadata.files.length !== 1) return false;
1296
+ const manifest = await legacyManifestForSource(source, legacyNames);
1297
+ if (!manifest) return false;
1298
+ const model = manifest.model;
1299
+ const [member] = metadata.files;
1300
+ if (model.url === member.url) return true;
1301
+ if (model.name === member.name && model.immutableUrl === member.url) return true;
1302
+ return Array.isArray(model.files)
1303
+ && model.files.length === 1
1304
+ && model.files[0]?.name === member.name
1305
+ && model.files[0]?.url === member.url;
1306
+ }
1307
+
1308
+ async function legacyCacheForSource(source, signal) {
1309
+ const names = storageName(source, { legacy: true });
1310
+ if (!await legacyNamespaceMatchesSource(source, names)) return null;
1311
+ const files = await storedModelFiles(names, signal);
1312
+ return files ? completeValue({ files, names }) : null;
1313
+ }
1314
+
1315
+ async function removeLegacyCacheAfterReplacement(source) {
1316
+ try {
1317
+ const names = storageName(source, { legacy: true });
1318
+ const manifest = await legacyManifestForSource(source, names);
1319
+ if (!sourceMetadata(source).legacy && !manifest) return;
1320
+ await removeNames(legacyStorageNames(source, names, manifest));
1321
+ } catch (error) {
1322
+ globalThis.console?.warn?.(
1323
+ "Arcane could not remove the superseded legacy browser model cache.",
1324
+ error,
1325
+ );
1326
+ }
1327
+ }
1328
+
1329
+ async function removeIncompleteReplacement(source, names) {
1330
+ try {
1331
+ await removeNames(names);
1332
+ await removeRangeParts(source, names);
1333
+ } catch (error) {
1334
+ globalThis.console?.warn?.(
1335
+ "Arcane could not remove an incomplete duplicate browser model cache.",
1336
+ error,
1337
+ );
1338
+ }
1339
+ }
1340
+
513
1341
  async function storagePolicy({ cached = false } = {}) {
514
1342
  return completeValue({
515
1343
  compatibility: "compatible",
@@ -520,13 +1348,104 @@ export function createDbopfsModelStore({
520
1348
  });
521
1349
  }
522
1350
 
523
- async function storedModelFiles(names, signal) {
1351
+ async function availableModelFiles(names, signal) {
524
1352
  const modelFiles = [];
525
1353
  for (const entry of names.models) {
526
1354
  throwIfAborted(signal, "install");
527
1355
  const modelFile = await file(entry.name);
528
- if (!modelFile) return null;
529
- modelFiles.push(modelFile);
1356
+ if (modelFile?.size === 0) {
1357
+ await removeEntry(entry.name);
1358
+ modelFiles.push(null);
1359
+ } else {
1360
+ modelFiles.push(modelFile);
1361
+ }
1362
+ }
1363
+ return modelFiles;
1364
+ }
1365
+
1366
+ async function storedModelFiles(names, signal) {
1367
+ const modelFiles = await availableModelFiles(names, signal);
1368
+ return modelFiles.every(Boolean) ? modelFiles : null;
1369
+ }
1370
+
1371
+ async function storedRangeCandidate(modelName, total, signal) {
1372
+ const partFiles = [];
1373
+ let lastModified = 0;
1374
+ for (const range of modelHttpRanges(total)) {
1375
+ throwIfAborted(signal, "install");
1376
+ const partName = rangePartName(modelName, range);
1377
+ const partFile = await file(partName);
1378
+ if (!partFile) return null;
1379
+ const expected = range.end - range.start + 1;
1380
+ if (partFile.size !== expected) {
1381
+ await removeEntry(partName);
1382
+ return null;
1383
+ }
1384
+ partFiles.push(partFile);
1385
+ if (Number.isFinite(partFile.lastModified)) {
1386
+ lastModified = Math.max(lastModified, partFile.lastModified);
1387
+ }
1388
+ }
1389
+ return completeValue({
1390
+ file: new Blob(partFiles, { type: "application/octet-stream" }),
1391
+ lastModified,
1392
+ total,
1393
+ });
1394
+ }
1395
+
1396
+ async function storedRangeMember(member, modelName, signal) {
1397
+ const declaredTotal = Number.isSafeInteger(member.bytes) && member.bytes > 0
1398
+ ? member.bytes
1399
+ : null;
1400
+ if (declaredTotal !== null) {
1401
+ const declared = await storedRangeCandidate(modelName, declaredTotal, signal);
1402
+ if (declared) {
1403
+ await removeStaleRangePartsAfterCompletion(
1404
+ modelName,
1405
+ modelHttpRanges(declaredTotal),
1406
+ );
1407
+ return declared.file;
1408
+ }
1409
+ }
1410
+
1411
+ const names = await memberRangePartNames(modelName);
1412
+ if (names === null) return null;
1413
+ const discoveredTotals = new Set();
1414
+ for (const name of names) {
1415
+ const details = rangePartDetails(modelName, name);
1416
+ if (details && details.total !== declaredTotal) discoveredTotals.add(details.total);
1417
+ }
1418
+ let selected = null;
1419
+ const totals = [...discoveredTotals].sort((left, right) => right - left);
1420
+ for (const total of totals) {
1421
+ const candidate = await storedRangeCandidate(modelName, total, signal);
1422
+ if (
1423
+ candidate
1424
+ && (
1425
+ selected === null
1426
+ || candidate.lastModified > selected.lastModified
1427
+ )
1428
+ ) selected = candidate;
1429
+ }
1430
+ if (selected === null) return null;
1431
+ await removeStaleRangePartsAfterCompletion(
1432
+ modelName,
1433
+ modelHttpRanges(selected.total),
1434
+ );
1435
+ return selected.file;
1436
+ }
1437
+
1438
+ async function availableResumableModelFiles(source, names, signal) {
1439
+ const members = sourceMetadata(source).files;
1440
+ const modelFiles = await availableModelFiles(names, signal);
1441
+ await removeRangePartsBehindWholeFiles(members, names, modelFiles);
1442
+ for (let memberIndex = 0; memberIndex < modelFiles.length; memberIndex += 1) {
1443
+ if (modelFiles[memberIndex]) continue;
1444
+ modelFiles[memberIndex] = await storedRangeMember(
1445
+ members[memberIndex],
1446
+ names.models[memberIndex].name,
1447
+ signal,
1448
+ );
530
1449
  }
531
1450
  return modelFiles;
532
1451
  }
@@ -534,32 +1453,22 @@ export function createDbopfsModelStore({
534
1453
  async function openCached(source, {
535
1454
  signal,
536
1455
  } = {}) {
537
- let names = storageName(source);
538
- let modelFiles = await storedModelFiles(names, signal);
539
- if (!modelFiles && sourceMetadata(source).legacy) {
540
- await removeNames(names);
541
- const legacyNames = storageName(source, { legacy: true });
542
- const legacyFiles = await storedModelFiles(legacyNames, signal);
543
- if (legacyFiles) {
544
- names = legacyNames;
545
- modelFiles = legacyFiles;
546
- } else {
547
- await removeNames(legacyNames);
1456
+ const names = storageName(source);
1457
+ let modelFiles = await availableResumableModelFiles(source, names, signal);
1458
+ if (modelFiles.every(Boolean)) {
1459
+ await removeLegacyCacheAfterReplacement(source);
1460
+ } else {
1461
+ const legacyCache = await legacyCacheForSource(source, signal);
1462
+ if (legacyCache) {
1463
+ await removeIncompleteReplacement(source, names);
1464
+ modelFiles = legacyCache.files;
548
1465
  }
549
1466
  }
550
- if (!modelFiles) {
551
- await removeNames(names);
552
- return null;
553
- }
554
- try {
555
- return completeValue({
556
- files: completeValue(modelFiles),
557
- file: modelFiles.length === 1 ? modelFiles[0] : null,
558
- });
559
- } catch (error) {
560
- if (!signal?.aborted) await removeNames(names);
561
- throw error;
562
- }
1467
+ if (!modelFiles.every(Boolean)) return null;
1468
+ return completeValue({
1469
+ files: completeValue(modelFiles),
1470
+ file: modelFiles.length === 1 ? modelFiles[0] : null,
1471
+ });
563
1472
  }
564
1473
 
565
1474
  async function install(source, { signal, onProgress = null } = {}) {
@@ -568,48 +1477,118 @@ export function createDbopfsModelStore({
568
1477
  }
569
1478
  const names = storageName(source);
570
1479
  const members = sourceMetadata(source).files;
571
- await remove(source);
572
- const modelFiles = [];
573
- try {
574
- for (let index = 0; index < members.length; index += 1) {
575
- const member = members[index];
576
- onProgress?.(completeValue({
577
- phase: "download",
578
- completed: index,
579
- total: members.length,
580
- unit: "files",
581
- heartbeat: false,
582
- }));
583
- const opened = await source.open(index, { signal });
1480
+ const modelFiles = await availableResumableModelFiles(source, names, signal);
1481
+ const linked = linkAbortSignal(signal);
1482
+ const downloadSignal = linked.controller.signal;
1483
+ let nextMemberIndex = 0;
1484
+ let failure = null;
1485
+
1486
+ function retainFailure(error) {
1487
+ if (failure === null) failure = error;
1488
+ if (!downloadSignal.aborted) linked.controller.abort(error);
1489
+ }
1490
+
1491
+ const progress = createDownloadProgressReporter(members, onProgress, retainFailure);
1492
+
1493
+ async function installMember(memberIndex) {
1494
+ let modelFile = null;
1495
+ modelFile = await installOneFile(
1496
+ source,
1497
+ memberIndex,
1498
+ names.models[memberIndex].name,
1499
+ {
1500
+ signal: downloadSignal,
1501
+ progress,
1502
+ retainFailure,
1503
+ multiFile: members.length > 1,
1504
+ rangeWorkerLimit: members.length === 1 ? workerLimit : 1,
1505
+ },
1506
+ );
1507
+ if (!modelFile) {
1508
+ progress.setMode(members.length === 1 ? "single" : "files");
1509
+ progress.beginTransfer();
1510
+ let opened = null;
584
1511
  try {
585
- await write(names.models[index].name, opened.body, { signal });
586
- const modelFile = await file(names.models[index].name);
587
- if (!modelFile) {
588
- throw fail(
589
- "ARCANE_AI_MODEL_CACHE_REJECTED",
590
- "A stored model file could not be reopened.",
591
- );
592
- }
593
- modelFiles.push(modelFile);
1512
+ opened = await source.open(memberIndex, { signal: downloadSignal });
1513
+ progress.setMemberTotal(memberIndex, opened.contentLength);
1514
+ modelFile = await writeOpenedModel(
1515
+ names.models[memberIndex].name,
1516
+ opened,
1517
+ {
1518
+ signal: downloadSignal,
1519
+ onChunk: progress.addBytes,
1520
+ onDiscard: progress.discardBytes,
1521
+ },
1522
+ );
1523
+ await removeRangePartsAfterWholeFile(
1524
+ names.models[memberIndex].name,
1525
+ opened.contentLength,
1526
+ );
594
1527
  } catch (error) {
595
- await opened.cancel?.(error).catch(() => undefined);
1528
+ retainFailure(error);
596
1529
  throw error;
1530
+ } finally {
1531
+ progress.endTransfer({ publishProgress: false });
597
1532
  }
598
1533
  }
599
- onProgress?.(completeValue({
600
- phase: "download",
601
- completed: members.length,
602
- total: members.length,
603
- unit: "files",
604
- heartbeat: false,
605
- }));
1534
+ throwIfAborted(downloadSignal, "install");
1535
+ modelFiles[memberIndex] = modelFile;
1536
+ progress.completeMember(memberIndex, modelFile.size);
1537
+ }
1538
+
1539
+ async function installWorker() {
1540
+ while (true) {
1541
+ const memberIndex = nextMemberIndex;
1542
+ nextMemberIndex += 1;
1543
+ if (memberIndex >= members.length) return;
1544
+ if (modelFiles[memberIndex]) continue;
1545
+ try {
1546
+ await installMember(memberIndex);
1547
+ } catch (error) {
1548
+ retainFailure(error);
1549
+ throw error;
1550
+ }
1551
+ }
1552
+ }
1553
+
1554
+ try {
1555
+ progress.start();
1556
+ let pendingMembers = members.length;
1557
+ for (let memberIndex = 0; memberIndex < modelFiles.length; memberIndex += 1) {
1558
+ const modelFile = modelFiles[memberIndex];
1559
+ if (!modelFile) continue;
1560
+ pendingMembers -= 1;
1561
+ progress.restoreMember(memberIndex, modelFile.size);
1562
+ }
1563
+ const workers = [];
1564
+ const workerCount = Math.min(workerLimit, pendingMembers);
1565
+ for (let index = 0; index < workerCount; index += 1) {
1566
+ workers.push(installWorker());
1567
+ }
1568
+ const results = await Promise.allSettled(workers);
1569
+ if (failure === null) {
1570
+ for (const result of results) {
1571
+ if (result.status === "rejected") {
1572
+ failure = result.reason;
1573
+ break;
1574
+ }
1575
+ }
1576
+ }
1577
+ if (failure !== null) throw failure;
1578
+ await removeLegacyCacheAfterReplacement(source);
1579
+ progress.finish();
1580
+ if (failure !== null) throw failure;
1581
+ throwIfAborted(downloadSignal, "install");
606
1582
  return completeValue({
607
1583
  files: completeValue(modelFiles),
608
1584
  file: modelFiles.length === 1 ? modelFiles[0] : null,
609
1585
  });
610
1586
  } catch (error) {
611
- await remove(source).catch(() => undefined);
612
- throw error;
1587
+ retainFailure(error);
1588
+ throw failure;
1589
+ } finally {
1590
+ progress.dispose();
1591
+ linked.release();
613
1592
  }
614
1593
  }
615
1594
 
@@ -650,6 +1629,7 @@ export function createDbopfsModelStore({
650
1629
  const store = completeValue({
651
1630
  kind: "arcane-dbopfs-model-store",
652
1631
  tableName,
1632
+ downloadConcurrency: workerLimit,
653
1633
  adapter: dbopfs,
654
1634
  ready: () => table().then(() => undefined),
655
1635
  install,