@variance-authority/core 0.8.1 → 0.10.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 +23 -0
- package/README.md +47 -21
- package/dist/attribute/boundary.d.ts +21 -14
- package/dist/attribute/boundary.js +23 -29
- package/dist/attribute/boundary.js.map +1 -1
- package/dist/attribute/component-hash.d.ts +21 -0
- package/dist/attribute/component-hash.js +48 -3
- package/dist/attribute/component-hash.js.map +1 -1
- package/dist/attribute/index.d.ts +3 -3
- package/dist/attribute/index.js +1 -1
- package/dist/attribute/index.js.map +1 -1
- package/dist/attribute/lexicon.d.ts +37 -0
- package/dist/attribute/lexicon.js +40 -12
- package/dist/attribute/lexicon.js.map +1 -1
- package/dist/attribute/rename.d.ts +18 -0
- package/dist/attribute/rename.js +24 -0
- package/dist/attribute/rename.js.map +1 -0
- package/dist/compare/cascade.d.ts +2 -0
- package/dist/compare/cascade.js +5 -1
- package/dist/compare/cascade.js.map +1 -1
- package/dist/format/hash.d.ts +20 -0
- package/dist/format/hash.js +27 -1
- package/dist/format/hash.js.map +1 -1
- package/dist/format/index.d.ts +1 -1
- package/dist/format/index.js +1 -1
- package/dist/format/index.js.map +1 -1
- package/dist/format/snapshot.d.ts +15 -0
- package/dist/format/snapshot.js.map +1 -1
- package/dist/judge/sensitivity.js +1 -2
- package/dist/judge/sensitivity.js.map +1 -1
- package/dist/relate/graph.d.ts +10 -0
- package/dist/relate/graph.js +11 -1
- package/dist/relate/graph.js.map +1 -1
- package/dist/relate/index.d.ts +1 -0
- package/dist/relate/index.js +1 -0
- package/dist/relate/index.js.map +1 -1
- package/dist/relate/narrow.d.ts +53 -0
- package/dist/relate/narrow.js +133 -0
- package/dist/relate/narrow.js.map +1 -0
- package/dist/relate/reach.d.ts +7 -0
- package/dist/relate/reach.js +5 -0
- package/dist/relate/reach.js.map +1 -1
- package/dist/relate/records.d.ts +10 -1
- package/dist/relate/records.js +19 -2
- package/dist/relate/records.js.map +1 -1
- package/dist/share/http-line.d.ts +30 -0
- package/dist/share/http-line.js +164 -0
- package/dist/share/http-line.js.map +1 -0
- package/dist/share/index.d.ts +9 -126
- package/dist/share/index.js +9 -168
- package/dist/share/index.js.map +1 -1
- package/dist/share/line.d.ts +153 -0
- package/dist/share/line.js +184 -0
- package/dist/share/line.js.map +1 -0
- package/dist/share/publish.d.ts +79 -0
- package/dist/share/publish.js +231 -0
- package/dist/share/publish.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A line: the latest of what CI derived on one mainline or one branch.
|
|
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.
|
|
10
|
+
*
|
|
11
|
+
* A line holds **entries**, each a versioned artifact derived at one commit: the
|
|
12
|
+
* run report, one record per suite, the suite index. They are entries rather
|
|
13
|
+
* than one bundle because they arrive from different jobs, and a publish replaces
|
|
14
|
+
* only what it carries. It also holds the **images** its entries name, by digest,
|
|
15
|
+
* because an image does not compress and costs nothing to keep once.
|
|
16
|
+
*
|
|
17
|
+
* Nothing here collapses a failure into a miss. A reader has to be told whether
|
|
18
|
+
* nothing was published, it lacks the credentials, the store could not be
|
|
19
|
+
* reached, or a newer writer left a format it cannot read, because each of those
|
|
20
|
+
* asks for a different action.
|
|
21
|
+
*/
|
|
22
|
+
import { sha256HexBytes } from '../format/sha256.js';
|
|
23
|
+
const ENTRY = /^([a-z][a-z0-9-]*)-v([1-9][0-9]*)(?:\/([A-Za-z0-9._-]+))?$/;
|
|
24
|
+
const COMMIT = /^[0-9a-f]{4,64}$/;
|
|
25
|
+
const DIGEST = /^[0-9a-f]{64}$/;
|
|
26
|
+
/** Read an entry name, or refuse it. */
|
|
27
|
+
export function parseEntryName(name) {
|
|
28
|
+
const match = ENTRY.exec(name);
|
|
29
|
+
if (match === null)
|
|
30
|
+
return undefined;
|
|
31
|
+
const qualifier = match[3];
|
|
32
|
+
return {
|
|
33
|
+
family: match[1],
|
|
34
|
+
version: Number(match[2]),
|
|
35
|
+
...(qualifier !== undefined && qualifier !== '.' && qualifier !== '..' ? { qualifier } : {}),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** Two entries are the same slot when they differ at most in version. */
|
|
39
|
+
export function sameSlot(a, b) {
|
|
40
|
+
return a.family === b.family && a.qualifier === b.qualifier;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The path of a line inside a store, `mainline/release/2.0`.
|
|
44
|
+
*
|
|
45
|
+
* Each segment of the branch name is folded to key characters, a
|
|
46
|
+
* deliberately many-to-one mapping. Two branches whose names differ only in
|
|
47
|
+
* folded characters share a line, and a reader tells them apart by whether its
|
|
48
|
+
* `HEAD` contains the commit the line names.
|
|
49
|
+
*/
|
|
50
|
+
export function linePath(line) {
|
|
51
|
+
const segments = line.name
|
|
52
|
+
.split('/')
|
|
53
|
+
.map((part) => part.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^[-.]+|[-.]+$/g, ''))
|
|
54
|
+
.filter((part) => part !== '');
|
|
55
|
+
return `${line.kind}/${segments.length === 0 ? 'unnamed' : segments.join('/')}`;
|
|
56
|
+
}
|
|
57
|
+
/** The digest an entry's bytes are held under. */
|
|
58
|
+
export function entryDigest(bytes) {
|
|
59
|
+
return sha256HexBytes(bytes);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The manifest as bytes: canonical, ASCII, byte-stable.
|
|
63
|
+
*
|
|
64
|
+
* Every field is checked to be ASCII before it is written — names, commits and
|
|
65
|
+
* digests already are — so no text encoder is needed and two runtimes cannot
|
|
66
|
+
* write two spellings of one manifest.
|
|
67
|
+
*/
|
|
68
|
+
export function encodeManifest(manifest) {
|
|
69
|
+
const entries = [...manifest.entries].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
70
|
+
const text = JSON.stringify({
|
|
71
|
+
format: manifest.format,
|
|
72
|
+
entries: entries.map((entry) => ({
|
|
73
|
+
name: entry.name,
|
|
74
|
+
commit: entry.commit,
|
|
75
|
+
...(entry.head !== undefined ? { head: entry.head } : {}),
|
|
76
|
+
...(entry.images !== undefined ? { images: [...entry.images].sort() } : {}),
|
|
77
|
+
digest: entry.digest,
|
|
78
|
+
size: entry.size,
|
|
79
|
+
})),
|
|
80
|
+
});
|
|
81
|
+
const bytes = new Uint8Array(text.length);
|
|
82
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
83
|
+
const code = text.charCodeAt(i);
|
|
84
|
+
if (code > 0x7e)
|
|
85
|
+
throw new Error(`a manifest holds ASCII only, and this one does not at ${String(i)}`);
|
|
86
|
+
bytes[i] = code;
|
|
87
|
+
}
|
|
88
|
+
return bytes;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Read a manifest, or say why it is not one.
|
|
92
|
+
*
|
|
93
|
+
* A `format` above 1 is a newer writer, not a broken one, and is answered as
|
|
94
|
+
* such: the reader's action is to upgrade, not to republish.
|
|
95
|
+
*/
|
|
96
|
+
export function decodeManifest(bytes) {
|
|
97
|
+
let text = '';
|
|
98
|
+
for (const byte of bytes) {
|
|
99
|
+
if (byte > 0x7e)
|
|
100
|
+
return { kind: 'unreadable', detail: 'the manifest is not ASCII' };
|
|
101
|
+
text += String.fromCharCode(byte);
|
|
102
|
+
}
|
|
103
|
+
let parsed;
|
|
104
|
+
try {
|
|
105
|
+
parsed = JSON.parse(text);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return { kind: 'unreadable', detail: 'the manifest is not JSON' };
|
|
109
|
+
}
|
|
110
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
111
|
+
return { kind: 'unreadable', detail: 'the manifest is not an object' };
|
|
112
|
+
}
|
|
113
|
+
const { format, entries } = parsed;
|
|
114
|
+
if (typeof format === 'number' && format > 1)
|
|
115
|
+
return { kind: 'newer', names: [`manifest format ${String(format)}`] };
|
|
116
|
+
if (format !== 1 || !Array.isArray(entries)) {
|
|
117
|
+
return { kind: 'unreadable', detail: 'the manifest has no format 1 entry list' };
|
|
118
|
+
}
|
|
119
|
+
const held = [];
|
|
120
|
+
for (const entry of entries) {
|
|
121
|
+
const checked = heldEntry(entry);
|
|
122
|
+
if (checked === undefined)
|
|
123
|
+
return { kind: 'unreadable', detail: 'the manifest holds a malformed entry' };
|
|
124
|
+
held.push(checked);
|
|
125
|
+
}
|
|
126
|
+
return { format: 1, entries: held };
|
|
127
|
+
}
|
|
128
|
+
function heldEntry(value) {
|
|
129
|
+
if (typeof value !== 'object' || value === null)
|
|
130
|
+
return undefined;
|
|
131
|
+
const { name, commit, head, images, digest, size } = value;
|
|
132
|
+
if (typeof name !== 'string' || parseEntryName(name) === undefined)
|
|
133
|
+
return undefined;
|
|
134
|
+
if (typeof commit !== 'string' || !COMMIT.test(commit))
|
|
135
|
+
return undefined;
|
|
136
|
+
if (head !== undefined && (typeof head !== 'string' || !COMMIT.test(head)))
|
|
137
|
+
return undefined;
|
|
138
|
+
if (typeof digest !== 'string' || !DIGEST.test(digest))
|
|
139
|
+
return undefined;
|
|
140
|
+
if (typeof size !== 'number' || !Number.isInteger(size) || size < 0)
|
|
141
|
+
return undefined;
|
|
142
|
+
if (images !== undefined) {
|
|
143
|
+
if (!Array.isArray(images) || !images.every((image) => typeof image === 'string' && DIGEST.test(image))) {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
name,
|
|
149
|
+
commit,
|
|
150
|
+
...(head !== undefined ? { head: head } : {}),
|
|
151
|
+
...(images !== undefined ? { images: images } : {}),
|
|
152
|
+
digest,
|
|
153
|
+
size,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/** Whether an entry offered for publishing is well formed, and why not. */
|
|
157
|
+
export function entryProblem(entry) {
|
|
158
|
+
if (parseEntryName(entry.name) === undefined) {
|
|
159
|
+
return `"${entry.name}" is not <family>-v<version>[/<qualifier>]`;
|
|
160
|
+
}
|
|
161
|
+
if (!COMMIT.test(entry.commit))
|
|
162
|
+
return `${entry.name}: "${entry.commit}" is not a commit`;
|
|
163
|
+
if (entry.head !== undefined && !COMMIT.test(entry.head))
|
|
164
|
+
return `${entry.name}: "${entry.head}" is not a commit`;
|
|
165
|
+
for (const image of entry.images ?? []) {
|
|
166
|
+
if (!DIGEST.test(image))
|
|
167
|
+
return `${entry.name}: "${image}" is not a sha-256 digest`;
|
|
168
|
+
}
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Every blob path a manifest names. A backend that holds one line as one tree,
|
|
173
|
+
* a git ref, keeps exactly these and drops the rest when it writes.
|
|
174
|
+
*/
|
|
175
|
+
export function manifestPaths(manifest) {
|
|
176
|
+
const paths = new Set();
|
|
177
|
+
for (const entry of manifest.entries) {
|
|
178
|
+
paths.add(`entries/${entry.digest}`);
|
|
179
|
+
for (const image of entry.images ?? [])
|
|
180
|
+
paths.add(`images/${image}`);
|
|
181
|
+
}
|
|
182
|
+
return [...paths].sort();
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=line.js.map
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What may replace what on a line, and what a reader is told.
|
|
3
|
+
*
|
|
4
|
+
* Decided here, once, over the {@link LineCell} a backend provides, so a
|
|
5
|
+
* directory, a bucket and a git ref hold a line by the same rules.
|
|
6
|
+
*/
|
|
7
|
+
import { type HeldEntry, type LineCell, type LineManifest, type ShareEntry, type ShareLine, type ShareMiss } from './line.js';
|
|
8
|
+
/**
|
|
9
|
+
* Whether `descendant` strictly descends from `ancestor`, or `undefined` when
|
|
10
|
+
* the history needed to say is not at hand. Git owns this answer; the caller
|
|
11
|
+
* asks it.
|
|
12
|
+
*/
|
|
13
|
+
export type Descends = (descendant: string, ancestor: string) => Promise<boolean | undefined>;
|
|
14
|
+
/** What a publish did, entry by entry. */
|
|
15
|
+
export interface Published {
|
|
16
|
+
/** Entries this publish wrote. */
|
|
17
|
+
readonly written: readonly string[];
|
|
18
|
+
/** Entries the line kept instead, and why. */
|
|
19
|
+
readonly kept: readonly {
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly commit: string;
|
|
22
|
+
readonly because: 'newer-commit' | 'newer-format';
|
|
23
|
+
}[];
|
|
24
|
+
/**
|
|
25
|
+
* Entries written although nobody could say whether the held one was newer.
|
|
26
|
+
* Replacing is the answer then, because a line that refuses when it cannot
|
|
27
|
+
* tell is a line one force-push jams for good.
|
|
28
|
+
*/
|
|
29
|
+
readonly unanswered: readonly string[];
|
|
30
|
+
/** How many times the manifest was read before a write held. */
|
|
31
|
+
readonly attempts: number;
|
|
32
|
+
}
|
|
33
|
+
export interface PublishOptions {
|
|
34
|
+
readonly descends: Descends;
|
|
35
|
+
/** The bytes of an image the line does not hold yet, by digest. */
|
|
36
|
+
readonly image: (digest: string) => Promise<Uint8Array>;
|
|
37
|
+
/** Reads of the manifest before giving up on a line other writers keep moving. Defaults to 8. */
|
|
38
|
+
readonly attempts?: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Offer entries to a line.
|
|
42
|
+
*
|
|
43
|
+
* Each entry replaces the held entry in its slot — same family, same qualifier,
|
|
44
|
+
* any version — except on a mainline, where the held one is kept when its
|
|
45
|
+
* commit strictly descends from the offered one: a slow run of an older commit
|
|
46
|
+
* 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.
|
|
51
|
+
*/
|
|
52
|
+
export declare function publishLine(cell: LineCell, line: ShareLine, entries: readonly ShareEntry[], options: PublishOptions): Promise<Published | ShareMiss>;
|
|
53
|
+
/** A line as a reader holds it: the manifest, and its blobs on demand. */
|
|
54
|
+
export interface HeldLine {
|
|
55
|
+
readonly line: ShareLine;
|
|
56
|
+
readonly manifest: LineManifest;
|
|
57
|
+
/** The bytes of one entry, checked against the digest the manifest names. */
|
|
58
|
+
entry(entry: HeldEntry): Promise<Uint8Array | ShareMiss>;
|
|
59
|
+
image(digest: string): Promise<Uint8Array | ShareMiss>;
|
|
60
|
+
}
|
|
61
|
+
/** Read what a line holds, or why it cannot be read. */
|
|
62
|
+
export declare function readLine(cell: LineCell, line: ShareLine): Promise<HeldLine | ShareMiss>;
|
|
63
|
+
/**
|
|
64
|
+
* The held entry a reader asked for by name, or why there is none.
|
|
65
|
+
*
|
|
66
|
+
* A reader of `suite-v1/web` that finds `suite-v2/web` is told `newer`, not
|
|
67
|
+
* `absent`: the line has what it wants, in a format it has to upgrade to read.
|
|
68
|
+
*/
|
|
69
|
+
export declare function findEntry(manifest: LineManifest, name: string): HeldEntry | ShareMiss;
|
|
70
|
+
/**
|
|
71
|
+
* A line cell in this process, for tests and for a run that shares with itself.
|
|
72
|
+
*
|
|
73
|
+
* Holds real versions, so a test can stage a writer that moves the line between
|
|
74
|
+
* another writer's read and its write.
|
|
75
|
+
*/
|
|
76
|
+
export declare function memoryLineCell(): LineCell & {
|
|
77
|
+
readonly held: Map<string, Uint8Array>;
|
|
78
|
+
};
|
|
79
|
+
//# sourceMappingURL=publish.d.ts.map
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What may replace what on a line, and what a reader is told.
|
|
3
|
+
*
|
|
4
|
+
* Decided here, once, over the {@link LineCell} a backend provides, so a
|
|
5
|
+
* directory, a bucket and a git ref hold a line by the same rules.
|
|
6
|
+
*/
|
|
7
|
+
import { decodeManifest, encodeManifest, entryDigest, entryProblem, parseEntryName, sameSlot, } from './line.js';
|
|
8
|
+
/**
|
|
9
|
+
* Offer entries to a line.
|
|
10
|
+
*
|
|
11
|
+
* Each entry replaces the held entry in its slot — same family, same qualifier,
|
|
12
|
+
* any version — except on a mainline, where the held one is kept when its
|
|
13
|
+
* commit strictly descends from the offered one: a slow run of an older commit
|
|
14
|
+
* 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.
|
|
19
|
+
*/
|
|
20
|
+
export async function publishLine(cell, line, entries, options) {
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
const problem = entryProblem(entry);
|
|
23
|
+
if (problem !== undefined)
|
|
24
|
+
throw new Error(`cannot publish ${problem}`);
|
|
25
|
+
}
|
|
26
|
+
const limit = options.attempts ?? 8;
|
|
27
|
+
for (let attempt = 1; attempt <= limit; attempt += 1) {
|
|
28
|
+
const loaded = await cell.load(line);
|
|
29
|
+
let held;
|
|
30
|
+
let expected;
|
|
31
|
+
if ('manifest' in loaded) {
|
|
32
|
+
const decoded = decodeManifest(loaded.manifest);
|
|
33
|
+
// A line a newer writer owns is not this writer's to replace; one that is
|
|
34
|
+
// unreadable is, since nothing can read it anyway.
|
|
35
|
+
if ('kind' in decoded) {
|
|
36
|
+
if (decoded.kind === 'newer')
|
|
37
|
+
return decoded;
|
|
38
|
+
held = { format: 1, entries: [] };
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
held = decoded;
|
|
42
|
+
}
|
|
43
|
+
expected = loaded.version;
|
|
44
|
+
}
|
|
45
|
+
else if (loaded.kind === 'absent') {
|
|
46
|
+
held = { format: 1, entries: [] };
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
return loaded;
|
|
50
|
+
}
|
|
51
|
+
const plan = await decide(line, held, entries, options.descends);
|
|
52
|
+
if (plan.write.length === 0) {
|
|
53
|
+
return { written: [], kept: plan.kept, unanswered: [], attempts: attempt };
|
|
54
|
+
}
|
|
55
|
+
const next = nextManifest(held, plan.write);
|
|
56
|
+
const blobs = await blobsFor(held, next, plan.write, options.image);
|
|
57
|
+
const outcome = await cell.store(line, {
|
|
58
|
+
manifest: encodeManifest(next),
|
|
59
|
+
blobs,
|
|
60
|
+
...(expected !== undefined ? { expected } : {}),
|
|
61
|
+
});
|
|
62
|
+
if (outcome === 'written') {
|
|
63
|
+
return {
|
|
64
|
+
written: plan.write.map((entry) => entry.name),
|
|
65
|
+
kept: plan.kept,
|
|
66
|
+
unanswered: plan.unanswered,
|
|
67
|
+
attempts: attempt,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (outcome !== 'conflict')
|
|
71
|
+
return outcome;
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
kind: 'unreachable',
|
|
75
|
+
detail: `the line moved under ${String(limit)} writes in a row; nothing was published`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
async function decide(line, held, entries, descends) {
|
|
79
|
+
const write = [];
|
|
80
|
+
const kept = [];
|
|
81
|
+
const unanswered = [];
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
const offered = parseEntryName(entry.name);
|
|
84
|
+
let occupant;
|
|
85
|
+
let version = 0;
|
|
86
|
+
for (const candidate of held.entries) {
|
|
87
|
+
const slot = parseEntryName(candidate.name);
|
|
88
|
+
if (sameSlot(slot, offered) && slot.version > version) {
|
|
89
|
+
occupant = candidate;
|
|
90
|
+
version = slot.version;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (occupant === undefined) {
|
|
94
|
+
write.push(entry);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (version > offered.version) {
|
|
98
|
+
kept.push({ name: occupant.name, commit: occupant.commit, because: 'newer-format' });
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
// TODO: shards of one run publish at one commit, so each replaces the
|
|
102
|
+
// last one's `suite-index-v1` and `report-v1`, and the line keeps the index
|
|
103
|
+
// and report of whichever shard published last, covering its subjects only.
|
|
104
|
+
// Nothing here merges two entries of one slot.
|
|
105
|
+
if (line.kind === 'branch' || occupant.commit === entry.commit) {
|
|
106
|
+
write.push(entry);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const newer = await descends(occupant.commit, entry.commit);
|
|
110
|
+
if (newer === true) {
|
|
111
|
+
kept.push({ name: occupant.name, commit: occupant.commit, because: 'newer-commit' });
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (newer === undefined)
|
|
115
|
+
unanswered.push(entry.name);
|
|
116
|
+
write.push(entry);
|
|
117
|
+
}
|
|
118
|
+
return { write, kept, unanswered };
|
|
119
|
+
}
|
|
120
|
+
function nextManifest(held, write) {
|
|
121
|
+
const replaced = write.map((entry) => parseEntryName(entry.name));
|
|
122
|
+
const survivors = held.entries.filter((entry) => !replaced.some((slot) => sameSlot(slot, parseEntryName(entry.name))));
|
|
123
|
+
const added = write.map((entry) => ({
|
|
124
|
+
name: entry.name,
|
|
125
|
+
commit: entry.commit,
|
|
126
|
+
...(entry.head !== undefined ? { head: entry.head } : {}),
|
|
127
|
+
...(entry.images !== undefined ? { images: [...new Set(entry.images)] } : {}),
|
|
128
|
+
digest: entryDigest(entry.bytes),
|
|
129
|
+
size: entry.bytes.length,
|
|
130
|
+
}));
|
|
131
|
+
return { format: 1, entries: [...survivors, ...added] };
|
|
132
|
+
}
|
|
133
|
+
/** Only what the held line lacks: entry bytes are new by construction, images often are not. */
|
|
134
|
+
async function blobsFor(held, next, write, image) {
|
|
135
|
+
const blobs = new Map();
|
|
136
|
+
const heldDigests = new Set(held.entries.map((entry) => entry.digest));
|
|
137
|
+
for (const entry of write) {
|
|
138
|
+
const digest = entryDigest(entry.bytes);
|
|
139
|
+
if (!heldDigests.has(digest))
|
|
140
|
+
blobs.set(`entries/${digest}`, entry.bytes);
|
|
141
|
+
}
|
|
142
|
+
const heldImages = new Set(held.entries.flatMap((entry) => entry.images ?? []));
|
|
143
|
+
for (const digest of new Set(next.entries.flatMap((entry) => entry.images ?? []))) {
|
|
144
|
+
if (!heldImages.has(digest))
|
|
145
|
+
blobs.set(`images/${digest}`, await image(digest));
|
|
146
|
+
}
|
|
147
|
+
return blobs;
|
|
148
|
+
}
|
|
149
|
+
/** Read what a line holds, or why it cannot be read. */
|
|
150
|
+
export async function readLine(cell, line) {
|
|
151
|
+
const loaded = await cell.load(line);
|
|
152
|
+
if (!('manifest' in loaded))
|
|
153
|
+
return loaded;
|
|
154
|
+
const manifest = decodeManifest(loaded.manifest);
|
|
155
|
+
if ('kind' in manifest)
|
|
156
|
+
return manifest;
|
|
157
|
+
return {
|
|
158
|
+
line,
|
|
159
|
+
manifest,
|
|
160
|
+
async entry(entry) {
|
|
161
|
+
const bytes = await cell.blob(line, `entries/${entry.digest}`);
|
|
162
|
+
if (!(bytes instanceof Uint8Array))
|
|
163
|
+
return bytes;
|
|
164
|
+
if (entryDigest(bytes) !== entry.digest) {
|
|
165
|
+
return { kind: 'unreadable', detail: `${entry.name}: the bytes do not match the manifest's digest` };
|
|
166
|
+
}
|
|
167
|
+
return bytes;
|
|
168
|
+
},
|
|
169
|
+
image(digest) {
|
|
170
|
+
return cell.blob(line, `images/${digest}`);
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* The held entry a reader asked for by name, or why there is none.
|
|
176
|
+
*
|
|
177
|
+
* A reader of `suite-v1/web` that finds `suite-v2/web` is told `newer`, not
|
|
178
|
+
* `absent`: the line has what it wants, in a format it has to upgrade to read.
|
|
179
|
+
*/
|
|
180
|
+
export function findEntry(manifest, name) {
|
|
181
|
+
const wanted = parseEntryName(name);
|
|
182
|
+
if (wanted === undefined)
|
|
183
|
+
throw new Error(`"${name}" is not <family>-v<version>[/<qualifier>]`);
|
|
184
|
+
const exact = manifest.entries.find((entry) => entry.name === name);
|
|
185
|
+
if (exact !== undefined)
|
|
186
|
+
return exact;
|
|
187
|
+
const newer = manifest.entries
|
|
188
|
+
.filter((entry) => {
|
|
189
|
+
const held = parseEntryName(entry.name);
|
|
190
|
+
return sameSlot(held, wanted) && held.version > wanted.version;
|
|
191
|
+
})
|
|
192
|
+
.map((entry) => entry.name);
|
|
193
|
+
return newer.length > 0 ? { kind: 'newer', names: newer } : { kind: 'absent' };
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* A line cell in this process, for tests and for a run that shares with itself.
|
|
197
|
+
*
|
|
198
|
+
* Holds real versions, so a test can stage a writer that moves the line between
|
|
199
|
+
* another writer's read and its write.
|
|
200
|
+
*/
|
|
201
|
+
export function memoryLineCell() {
|
|
202
|
+
const held = new Map();
|
|
203
|
+
const versions = new Map();
|
|
204
|
+
const key = (line) => `${line.kind}\u0000${line.name}`;
|
|
205
|
+
return {
|
|
206
|
+
held,
|
|
207
|
+
async load(line) {
|
|
208
|
+
const manifest = held.get(`${key(line)}\u0000manifest`);
|
|
209
|
+
if (manifest === undefined)
|
|
210
|
+
return { kind: 'absent' };
|
|
211
|
+
return { manifest, version: String(versions.get(key(line)) ?? 0) };
|
|
212
|
+
},
|
|
213
|
+
async blob(line, path) {
|
|
214
|
+
const bytes = held.get(path.startsWith('images/') ? path : `${key(line)}\u0000${path}`);
|
|
215
|
+
return bytes === undefined ? { kind: 'absent' } : bytes.slice();
|
|
216
|
+
},
|
|
217
|
+
async store(line, write) {
|
|
218
|
+
const current = versions.get(key(line));
|
|
219
|
+
const now = current === undefined ? undefined : String(current);
|
|
220
|
+
if (now !== write.expected)
|
|
221
|
+
return 'conflict';
|
|
222
|
+
for (const [path, bytes] of write.blobs) {
|
|
223
|
+
held.set(path.startsWith('images/') ? path : `${key(line)}\u0000${path}`, bytes.slice());
|
|
224
|
+
}
|
|
225
|
+
held.set(`${key(line)}\u0000manifest`, write.manifest.slice());
|
|
226
|
+
versions.set(key(line), (current ?? 0) + 1);
|
|
227
|
+
return 'written';
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
//# sourceMappingURL=publish.js.map
|
|
@@ -0,0 +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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@variance-authority/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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",
|