@volter/browser-runtime 0.1.166 → 0.1.168
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/LICENSE +1 -0
- package/dist/browser-file-coherence.d.ts +1 -1
- package/dist/browser-file-coherence.d.ts.map +1 -1
- package/dist/browser-file-coherence.js +3 -2
- package/dist/browser-file-coherence.js.map +1 -1
- package/dist/browser-file-times.d.ts +8 -0
- package/dist/browser-file-times.d.ts.map +1 -0
- package/dist/browser-file-times.js +8 -0
- package/dist/browser-file-times.js.map +1 -0
- package/dist/browser-filesystem-channel.d.ts +2 -0
- package/dist/browser-filesystem-channel.d.ts.map +1 -1
- package/dist/browser-filesystem-channel.js +11 -2
- package/dist/browser-filesystem-channel.js.map +1 -1
- package/dist/browser-filesystem.d.ts +12 -0
- package/dist/browser-filesystem.d.ts.map +1 -1
- package/dist/browser-filesystem.js.map +1 -1
- package/dist/browser-image-ownership.d.ts +15 -0
- package/dist/browser-image-ownership.d.ts.map +1 -0
- package/dist/browser-image-ownership.js +119 -0
- package/dist/browser-image-ownership.js.map +1 -0
- package/dist/browser-image-ranges.d.ts +2 -1
- package/dist/browser-image-ranges.d.ts.map +1 -1
- package/dist/browser-image-ranges.js +8 -1
- package/dist/browser-image-ranges.js.map +1 -1
- package/dist/browser-memory-filesystem.d.ts +2 -0
- package/dist/browser-memory-filesystem.d.ts.map +1 -1
- package/dist/browser-memory-filesystem.js +11 -1
- package/dist/browser-memory-filesystem.js.map +1 -1
- package/dist/browser-opfs-access.d.ts +20 -0
- package/dist/browser-opfs-access.d.ts.map +1 -0
- package/dist/browser-opfs-access.js +48 -0
- package/dist/browser-opfs-access.js.map +1 -0
- package/dist/browser-opfs-filesystem.d.ts.map +1 -1
- package/dist/browser-opfs-filesystem.js +6 -0
- package/dist/browser-opfs-filesystem.js.map +1 -1
- package/dist/browser-opfs-storage-worker.d.ts.map +1 -1
- package/dist/browser-opfs-storage-worker.js +3 -2
- package/dist/browser-opfs-storage-worker.js.map +1 -1
- package/dist/browser-pack-store.d.ts +24 -1
- package/dist/browser-pack-store.d.ts.map +1 -1
- package/dist/browser-pack-store.js +295 -37
- package/dist/browser-pack-store.js.map +1 -1
- package/dist/browser-posix-metadata.d.ts +2 -0
- package/dist/browser-posix-metadata.d.ts.map +1 -1
- package/dist/browser-posix-metadata.js +16 -0
- package/dist/browser-posix-metadata.js.map +1 -1
- package/dist/browser-process-filesystem-policy.d.ts.map +1 -1
- package/dist/browser-process-filesystem-policy.js +1 -0
- package/dist/browser-process-filesystem-policy.js.map +1 -1
- package/dist/browser-process-filesystem.d.ts +2 -1
- package/dist/browser-process-filesystem.d.ts.map +1 -1
- package/dist/browser-process-filesystem.js +29 -48
- package/dist/browser-process-filesystem.js.map +1 -1
- package/dist/browser-process-host.d.ts.map +1 -1
- package/dist/browser-process-host.js +9 -3
- package/dist/browser-process-host.js.map +1 -1
- package/dist/browser-process.d.ts +2 -0
- package/dist/browser-process.d.ts.map +1 -1
- package/dist/browser-project-archive.d.ts.map +1 -1
- package/dist/browser-project-archive.js +2 -1
- package/dist/browser-project-archive.js.map +1 -1
- package/dist/browser-project-store.d.ts.map +1 -1
- package/dist/browser-project-store.js +2 -1
- package/dist/browser-project-store.js.map +1 -1
- package/dist/browser-store-filesystem.d.ts +1 -0
- package/dist/browser-store-filesystem.d.ts.map +1 -1
- package/dist/browser-store-filesystem.js +3 -0
- package/dist/browser-store-filesystem.js.map +1 -1
- package/dist/kernel-processes.d.ts +11 -1
- package/dist/kernel-processes.d.ts.map +1 -1
- package/dist/kernel-processes.js +79 -2
- package/dist/kernel-processes.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { adaptBrowserOpfsSyncAccess, browserOpfsAccessError, isBrowserOpfsAccessContention } from "./browser-opfs-access.js";
|
|
1
2
|
import { encodeSharedIndex, OverlaidChildren, OverlaidIndex, SharedIndex, sameIndexEntry } from "./browser-pack-index.js";
|
|
2
3
|
import { PACKAGES_STORE, STORE_BASE_DIRECTORY, STORE_STATE_FILE } from "./browser-store-layout.js";
|
|
3
4
|
import { ImageRangeReader, IMAGE_RANGES_FILE, validateImageRanges } from "./browser-image-ranges.js";
|
|
5
|
+
import { ImageExtentOwnership } from "./browser-image-ownership.js";
|
|
4
6
|
/** The one shared store of an origin: every package version's files, once, under `/packages/<name>@<version>/`. */
|
|
5
7
|
export { PACKAGES_STORE };
|
|
6
8
|
export const PACKAGES_ROOT = "/packages";
|
|
@@ -64,6 +66,10 @@ const DIRECTORY_MODE = 0o755;
|
|
|
64
66
|
const FILE_MODE = 0o644;
|
|
65
67
|
/** The store, bound to the worker that holds its handles. */
|
|
66
68
|
export function createPackStore(environment, options = {}) {
|
|
69
|
+
return createPackStoreInternal(environment, options);
|
|
70
|
+
}
|
|
71
|
+
/** Maintenance callers already hold PACKAGES_OPEN_LOCK exclusively. */
|
|
72
|
+
function createPackStoreInternal(environment, options = {}, maintenance = false) {
|
|
67
73
|
const explicitBytes = options.limits?.maxBytes;
|
|
68
74
|
const maximumEntries = options.limits?.maxEntries ?? MAX_ENTRIES;
|
|
69
75
|
if ((explicitBytes !== undefined && (!Number.isSafeInteger(explicitBytes) || explicitBytes < 1))
|
|
@@ -82,6 +88,8 @@ export function createPackStore(environment, options = {}) {
|
|
|
82
88
|
let linkedBytes = 0;
|
|
83
89
|
/** Linked bytes under the retention root, which the project budget excludes. */
|
|
84
90
|
let releaseLock;
|
|
91
|
+
let releaseUsersLock;
|
|
92
|
+
let committedPredecessor;
|
|
85
93
|
let projectId;
|
|
86
94
|
let generation;
|
|
87
95
|
let entries = new Map();
|
|
@@ -90,6 +98,7 @@ export function createPackStore(environment, options = {}) {
|
|
|
90
98
|
let packAccess;
|
|
91
99
|
let imageRangesDigest;
|
|
92
100
|
let imageReader;
|
|
101
|
+
let imageOwnership;
|
|
93
102
|
let deferredImage;
|
|
94
103
|
let receivedImageCompletion;
|
|
95
104
|
let receivedImageLine;
|
|
@@ -366,7 +375,13 @@ export function createPackStore(environment, options = {}) {
|
|
|
366
375
|
if (!access)
|
|
367
376
|
throw new Error("OPFS filesystem worker is not initialized.");
|
|
368
377
|
const value = new Uint8Array(length);
|
|
369
|
-
|
|
378
|
+
const aheadStart = Math.floor(offset / READ_AHEAD_BYTES) * READ_AHEAD_BYTES;
|
|
379
|
+
const aheadEnd = Math.min(Math.ceil((offset + length) / READ_AHEAD_BYTES) * READ_AHEAD_BYTES, immutableReadEnd);
|
|
380
|
+
// Recovered files must not fetch an unrelated old range merely to
|
|
381
|
+
// fill an aligned cache block containing still-unacquired neighbours.
|
|
382
|
+
const boundedOwnership = imageReader && ownsImageExtent(offset, length)
|
|
383
|
+
&& !ownsImageExtent(aheadStart, Math.max(0, aheadEnd - aheadStart));
|
|
384
|
+
if (!boundedOwnership && length > 0 && length <= READ_AHEAD_BYTES && offset >= 0 && offset + length <= immutableReadEnd) {
|
|
370
385
|
let copied = 0;
|
|
371
386
|
while (copied < length) {
|
|
372
387
|
const position = offset + copied;
|
|
@@ -410,6 +425,17 @@ export function createPackStore(environment, options = {}) {
|
|
|
410
425
|
const opening = new AbortController();
|
|
411
426
|
openingController = opening;
|
|
412
427
|
try {
|
|
428
|
+
if (!maintenance) {
|
|
429
|
+
const locks = environment.navigator.locks;
|
|
430
|
+
if (!locks)
|
|
431
|
+
throw new Error("Opening a store requires storage ownership locks.");
|
|
432
|
+
await new Promise((ready, fail) => {
|
|
433
|
+
void locks.request(PACKAGES_OPEN_LOCK, { mode: "shared", signal: opening.signal }, () => new Promise(release => {
|
|
434
|
+
releaseUsersLock = release;
|
|
435
|
+
ready();
|
|
436
|
+
})).catch(fail);
|
|
437
|
+
});
|
|
438
|
+
}
|
|
413
439
|
// A closed holder can be reopened, but no index or failed-write latch from
|
|
414
440
|
// its previous workspace may become the opening state of the next one.
|
|
415
441
|
entries = new Map();
|
|
@@ -577,10 +603,10 @@ export function createPackStore(environment, options = {}) {
|
|
|
577
603
|
}
|
|
578
604
|
if (!shared) {
|
|
579
605
|
packAccess.truncate(packBytes);
|
|
580
|
-
//
|
|
581
|
-
//
|
|
582
|
-
|
|
583
|
-
|
|
606
|
+
// A committed index does not identify interrupted staging files as ours
|
|
607
|
+
// to delete. Each atomic writer cleans only its own temporary file.
|
|
608
|
+
// An open cannot identify a validated predecessor; unknown generations
|
|
609
|
+
// remain available for interrupted-operation diagnosis.
|
|
584
610
|
opening.signal.throwIfAborted();
|
|
585
611
|
if (deadBytes > COMPACTION_FLOOR_BYTES && deadBytes > liveBytes)
|
|
586
612
|
await compact();
|
|
@@ -614,7 +640,14 @@ export function createPackStore(environment, options = {}) {
|
|
|
614
640
|
catch (error) {
|
|
615
641
|
failures.push(error);
|
|
616
642
|
}
|
|
643
|
+
try {
|
|
644
|
+
releaseUsersLock?.();
|
|
645
|
+
}
|
|
646
|
+
catch (error) {
|
|
647
|
+
failures.push(error);
|
|
648
|
+
}
|
|
617
649
|
releaseLock = undefined;
|
|
650
|
+
releaseUsersLock = undefined;
|
|
618
651
|
project = undefined;
|
|
619
652
|
if (failures.length > 1)
|
|
620
653
|
throw new AggregateError(failures, "OPFS workspace open and cleanup failed.");
|
|
@@ -731,14 +764,21 @@ export function createPackStore(environment, options = {}) {
|
|
|
731
764
|
abort();
|
|
732
765
|
});
|
|
733
766
|
const opened = await Promise.race([
|
|
734
|
-
pending.then((access) => ({ access })).catch((cause) => {
|
|
767
|
+
pending.then((access) => ({ access })).catch((cause) => {
|
|
768
|
+
refusal = cause;
|
|
769
|
+
// Keep the unsupported sharing-mode explanation below; retry only
|
|
770
|
+
// actual access contention, not quota/security/device failures.
|
|
771
|
+
if (!(cause instanceof TypeError) && !isBrowserOpfsAccessContention(cause))
|
|
772
|
+
throw browserOpfsAccessError("open", cause);
|
|
773
|
+
return undefined;
|
|
774
|
+
}),
|
|
735
775
|
new Promise((resolve) => { timer = setTimeout(() => resolve(undefined), OPEN_ATTEMPT_MS); }),
|
|
736
776
|
cancelled,
|
|
737
777
|
]);
|
|
738
778
|
signal?.throwIfAborted();
|
|
739
779
|
if (opened) {
|
|
740
780
|
claimed = true;
|
|
741
|
-
return opened.access;
|
|
781
|
+
return adaptBrowserOpfsSyncAccess(opened.access);
|
|
742
782
|
}
|
|
743
783
|
}
|
|
744
784
|
finally {
|
|
@@ -756,8 +796,10 @@ export function createPackStore(environment, options = {}) {
|
|
|
756
796
|
}
|
|
757
797
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
758
798
|
}
|
|
759
|
-
|
|
760
|
-
|
|
799
|
+
if (isBrowserOpfsAccessContention(refusal))
|
|
800
|
+
throw browserOpfsAccessError("open", refusal);
|
|
801
|
+
// No observed permission error means no evidence that another page owns it.
|
|
802
|
+
throw new Error(`This browser workspace's ${name} access handle did not become available.`);
|
|
761
803
|
}
|
|
762
804
|
async function openGenerationHandles(directory) {
|
|
763
805
|
closeGenerationHandles();
|
|
@@ -800,7 +842,9 @@ export function createPackStore(environment, options = {}) {
|
|
|
800
842
|
if (!line)
|
|
801
843
|
receivedImageLine?.port.close();
|
|
802
844
|
receivedImageLine = undefined;
|
|
803
|
-
|
|
845
|
+
imageOwnership = new ImageExtentOwnership();
|
|
846
|
+
await imageOwnership.open(directory, ranges.bytes, !readOnly);
|
|
847
|
+
imageReader = new ImageRangeReader(ranges, directory, `${projectId}.${generation}`, completion?.completed, receiptKey, line, (offset, length) => imageOwnership?.covers(offset, length) ?? false);
|
|
804
848
|
// Whether this reader shares its owner's completion: one that does not
|
|
805
849
|
// asks the materializer for every range the owner already placed.
|
|
806
850
|
console.log(`[boot-trace] ${JSON.stringify({ event: "image-reader-open", at: Date.now(), reader: `${projectId}.${generation}`, offered: supplied !== undefined,
|
|
@@ -817,6 +861,8 @@ export function createPackStore(environment, options = {}) {
|
|
|
817
861
|
function closeGenerationHandles() {
|
|
818
862
|
imageReader?.close();
|
|
819
863
|
imageReader = undefined;
|
|
864
|
+
imageOwnership?.close();
|
|
865
|
+
imageOwnership = undefined;
|
|
820
866
|
deferredImage = undefined;
|
|
821
867
|
const pack = packAccess, log = logAccess;
|
|
822
868
|
packAccess = undefined;
|
|
@@ -969,12 +1015,6 @@ export function createPackStore(environment, options = {}) {
|
|
|
969
1015
|
clearTimeout(timer);
|
|
970
1016
|
}
|
|
971
1017
|
}
|
|
972
|
-
function validProjectId(value) {
|
|
973
|
-
if (typeof value !== "string" || !/^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/u.test(value)) {
|
|
974
|
-
throw new Error("OPFS filesystem projectId is invalid.");
|
|
975
|
-
}
|
|
976
|
-
return value;
|
|
977
|
-
}
|
|
978
1018
|
async function projectDirectory() {
|
|
979
1019
|
if (!project)
|
|
980
1020
|
throw new Error("OPFS filesystem worker is not initialized.");
|
|
@@ -1000,8 +1040,13 @@ export function createPackStore(environment, options = {}) {
|
|
|
1000
1040
|
}
|
|
1001
1041
|
}
|
|
1002
1042
|
/** Only validated committed state establishes which generations are unreachable. */
|
|
1003
|
-
async function
|
|
1043
|
+
async function discardPredecessor(previous, retain) {
|
|
1044
|
+
if (!previous || previous === retain)
|
|
1045
|
+
return;
|
|
1004
1046
|
const directory = await projectDirectory();
|
|
1047
|
+
const state = readStorageState(await readJson(directory, STATE_FILE));
|
|
1048
|
+
if (!state || state.generation !== retain)
|
|
1049
|
+
throw new Error("Committed state changed before predecessor release; generations were retained.");
|
|
1005
1050
|
let root;
|
|
1006
1051
|
try {
|
|
1007
1052
|
root = await directory.getDirectoryHandle(GENERATIONS_DIRECTORY);
|
|
@@ -1011,9 +1056,12 @@ export function createPackStore(environment, options = {}) {
|
|
|
1011
1056
|
return;
|
|
1012
1057
|
throw cause;
|
|
1013
1058
|
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1059
|
+
try {
|
|
1060
|
+
await root.removeEntry(previous, { recursive: true });
|
|
1061
|
+
}
|
|
1062
|
+
catch (cause) {
|
|
1063
|
+
if (!notFound(cause))
|
|
1064
|
+
throw cause;
|
|
1017
1065
|
}
|
|
1018
1066
|
}
|
|
1019
1067
|
// ---------------------------------------------------------------- writes
|
|
@@ -1771,7 +1819,7 @@ export function createPackStore(environment, options = {}) {
|
|
|
1771
1819
|
pack.flush();
|
|
1772
1820
|
log.flush();
|
|
1773
1821
|
flushStorage();
|
|
1774
|
-
const previous = { pack: packAccess, log: logAccess };
|
|
1822
|
+
const previous = { pack: packAccess, log: logAccess, generation };
|
|
1775
1823
|
packAccess = pack;
|
|
1776
1824
|
logAccess = log;
|
|
1777
1825
|
logBytes = 0;
|
|
@@ -1799,7 +1847,7 @@ export function createPackStore(environment, options = {}) {
|
|
|
1799
1847
|
throw mutationFailure;
|
|
1800
1848
|
}
|
|
1801
1849
|
named();
|
|
1802
|
-
await
|
|
1850
|
+
await discardPredecessor(previous.generation, next);
|
|
1803
1851
|
}
|
|
1804
1852
|
finally {
|
|
1805
1853
|
if (!switched) {
|
|
@@ -1841,10 +1889,10 @@ export function createPackStore(environment, options = {}) {
|
|
|
1841
1889
|
if (changed)
|
|
1842
1890
|
await checkpoint();
|
|
1843
1891
|
}
|
|
1844
|
-
function retain(keep) {
|
|
1845
|
-
return rewriteGeneration(() => retainGeneration(keep));
|
|
1892
|
+
function retain(keep, options = {}) {
|
|
1893
|
+
return rewriteGeneration(() => retainGeneration(keep, options.preserveOffsets));
|
|
1846
1894
|
}
|
|
1847
|
-
async function retainGeneration(keep) {
|
|
1895
|
+
async function retainGeneration(keep, preserveOffsets = false) {
|
|
1848
1896
|
const source = packAccess;
|
|
1849
1897
|
if (!source)
|
|
1850
1898
|
throw new Error("OPFS filesystem worker is not initialized.");
|
|
@@ -1868,7 +1916,7 @@ export function createPackStore(environment, options = {}) {
|
|
|
1868
1916
|
index.set(path, { ...entry, offset: 0, length: 0 });
|
|
1869
1917
|
continue;
|
|
1870
1918
|
}
|
|
1871
|
-
const offset = cursor;
|
|
1919
|
+
const offset = preserveOffsets ? entry.offset : cursor;
|
|
1872
1920
|
moved.set(entry.offset, offset);
|
|
1873
1921
|
let copied = 0;
|
|
1874
1922
|
while (copied < entry.length) {
|
|
@@ -1887,7 +1935,7 @@ export function createPackStore(environment, options = {}) {
|
|
|
1887
1935
|
}
|
|
1888
1936
|
copied += span;
|
|
1889
1937
|
}
|
|
1890
|
-
cursor
|
|
1938
|
+
cursor = Math.max(cursor, offset + entry.length);
|
|
1891
1939
|
const { capacity: _room, ...extent } = entry;
|
|
1892
1940
|
index.set(path, { ...extent, offset, length: entry.length });
|
|
1893
1941
|
}
|
|
@@ -1911,6 +1959,7 @@ export function createPackStore(environment, options = {}) {
|
|
|
1911
1959
|
}
|
|
1912
1960
|
}
|
|
1913
1961
|
async function commitGenerationIn(name, directory, index, bytes, prepared, rangesDigest) {
|
|
1962
|
+
const previous = generation;
|
|
1914
1963
|
// Publish only a generation whose pack, index and initial journal exist.
|
|
1915
1964
|
// Opening a committed generation must never fabricate missing data files.
|
|
1916
1965
|
const journal = await directory.getFileHandle(LOG_FILE, { create: true });
|
|
@@ -1952,8 +2001,9 @@ export function createPackStore(environment, options = {}) {
|
|
|
1952
2001
|
measureBytes();
|
|
1953
2002
|
// Another tab may hold a shared store's older generation open; only a
|
|
1954
2003
|
// project's own are discarded.
|
|
2004
|
+
committedPredecessor = previous;
|
|
1955
2005
|
if (!shared)
|
|
1956
|
-
await
|
|
2006
|
+
await discardPredecessor(previous, name);
|
|
1957
2007
|
}
|
|
1958
2008
|
async function installPreparedImage(prepared, json, binary, suppliedRanges, verified = false) {
|
|
1959
2009
|
assertWritable();
|
|
@@ -2522,9 +2572,6 @@ export function createPackStore(environment, options = {}) {
|
|
|
2522
2572
|
segments += 1;
|
|
2523
2573
|
return segments;
|
|
2524
2574
|
}
|
|
2525
|
-
function notFound(cause) {
|
|
2526
|
-
return cause instanceof DOMException && cause.name === "NotFoundError";
|
|
2527
|
-
}
|
|
2528
2575
|
/** FNV-1a: enough to tell a whole record from a torn one. */
|
|
2529
2576
|
function fingerprint(value) {
|
|
2530
2577
|
let hash = 0x811c9dc5;
|
|
@@ -2542,6 +2589,7 @@ export function createPackStore(environment, options = {}) {
|
|
|
2542
2589
|
logAccess?.flush();
|
|
2543
2590
|
closeGenerationHandles();
|
|
2544
2591
|
releaseLock?.();
|
|
2592
|
+
releaseUsersLock?.();
|
|
2545
2593
|
});
|
|
2546
2594
|
function applyNow(mutations, options = {}) {
|
|
2547
2595
|
assertWritable();
|
|
@@ -2598,6 +2646,50 @@ export function createPackStore(environment, options = {}) {
|
|
|
2598
2646
|
}
|
|
2599
2647
|
}, options);
|
|
2600
2648
|
}
|
|
2649
|
+
function ownsImageExtent(offset, length) {
|
|
2650
|
+
return imageOwnership?.covers(offset, length) ?? true;
|
|
2651
|
+
}
|
|
2652
|
+
function materializeImageExtentsNow(bytes, files, sourceDigest) {
|
|
2653
|
+
assertWritable();
|
|
2654
|
+
if (!shared || !imageReader || !imageOwnership || !packAccess)
|
|
2655
|
+
throw new Error("Only a reserved shared image can acquire existing immutable extents.");
|
|
2656
|
+
if (!/^[a-f0-9]{64}$/u.test(sourceDigest))
|
|
2657
|
+
throw new Error("Immutable acquisition requires its verified source digest.");
|
|
2658
|
+
const acquired = [];
|
|
2659
|
+
// Validate the entire unit before its first write. Matching metadata
|
|
2660
|
+
// comes from an integrity-addressed unit, never a filename substitution.
|
|
2661
|
+
for (const file of files) {
|
|
2662
|
+
const entry = entries.get(requiredPath(file.path));
|
|
2663
|
+
if (!entry || entry.pack || entry.type !== file.type || entry.mode !== file.mode
|
|
2664
|
+
|| (file.type !== "directory" && (entry.length !== file.length || file.offset === undefined
|
|
2665
|
+
|| !Number.isSafeInteger(file.offset) || file.offset < 0 || file.offset + entry.length > bytes.byteLength))) {
|
|
2666
|
+
throw new Error(`The current image's immutable unit disagrees with retained metadata at ${file.path}. Stored offsets were retained.`);
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
for (const file of files) {
|
|
2670
|
+
const entry = entries.get(file.path);
|
|
2671
|
+
if (entry.type === "directory" || entry.length === 0 || ownsImageExtent(entry.offset, entry.length))
|
|
2672
|
+
continue;
|
|
2673
|
+
const content = bytes.subarray(file.offset, file.offset + entry.length);
|
|
2674
|
+
writePackAt(entry.offset, content);
|
|
2675
|
+
acquired.push([entry.offset, entry.length]);
|
|
2676
|
+
}
|
|
2677
|
+
if (acquired.length === 0)
|
|
2678
|
+
return;
|
|
2679
|
+
// Adjacent verified files are one ownership interval; no gap is inferred.
|
|
2680
|
+
acquired.sort((left, right) => left[0] - right[0]);
|
|
2681
|
+
const joined = [];
|
|
2682
|
+
for (const [offset, length] of acquired) {
|
|
2683
|
+
const last = joined.at(-1);
|
|
2684
|
+
if (last && last[0] + last[1] === offset)
|
|
2685
|
+
last[1] += length;
|
|
2686
|
+
else
|
|
2687
|
+
joined.push([offset, length]);
|
|
2688
|
+
}
|
|
2689
|
+
packAccess.flush();
|
|
2690
|
+
imageOwnership.record(joined, sourceDigest);
|
|
2691
|
+
readAhead.clear();
|
|
2692
|
+
}
|
|
2601
2693
|
/** One batch of index changes: applied, bounded, recorded as one record, or rolled back whole. */
|
|
2602
2694
|
function runBatch(apply, options) {
|
|
2603
2695
|
if (!packAccess || !logAccess)
|
|
@@ -2696,10 +2788,13 @@ export function createPackStore(environment, options = {}) {
|
|
|
2696
2788
|
}
|
|
2697
2789
|
finally {
|
|
2698
2790
|
const release = releaseLock;
|
|
2791
|
+
const releaseUsers = releaseUsersLock;
|
|
2699
2792
|
releaseLock = undefined;
|
|
2793
|
+
releaseUsersLock = undefined;
|
|
2700
2794
|
project = undefined;
|
|
2701
2795
|
try {
|
|
2702
2796
|
release?.();
|
|
2797
|
+
releaseUsers?.();
|
|
2703
2798
|
}
|
|
2704
2799
|
finally {
|
|
2705
2800
|
if (options.ownsPackages)
|
|
@@ -2718,6 +2813,13 @@ export function createPackStore(environment, options = {}) {
|
|
|
2718
2813
|
readExtent,
|
|
2719
2814
|
descendants,
|
|
2720
2815
|
retain,
|
|
2816
|
+
discardUnreferencedGenerations: async () => {
|
|
2817
|
+
assertWritable();
|
|
2818
|
+
if (!generation)
|
|
2819
|
+
throw new Error("No committed generation establishes what can be released.");
|
|
2820
|
+
await discardPredecessor(committedPredecessor, generation);
|
|
2821
|
+
committedPredecessor = undefined;
|
|
2822
|
+
},
|
|
2721
2823
|
remapLinks,
|
|
2722
2824
|
childrenOf: (path) => [...(children.get(path) ?? [])].sort(),
|
|
2723
2825
|
refresh,
|
|
@@ -2733,6 +2835,8 @@ export function createPackStore(environment, options = {}) {
|
|
|
2733
2835
|
checkpoint,
|
|
2734
2836
|
deferMaintenance,
|
|
2735
2837
|
installPreparedImage,
|
|
2838
|
+
ownsImageExtent,
|
|
2839
|
+
materializeImageExtentsNow,
|
|
2736
2840
|
installPreparedProject,
|
|
2737
2841
|
get deferredImage() { return deferredImage; },
|
|
2738
2842
|
get imagePlacedAt() { return project && imageRangesDigest ? generationInitializedAt : undefined; },
|
|
@@ -2774,6 +2878,7 @@ export async function openPackReader(environment, projectId, seed, imageLine) {
|
|
|
2774
2878
|
throw cause;
|
|
2775
2879
|
}
|
|
2776
2880
|
if (!opened && !project.opened()) {
|
|
2881
|
+
project.close();
|
|
2777
2882
|
packages.close();
|
|
2778
2883
|
return undefined;
|
|
2779
2884
|
}
|
|
@@ -2785,6 +2890,158 @@ const SWEEP_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
|
2785
2890
|
const SWEEP_MARK_FILE = "sweep.json";
|
|
2786
2891
|
/** A package or module linked this recently is kept, linked now or not: two projects taking turns on one origin keep both trees. */
|
|
2787
2892
|
const SWEEP_GRACE_MS = 14 * 24 * 60 * 60 * 1000;
|
|
2893
|
+
/** Stop/dispose the selected project first. Reclamation never relocates another project's offsets. */
|
|
2894
|
+
export async function releaseProjectStore(environment, id, progress) {
|
|
2895
|
+
validProjectId(id);
|
|
2896
|
+
if (id === PACKAGES_STORE || id === STAGING_DIRECTORY || id.startsWith(TREE_STORE_PREFIX))
|
|
2897
|
+
throw new Error("Select an application project to release.");
|
|
2898
|
+
const locks = environment.navigator.locks;
|
|
2899
|
+
if (!locks)
|
|
2900
|
+
throw new Error("Project release requires storage ownership locks.");
|
|
2901
|
+
return locks.request(`supercode-browser-project:${id}`, { mode: "exclusive", ifAvailable: true }, async (projectLock) => {
|
|
2902
|
+
if (!projectLock)
|
|
2903
|
+
throw new Error("This project is still open. Stop it before releasing its saved files.");
|
|
2904
|
+
return locks.request(PACKAGES_OPEN_LOCK, { mode: "exclusive", ifAvailable: true }, async (packagesLock) => {
|
|
2905
|
+
if (!packagesLock)
|
|
2906
|
+
throw new Error("Another project still uses shared packages. Close its page before releasing saved files.");
|
|
2907
|
+
const root = await environment.navigator.storage.getDirectory();
|
|
2908
|
+
let base;
|
|
2909
|
+
try {
|
|
2910
|
+
base = await root.getDirectoryHandle(BASE_DIRECTORY);
|
|
2911
|
+
}
|
|
2912
|
+
catch (cause) {
|
|
2913
|
+
if (!notFound(cause))
|
|
2914
|
+
throw cause;
|
|
2915
|
+
return { project: id, removed: false, projectsRetained: 0, unitsReleased: 0, bytesReleased: 0, before: [], after: [] };
|
|
2916
|
+
}
|
|
2917
|
+
const ids = [];
|
|
2918
|
+
let exists = false, hasPackages = false;
|
|
2919
|
+
for await (const [name, handle] of base.entries()) {
|
|
2920
|
+
if (handle.kind !== "directory")
|
|
2921
|
+
continue;
|
|
2922
|
+
if (name === id) {
|
|
2923
|
+
exists = true;
|
|
2924
|
+
continue;
|
|
2925
|
+
}
|
|
2926
|
+
if (name === PACKAGES_STORE) {
|
|
2927
|
+
hasPackages = true;
|
|
2928
|
+
continue;
|
|
2929
|
+
}
|
|
2930
|
+
if (name === STAGING_DIRECTORY || name.startsWith(TREE_STORE_PREFIX))
|
|
2931
|
+
continue;
|
|
2932
|
+
ids.push(name);
|
|
2933
|
+
}
|
|
2934
|
+
if (!exists)
|
|
2935
|
+
throw new Error(`Saved project ${id} was not found; no files were released.`);
|
|
2936
|
+
ids.sort();
|
|
2937
|
+
const shared = hasPackages ? createPackStoreInternal(environment, {}, true) : undefined;
|
|
2938
|
+
const referenced = new Set();
|
|
2939
|
+
const before = [];
|
|
2940
|
+
try {
|
|
2941
|
+
if (shared)
|
|
2942
|
+
await shared.initialize(PACKAGES_STORE, { shared: true });
|
|
2943
|
+
const known = new Map();
|
|
2944
|
+
for (const entry of shared?.entries.values() ?? [])
|
|
2945
|
+
if (entry.type !== "directory" && entry.length > 0 && !entry.pack) {
|
|
2946
|
+
const same = known.get(entry.offset) ?? [];
|
|
2947
|
+
same.push(entry);
|
|
2948
|
+
known.set(entry.offset, same);
|
|
2949
|
+
}
|
|
2950
|
+
// Read every committed index without maintenance on another project's
|
|
2951
|
+
// generations. Any unreadable or partial state stops before deletion.
|
|
2952
|
+
for (const [position, name] of [...ids, id].entries()) {
|
|
2953
|
+
progress?.(`Checking saved project ${position + 1} of ${ids.length + 1}`);
|
|
2954
|
+
await (await base.getDirectoryHandle(name)).getFileHandle(STATE_FILE);
|
|
2955
|
+
const store = createPackStoreInternal(environment, shared ? { packages: shared } : {}, true);
|
|
2956
|
+
try {
|
|
2957
|
+
await store.initialize(name, { readOnly: true });
|
|
2958
|
+
before.push({ project: name, entries: [...store.entries].sort(([left], [right]) => left.localeCompare(right)) });
|
|
2959
|
+
for (const entry of store.entries.values()) {
|
|
2960
|
+
if (!entry.pack || entry.length === 0)
|
|
2961
|
+
continue;
|
|
2962
|
+
if (!shared)
|
|
2963
|
+
throw new Error(`Retained project ${name} has unknown shared package references.`);
|
|
2964
|
+
if (entry.offset + entry.length > shared.packBytes || !known.get(entry.offset)?.some(source => source.type === entry.type && source.length === entry.length)) {
|
|
2965
|
+
throw new Error(`Project ${name} has an unknown shared extent ${entry.offset}+${entry.length}; all stores were retained.`);
|
|
2966
|
+
}
|
|
2967
|
+
if (name !== id)
|
|
2968
|
+
referenced.add(entry.offset);
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
finally {
|
|
2972
|
+
store.close();
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
const kept = new Set(), units = new Set(), unitBytes = new Map();
|
|
2976
|
+
if (shared) {
|
|
2977
|
+
for (const [path, entry] of shared.entries) {
|
|
2978
|
+
const unit = unitOf(path);
|
|
2979
|
+
if (unit === undefined)
|
|
2980
|
+
continue;
|
|
2981
|
+
units.add(unit);
|
|
2982
|
+
unitBytes.set(unit, (unitBytes.get(unit) ?? 0) + entry.length);
|
|
2983
|
+
if (entry.type !== "directory" && entry.length > 0 && referenced.has(entry.offset))
|
|
2984
|
+
kept.add(unit);
|
|
2985
|
+
}
|
|
2986
|
+
// An empty unit costs no file bytes; its zero-length links cannot
|
|
2987
|
+
// identify a unique shared offset, so keep its metadata conservatively.
|
|
2988
|
+
for (const [unit, bytes] of unitBytes)
|
|
2989
|
+
if (bytes === 0)
|
|
2990
|
+
kept.add(unit);
|
|
2991
|
+
}
|
|
2992
|
+
const released = [...units].filter(unit => !kept.has(unit));
|
|
2993
|
+
if (released.length) {
|
|
2994
|
+
kept.delete(".layers");
|
|
2995
|
+
kept.delete(".layers-deferred");
|
|
2996
|
+
kept.delete(".layers-local");
|
|
2997
|
+
}
|
|
2998
|
+
// Removing the selected store first is fail-safe: an interrupted
|
|
2999
|
+
// reclamation leaves unused packages, never a project pointing at holes.
|
|
3000
|
+
if (exists) {
|
|
3001
|
+
progress?.("Removing this project's saved files");
|
|
3002
|
+
await base.removeEntry(id, { recursive: true });
|
|
3003
|
+
}
|
|
3004
|
+
let bytesReleased = 0;
|
|
3005
|
+
if (shared && released.length) {
|
|
3006
|
+
bytesReleased = released.reduce((sum, unit) => sum + (unitBytes.get(unit) ?? 0), 0);
|
|
3007
|
+
progress?.("Reclaiming packages no retained project uses");
|
|
3008
|
+
await shared.retain(path => { const unit = unitOf(path); return unit === undefined || kept.has(unit); }, { preserveOffsets: true });
|
|
3009
|
+
// All package users are excluded; every retained link still names
|
|
3010
|
+
// the same offset in the newly committed sparse generation.
|
|
3011
|
+
await shared.discardUnreferencedGenerations();
|
|
3012
|
+
}
|
|
3013
|
+
const after = [];
|
|
3014
|
+
for (const name of ids) {
|
|
3015
|
+
const store = createPackStoreInternal(environment, shared ? { packages: shared } : {}, true);
|
|
3016
|
+
try {
|
|
3017
|
+
await store.initialize(name, { readOnly: true });
|
|
3018
|
+
after.push({ project: name, entries: [...store.entries].sort(([left], [right]) => left.localeCompare(right)) });
|
|
3019
|
+
}
|
|
3020
|
+
finally {
|
|
3021
|
+
store.close();
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
return { project: id, removed: exists, projectsRetained: ids.length, unitsReleased: released.length, bytesReleased, before, after };
|
|
3025
|
+
}
|
|
3026
|
+
catch (cause) {
|
|
3027
|
+
throw new Error(`Project release did not finish: ${cause instanceof Error ? cause.message : String(cause)} Other projects were retained; shared reclamation is not complete.`, { cause });
|
|
3028
|
+
}
|
|
3029
|
+
finally {
|
|
3030
|
+
shared?.close();
|
|
3031
|
+
}
|
|
3032
|
+
});
|
|
3033
|
+
});
|
|
3034
|
+
}
|
|
3035
|
+
/** Shared by store initialization and the origin's project-release door. */
|
|
3036
|
+
function validProjectId(value) {
|
|
3037
|
+
if (typeof value !== "string" || !/^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/u.test(value)) {
|
|
3038
|
+
throw new Error("OPFS filesystem projectId is invalid.");
|
|
3039
|
+
}
|
|
3040
|
+
return value;
|
|
3041
|
+
}
|
|
3042
|
+
function notFound(cause) {
|
|
3043
|
+
return cause instanceof DOMException && cause.name === "NotFoundError";
|
|
3044
|
+
}
|
|
2788
3045
|
/**
|
|
2789
3046
|
* Packages and modules no project links leave the shared store. Runs only
|
|
2790
3047
|
* when no tab holds the store open, so no reader is under it: every
|
|
@@ -2824,7 +3081,7 @@ export async function sweepSharedPackages(environment, progress) {
|
|
|
2824
3081
|
if (handle.kind === "directory" && name !== PACKAGES_STORE && name !== STAGING_DIRECTORY && !name.startsWith(TREE_STORE_PREFIX))
|
|
2825
3082
|
projectIds.push(name);
|
|
2826
3083
|
}
|
|
2827
|
-
const shared =
|
|
3084
|
+
const shared = createPackStoreInternal(environment, {}, true);
|
|
2828
3085
|
if (!await shared.initialize(PACKAGES_STORE, { shared: true })) {
|
|
2829
3086
|
shared.close();
|
|
2830
3087
|
return undefined;
|
|
@@ -2836,7 +3093,7 @@ export async function sweepSharedPackages(environment, progress) {
|
|
|
2836
3093
|
// A sweep has no fixed length; each project opened is a sign of life
|
|
2837
3094
|
// for the page waiting on the worker's start.
|
|
2838
3095
|
progress?.(`sweeping the origin's packages, project ${index + 1} of ${projectIds.length}`);
|
|
2839
|
-
const project =
|
|
3096
|
+
const project = createPackStoreInternal(environment, { packages: shared }, true);
|
|
2840
3097
|
try {
|
|
2841
3098
|
const opened = await project.initialize(id);
|
|
2842
3099
|
if (!opened && !project.opened()) {
|
|
@@ -2844,9 +3101,9 @@ export async function sweepSharedPackages(environment, progress) {
|
|
|
2844
3101
|
continue;
|
|
2845
3102
|
}
|
|
2846
3103
|
}
|
|
2847
|
-
catch {
|
|
3104
|
+
catch (cause) {
|
|
2848
3105
|
project.close();
|
|
2849
|
-
|
|
3106
|
+
throw new Error(`Shared package references for project ${id} are unknown; stored packages were retained.`, { cause });
|
|
2850
3107
|
}
|
|
2851
3108
|
for (const entry of project.entries.values())
|
|
2852
3109
|
if (entry.pack && entry.length > 0)
|
|
@@ -2879,9 +3136,10 @@ export async function sweepSharedPackages(environment, progress) {
|
|
|
2879
3136
|
}
|
|
2880
3137
|
// A layers document's marker says every unit it names is here: once any
|
|
2881
3138
|
// unit goes, none of those markers may stand (ADR-0045).
|
|
2882
|
-
if ([...units].some((unit) => !kept.has(unit) && unit !== ".layers" && unit !== ".layers-deferred")) {
|
|
3139
|
+
if ([...units].some((unit) => !kept.has(unit) && unit !== ".layers" && unit !== ".layers-deferred" && unit !== ".layers-local")) {
|
|
2883
3140
|
kept.delete(".layers");
|
|
2884
3141
|
kept.delete(".layers-deferred");
|
|
3142
|
+
kept.delete(".layers-local");
|
|
2885
3143
|
}
|
|
2886
3144
|
for (const unit of units)
|
|
2887
3145
|
if (!kept.has(unit))
|
|
@@ -2945,7 +3203,7 @@ function unitOf(path) {
|
|
|
2945
3203
|
return parts.length >= 4 ? parts.slice(0, 4).join("/") : undefined;
|
|
2946
3204
|
// Each program's tree is its own unit: kept while a project links it or it
|
|
2947
3205
|
// was placed lately, swept with its receipt when neither holds.
|
|
2948
|
-
if (parts[0] === ".programs")
|
|
3206
|
+
if (parts[0] === ".programs" || parts[0] === ".prepared" || parts[0] === ".trees")
|
|
2949
3207
|
return parts.length >= 2 ? parts.slice(0, 2).join("/") : undefined;
|
|
2950
3208
|
if (parts[0] === ".browser-packs") {
|
|
2951
3209
|
if (parts[1] === "objects" && parts.length >= 3)
|