@volter/browser-runtime 0.1.83 → 0.1.85

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.
@@ -1,7 +1,8 @@
1
1
  import { encodeSharedIndex, OverlaidChildren, OverlaidIndex, SharedIndex, sameIndexEntry } from "./browser-pack-index.js";
2
+ import { PACKAGES_STORE, STORE_BASE_DIRECTORY, STORE_STATE_FILE } from "./browser-store-layout.js";
2
3
  import { ImageRangeReader, IMAGE_RANGES_FILE, validateImageRanges } from "./browser-image-ranges.js";
3
4
  /** The one shared store of an origin: every package version's files, once, under `/packages/<name>@<version>/`. */
4
- export const PACKAGES_STORE = "packages";
5
+ export { PACKAGES_STORE };
5
6
  export const PACKAGES_ROOT = "/packages";
6
7
  /**
7
8
  * A project's live-pack retention roots (W62): links that keep an installed
@@ -22,7 +23,7 @@ export const PACKAGES_LOCK = "supercode-browser-packages";
22
23
  export const PACKAGES_OPEN_LOCK = "supercode-browser-packages-open";
23
24
  const encoder = new TextEncoder();
24
25
  const decoder = new TextDecoder("utf-8", { fatal: true });
25
- const BASE_DIRECTORY = "supercode-browser-filesystems";
26
+ const BASE_DIRECTORY = STORE_BASE_DIRECTORY;
26
27
  /**
27
28
  * A store whose name begins so is a tree a host installed (ADR-0048), not a
28
29
  * project: it links no shared packages, and a sweep of them leaves it alone.
@@ -31,7 +32,7 @@ const BASE_DIRECTORY = "supercode-browser-filesystems";
31
32
  export const TREE_STORE_PREFIX = "host-tree.";
32
33
  const GENERATIONS_DIRECTORY = "generations";
33
34
  const STAGING_DIRECTORY = "atomic-staging";
34
- const STATE_FILE = "state.json";
35
+ const STATE_FILE = STORE_STATE_FILE;
35
36
  const INDEX_FILE = "index.json";
36
37
  const LOG_FILE = "index-log.bin";
37
38
  const PACK_FILE = "pack.bin";
@@ -85,6 +86,7 @@ export function createPackStore(environment, options = {}) {
85
86
  let imageReader;
86
87
  let deferredImage;
87
88
  let receivedImageCompletion;
89
+ let receivedImageLine;
88
90
  let generationInitializedAt = 0;
89
91
  let logAccess;
90
92
  /** Committed package bytes captured under the writer lock; never mutable workspace bytes. */
@@ -220,7 +222,8 @@ export function createPackStore(environment, options = {}) {
220
222
  function imageCompletionSeed() {
221
223
  if (!imageReader || !imageRangesDigest || !generation || initializing || transitions > 0 || closing)
222
224
  return undefined;
223
- return { generation, rangesDigest: imageRangesDigest, completed: imageReader.completionBuffer };
225
+ return { generation, rangesDigest: imageRangesDigest, completed: imageReader.completionBuffer,
226
+ ...(imageReader.receiptKey ? { metadata: { ranges: imageReader.verifiedRanges, receiptKey: imageReader.receiptKey } } : {}) };
224
227
  }
225
228
  /**
226
229
  * The owner stands on the seed it just encoded, as its readers do: the index
@@ -417,6 +420,7 @@ export function createPackStore(environment, options = {}) {
417
420
  projectId = validProjectId(requestedProjectId);
418
421
  readOnly = openOptions.readOnly === true;
419
422
  receivedImageCompletion = readOnly ? openOptions.imageCompletion : undefined;
423
+ receivedImageLine = readOnly ? openOptions.imageLine : undefined;
420
424
  extentsOnly = readOnly && openOptions.extentsOnly === true;
421
425
  shared = openOptions.shared === true || readOnly;
422
426
  if (!shared)
@@ -753,17 +757,38 @@ export function createPackStore(environment, options = {}) {
753
757
  logAccess = await openSyncAccess(log, "record of changes");
754
758
  logBytes = logAccess.getSize();
755
759
  if (imageRangesDigest) {
756
- const file = await (await directory.getFileHandle(IMAGE_RANGES_FILE)).getFile();
757
- const bytes = new Uint8Array(await file.arrayBuffer());
758
- if (await indexDigest(bytes) !== imageRangesDigest)
759
- throw new Error("The committed immutable image ranges have changed. Stored data was retained.");
760
- const ranges = validateImageRanges(JSON.parse(decoder.decode(bytes)));
760
+ const supplied = receivedImageCompletion;
761
+ const completion = supplied && supplied.generation === generation && supplied.rangesDigest === imageRangesDigest
762
+ ? supplied : undefined;
763
+ let ranges;
764
+ let receiptKey;
765
+ if (completion?.metadata) {
766
+ // This is the existing private owner/reader seed, not an image or
767
+ // guest-supplied claim. Current committed state binds its identity.
768
+ ranges = validateImageRanges(completion.metadata.ranges);
769
+ receiptKey = completion.metadata.receiptKey;
770
+ if (!/^[a-f0-9]{64}$/u.test(receiptKey))
771
+ throw new Error("The owner's immutable receipt key is invalid.");
772
+ }
773
+ else {
774
+ const file = await (await directory.getFileHandle(IMAGE_RANGES_FILE)).getFile();
775
+ const bytes = new Uint8Array(await file.arrayBuffer());
776
+ if (await indexDigest(bytes) !== imageRangesDigest)
777
+ throw new Error("The committed immutable image ranges have changed. Stored data was retained.");
778
+ ranges = validateImageRanges(JSON.parse(decoder.decode(bytes)));
779
+ // The ledger key retains ADR-0076's canonical validated-table hash,
780
+ // including for old files whose JSON whitespace differs.
781
+ receiptKey = await indexDigest(encoder.encode(JSON.stringify(ranges)));
782
+ }
761
783
  if (ranges.bytes > packAccess.getSize())
762
784
  throw new Error("The immutable image reservation is incomplete. Stored data was retained.");
763
- const completion = receivedImageCompletion;
764
- const completed = completion && completion.generation === generation && completion.rangesDigest === imageRangesDigest
765
- ? completion.completed : undefined;
766
- imageReader = new ImageRangeReader(ranges, directory, `${projectId}.${generation}`, completed);
785
+ // The owner's line serves only the generation and table its completion names.
786
+ const line = completion ? receivedImageLine : undefined;
787
+ if (!line)
788
+ receivedImageLine?.port.close();
789
+ receivedImageLine = undefined;
790
+ imageReader = new ImageRangeReader(ranges, directory, `${projectId}.${generation}`, completion?.completed, receiptKey, line);
791
+ await imageReader.initialize();
767
792
  deferredImage = ranges.image;
768
793
  }
769
794
  }
@@ -1900,7 +1925,7 @@ export function createPackStore(environment, options = {}) {
1900
1925
  if (!shared)
1901
1926
  await discardGenerations(name);
1902
1927
  }
1903
- async function installPreparedImage(prepared, json, binary, suppliedRanges) {
1928
+ async function installPreparedImage(prepared, json, binary, suppliedRanges, verified = false) {
1904
1929
  assertWritable();
1905
1930
  if (!shared || projectId !== PACKAGES_STORE)
1906
1931
  throw new Error("Only the shared package store can reserve immutable image extents.");
@@ -1911,7 +1936,7 @@ export function createPackStore(environment, options = {}) {
1911
1936
  const ranges = validateImageRanges(suppliedRanges);
1912
1937
  if (ranges.bytes !== prepared.packBytes || prepared.entries > MAX_ENTRIES || prepared.liveBytes > prepared.packBytes
1913
1938
  || json.byteLength !== prepared.json.raw || binary.byteLength !== prepared.binary.raw
1914
- || await indexDigest(json) !== prepared.json.sha256 || await indexDigest(binary) !== prepared.binary.sha256) {
1939
+ || !await matchesDigest(json, prepared.json.sha256, verified) || !await matchesDigest(binary, prepared.binary.sha256, verified)) {
1915
1940
  throw new Error("The image's prepared metadata does not match its document.");
1916
1941
  }
1917
1942
  // The pinned host emits the authoritative JSON checkpoint and its matching
@@ -1923,7 +1948,8 @@ export function createPackStore(environment, options = {}) {
1923
1948
  const retained = JSON.parse(`${decoder.decode(json.subarray(0, newline - 1))}}`);
1924
1949
  const measures = { packBytes: prepared.packBytes, liveBytes: prepared.liveBytes, linked: 0, linkedBytes: 0 };
1925
1950
  const header = retained.retainedIndex;
1926
- if (header.version !== 1 || header.digest !== await retainedIndexDigest(binary, measures)
1951
+ // The binary's digest is the descriptor's, now matched: it is not hashed a second time.
1952
+ if (header.version !== 1 || header.digest !== await retainedIndexDigestOf(prepared.binary.sha256, measures)
1927
1953
  || header.packBytes !== measures.packBytes || header.liveBytes !== measures.liveBytes || header.linked !== 0 || header.linkedBytes !== 0) {
1928
1954
  throw new Error("The prepared index's retained measures are inconsistent.");
1929
1955
  }
@@ -1951,14 +1977,14 @@ export function createPackStore(environment, options = {}) {
1951
1977
  return true;
1952
1978
  });
1953
1979
  }
1954
- async function installPreparedProject(prepared, json, binary) {
1980
+ async function installPreparedProject(prepared, json, binary, verified = false, imageInstalled) {
1955
1981
  assertWritable();
1956
- if (shared || !options.packages?.deferredImage)
1982
+ if (shared || !options.packages || (!imageInstalled && !options.packages.deferredImage))
1957
1983
  throw new Error("Prepared project paths require a verified shared image.");
1958
1984
  if (packBytes !== 0 || [...entries.values()].some(entry => entry.type !== "directory"))
1959
1985
  return false;
1960
1986
  if (json.byteLength !== prepared.json.raw || binary.byteLength !== prepared.binary.raw
1961
- || await indexDigest(json) !== prepared.json.sha256 || await indexDigest(binary) !== prepared.binary.sha256) {
1987
+ || !await matchesDigest(json, prepared.json.sha256, verified) || !await matchesDigest(binary, prepared.binary.sha256, verified)) {
1962
1988
  throw new Error("The image's prepared project does not match its descriptor.");
1963
1989
  }
1964
1990
  const newline = json.subarray(0, 4096).indexOf(10);
@@ -1966,7 +1992,7 @@ export function createPackStore(environment, options = {}) {
1966
1992
  throw new Error("The prepared project has no bounded retained header.");
1967
1993
  const header = JSON.parse(`${decoder.decode(json.subarray(0, newline - 1))}}`).retainedIndex;
1968
1994
  const measures = { packBytes: 0, liveBytes: 0, linked: prepared.linked, linkedBytes: prepared.linkedBytes };
1969
- if (header.version !== 1 || header.digest !== await retainedIndexDigest(binary, measures)
1995
+ if (header.version !== 1 || header.digest !== await retainedIndexDigestOf(prepared.binary.sha256, measures)
1970
1996
  || header.packBytes !== 0 || header.liveBytes !== 0 || header.linked !== prepared.linked || header.linkedBytes !== prepared.linkedBytes) {
1971
1997
  throw new Error("The prepared project's retained measures are inconsistent.");
1972
1998
  }
@@ -1988,6 +2014,12 @@ export function createPackStore(environment, options = {}) {
1988
2014
  }
1989
2015
  await writeBytesAtomically(directory, RETAINED_INDEX_FILE, binary);
1990
2016
  await writeBytesAtomically(directory, INDEX_FILE, json);
2017
+ // Written beside the shared image's install; committed only over it. A
2018
+ // generation left uncommitted is swept with any other on the next open.
2019
+ if (imageInstalled && !await imageInstalled)
2020
+ return false;
2021
+ if (!options.packages?.deferredImage)
2022
+ throw new Error("Prepared project paths require a verified shared image.");
1991
2023
  await commitGeneration(name, directory, new Map(), 0, { index, ...measures });
1992
2024
  return true;
1993
2025
  });
@@ -2061,6 +2093,18 @@ export function createPackStore(environment, options = {}) {
2061
2093
  const hash = await crypto.subtle.digest("SHA-256", bytes);
2062
2094
  return Array.from(new Uint8Array(hash), (byte) => byte.toString(16).padStart(2, "0")).join("");
2063
2095
  }
2096
+ /**
2097
+ * Whether these bytes are the ones a digest names: hashed here, unless they
2098
+ * were already checked against this same digest where they were inflated.
2099
+ */
2100
+ async function matchesDigest(bytes, digest, verified) {
2101
+ return verified || await indexDigest(bytes) === digest;
2102
+ }
2103
+ /** The retained index digest from the binary index's own SHA-256, when that is already known. */
2104
+ function retainedIndexDigestOf(binaryDigest, measures) {
2105
+ const { packBytes, liveBytes, linked, linkedBytes, journalPrefix } = measures;
2106
+ return indexDigest(encoder.encode(`${JSON.stringify({ packBytes, liveBytes, linked, linkedBytes, ...(journalPrefix ? { journalPrefix } : {}) })}:${binaryDigest}`));
2107
+ }
2064
2108
  async function retainedIndexDigest(bytes, measures) {
2065
2109
  const { packBytes, liveBytes, linked, linkedBytes, journalPrefix } = measures;
2066
2110
  // Bind the byte measures too: none may be trusted independently of the
@@ -2651,6 +2695,14 @@ export function createPackStore(environment, options = {}) {
2651
2695
  clock,
2652
2696
  readerSeed,
2653
2697
  imageCompletionSeed,
2698
+ imageLine: () => imageReader?.lineForReader() ?? options.packages?.imageLine(),
2699
+ imageStartupSet: () => imageReader?.readDigests() ?? imageReader?.completedDigests() ?? [],
2700
+ prefetchImage: (digests, streams) => imageReader?.prefetch(digests, streams) ?? { queued: 0, done: Promise.resolve() },
2701
+ imageBlockedReads: () => {
2702
+ const own = imageReader?.blockedReads ?? { ranges: 0, ms: 0 };
2703
+ const linked = options.packages?.imageBlockedReads() ?? { ranges: 0, ms: 0 };
2704
+ return { ranges: own.ranges + linked.ranges, ms: own.ms + linked.ms };
2705
+ },
2654
2706
  seeded: () => startedFromSeed,
2655
2707
  get logBytes() { return logBytes; },
2656
2708
  checkpoint,
@@ -2682,10 +2734,10 @@ export function createPackStore(environment, options = {}) {
2682
2734
  * the origin's packages it links, both open for reading beside their
2683
2735
  * owners. Undefined when the project has no store yet.
2684
2736
  */
2685
- export async function openPackReader(environment, projectId, seed) {
2737
+ export async function openPackReader(environment, projectId, seed, imageLine) {
2686
2738
  const packages = createPackStore(environment);
2687
2739
  await packages.initialize(PACKAGES_STORE, { readOnly: true, extentsOnly: true,
2688
- ...(seed?.packagesImage ? { imageCompletion: seed.packagesImage } : {}) });
2740
+ ...(seed?.packagesImage ? { imageCompletion: seed.packagesImage, ...(imageLine ? { imageLine } : {}) } : {}) });
2689
2741
  const project = createPackStore(environment, { packages, ownsPackages: true });
2690
2742
  let opened;
2691
2743
  try {