@variance-authority/core 0.22.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # @variance-authority/core
2
2
 
3
+ ## 0.26.0
4
+
5
+ No changes in this release.
6
+
7
+ ## 0.25.0
8
+
9
+ ### Minor Changes
10
+
11
+ - 3b2a920: A checkout behind the mainline's tip is measured against the record at its merge base
12
+
13
+ A share in git now keeps each mainline publish at the commit it ran at, under `refs/variance/at/<commit>/mainline/<name>`, beside the latest. Each publish says what it kept: `kept the record at <commit> beside the latest.` When the latest is not at `HEAD`'s merge base with the mainline, every reader asks for the record kept there first: `share`, `select`, `review`, `ask` and `serve`. Then it reads the nearest record kept behind the merge base, and the latest only when none is kept at or behind it. Nothing removes a kept record until you ask: `variance share --purge-older-than <days>` deletes those older, and the latest stays. A release branch, or a pull request cut days ago, is no longer charged with every commit the mainline gained since. A record behind the merge base charges the mainline's commits between it and the merge base with yours, and the tests they reach run: it replaces the skip that left those tests to the mainline, which could miss a test the mainline's commits turned onto your change. `variance share --publish` exits 2 when it wrote the latest and could not keep it by commit, and `--purge-older-than` removes from the share of a named `--config`, whatever the root config says. A directory or an HTTP share keeps the latest only, as before.
14
+
15
+ ## 0.24.1
16
+
17
+ Lockstep release — nothing in this package changed. Every `@variance-authority/*` package shares one version.
18
+
19
+ ## 0.24.0
20
+
21
+ Lockstep release — nothing in this package changed. Every `@variance-authority/*` package shares one version.
22
+
23
+ ## 0.23.0
24
+
25
+ Lockstep release — nothing in this package changed. Every `@variance-authority/*` package shares one version.
26
+
3
27
  ## 0.22.0
4
28
 
5
29
  ### Minor Changes
@@ -0,0 +1,28 @@
1
+ /**
2
+ * A mainline's record kept at the commit it ran at, beside the latest.
3
+ *
4
+ * The latest answers a checkout at the mainline's tip. A checkout behind it —
5
+ * a release branch, a pull request cut last week, a user who does not track
6
+ * the tip — is measured against its merge base, and the latest is a record of
7
+ * code it does not have: every commit between is charged to it. So each
8
+ * mainline publish also writes the line `at/<commit>/…` (`publishLine`),
9
+ * and a reader asks for its merge base's, then the nearest kept behind it,
10
+ * before the latest.
11
+ *
12
+ * Nothing is removed unless somebody asks ({@link purgeKept}). A store whose
13
+ * cell cannot list lines keeps the latest only.
14
+ */
15
+ import type { LineCell, ShareLine, ShareMiss } from './line.js';
16
+ /** The line that holds `line`'s record at `commit`. */
17
+ export declare function atCommit(line: ShareLine, commit: string): ShareLine;
18
+ /** The commits a publish kept its record at, and the miss that stopped it before it finished. */
19
+ export interface KeptCommits {
20
+ readonly kept: readonly string[];
21
+ readonly miss?: ShareMiss;
22
+ }
23
+ /**
24
+ * Remove what `line` keeps by commit written before `before`, and name the
25
+ * commits removed. The latest stays, and a store that keeps none removes none.
26
+ */
27
+ export declare function purgeKept(cell: LineCell, line: ShareLine, before: number): Promise<readonly string[] | ShareMiss>;
28
+ //# sourceMappingURL=at-commit.d.ts.map
@@ -0,0 +1,35 @@
1
+ /**
2
+ * A mainline's record kept at the commit it ran at, beside the latest.
3
+ *
4
+ * The latest answers a checkout at the mainline's tip. A checkout behind it —
5
+ * a release branch, a pull request cut last week, a user who does not track
6
+ * the tip — is measured against its merge base, and the latest is a record of
7
+ * code it does not have: every commit between is charged to it. So each
8
+ * mainline publish also writes the line `at/<commit>/…` (`publishLine`),
9
+ * and a reader asks for its merge base's, then the nearest kept behind it,
10
+ * before the latest.
11
+ *
12
+ * Nothing is removed unless somebody asks ({@link purgeKept}). A store whose
13
+ * cell cannot list lines keeps the latest only.
14
+ */
15
+ /** The line that holds `line`'s record at `commit`. */
16
+ export function atCommit(line, commit) {
17
+ return { kind: line.kind, name: line.name, at: commit };
18
+ }
19
+ /**
20
+ * Remove what `line` keeps by commit written before `before`, and name the
21
+ * commits removed. The latest stays, and a store that keeps none removes none.
22
+ */
23
+ export async function purgeKept(cell, line, before) {
24
+ if (cell.commits === undefined || cell.drop === undefined)
25
+ return [];
26
+ const held = await cell.commits(line);
27
+ if (!Array.isArray(held))
28
+ return held;
29
+ const old = held.filter((one) => one.written < before).map((one) => one.commit);
30
+ if (old.length === 0)
31
+ return [];
32
+ const dropped = await cell.drop(old.map((commit) => atCommit(line, commit)));
33
+ return dropped === 'dropped' ? old : dropped;
34
+ }
35
+ //# sourceMappingURL=at-commit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"at-commit.js","sourceRoot":"","sources":["../../src/share/at-commit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,uDAAuD;AACvD,MAAM,UAAU,QAAQ,CAAC,IAAe,EAAE,MAAc;IACtD,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC;AAC1D,CAAC;AAQD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,IAAc,EAAE,IAAe,EAAE,MAAc;IAC7E,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IACrE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,OAAO,IAAiB,CAAC;IACnD,MAAM,GAAG,GAAI,IAA0B,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACvG,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAChC,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;IAC7E,OAAO,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC;AAC/C,CAAC","sourcesContent":["/**\n * A mainline's record kept at the commit it ran at, beside the latest.\n *\n * The latest answers a checkout at the mainline's tip. A checkout behind it —\n * a release branch, a pull request cut last week, a user who does not track\n * the tip — is measured against its merge base, and the latest is a record of\n * code it does not have: every commit between is charged to it. So each\n * mainline publish also writes the line `at/<commit>/…` (`publishLine`),\n * and a reader asks for its merge base's, then the nearest kept behind it,\n * before the latest.\n *\n * Nothing is removed unless somebody asks ({@link purgeKept}). A store whose\n * cell cannot list lines keeps the latest only.\n */\n\nimport type { KeptAt, LineCell, ShareLine, ShareMiss } from './line.js';\n\n/** The line that holds `line`'s record at `commit`. */\nexport function atCommit(line: ShareLine, commit: string): ShareLine {\n return { kind: line.kind, name: line.name, at: commit };\n}\n\n/** The commits a publish kept its record at, and the miss that stopped it before it finished. */\nexport interface KeptCommits {\n readonly kept: readonly string[];\n readonly miss?: ShareMiss;\n}\n\n/**\n * Remove what `line` keeps by commit written before `before`, and name the\n * commits removed. The latest stays, and a store that keeps none removes none.\n */\nexport async function purgeKept(cell: LineCell, line: ShareLine, before: number): Promise<readonly string[] | ShareMiss> {\n if (cell.commits === undefined || cell.drop === undefined) return [];\n const held = await cell.commits(line);\n if (!Array.isArray(held)) return held as ShareMiss;\n const old = (held as readonly KeptAt[]).filter((one) => one.written < before).map((one) => one.commit);\n if (old.length === 0) return [];\n const dropped = await cell.drop(old.map((commit) => atCommit(line, commit)));\n return dropped === 'dropped' ? old : dropped;\n}\n"]}
@@ -1,31 +1,20 @@
1
1
  /**
2
2
  * `@variance-authority/core/share` — one machine's evaluation, reachable from another.
3
3
  *
4
- * A run derives a great deal that is not about the run: what the suite is made
5
- * of, which subjects exist, every name each one carries, which files declare
6
- * which components. All of it is a fact about a *commit*, and all of it is
7
- * expensive — a source scan, a browser, a composition pass — while being
8
- * identical on every machine that starts from the same tree.
4
+ * What a run derives about a commit — the suite, its subjects, their names,
5
+ * which files declare which components — is expensive and identical on every
6
+ * machine that starts from the same tree, so a pull-request runner reads what
7
+ * mainline already published. A `LineCell` holds the latest of one mainline or
8
+ * branch, and a mainline's at each commit it published; `publishLine` writes it and
9
+ * `readLine` reads it back, with the reason when it cannot.
9
10
  *
10
- * So the second machine should not derive it again. A pull-request runner wants
11
- * what mainline already knows, and mainline already knew it an hour ago on a
12
- * runner that has since been destroyed. This module is the seam between those
13
- * two: a `LineCell` holds the latest of what one mainline or one branch
14
- * published, `publishLine` replaces it, and `readLine` reads it back
15
- * with the reason when it cannot.
16
- *
17
- * ## What this is not
18
- *
19
- * Not a baseline store. A baseline is the thing a comparison is *against*, it
20
- * cannot be re-derived, and losing one silently is how a suite records whatever
21
- * happened to be on screen — which is why
22
- * [`RasterStore`](https://github.com/Variance-Authority/variance-authority/blob/main/packages/raster/src/store.ts) throws on every failure. A line
23
- * holds what can be derived again from the commit it names, so a miss costs the
24
- * derivation. The two are configured separately because they are answers to
25
- * different questions, and a share is disposable in a way a baseline may never
26
- * be.
11
+ * Not a baseline store: a line holds only what its commit can derive again, so
12
+ * a miss costs the derivation, where a baseline cannot be derived again and
13
+ * [`RasterStore`](https://github.com/Variance-Authority/variance-authority/blob/main/packages/raster/src/store.ts)
14
+ * throws on every failure.
27
15
  */
28
16
  export * from './line.js';
29
17
  export * from './publish.js';
18
+ export * from './at-commit.js';
30
19
  export * from './http-line.js';
31
20
  //# sourceMappingURL=index.d.ts.map
@@ -1,31 +1,20 @@
1
1
  /**
2
2
  * `@variance-authority/core/share` — one machine's evaluation, reachable from another.
3
3
  *
4
- * A run derives a great deal that is not about the run: what the suite is made
5
- * of, which subjects exist, every name each one carries, which files declare
6
- * which components. All of it is a fact about a *commit*, and all of it is
7
- * expensive — a source scan, a browser, a composition pass — while being
8
- * identical on every machine that starts from the same tree.
4
+ * What a run derives about a commit — the suite, its subjects, their names,
5
+ * which files declare which components — is expensive and identical on every
6
+ * machine that starts from the same tree, so a pull-request runner reads what
7
+ * mainline already published. A `LineCell` holds the latest of one mainline or
8
+ * branch, and a mainline's at each commit it published; `publishLine` writes it and
9
+ * `readLine` reads it back, with the reason when it cannot.
9
10
  *
10
- * So the second machine should not derive it again. A pull-request runner wants
11
- * what mainline already knows, and mainline already knew it an hour ago on a
12
- * runner that has since been destroyed. This module is the seam between those
13
- * two: a `LineCell` holds the latest of what one mainline or one branch
14
- * published, `publishLine` replaces it, and `readLine` reads it back
15
- * with the reason when it cannot.
16
- *
17
- * ## What this is not
18
- *
19
- * Not a baseline store. A baseline is the thing a comparison is *against*, it
20
- * cannot be re-derived, and losing one silently is how a suite records whatever
21
- * happened to be on screen — which is why
22
- * [`RasterStore`](https://github.com/Variance-Authority/variance-authority/blob/main/packages/raster/src/store.ts) throws on every failure. A line
23
- * holds what can be derived again from the commit it names, so a miss costs the
24
- * derivation. The two are configured separately because they are answers to
25
- * different questions, and a share is disposable in a way a baseline may never
26
- * be.
11
+ * Not a baseline store: a line holds only what its commit can derive again, so
12
+ * a miss costs the derivation, where a baseline cannot be derived again and
13
+ * [`RasterStore`](https://github.com/Variance-Authority/variance-authority/blob/main/packages/raster/src/store.ts)
14
+ * throws on every failure.
27
15
  */
28
16
  export * from './line.js';
29
17
  export * from './publish.js';
18
+ export * from './at-commit.js';
30
19
  export * from './http-line.js';
31
20
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/share/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,cAAc,WAAW,CAAC;AAC1B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC","sourcesContent":["/**\n * `@variance-authority/core/share` — one machine's evaluation, reachable from another.\n *\n * A run derives a great deal that is not about the run: what the suite is made\n * of, which subjects exist, every name each one carries, which files declare\n * which components. All of it is a fact about a *commit*, and all of it is\n * expensive — a source scan, a browser, a composition pass — while being\n * identical on every machine that starts from the same tree.\n *\n * So the second machine should not derive it again. A pull-request runner wants\n * what mainline already knows, and mainline already knew it an hour ago on a\n * runner that has since been destroyed. This module is the seam between those\n * two: a `LineCell` holds the latest of what one mainline or one branch\n * published, `publishLine` replaces it, and `readLine` reads it back\n * with the reason when it cannot.\n *\n * ## What this is not\n *\n * Not a baseline store. A baseline is the thing a comparison is *against*, it\n * cannot be re-derived, and losing one silently is how a suite records whatever\n * happened to be on screen — which is why\n * [`RasterStore`](https://github.com/Variance-Authority/variance-authority/blob/main/packages/raster/src/store.ts) throws on every failure. A line\n * holds what can be derived again from the commit it names, so a miss costs the\n * derivation. The two are configured separately because they are answers to\n * different questions, and a share is disposable in a way a baseline may never\n * be.\n */\n\nexport * from './line.js';\nexport * from './publish.js';\nexport * from './http-line.js';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/share/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,cAAc,WAAW,CAAC;AAC1B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC","sourcesContent":["/**\n * `@variance-authority/core/share` — one machine's evaluation, reachable from another.\n *\n * What a run derives about a commit — the suite, its subjects, their names,\n * which files declare which components — is expensive and identical on every\n * machine that starts from the same tree, so a pull-request runner reads what\n * mainline already published. A `LineCell` holds the latest of one mainline or\n * branch, and a mainline's at each commit it published; `publishLine` writes it and\n * `readLine` reads it back, with the reason when it cannot.\n *\n * Not a baseline store: a line holds only what its commit can derive again, so\n * a miss costs the derivation, where a baseline cannot be derived again and\n * [`RasterStore`](https://github.com/Variance-Authority/variance-authority/blob/main/packages/raster/src/store.ts)\n * throws on every failure.\n */\n\nexport * from './line.js';\nexport * from './publish.js';\nexport * from './at-commit.js';\nexport * from './http-line.js';\n"]}
@@ -1,12 +1,11 @@
1
1
  /**
2
2
  * A line: the latest of what CI derived on one mainline or one branch.
3
3
  *
4
- * A line answers the question a checkout actually has — *what is mainline now*
5
- * — by holding one version and replacing it, rather than keeping one artifact
6
- * per commit for a reader to walk history in search of. Work is branched off a
7
- * mainline and brought up to date with it before it merges, so the latest is
8
- * the right answer, and the distance from a checkout to it is the instruction
9
- * when it is not.
4
+ * A line answers *what is mainline now* by holding one version and replacing
5
+ * it. A checkout behind the mainline's tip — a release branch, a pull request
6
+ * cut last week — is measured against its merge base, not the tip, so a
7
+ * mainline's record is also kept at the commit it ran at (`at-commit.ts`), and
8
+ * the latest answers when nothing at or behind a reader's commit is kept.
10
9
  *
11
10
  * A line holds **entries**, each a versioned artifact derived at one commit: the
12
11
  * run report, one record per suite, the suite index. They are entries rather
@@ -24,6 +23,8 @@ export interface ShareLine {
24
23
  readonly kind: 'mainline' | 'branch';
25
24
  /** The branch name as git spells it, `release/2.0` included. */
26
25
  readonly name: string;
26
+ /** The commit whose record this line holds, kept beside the latest. Absent: the latest. */
27
+ readonly at?: string;
27
28
  }
28
29
  /** An artifact offered to a line. */
29
30
  export interface ShareEntry {
@@ -106,6 +107,21 @@ export interface LineCell {
106
107
  readonly blobs: ReadonlyMap<BlobPath, Uint8Array>;
107
108
  readonly expected?: string;
108
109
  }): Promise<'written' | 'conflict' | ShareMiss>;
110
+ /**
111
+ * The commits a mainline's record is kept at, and when each was written.
112
+ * A store without it keeps the latest only, and nothing is kept by commit.
113
+ */
114
+ commits?(line: ShareLine): Promise<readonly KeptAt[] | ShareMiss>;
115
+ /**
116
+ * Remove lines. A miss can follow some removed, and listing again names the
117
+ * rest. Present wherever `commits` is.
118
+ */
119
+ drop?(lines: readonly ShareLine[]): Promise<'dropped' | ShareMiss>;
120
+ }
121
+ /** One commit a mainline's record is kept at, and when it was written, in ms since the epoch. */
122
+ export interface KeptAt {
123
+ readonly commit: string;
124
+ readonly written: number;
109
125
  }
110
126
  /** An entry name, read. */
111
127
  export interface EntryName {
@@ -118,7 +134,9 @@ export declare function parseEntryName(name: string): EntryName | undefined;
118
134
  /** Two entries are the same slot when they differ at most in version. */
119
135
  export declare function sameSlot(a: EntryName, b: EntryName): boolean;
120
136
  /**
121
- * The path of a line inside a store, `mainline/release/2.0`.
137
+ * The path of a line inside a store, `mainline/release/2.0`, and
138
+ * `at/<commit>/mainline/release/2.0` for its record kept at one commit: the
139
+ * commit comes first, because git cannot hold a ref inside another ref's name.
122
140
  *
123
141
  * Each segment of the branch name is folded to key characters, a
124
142
  * deliberately many-to-one mapping. Two branches whose names differ only in
@@ -126,6 +144,10 @@ export declare function sameSlot(a: EntryName, b: EntryName): boolean;
126
144
  * `HEAD` contains the commit the line names.
127
145
  */
128
146
  export declare function linePath(line: ShareLine): string;
147
+ /** The pattern of every path {@link linePath} gives `line` kept at a commit, the commit a `*`. */
148
+ export declare function keptAtPattern(line: ShareLine): string;
149
+ /** The commit `path` keeps `line`'s record at, when it is one {@link linePath} gives. */
150
+ export declare function keptAtOf(line: ShareLine, path: string): string | undefined;
129
151
  /** The digest an entry's bytes are held under. */
130
152
  export declare function entryDigest(bytes: Uint8Array): string;
131
153
  /**
@@ -1,12 +1,11 @@
1
1
  /**
2
2
  * A line: the latest of what CI derived on one mainline or one branch.
3
3
  *
4
- * A line answers the question a checkout actually has — *what is mainline now*
5
- * — by holding one version and replacing it, rather than keeping one artifact
6
- * per commit for a reader to walk history in search of. Work is branched off a
7
- * mainline and brought up to date with it before it merges, so the latest is
8
- * the right answer, and the distance from a checkout to it is the instruction
9
- * when it is not.
4
+ * A line answers *what is mainline now* by holding one version and replacing
5
+ * it. A checkout behind the mainline's tip — a release branch, a pull request
6
+ * cut last week — is measured against its merge base, not the tip, so a
7
+ * mainline's record is also kept at the commit it ran at (`at-commit.ts`), and
8
+ * the latest answers when nothing at or behind a reader's commit is kept.
10
9
  *
11
10
  * A line holds **entries**, each a versioned artifact derived at one commit: the
12
11
  * run report, one record per suite, the suite index. They are entries rather
@@ -40,7 +39,9 @@ export function sameSlot(a, b) {
40
39
  return a.family === b.family && a.qualifier === b.qualifier;
41
40
  }
42
41
  /**
43
- * The path of a line inside a store, `mainline/release/2.0`.
42
+ * The path of a line inside a store, `mainline/release/2.0`, and
43
+ * `at/<commit>/mainline/release/2.0` for its record kept at one commit: the
44
+ * commit comes first, because git cannot hold a ref inside another ref's name.
44
45
  *
45
46
  * Each segment of the branch name is folded to key characters, a
46
47
  * deliberately many-to-one mapping. Two branches whose names differ only in
@@ -52,7 +53,24 @@ export function linePath(line) {
52
53
  .split('/')
53
54
  .map((part) => part.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^[-.]+|[-.]+$/g, ''))
54
55
  .filter((part) => part !== '');
55
- return `${line.kind}/${segments.length === 0 ? 'unnamed' : segments.join('/')}`;
56
+ const path = `${line.kind}/${segments.length === 0 ? 'unnamed' : segments.join('/')}`;
57
+ if (line.at === undefined)
58
+ return path;
59
+ if (!COMMIT.test(line.at))
60
+ throw new Error(`"${line.at}" is not a commit, so no line is kept at it`);
61
+ return `at/${line.at}/${path}`;
62
+ }
63
+ /** The pattern of every path {@link linePath} gives `line` kept at a commit, the commit a `*`. */
64
+ export function keptAtPattern(line) {
65
+ return `at/*/${linePath({ kind: line.kind, name: line.name })}`;
66
+ }
67
+ /** The commit `path` keeps `line`'s record at, when it is one {@link linePath} gives. */
68
+ export function keptAtOf(line, path) {
69
+ const [head, tail] = keptAtPattern(line).split('*');
70
+ if (!path.startsWith(head) || !path.endsWith(tail))
71
+ return undefined;
72
+ const commit = path.slice(head.length, path.length - tail.length);
73
+ return COMMIT.test(commit) ? commit : undefined;
56
74
  }
57
75
  /** The digest an entry's bytes are held under. */
58
76
  export function entryDigest(bytes) {
@@ -1 +1 @@
1
- {"version":3,"file":"line.js","sourceRoot":"","sources":["../../src/share/line.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A line: the latest of what CI derived on one mainline or one branch.\n *\n * A line answers the question a checkout actually has — *what is mainline now*\n * — by holding one version and replacing it, rather than keeping one artifact\n * per commit for a reader to walk history in search of. Work is branched off a\n * mainline and brought up to date with it before it merges, so the latest is\n * the right answer, and the distance from a checkout to it is the instruction\n * when it is not.\n *\n * A line holds **entries**, each a versioned artifact derived at one commit: the\n * run report, one record per suite, the suite index. They are entries rather\n * than one bundle because they arrive from different jobs, and a publish replaces\n * only what it carries. It also holds the **images** its entries name, by digest,\n * because an image does not compress and costs nothing to keep once.\n *\n * Nothing here collapses a failure into a miss. A reader has to be told whether\n * nothing was published, it lacks the credentials, the store could not be\n * reached, or a newer writer left a format it cannot read, because each of those\n * asks for a different action.\n */\n\nimport { sha256HexBytes } from '../format/sha256.js';\n\n/** One line a share holds the latest of. */\nexport interface ShareLine {\n readonly kind: 'mainline' | 'branch';\n /** The branch name as git spells it, `release/2.0` included. */\n readonly name: string;\n}\n\n/** An artifact offered to a line. */\nexport interface ShareEntry {\n /** `<family>-v<version>`, optionally `/<qualifier>`: `report-v1`, `suite-v1/web`. */\n readonly name: string;\n /** The commit it was derived at. For a pull request, the merge CI ran on. */\n readonly commit: string;\n /** The commit a pull request pointed at, when `commit` is the merge made for it. */\n readonly head?: string;\n /** Digests of the images this entry names, which the line keeps while it keeps the entry. */\n readonly images?: readonly string[];\n readonly bytes: Uint8Array;\n}\n\n/** An entry as the line holds it: what it is, where its bytes are, and nothing else. */\nexport interface HeldEntry {\n readonly name: string;\n readonly commit: string;\n readonly head?: string;\n readonly images?: readonly string[];\n readonly digest: string;\n readonly size: number;\n}\n\n/** What a line holds now. */\nexport interface LineManifest {\n readonly format: 1;\n /** Sorted by name, in code-unit order. */\n readonly entries: readonly HeldEntry[];\n}\n\n/**\n * Why a line could not answer.\n *\n * - `absent`: nothing was published under that name.\n * - `newer`: something was, in a format this reader does not know.\n * - `refused`: the store answered, and said no.\n * - `unreachable`: the store did not answer, or not in time.\n * - `unreadable`: the store answered with bytes that are not what they claim.\n */\nexport type ShareMiss =\n | { readonly kind: 'absent' }\n | { readonly kind: 'newer'; readonly names: readonly string[] }\n | { readonly kind: 'refused'; readonly detail: string }\n | { readonly kind: 'unreachable'; readonly detail: string }\n | { readonly kind: 'unreadable'; readonly detail: string };\n\n/** A path inside a line: an entry's bytes or an image, both by digest. */\nexport type BlobPath = `entries/${string}` | `images/${string}`;\n\n/**\n * The one thing a backend has to provide: a manifest that is replaced only if\n * nobody replaced it first, and the blobs it names.\n *\n * Deliberately below the rules. What may replace what, which images to send and\n * how a reader tells a newer format from nothing are decided once, in\n * {@link publishLine} and {@link readLine}, so a directory, a bucket and a git\n * ref cannot disagree about them.\n */\nexport interface LineCell {\n /** The manifest and an opaque version to write against, or why not. */\n load(line: ShareLine): Promise<{ readonly manifest: Uint8Array; readonly version: string } | ShareMiss>;\n blob(line: ShareLine, path: BlobPath): Promise<Uint8Array | ShareMiss>;\n /**\n * Replace the manifest, if the held version is still `expected`.\n *\n * `expected` absent means the line must not exist yet. `blobs` holds only\n * what the held line lacks; every other path the manifest names is already\n * there, and a backend that builds on the held version relies on that.\n */\n store(\n line: ShareLine,\n write: {\n readonly manifest: Uint8Array;\n readonly blobs: ReadonlyMap<BlobPath, Uint8Array>;\n readonly expected?: string;\n },\n ): Promise<'written' | 'conflict' | ShareMiss>;\n}\n\n/** An entry name, read. */\nexport interface EntryName {\n readonly family: string;\n readonly version: number;\n readonly qualifier?: string;\n}\n\nconst ENTRY = /^([a-z][a-z0-9-]*)-v([1-9][0-9]*)(?:\\/([A-Za-z0-9._-]+))?$/;\nconst COMMIT = /^[0-9a-f]{4,64}$/;\nconst DIGEST = /^[0-9a-f]{64}$/;\n\n/** Read an entry name, or refuse it. */\nexport function parseEntryName(name: string): EntryName | undefined {\n const match = ENTRY.exec(name);\n if (match === null) return undefined;\n const qualifier = match[3];\n return {\n family: match[1]!,\n version: Number(match[2]),\n ...(qualifier !== undefined && qualifier !== '.' && qualifier !== '..' ? { qualifier } : {}),\n };\n}\n\n/** Two entries are the same slot when they differ at most in version. */\nexport function sameSlot(a: EntryName, b: EntryName): boolean {\n return a.family === b.family && a.qualifier === b.qualifier;\n}\n\n/**\n * The path of a line inside a store, `mainline/release/2.0`.\n *\n * Each segment of the branch name is folded to key characters, a\n * deliberately many-to-one mapping. Two branches whose names differ only in\n * folded characters share a line, and a reader tells them apart by whether its\n * `HEAD` contains the commit the line names.\n */\nexport function linePath(line: ShareLine): string {\n const segments = line.name\n .split('/')\n .map((part) => part.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^[-.]+|[-.]+$/g, ''))\n .filter((part) => part !== '');\n return `${line.kind}/${segments.length === 0 ? 'unnamed' : segments.join('/')}`;\n}\n\n/** The digest an entry's bytes are held under. */\nexport function entryDigest(bytes: Uint8Array): string {\n return sha256HexBytes(bytes);\n}\n\n/**\n * The manifest as bytes: canonical, ASCII, byte-stable.\n *\n * Every field is checked to be ASCII before it is written — names, commits and\n * digests already are — so no text encoder is needed and two runtimes cannot\n * write two spellings of one manifest.\n */\nexport function encodeManifest(manifest: LineManifest): Uint8Array {\n const entries = [...manifest.entries].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));\n const text = JSON.stringify({\n format: manifest.format,\n entries: entries.map((entry) => ({\n name: entry.name,\n commit: entry.commit,\n ...(entry.head !== undefined ? { head: entry.head } : {}),\n ...(entry.images !== undefined ? { images: [...entry.images].sort() } : {}),\n digest: entry.digest,\n size: entry.size,\n })),\n });\n const bytes = new Uint8Array(text.length);\n for (let i = 0; i < text.length; i += 1) {\n const code = text.charCodeAt(i);\n if (code > 0x7e) throw new Error(`a manifest holds ASCII only, and this one does not at ${String(i)}`);\n bytes[i] = code;\n }\n return bytes;\n}\n\n/**\n * Read a manifest, or say why it is not one.\n *\n * A `format` above 1 is a newer writer, not a broken one, and is answered as\n * such: the reader's action is to upgrade, not to republish.\n */\nexport function decodeManifest(bytes: Uint8Array): LineManifest | ShareMiss {\n let text = '';\n for (const byte of bytes) {\n if (byte > 0x7e) return { kind: 'unreadable', detail: 'the manifest is not ASCII' };\n text += String.fromCharCode(byte);\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n return { kind: 'unreadable', detail: 'the manifest is not JSON' };\n }\n if (typeof parsed !== 'object' || parsed === null) {\n return { kind: 'unreadable', detail: 'the manifest is not an object' };\n }\n const { format, entries } = parsed as { format?: unknown; entries?: unknown };\n if (typeof format === 'number' && format > 1) return { kind: 'newer', names: [`manifest format ${String(format)}`] };\n if (format !== 1 || !Array.isArray(entries)) {\n return { kind: 'unreadable', detail: 'the manifest has no format 1 entry list' };\n }\n const held: HeldEntry[] = [];\n for (const entry of entries as unknown[]) {\n const checked = heldEntry(entry);\n if (checked === undefined) return { kind: 'unreadable', detail: 'the manifest holds a malformed entry' };\n held.push(checked);\n }\n return { format: 1, entries: held };\n}\n\nfunction heldEntry(value: unknown): HeldEntry | undefined {\n if (typeof value !== 'object' || value === null) return undefined;\n const { name, commit, head, images, digest, size } = value as Record<string, unknown>;\n if (typeof name !== 'string' || parseEntryName(name) === undefined) return undefined;\n if (typeof commit !== 'string' || !COMMIT.test(commit)) return undefined;\n if (head !== undefined && (typeof head !== 'string' || !COMMIT.test(head))) return undefined;\n if (typeof digest !== 'string' || !DIGEST.test(digest)) return undefined;\n if (typeof size !== 'number' || !Number.isInteger(size) || size < 0) return undefined;\n if (images !== undefined) {\n if (!Array.isArray(images) || !images.every((image) => typeof image === 'string' && DIGEST.test(image))) {\n return undefined;\n }\n }\n return {\n name,\n commit,\n ...(head !== undefined ? { head: head as string } : {}),\n ...(images !== undefined ? { images: images as string[] } : {}),\n digest,\n size,\n };\n}\n\n/** Whether an entry offered for publishing is well formed, and why not. */\nexport function entryProblem(entry: ShareEntry): string | undefined {\n if (parseEntryName(entry.name) === undefined) {\n return `\"${entry.name}\" is not <family>-v<version>[/<qualifier>]`;\n }\n if (!COMMIT.test(entry.commit)) return `${entry.name}: \"${entry.commit}\" is not a commit`;\n if (entry.head !== undefined && !COMMIT.test(entry.head)) return `${entry.name}: \"${entry.head}\" is not a commit`;\n for (const image of entry.images ?? []) {\n if (!DIGEST.test(image)) return `${entry.name}: \"${image}\" is not a sha-256 digest`;\n }\n return undefined;\n}\n\n/**\n * Every blob path a manifest names. A backend that holds one line as one tree,\n * a git ref, keeps exactly these and drops the rest when it writes.\n */\nexport function manifestPaths(manifest: LineManifest): BlobPath[] {\n const paths = new Set<BlobPath>();\n for (const entry of manifest.entries) {\n paths.add(`entries/${entry.digest}`);\n for (const image of entry.images ?? []) paths.add(`images/${image}`);\n }\n return [...paths].sort();\n}\n"]}
1
+ {"version":3,"file":"line.js","sourceRoot":"","sources":["../../src/share/line.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A line: the latest of what CI derived on one mainline or one branch.\n *\n * A line answers *what is mainline now* by holding one version and replacing\n * it. A checkout behind the mainline's tip — a release branch, a pull request\n * cut last week — is measured against its merge base, not the tip, so a\n * mainline's record is also kept at the commit it ran at (`at-commit.ts`), and\n * the latest answers when nothing at or behind a reader's commit is kept.\n *\n * A line holds **entries**, each a versioned artifact derived at one commit: the\n * run report, one record per suite, the suite index. They are entries rather\n * than one bundle because they arrive from different jobs, and a publish replaces\n * only what it carries. It also holds the **images** its entries name, by digest,\n * because an image does not compress and costs nothing to keep once.\n *\n * Nothing here collapses a failure into a miss. A reader has to be told whether\n * nothing was published, it lacks the credentials, the store could not be\n * reached, or a newer writer left a format it cannot read, because each of those\n * asks for a different action.\n */\n\nimport { sha256HexBytes } from '../format/sha256.js';\n\n/** One line a share holds the latest of. */\nexport interface ShareLine {\n readonly kind: 'mainline' | 'branch';\n /** The branch name as git spells it, `release/2.0` included. */\n readonly name: string;\n /** The commit whose record this line holds, kept beside the latest. Absent: the latest. */\n readonly at?: string;\n}\n\n/** An artifact offered to a line. */\nexport interface ShareEntry {\n /** `<family>-v<version>`, optionally `/<qualifier>`: `report-v1`, `suite-v1/web`. */\n readonly name: string;\n /** The commit it was derived at. For a pull request, the merge CI ran on. */\n readonly commit: string;\n /** The commit a pull request pointed at, when `commit` is the merge made for it. */\n readonly head?: string;\n /** Digests of the images this entry names, which the line keeps while it keeps the entry. */\n readonly images?: readonly string[];\n readonly bytes: Uint8Array;\n}\n\n/** An entry as the line holds it: what it is, where its bytes are, and nothing else. */\nexport interface HeldEntry {\n readonly name: string;\n readonly commit: string;\n readonly head?: string;\n readonly images?: readonly string[];\n readonly digest: string;\n readonly size: number;\n}\n\n/** What a line holds now. */\nexport interface LineManifest {\n readonly format: 1;\n /** Sorted by name, in code-unit order. */\n readonly entries: readonly HeldEntry[];\n}\n\n/**\n * Why a line could not answer.\n *\n * - `absent`: nothing was published under that name.\n * - `newer`: something was, in a format this reader does not know.\n * - `refused`: the store answered, and said no.\n * - `unreachable`: the store did not answer, or not in time.\n * - `unreadable`: the store answered with bytes that are not what they claim.\n */\nexport type ShareMiss =\n | { readonly kind: 'absent' }\n | { readonly kind: 'newer'; readonly names: readonly string[] }\n | { readonly kind: 'refused'; readonly detail: string }\n | { readonly kind: 'unreachable'; readonly detail: string }\n | { readonly kind: 'unreadable'; readonly detail: string };\n\n/** A path inside a line: an entry's bytes or an image, both by digest. */\nexport type BlobPath = `entries/${string}` | `images/${string}`;\n\n/**\n * The one thing a backend has to provide: a manifest that is replaced only if\n * nobody replaced it first, and the blobs it names.\n *\n * Deliberately below the rules. What may replace what, which images to send and\n * how a reader tells a newer format from nothing are decided once, in\n * {@link publishLine} and {@link readLine}, so a directory, a bucket and a git\n * ref cannot disagree about them.\n */\nexport interface LineCell {\n /** The manifest and an opaque version to write against, or why not. */\n load(line: ShareLine): Promise<{ readonly manifest: Uint8Array; readonly version: string } | ShareMiss>;\n blob(line: ShareLine, path: BlobPath): Promise<Uint8Array | ShareMiss>;\n /**\n * Replace the manifest, if the held version is still `expected`.\n *\n * `expected` absent means the line must not exist yet. `blobs` holds only\n * what the held line lacks; every other path the manifest names is already\n * there, and a backend that builds on the held version relies on that.\n */\n store(\n line: ShareLine,\n write: {\n readonly manifest: Uint8Array;\n readonly blobs: ReadonlyMap<BlobPath, Uint8Array>;\n readonly expected?: string;\n },\n ): Promise<'written' | 'conflict' | ShareMiss>;\n /**\n * The commits a mainline's record is kept at, and when each was written.\n * A store without it keeps the latest only, and nothing is kept by commit.\n */\n commits?(line: ShareLine): Promise<readonly KeptAt[] | ShareMiss>;\n /**\n * Remove lines. A miss can follow some removed, and listing again names the\n * rest. Present wherever `commits` is.\n */\n drop?(lines: readonly ShareLine[]): Promise<'dropped' | ShareMiss>;\n}\n\n/** One commit a mainline's record is kept at, and when it was written, in ms since the epoch. */\nexport interface KeptAt {\n readonly commit: string;\n readonly written: number;\n}\n\n/** An entry name, read. */\nexport interface EntryName {\n readonly family: string;\n readonly version: number;\n readonly qualifier?: string;\n}\n\nconst ENTRY = /^([a-z][a-z0-9-]*)-v([1-9][0-9]*)(?:\\/([A-Za-z0-9._-]+))?$/;\nconst COMMIT = /^[0-9a-f]{4,64}$/;\nconst DIGEST = /^[0-9a-f]{64}$/;\n\n/** Read an entry name, or refuse it. */\nexport function parseEntryName(name: string): EntryName | undefined {\n const match = ENTRY.exec(name);\n if (match === null) return undefined;\n const qualifier = match[3];\n return {\n family: match[1]!,\n version: Number(match[2]),\n ...(qualifier !== undefined && qualifier !== '.' && qualifier !== '..' ? { qualifier } : {}),\n };\n}\n\n/** Two entries are the same slot when they differ at most in version. */\nexport function sameSlot(a: EntryName, b: EntryName): boolean {\n return a.family === b.family && a.qualifier === b.qualifier;\n}\n\n/**\n * The path of a line inside a store, `mainline/release/2.0`, and\n * `at/<commit>/mainline/release/2.0` for its record kept at one commit: the\n * commit comes first, because git cannot hold a ref inside another ref's name.\n *\n * Each segment of the branch name is folded to key characters, a\n * deliberately many-to-one mapping. Two branches whose names differ only in\n * folded characters share a line, and a reader tells them apart by whether its\n * `HEAD` contains the commit the line names.\n */\nexport function linePath(line: ShareLine): string {\n const segments = line.name\n .split('/')\n .map((part) => part.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^[-.]+|[-.]+$/g, ''))\n .filter((part) => part !== '');\n const path = `${line.kind}/${segments.length === 0 ? 'unnamed' : segments.join('/')}`;\n if (line.at === undefined) return path;\n if (!COMMIT.test(line.at)) throw new Error(`\"${line.at}\" is not a commit, so no line is kept at it`);\n return `at/${line.at}/${path}`;\n}\n\n/** The pattern of every path {@link linePath} gives `line` kept at a commit, the commit a `*`. */\nexport function keptAtPattern(line: ShareLine): string {\n return `at/*/${linePath({ kind: line.kind, name: line.name })}`;\n}\n\n/** The commit `path` keeps `line`'s record at, when it is one {@link linePath} gives. */\nexport function keptAtOf(line: ShareLine, path: string): string | undefined {\n const [head, tail] = keptAtPattern(line).split('*') as [string, string];\n if (!path.startsWith(head) || !path.endsWith(tail)) return undefined;\n const commit = path.slice(head.length, path.length - tail.length);\n return COMMIT.test(commit) ? commit : undefined;\n}\n\n/** The digest an entry's bytes are held under. */\nexport function entryDigest(bytes: Uint8Array): string {\n return sha256HexBytes(bytes);\n}\n\n/**\n * The manifest as bytes: canonical, ASCII, byte-stable.\n *\n * Every field is checked to be ASCII before it is written — names, commits and\n * digests already are — so no text encoder is needed and two runtimes cannot\n * write two spellings of one manifest.\n */\nexport function encodeManifest(manifest: LineManifest): Uint8Array {\n const entries = [...manifest.entries].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));\n const text = JSON.stringify({\n format: manifest.format,\n entries: entries.map((entry) => ({\n name: entry.name,\n commit: entry.commit,\n ...(entry.head !== undefined ? { head: entry.head } : {}),\n ...(entry.images !== undefined ? { images: [...entry.images].sort() } : {}),\n digest: entry.digest,\n size: entry.size,\n })),\n });\n const bytes = new Uint8Array(text.length);\n for (let i = 0; i < text.length; i += 1) {\n const code = text.charCodeAt(i);\n if (code > 0x7e) throw new Error(`a manifest holds ASCII only, and this one does not at ${String(i)}`);\n bytes[i] = code;\n }\n return bytes;\n}\n\n/**\n * Read a manifest, or say why it is not one.\n *\n * A `format` above 1 is a newer writer, not a broken one, and is answered as\n * such: the reader's action is to upgrade, not to republish.\n */\nexport function decodeManifest(bytes: Uint8Array): LineManifest | ShareMiss {\n let text = '';\n for (const byte of bytes) {\n if (byte > 0x7e) return { kind: 'unreadable', detail: 'the manifest is not ASCII' };\n text += String.fromCharCode(byte);\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n return { kind: 'unreadable', detail: 'the manifest is not JSON' };\n }\n if (typeof parsed !== 'object' || parsed === null) {\n return { kind: 'unreadable', detail: 'the manifest is not an object' };\n }\n const { format, entries } = parsed as { format?: unknown; entries?: unknown };\n if (typeof format === 'number' && format > 1) return { kind: 'newer', names: [`manifest format ${String(format)}`] };\n if (format !== 1 || !Array.isArray(entries)) {\n return { kind: 'unreadable', detail: 'the manifest has no format 1 entry list' };\n }\n const held: HeldEntry[] = [];\n for (const entry of entries as unknown[]) {\n const checked = heldEntry(entry);\n if (checked === undefined) return { kind: 'unreadable', detail: 'the manifest holds a malformed entry' };\n held.push(checked);\n }\n return { format: 1, entries: held };\n}\n\nfunction heldEntry(value: unknown): HeldEntry | undefined {\n if (typeof value !== 'object' || value === null) return undefined;\n const { name, commit, head, images, digest, size } = value as Record<string, unknown>;\n if (typeof name !== 'string' || parseEntryName(name) === undefined) return undefined;\n if (typeof commit !== 'string' || !COMMIT.test(commit)) return undefined;\n if (head !== undefined && (typeof head !== 'string' || !COMMIT.test(head))) return undefined;\n if (typeof digest !== 'string' || !DIGEST.test(digest)) return undefined;\n if (typeof size !== 'number' || !Number.isInteger(size) || size < 0) return undefined;\n if (images !== undefined) {\n if (!Array.isArray(images) || !images.every((image) => typeof image === 'string' && DIGEST.test(image))) {\n return undefined;\n }\n }\n return {\n name,\n commit,\n ...(head !== undefined ? { head: head as string } : {}),\n ...(images !== undefined ? { images: images as string[] } : {}),\n digest,\n size,\n };\n}\n\n/** Whether an entry offered for publishing is well formed, and why not. */\nexport function entryProblem(entry: ShareEntry): string | undefined {\n if (parseEntryName(entry.name) === undefined) {\n return `\"${entry.name}\" is not <family>-v<version>[/<qualifier>]`;\n }\n if (!COMMIT.test(entry.commit)) return `${entry.name}: \"${entry.commit}\" is not a commit`;\n if (entry.head !== undefined && !COMMIT.test(entry.head)) return `${entry.name}: \"${entry.head}\" is not a commit`;\n for (const image of entry.images ?? []) {\n if (!DIGEST.test(image)) return `${entry.name}: \"${image}\" is not a sha-256 digest`;\n }\n return undefined;\n}\n\n/**\n * Every blob path a manifest names. A backend that holds one line as one tree,\n * a git ref, keeps exactly these and drops the rest when it writes.\n */\nexport function manifestPaths(manifest: LineManifest): BlobPath[] {\n const paths = new Set<BlobPath>();\n for (const entry of manifest.entries) {\n paths.add(`entries/${entry.digest}`);\n for (const image of entry.images ?? []) paths.add(`images/${image}`);\n }\n return [...paths].sort();\n}\n"]}
@@ -5,6 +5,7 @@
5
5
  * directory, a bucket and a git ref hold a line by the same rules.
6
6
  */
7
7
  import { type HeldEntry, type LineCell, type LineManifest, type ShareEntry, type ShareLine, type ShareMiss } from './line.js';
8
+ import { type KeptCommits } from './at-commit.js';
8
9
  /**
9
10
  * Whether `descendant` strictly descends from `ancestor`, or `undefined` when
10
11
  * the history needed to say is not at hand. Git owns this answer; the caller
@@ -29,6 +30,11 @@ export interface Published {
29
30
  readonly unanswered: readonly string[];
30
31
  /** How many times the manifest was read before a write held. */
31
32
  readonly attempts: number;
33
+ /**
34
+ * On a mainline whose store lists lines, the commits its record was kept at.
35
+ * Absent anywhere else.
36
+ */
37
+ readonly atCommit?: KeptCommits;
32
38
  }
33
39
  export interface PublishOptions {
34
40
  readonly descends: Descends;
@@ -44,10 +50,9 @@ export interface PublishOptions {
44
50
  * any version — except on a mainline, where the held one is kept when its
45
51
  * commit strictly descends from the offered one: a slow run of an older commit
46
52
  * finishing last. A held entry in a newer format is kept on any line, so an old
47
- * CLI cannot take a line back a version. Every other held entry is left as it
48
- * is, which is how suites published by different jobs land on one line.
49
- *
50
- * A write that loses to another writer reads the line again and decides again.
53
+ * CLI cannot take a line back a version. Other held entries stay, which is how
54
+ * suites published by different jobs land on one line. A losing write reads
55
+ * the line again and decides again.
51
56
  */
52
57
  export declare function publishLine(cell: LineCell, line: ShareLine, entries: readonly ShareEntry[], options: PublishOptions): Promise<Published | ShareMiss>;
53
58
  /** A line as a reader holds it: the manifest, and its blobs on demand. */
@@ -71,9 +76,10 @@ export declare function findEntry(manifest: LineManifest, name: string): HeldEnt
71
76
  * A line cell in this process, for tests and for a run that shares with itself.
72
77
  *
73
78
  * Holds real versions, so a test can stage a writer that moves the line between
74
- * another writer's read and its write.
79
+ * another writer's read and its write, and reads `clock` for when a line was
80
+ * written, so a test can age what it keeps.
75
81
  */
76
- export declare function memoryLineCell(): LineCell & {
82
+ export declare function memoryLineCell(clock?: () => number): LineCell & {
77
83
  readonly held: Map<string, Uint8Array>;
78
84
  };
79
85
  //# sourceMappingURL=publish.d.ts.map
@@ -5,6 +5,7 @@
5
5
  * directory, a bucket and a git ref hold a line by the same rules.
6
6
  */
7
7
  import { decodeManifest, encodeManifest, entryDigest, entryProblem, parseEntryName, sameSlot, } from './line.js';
8
+ import { atCommit } from './at-commit.js';
8
9
  /**
9
10
  * Offer entries to a line.
10
11
  *
@@ -12,12 +13,38 @@ import { decodeManifest, encodeManifest, entryDigest, entryProblem, parseEntryNa
12
13
  * any version — except on a mainline, where the held one is kept when its
13
14
  * commit strictly descends from the offered one: a slow run of an older commit
14
15
  * finishing last. A held entry in a newer format is kept on any line, so an old
15
- * CLI cannot take a line back a version. Every other held entry is left as it
16
- * is, which is how suites published by different jobs land on one line.
17
- *
18
- * A write that loses to another writer reads the line again and decides again.
16
+ * CLI cannot take a line back a version. Other held entries stay, which is how
17
+ * suites published by different jobs land on one line. A losing write reads
18
+ * the line again and decides again.
19
19
  */
20
20
  export async function publishLine(cell, line, entries, options) {
21
+ const latest = await publishOne(cell, line, entries, options);
22
+ if ('kind' in latest || line.kind !== 'mainline')
23
+ return latest;
24
+ const kept = await keepAtCommits(cell, line, entries, options);
25
+ return kept === undefined ? latest : { ...latest, atCommit: kept };
26
+ }
27
+ /**
28
+ * A mainline's entries kept at the commits they were derived at, the same rules
29
+ * deciding on each, when the store can list lines (`at-commit.ts`). The latest
30
+ * is written first and stands whatever happens to those.
31
+ */
32
+ async function keepAtCommits(cell, line, entries, options) {
33
+ if (cell.commits === undefined)
34
+ return undefined;
35
+ const byCommit = new Map();
36
+ for (const entry of entries)
37
+ byCommit.set(entry.commit, [...(byCommit.get(entry.commit) ?? []), entry]);
38
+ const kept = [];
39
+ for (const [commit, group] of byCommit) {
40
+ const written = await publishOne(cell, atCommit(line, commit), group, options);
41
+ if ('kind' in written)
42
+ return { kept, miss: written };
43
+ kept.push(commit);
44
+ }
45
+ return { kept };
46
+ }
47
+ async function publishOne(cell, line, entries, options) {
21
48
  for (const entry of entries) {
22
49
  const problem = entryProblem(entry);
23
50
  if (problem !== undefined)
@@ -196,12 +223,14 @@ export function findEntry(manifest, name) {
196
223
  * A line cell in this process, for tests and for a run that shares with itself.
197
224
  *
198
225
  * Holds real versions, so a test can stage a writer that moves the line between
199
- * another writer's read and its write.
226
+ * another writer's read and its write, and reads `clock` for when a line was
227
+ * written, so a test can age what it keeps.
200
228
  */
201
- export function memoryLineCell() {
229
+ export function memoryLineCell(clock = Date.now) {
202
230
  const held = new Map();
203
231
  const versions = new Map();
204
- const key = (line) => `${line.kind}\u0000${line.name}`;
232
+ const written = new Map();
233
+ const key = (line) => `${line.kind}\u0000${line.name}\u0000${line.at ?? ''}`;
205
234
  return {
206
235
  held,
207
236
  async load(line) {
@@ -224,8 +253,25 @@ export function memoryLineCell() {
224
253
  }
225
254
  held.set(`${key(line)}\u0000manifest`, write.manifest.slice());
226
255
  versions.set(key(line), (current ?? 0) + 1);
256
+ written.set(key(line), { line, at: clock() });
227
257
  return 'written';
228
258
  },
259
+ async commits(line) {
260
+ return [...written.values()]
261
+ .filter((one) => one.line.at !== undefined && one.line.kind === line.kind && one.line.name === line.name)
262
+ .map((one) => ({ commit: one.line.at, written: one.at }));
263
+ },
264
+ async drop(lines) {
265
+ for (const line of lines) {
266
+ const prefix = `${key(line)}\u0000`;
267
+ for (const path of held.keys())
268
+ if (path.startsWith(prefix))
269
+ held.delete(path);
270
+ versions.delete(key(line));
271
+ written.delete(key(line));
272
+ }
273
+ return 'dropped';
274
+ },
229
275
  };
230
276
  }
231
277
  //# sourceMappingURL=publish.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"publish.js","sourceRoot":"","sources":["../../src/share/publish.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What may replace what on a line, and what a reader is told.\n *\n * Decided here, once, over the {@link LineCell} a backend provides, so a\n * directory, a bucket and a git ref hold a line by the same rules.\n */\n\nimport {\n decodeManifest,\n encodeManifest,\n entryDigest,\n entryProblem,\n parseEntryName,\n sameSlot,\n type BlobPath,\n type HeldEntry,\n type LineCell,\n type LineManifest,\n type ShareEntry,\n type ShareLine,\n type ShareMiss,\n} from './line.js';\n\n/**\n * Whether `descendant` strictly descends from `ancestor`, or `undefined` when\n * the history needed to say is not at hand. Git owns this answer; the caller\n * asks it.\n */\nexport type Descends = (descendant: string, ancestor: string) => Promise<boolean | undefined>;\n\n/** What a publish did, entry by entry. */\nexport interface Published {\n /** Entries this publish wrote. */\n readonly written: readonly string[];\n /** Entries the line kept instead, and why. */\n readonly kept: readonly { readonly name: string; readonly commit: string; readonly because: 'newer-commit' | 'newer-format' }[];\n /**\n * Entries written although nobody could say whether the held one was newer.\n * Replacing is the answer then, because a line that refuses when it cannot\n * tell is a line one force-push jams for good.\n */\n readonly unanswered: readonly string[];\n /** How many times the manifest was read before a write held. */\n readonly attempts: number;\n}\n\nexport interface PublishOptions {\n readonly descends: Descends;\n /** The bytes of an image the line does not hold yet, by digest. */\n readonly image: (digest: string) => Promise<Uint8Array>;\n /** Reads of the manifest before giving up on a line other writers keep moving. Defaults to 8. */\n readonly attempts?: number;\n}\n\n/**\n * Offer entries to a line.\n *\n * Each entry replaces the held entry in its slot — same family, same qualifier,\n * any version — except on a mainline, where the held one is kept when its\n * commit strictly descends from the offered one: a slow run of an older commit\n * finishing last. A held entry in a newer format is kept on any line, so an old\n * CLI cannot take a line back a version. Every other held entry is left as it\n * is, which is how suites published by different jobs land on one line.\n *\n * A write that loses to another writer reads the line again and decides again.\n */\nexport async function publishLine(\n cell: LineCell,\n line: ShareLine,\n entries: readonly ShareEntry[],\n options: PublishOptions,\n): Promise<Published | ShareMiss> {\n for (const entry of entries) {\n const problem = entryProblem(entry);\n if (problem !== undefined) throw new Error(`cannot publish ${problem}`);\n }\n\n const limit = options.attempts ?? 8;\n for (let attempt = 1; attempt <= limit; attempt += 1) {\n const loaded = await cell.load(line);\n let held: LineManifest;\n let expected: string | undefined;\n if ('manifest' in loaded) {\n const decoded = decodeManifest(loaded.manifest);\n // A line a newer writer owns is not this writer's to replace; one that is\n // unreadable is, since nothing can read it anyway.\n if ('kind' in decoded) {\n if (decoded.kind === 'newer') return decoded;\n held = { format: 1, entries: [] };\n } else {\n held = decoded;\n }\n expected = loaded.version;\n } else if (loaded.kind === 'absent') {\n held = { format: 1, entries: [] };\n } else {\n return loaded;\n }\n\n const plan = await decide(line, held, entries, options.descends);\n if (plan.write.length === 0) {\n return { written: [], kept: plan.kept, unanswered: [], attempts: attempt };\n }\n\n const next = nextManifest(held, plan.write);\n const blobs = await blobsFor(held, next, plan.write, options.image);\n const outcome = await cell.store(line, {\n manifest: encodeManifest(next),\n blobs,\n ...(expected !== undefined ? { expected } : {}),\n });\n if (outcome === 'written') {\n return {\n written: plan.write.map((entry) => entry.name),\n kept: plan.kept,\n unanswered: plan.unanswered,\n attempts: attempt,\n };\n }\n if (outcome !== 'conflict') return outcome;\n }\n return {\n kind: 'unreachable',\n detail: `the line moved under ${String(limit)} writes in a row; nothing was published`,\n };\n}\n\nasync function decide(\n line: ShareLine,\n held: LineManifest,\n entries: readonly ShareEntry[],\n descends: Descends,\n): Promise<{ write: ShareEntry[]; kept: Published['kept'][number][]; unanswered: string[] }> {\n const write: ShareEntry[] = [];\n const kept: Published['kept'][number][] = [];\n const unanswered: string[] = [];\n for (const entry of entries) {\n const offered = parseEntryName(entry.name)!;\n let occupant: HeldEntry | undefined;\n let version = 0;\n for (const candidate of held.entries) {\n const slot = parseEntryName(candidate.name)!;\n if (sameSlot(slot, offered) && slot.version > version) {\n occupant = candidate;\n version = slot.version;\n }\n }\n if (occupant === undefined) {\n write.push(entry);\n continue;\n }\n if (version > offered.version) {\n kept.push({ name: occupant.name, commit: occupant.commit, because: 'newer-format' });\n continue;\n }\n // TODO: shards of one run publish at one commit, so each replaces the\n // last one's `suite-index-v1` and `report-v1`, and the line keeps the index\n // and report of whichever shard published last, covering its subjects only.\n // Nothing here merges two entries of one slot.\n if (line.kind === 'branch' || occupant.commit === entry.commit) {\n write.push(entry);\n continue;\n }\n const newer = await descends(occupant.commit, entry.commit);\n if (newer === true) {\n kept.push({ name: occupant.name, commit: occupant.commit, because: 'newer-commit' });\n continue;\n }\n if (newer === undefined) unanswered.push(entry.name);\n write.push(entry);\n }\n return { write, kept, unanswered };\n}\n\nfunction nextManifest(held: LineManifest, write: readonly ShareEntry[]): LineManifest {\n const replaced = write.map((entry) => parseEntryName(entry.name)!);\n const survivors = held.entries.filter(\n (entry) => !replaced.some((slot) => sameSlot(slot, parseEntryName(entry.name)!)),\n );\n const added: HeldEntry[] = write.map((entry) => ({\n name: entry.name,\n commit: entry.commit,\n ...(entry.head !== undefined ? { head: entry.head } : {}),\n ...(entry.images !== undefined ? { images: [...new Set(entry.images)] } : {}),\n digest: entryDigest(entry.bytes),\n size: entry.bytes.length,\n }));\n return { format: 1, entries: [...survivors, ...added] };\n}\n\n/** Only what the held line lacks: entry bytes are new by construction, images often are not. */\nasync function blobsFor(\n held: LineManifest,\n next: LineManifest,\n write: readonly ShareEntry[],\n image: (digest: string) => Promise<Uint8Array>,\n): Promise<Map<BlobPath, Uint8Array>> {\n const blobs = new Map<BlobPath, Uint8Array>();\n const heldDigests = new Set(held.entries.map((entry) => entry.digest));\n for (const entry of write) {\n const digest = entryDigest(entry.bytes);\n if (!heldDigests.has(digest)) blobs.set(`entries/${digest}`, entry.bytes);\n }\n const heldImages = new Set(held.entries.flatMap((entry) => entry.images ?? []));\n for (const digest of new Set(next.entries.flatMap((entry) => entry.images ?? []))) {\n if (!heldImages.has(digest)) blobs.set(`images/${digest}`, await image(digest));\n }\n return blobs;\n}\n\n/** A line as a reader holds it: the manifest, and its blobs on demand. */\nexport interface HeldLine {\n readonly line: ShareLine;\n readonly manifest: LineManifest;\n /** The bytes of one entry, checked against the digest the manifest names. */\n entry(entry: HeldEntry): Promise<Uint8Array | ShareMiss>;\n image(digest: string): Promise<Uint8Array | ShareMiss>;\n}\n\n/** Read what a line holds, or why it cannot be read. */\nexport async function readLine(cell: LineCell, line: ShareLine): Promise<HeldLine | ShareMiss> {\n const loaded = await cell.load(line);\n if (!('manifest' in loaded)) return loaded;\n const manifest = decodeManifest(loaded.manifest);\n if ('kind' in manifest) return manifest;\n return {\n line,\n manifest,\n async entry(entry) {\n const bytes = await cell.blob(line, `entries/${entry.digest}`);\n if (!(bytes instanceof Uint8Array)) return bytes;\n if (entryDigest(bytes) !== entry.digest) {\n return { kind: 'unreadable', detail: `${entry.name}: the bytes do not match the manifest's digest` };\n }\n return bytes;\n },\n image(digest) {\n return cell.blob(line, `images/${digest}`);\n },\n };\n}\n\n/**\n * The held entry a reader asked for by name, or why there is none.\n *\n * A reader of `suite-v1/web` that finds `suite-v2/web` is told `newer`, not\n * `absent`: the line has what it wants, in a format it has to upgrade to read.\n */\nexport function findEntry(manifest: LineManifest, name: string): HeldEntry | ShareMiss {\n const wanted = parseEntryName(name);\n if (wanted === undefined) throw new Error(`\"${name}\" is not <family>-v<version>[/<qualifier>]`);\n const exact = manifest.entries.find((entry) => entry.name === name);\n if (exact !== undefined) return exact;\n const newer = manifest.entries\n .filter((entry) => {\n const held = parseEntryName(entry.name)!;\n return sameSlot(held, wanted) && held.version > wanted.version;\n })\n .map((entry) => entry.name);\n return newer.length > 0 ? { kind: 'newer', names: newer } : { kind: 'absent' };\n}\n\n/**\n * A line cell in this process, for tests and for a run that shares with itself.\n *\n * Holds real versions, so a test can stage a writer that moves the line between\n * another writer's read and its write.\n */\nexport function memoryLineCell(): LineCell & { readonly held: Map<string, Uint8Array> } {\n const held = new Map<string, Uint8Array>();\n const versions = new Map<string, number>();\n const key = (line: ShareLine): string => `${line.kind}\\u0000${line.name}`;\n return {\n held,\n async load(line) {\n const manifest = held.get(`${key(line)}\\u0000manifest`);\n if (manifest === undefined) return { kind: 'absent' };\n return { manifest, version: String(versions.get(key(line)) ?? 0) };\n },\n async blob(line, path) {\n const bytes = held.get(path.startsWith('images/') ? path : `${key(line)}\\u0000${path}`);\n return bytes === undefined ? { kind: 'absent' } : bytes.slice();\n },\n async store(line, write) {\n const current = versions.get(key(line));\n const now = current === undefined ? undefined : String(current);\n if (now !== write.expected) return 'conflict';\n for (const [path, bytes] of write.blobs) {\n held.set(path.startsWith('images/') ? path : `${key(line)}\\u0000${path}`, bytes.slice());\n }\n held.set(`${key(line)}\\u0000manifest`, write.manifest.slice());\n versions.set(key(line), (current ?? 0) + 1);\n return 'written';\n },\n };\n}\n"]}
1
+ {"version":3,"file":"publish.js","sourceRoot":"","sources":["../../src/share/publish.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What may replace what on a line, and what a reader is told.\n *\n * Decided here, once, over the {@link LineCell} a backend provides, so a\n * directory, a bucket and a git ref hold a line by the same rules.\n */\n\nimport {\n decodeManifest,\n encodeManifest,\n entryDigest,\n entryProblem,\n parseEntryName,\n sameSlot,\n type BlobPath,\n type HeldEntry,\n type LineCell,\n type LineManifest,\n type ShareEntry,\n type ShareLine,\n type ShareMiss,\n} from './line.js';\nimport { atCommit, type KeptCommits } from './at-commit.js';\n\n/**\n * Whether `descendant` strictly descends from `ancestor`, or `undefined` when\n * the history needed to say is not at hand. Git owns this answer; the caller\n * asks it.\n */\nexport type Descends = (descendant: string, ancestor: string) => Promise<boolean | undefined>;\n\n/** What a publish did, entry by entry. */\nexport interface Published {\n /** Entries this publish wrote. */\n readonly written: readonly string[];\n /** Entries the line kept instead, and why. */\n readonly kept: readonly { readonly name: string; readonly commit: string; readonly because: 'newer-commit' | 'newer-format' }[];\n /**\n * Entries written although nobody could say whether the held one was newer.\n * Replacing is the answer then, because a line that refuses when it cannot\n * tell is a line one force-push jams for good.\n */\n readonly unanswered: readonly string[];\n /** How many times the manifest was read before a write held. */\n readonly attempts: number;\n /**\n * On a mainline whose store lists lines, the commits its record was kept at.\n * Absent anywhere else.\n */\n readonly atCommit?: KeptCommits;\n}\n\nexport interface PublishOptions {\n readonly descends: Descends;\n /** The bytes of an image the line does not hold yet, by digest. */\n readonly image: (digest: string) => Promise<Uint8Array>;\n /** Reads of the manifest before giving up on a line other writers keep moving. Defaults to 8. */\n readonly attempts?: number;\n}\n\n/**\n * Offer entries to a line.\n *\n * Each entry replaces the held entry in its slot — same family, same qualifier,\n * any version — except on a mainline, where the held one is kept when its\n * commit strictly descends from the offered one: a slow run of an older commit\n * finishing last. A held entry in a newer format is kept on any line, so an old\n * CLI cannot take a line back a version. Other held entries stay, which is how\n * suites published by different jobs land on one line. A losing write reads\n * the line again and decides again.\n */\nexport async function publishLine(\n cell: LineCell,\n line: ShareLine,\n entries: readonly ShareEntry[],\n options: PublishOptions,\n): Promise<Published | ShareMiss> {\n const latest = await publishOne(cell, line, entries, options);\n if ('kind' in latest || line.kind !== 'mainline') return latest;\n const kept = await keepAtCommits(cell, line, entries, options);\n return kept === undefined ? latest : { ...latest, atCommit: kept };\n}\n\n/**\n * A mainline's entries kept at the commits they were derived at, the same rules\n * deciding on each, when the store can list lines (`at-commit.ts`). The latest\n * is written first and stands whatever happens to those.\n */\nasync function keepAtCommits(\n cell: LineCell,\n line: ShareLine,\n entries: readonly ShareEntry[],\n options: PublishOptions,\n): Promise<KeptCommits | undefined> {\n if (cell.commits === undefined) return undefined;\n const byCommit = new Map<string, ShareEntry[]>();\n for (const entry of entries) byCommit.set(entry.commit, [...(byCommit.get(entry.commit) ?? []), entry]);\n const kept: string[] = [];\n for (const [commit, group] of byCommit) {\n const written = await publishOne(cell, atCommit(line, commit), group, options);\n if ('kind' in written) return { kept, miss: written };\n kept.push(commit);\n }\n return { kept };\n}\n\nasync function publishOne(\n cell: LineCell,\n line: ShareLine,\n entries: readonly ShareEntry[],\n options: PublishOptions,\n): Promise<Published | ShareMiss> {\n for (const entry of entries) {\n const problem = entryProblem(entry);\n if (problem !== undefined) throw new Error(`cannot publish ${problem}`);\n }\n\n const limit = options.attempts ?? 8;\n for (let attempt = 1; attempt <= limit; attempt += 1) {\n const loaded = await cell.load(line);\n let held: LineManifest;\n let expected: string | undefined;\n if ('manifest' in loaded) {\n const decoded = decodeManifest(loaded.manifest);\n // A line a newer writer owns is not this writer's to replace; one that is\n // unreadable is, since nothing can read it anyway.\n if ('kind' in decoded) {\n if (decoded.kind === 'newer') return decoded;\n held = { format: 1, entries: [] };\n } else {\n held = decoded;\n }\n expected = loaded.version;\n } else if (loaded.kind === 'absent') {\n held = { format: 1, entries: [] };\n } else {\n return loaded;\n }\n\n const plan = await decide(line, held, entries, options.descends);\n if (plan.write.length === 0) {\n return { written: [], kept: plan.kept, unanswered: [], attempts: attempt };\n }\n\n const next = nextManifest(held, plan.write);\n const blobs = await blobsFor(held, next, plan.write, options.image);\n const outcome = await cell.store(line, {\n manifest: encodeManifest(next),\n blobs,\n ...(expected !== undefined ? { expected } : {}),\n });\n if (outcome === 'written') {\n return {\n written: plan.write.map((entry) => entry.name),\n kept: plan.kept,\n unanswered: plan.unanswered,\n attempts: attempt,\n };\n }\n if (outcome !== 'conflict') return outcome;\n }\n return {\n kind: 'unreachable',\n detail: `the line moved under ${String(limit)} writes in a row; nothing was published`,\n };\n}\n\nasync function decide(\n line: ShareLine,\n held: LineManifest,\n entries: readonly ShareEntry[],\n descends: Descends,\n): Promise<{ write: ShareEntry[]; kept: Published['kept'][number][]; unanswered: string[] }> {\n const write: ShareEntry[] = [];\n const kept: Published['kept'][number][] = [];\n const unanswered: string[] = [];\n for (const entry of entries) {\n const offered = parseEntryName(entry.name)!;\n let occupant: HeldEntry | undefined;\n let version = 0;\n for (const candidate of held.entries) {\n const slot = parseEntryName(candidate.name)!;\n if (sameSlot(slot, offered) && slot.version > version) {\n occupant = candidate;\n version = slot.version;\n }\n }\n if (occupant === undefined) {\n write.push(entry);\n continue;\n }\n if (version > offered.version) {\n kept.push({ name: occupant.name, commit: occupant.commit, because: 'newer-format' });\n continue;\n }\n // TODO: shards of one run publish at one commit, so each replaces the\n // last one's `suite-index-v1` and `report-v1`, and the line keeps the index\n // and report of whichever shard published last, covering its subjects only.\n // Nothing here merges two entries of one slot.\n if (line.kind === 'branch' || occupant.commit === entry.commit) {\n write.push(entry);\n continue;\n }\n const newer = await descends(occupant.commit, entry.commit);\n if (newer === true) {\n kept.push({ name: occupant.name, commit: occupant.commit, because: 'newer-commit' });\n continue;\n }\n if (newer === undefined) unanswered.push(entry.name);\n write.push(entry);\n }\n return { write, kept, unanswered };\n}\n\nfunction nextManifest(held: LineManifest, write: readonly ShareEntry[]): LineManifest {\n const replaced = write.map((entry) => parseEntryName(entry.name)!);\n const survivors = held.entries.filter(\n (entry) => !replaced.some((slot) => sameSlot(slot, parseEntryName(entry.name)!)),\n );\n const added: HeldEntry[] = write.map((entry) => ({\n name: entry.name,\n commit: entry.commit,\n ...(entry.head !== undefined ? { head: entry.head } : {}),\n ...(entry.images !== undefined ? { images: [...new Set(entry.images)] } : {}),\n digest: entryDigest(entry.bytes),\n size: entry.bytes.length,\n }));\n return { format: 1, entries: [...survivors, ...added] };\n}\n\n/** Only what the held line lacks: entry bytes are new by construction, images often are not. */\nasync function blobsFor(\n held: LineManifest,\n next: LineManifest,\n write: readonly ShareEntry[],\n image: (digest: string) => Promise<Uint8Array>,\n): Promise<Map<BlobPath, Uint8Array>> {\n const blobs = new Map<BlobPath, Uint8Array>();\n const heldDigests = new Set(held.entries.map((entry) => entry.digest));\n for (const entry of write) {\n const digest = entryDigest(entry.bytes);\n if (!heldDigests.has(digest)) blobs.set(`entries/${digest}`, entry.bytes);\n }\n const heldImages = new Set(held.entries.flatMap((entry) => entry.images ?? []));\n for (const digest of new Set(next.entries.flatMap((entry) => entry.images ?? []))) {\n if (!heldImages.has(digest)) blobs.set(`images/${digest}`, await image(digest));\n }\n return blobs;\n}\n\n/** A line as a reader holds it: the manifest, and its blobs on demand. */\nexport interface HeldLine {\n readonly line: ShareLine;\n readonly manifest: LineManifest;\n /** The bytes of one entry, checked against the digest the manifest names. */\n entry(entry: HeldEntry): Promise<Uint8Array | ShareMiss>;\n image(digest: string): Promise<Uint8Array | ShareMiss>;\n}\n\n/** Read what a line holds, or why it cannot be read. */\nexport async function readLine(cell: LineCell, line: ShareLine): Promise<HeldLine | ShareMiss> {\n const loaded = await cell.load(line);\n if (!('manifest' in loaded)) return loaded;\n const manifest = decodeManifest(loaded.manifest);\n if ('kind' in manifest) return manifest;\n return {\n line,\n manifest,\n async entry(entry) {\n const bytes = await cell.blob(line, `entries/${entry.digest}`);\n if (!(bytes instanceof Uint8Array)) return bytes;\n if (entryDigest(bytes) !== entry.digest) {\n return { kind: 'unreadable', detail: `${entry.name}: the bytes do not match the manifest's digest` };\n }\n return bytes;\n },\n image(digest) {\n return cell.blob(line, `images/${digest}`);\n },\n };\n}\n\n/**\n * The held entry a reader asked for by name, or why there is none.\n *\n * A reader of `suite-v1/web` that finds `suite-v2/web` is told `newer`, not\n * `absent`: the line has what it wants, in a format it has to upgrade to read.\n */\nexport function findEntry(manifest: LineManifest, name: string): HeldEntry | ShareMiss {\n const wanted = parseEntryName(name);\n if (wanted === undefined) throw new Error(`\"${name}\" is not <family>-v<version>[/<qualifier>]`);\n const exact = manifest.entries.find((entry) => entry.name === name);\n if (exact !== undefined) return exact;\n const newer = manifest.entries\n .filter((entry) => {\n const held = parseEntryName(entry.name)!;\n return sameSlot(held, wanted) && held.version > wanted.version;\n })\n .map((entry) => entry.name);\n return newer.length > 0 ? { kind: 'newer', names: newer } : { kind: 'absent' };\n}\n\n/**\n * A line cell in this process, for tests and for a run that shares with itself.\n *\n * Holds real versions, so a test can stage a writer that moves the line between\n * another writer's read and its write, and reads `clock` for when a line was\n * written, so a test can age what it keeps.\n */\nexport function memoryLineCell(\n clock: () => number = Date.now,\n): LineCell & { readonly held: Map<string, Uint8Array> } {\n const held = new Map<string, Uint8Array>();\n const versions = new Map<string, number>();\n const written = new Map<string, { readonly line: ShareLine; readonly at: number }>();\n const key = (line: ShareLine): string => `${line.kind}\\u0000${line.name}\\u0000${line.at ?? ''}`;\n return {\n held,\n async load(line) {\n const manifest = held.get(`${key(line)}\\u0000manifest`);\n if (manifest === undefined) return { kind: 'absent' };\n return { manifest, version: String(versions.get(key(line)) ?? 0) };\n },\n async blob(line, path) {\n const bytes = held.get(path.startsWith('images/') ? path : `${key(line)}\\u0000${path}`);\n return bytes === undefined ? { kind: 'absent' } : bytes.slice();\n },\n async store(line, write) {\n const current = versions.get(key(line));\n const now = current === undefined ? undefined : String(current);\n if (now !== write.expected) return 'conflict';\n for (const [path, bytes] of write.blobs) {\n held.set(path.startsWith('images/') ? path : `${key(line)}\\u0000${path}`, bytes.slice());\n }\n held.set(`${key(line)}\\u0000manifest`, write.manifest.slice());\n versions.set(key(line), (current ?? 0) + 1);\n written.set(key(line), { line, at: clock() });\n return 'written';\n },\n async commits(line) {\n return [...written.values()]\n .filter((one) => one.line.at !== undefined && one.line.kind === line.kind && one.line.name === line.name)\n .map((one) => ({ commit: one.line.at!, written: one.at }));\n },\n async drop(lines) {\n for (const line of lines) {\n const prefix = `${key(line)}\\u0000`;\n for (const path of held.keys()) if (path.startsWith(prefix)) held.delete(path);\n versions.delete(key(line));\n written.delete(key(line));\n }\n return 'dropped';\n },\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@variance-authority/core",
3
- "version": "0.22.0",
3
+ "version": "0.26.0",
4
4
  "description": "The Variance Authority format, rules, comparison, attribution and verdicts. Pure data in, pure data out, no DOM and no I/O.",
5
5
  "keywords": [
6
6
  "variance-authority",
@@ -79,6 +79,6 @@
79
79
  "build": "tsc --build"
80
80
  },
81
81
  "devDependencies": {
82
- "@variance-authority/sense": "^0.22.0"
82
+ "@variance-authority/sense": "^0.26.0"
83
83
  }
84
84
  }