@variance-authority/core 0.9.0 → 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 +17 -0
- package/README.md +46 -20
- 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/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
package/dist/format/hash.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hash.js","sourceRoot":"","sources":["../../src/format/hash.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAkBxD,MAAM,MAAM,GAAG,IAAI,CAAC;AACpB,MAAM,UAAU,GAAG,EAAE,CAAC;AAEtB;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,OAAO,GAAG,MAAM,IAAI,SAAS,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,CAAC;AAC9D,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,OAAO,GAAG,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,CAAC;AACjD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,KAAiB;IAC3C,OAAO,GAAG,MAAM,IAAI,cAAc,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,CAAC;AACnE,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,WAAW,CAAC,KAAqB;IAC/C,OAAO,YAAY,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;AAC3C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,MAAc,EAAE,KAAwB;IACpE,OAAO,YAAY,CAAC,GAAG,MAAM,SAAS,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;AAChE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,GAAG,GAAG,CAAC;AAExB;;;;;;;;;GASG;AACH,MAAM,UAAU,WAAW,CAAC,EAAU;IACpC,MAAM,OAAO,GAAG,kBAAkB,CAAC,EAAE,CAAC,CAAC;IACvC,IAAI,OAAO,CAAC,MAAM,IAAI,WAAW;QAAE,OAAO,OAAO,CAAC;IAClD,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,WAAW,CAAC,IAAI,YAAY,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"hash.js","sourceRoot":"","sources":["../../src/format/hash.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAkBxD,MAAM,MAAM,GAAG,IAAI,CAAC;AACpB,MAAM,UAAU,GAAG,EAAE,CAAC;AAEtB;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,OAAO,GAAG,MAAM,IAAI,SAAS,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,CAAC;AAC9D,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,OAAO,GAAG,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,CAAC;AACjD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,KAAiB;IAC3C,OAAO,GAAG,MAAM,IAAI,cAAc,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,CAAC;AACnE,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,WAAW,CAAC,KAAqB;IAC/C,OAAO,YAAY,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;AAC3C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,MAAc,EAAE,KAAwB;IACpE,OAAO,YAAY,CAAC,GAAG,MAAM,SAAS,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;AAChE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,GAAG,GAAG,CAAC;AAExB;;;;;;;;;GASG;AACH,MAAM,UAAU,WAAW,CAAC,EAAU;IACpC,MAAM,OAAO,GAAG,kBAAkB,CAAC,EAAE,CAAC,CAAC;IACvC,IAAI,OAAO,CAAC,MAAM,IAAI,WAAW;QAAE,OAAO,OAAO,CAAC;IAClD,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,WAAW,CAAC,IAAI,cAAc,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;AAChF,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,MAAc;IAC3C,OAAO,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,SAAS,GAAG,IAAI,MAAM,CAAC,IAAI,MAAM,iBAAiB,UAAU,KAAK,CAAC,CAAC;AAEzE;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,MAAM,GAAG,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACtC,OAAO,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,GAAG,EAAE,CAAC;AAC5D,CAAC","sourcesContent":["import { canonicalize, type CanonicalValue } from './canonical.js';\nimport { sha256Hex, sha256HexBytes } from './sha256.js';\n\n/**\n * Content addressing (Principle 4).\n *\n * Hashes are the system's only identity. A baseline is addressed by what it *is*,\n * never by the branch or commit it arrived on, which is why a rebase cannot\n * invalidate one.\n *\n * Digests are prefixed with their algorithm and truncated to 128 bits. The prefix\n * makes a future algorithm change a visible, greppable migration rather than a\n * silent collision domain; 128 bits is far beyond collision risk for a corpus\n * bounded by the number of subjects a repository has, and keeps `variance.lock`\n * readable as a diff — a manifest a human refuses to read is a manifest nobody\n * reviews.\n */\nexport type Digest = string;\n\nconst PREFIX = 'v1';\nconst HEX_LENGTH = 32;\n\n/**\n * Hash an already-canonical string.\n *\n * Synchronous, and must stay so. `propsDigest` runs inside the rendering page,\n * where a prop may be a function or an element that cannot be serialized out —\n * so the digest is taken where the value still exists. `crypto.subtle` is async\n * and would colour the whole normalizer; see `sha256.ts`.\n */\nexport function digestString(input: string): Digest {\n return `${PREFIX}:${sha256Hex(input).slice(0, HEX_LENGTH)}`;\n}\n\n/**\n * The digest of a SHA-256 some other implementation already computed.\n *\n * The algorithm in `sha256.ts` is portable TypeScript because `core` has to run\n * inside the page. A host that is not a page — a transform, a scanner, anything\n * reading files off a disk — has the same hash natively and an order of\n * magnitude faster, and FIPS 180-4 leaves no room for the two to disagree.\n *\n * What such a host must not also own is the shape. The prefix is what makes an\n * algorithm change a greppable migration, and the truncation is what keeps a\n * manifest readable; a second place deciding either is a digest domain that\n * splits without anybody noticing. So the host brings the hex and this brings\n * the identity.\n */\nexport function digestOfSha256(hex: string): Digest {\n return `${PREFIX}:${hex.slice(0, HEX_LENGTH)}`;\n}\n\n/**\n * Hash raw bytes — an image, a font file, anything whose identity is its octets.\n *\n * Same prefix and same truncation as every other digest here, so an asset hash\n * and a render hash are the same kind of value and neither can be mistaken for\n * the other's domain. What it is for is `EnvironmentInputs.assets`: a URL is not\n * an identity, and the same `url(...)` can resolve to different bytes tomorrow.\n */\nexport function digestBytes(bytes: Uint8Array): Digest {\n return `${PREFIX}:${sha256HexBytes(bytes).slice(0, HEX_LENGTH)}`;\n}\n\n/** Canonicalize then hash. The only correct way to hash a structure. */\nexport function digestValue(value: CanonicalValue): Digest {\n return digestString(canonicalize(value));\n}\n\n/**\n * Combine digests under a domain label.\n *\n * The label prevents cross-domain collision: an environment key and a render hash\n * built from identical component digests must not be equal, or an environment\n * change could masquerade as a content match.\n */\nexport function digestCombine(domain: string, parts: readonly Digest[]): Digest {\n return digestString(`${domain}\\u0000${parts.join('\\u0000')}`);\n}\n\n/**\n * How much of an encoded id a filename may carry.\n *\n * A filename is capped at 255 bytes on every filesystem anybody runs a suite on,\n * and a caller's own suffix -- an extension, a temporary marker with a pid and a\n * UUID in it -- comes out of the same 255. What is left is the budget.\n */\nconst NAME_BUDGET = 140;\n\n/**\n * A filesystem name for an id that was not written with filesystems in mind.\n *\n * An id long enough to overrun a filename is not an exotic case: a suite that\n * names subjects after the test that produced them -- a file path and a full\n * test name -- passes 255 bytes on ordinary tests, and the whole point of that\n * convention is that the id says where the subject came from. Truncating alone\n * would put two tests in one file, so a long id keeps a readable prefix and\n * earns a digest of the whole id, which is what actually distinguishes it.\n */\nexport function fileNameFor(id: string): string {\n const encoded = encodeURIComponent(id);\n if (encoded.length <= NAME_BUDGET) return encoded;\n return `${encoded.slice(0, NAME_BUDGET)}~${digestFileName(digestString(id))}`;\n}\n\n/**\n * A digest spelled so that a filesystem accepts it: `v1-<hex>` for `v1:<hex>`.\n *\n * The colon is the one character in a digest that is not portable. NTFS refuses\n * it in a name, so a directory named by a raw digest is a repository that cannot\n * be checked out on Windows, and `actions/upload-artifact` refuses the path for\n * the same reason. Every name made from a digest goes through here, so the\n * spelling has one owner and {@link digestOfFileName} can read it back.\n */\nexport function digestFileName(digest: Digest): string {\n return digest.replace(':', '-');\n}\n\nconst FILE_NAME = new RegExp(`^${PREFIX}[-:]([0-9a-f]{${HEX_LENGTH}})$`);\n\n/**\n * The digest a name spells, or `undefined` because it spells none.\n *\n * Reads the raw `v1:<hex>` spelling as well as {@link digestFileName}'s. The\n * file-backed store named its identity partitions with the raw digest before\n * this spelling existed, and a baseline kept under that name is still a\n * baseline: a reader that knew only the new spelling would call it `new` and\n * record over it.\n */\nexport function digestOfFileName(name: string): Digest | undefined {\n const hex = FILE_NAME.exec(name)?.[1];\n return hex === undefined ? undefined : `${PREFIX}:${hex}`;\n}\n"]}
|
package/dist/format/index.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export { canonicalize, canonicalNumber } from './canonical.js';
|
|
|
16
16
|
export type { CapturedValue, ShapeValueOptions, ValueShaping } from './value.js';
|
|
17
17
|
export { shapeValue, pointerShape, pointerToken, DROPPED, VALUE_RECIPE } from './value.js';
|
|
18
18
|
export type { Digest } from './hash.js';
|
|
19
|
-
export { digestString, digestBytes, digestValue, digestCombine, digestOfSha256, fileNameFor } from './hash.js';
|
|
19
|
+
export { digestString, digestBytes, digestValue, digestCombine, digestOfSha256, fileNameFor, digestFileName, digestOfFileName } from './hash.js';
|
|
20
20
|
export type { AccessibilitySnapshot } from './accessibility.js';
|
|
21
21
|
export { accessibilitySnapshot } from './accessibility.js';
|
|
22
22
|
export type { ObservationProfile, ProfileId } from './profile.js';
|
package/dist/format/index.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
export { canonicalize, canonicalNumber } from './canonical.js';
|
|
15
15
|
export { shapeValue, pointerShape, pointerToken, DROPPED, VALUE_RECIPE } from './value.js';
|
|
16
|
-
export { digestString, digestBytes, digestValue, digestCombine, digestOfSha256, fileNameFor } from './hash.js';
|
|
16
|
+
export { digestString, digestBytes, digestValue, digestCombine, digestOfSha256, fileNameFor, digestFileName, digestOfFileName } from './hash.js';
|
|
17
17
|
export { accessibilitySnapshot } from './accessibility.js';
|
|
18
18
|
export { JSDOM_PROFILE, CHROMIUM_PROFILE, profileById, tierOfProfile } from './profile.js';
|
|
19
19
|
export { tierReaches } from './tier.js';
|
package/dist/format/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/format/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAG/D,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG3F,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/format/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAG/D,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG3F,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,cAAc,EAAE,WAAW,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAGjJ,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAG3D,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAG3F,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AASxC,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEpE,OAAO,EACL,aAAa,EACb,eAAe,EACf,cAAc,EACd,aAAa,EACb,aAAa,EACb,cAAc,EACd,aAAa,EACb,SAAS,EACT,cAAc,EACd,wBAAwB,EACxB,YAAY,EACZ,aAAa,EACb,OAAO,EACP,SAAS,EACT,gBAAgB,EAChB,QAAQ,EACR,SAAS,EACT,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,cAAc,GACf,MAAM,gBAAgB,CAAC;AAUxB,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAGrG,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAY9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAWhD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,cAAc,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC","sourcesContent":["/**\n * `@variance-authority/core/format` — what a subject *is*, as a value.\n *\n * The shapes every other group reads and none of them may redefine: a raw\n * capture as a collector hands it over, the normalized snapshot it becomes, the\n * document a renderer paints, the identity that addresses the result, and the\n * hashing that makes any of it comparable.\n *\n * A consumer writing their own collector needs exactly this and nothing else.\n * That is the entrypoint's reason to exist: the format is a contract other people\n * implement, and implementing it should not require reading the rules that\n * interpret it or the verdicts drawn from it.\n */\n\n// compass: variance-authority.normalization\n\nexport type { CanonicalValue } from './canonical.js';\nexport { canonicalize, canonicalNumber } from './canonical.js';\n\nexport type { CapturedValue, ShapeValueOptions, ValueShaping } from './value.js';\nexport { shapeValue, pointerShape, pointerToken, DROPPED, VALUE_RECIPE } from './value.js';\n\nexport type { Digest } from './hash.js';\nexport { digestString, digestBytes, digestValue, digestCombine, digestOfSha256, fileNameFor, digestFileName, digestOfFileName } from './hash.js';\n\nexport type { AccessibilitySnapshot } from './accessibility.js';\nexport { accessibilitySnapshot } from './accessibility.js';\n\nexport type { ObservationProfile, ProfileId } from './profile.js';\nexport { JSDOM_PROFILE, CHROMIUM_PROFILE, profileById, tierOfProfile } from './profile.js';\n\nexport type { Tier } from './tier.js';\nexport { tierReaches } from './tier.js';\n\nexport type {\n EnvironmentInputs,\n EnvironmentKey,\n EnvironmentDelta,\n EnvironmentField,\n Viewport,\n} from './environment.js';\nexport { environmentKey, diffEnvironments } from './environment.js';\n\nexport {\n INTERVENTIONS,\n SEMANTIC_RECIPE,\n COLLECT_RECIPE,\n LAYOUT_RECIPE,\n RASTER_RECIPE,\n holdAnimations,\n pinAnimations,\n hideCaret,\n hideScrollbars,\n hidePresentationalImages,\n waitForFonts,\n waitForImages,\n forTier,\n conflicts,\n interventionById,\n recipeOf,\n recipeCss,\n recipeScreenshot,\n recipeDigest,\n settleRecipe,\n describeRecipe,\n} from './stabilize.js';\nexport type {\n Intervention,\n Recipe,\n Trick,\n ScreenshotOptions,\n SettleTarget,\n} from './stabilize.js';\n\nexport type { Provenance, OwnerFrame, SourceLocation, StackFrame } from './provenance.js';\nexport { propsDigest, heldDigest, jsxSourceOf, relativizeSource, JSX_SOURCE } from './provenance.js';\nexport type { Holding, HeldCell, HeldValue } from './holding.js';\nexport type { Wiring } from './wiring.js';\nexport { keyedByPosition } from './wiring.js';\n\nexport type {\n RawCapture,\n RawNode,\n RawAria,\n MatchedRule,\n Declaration,\n Rect,\n SubjectRef,\n Diagnostic,\n} from './capture.js';\nexport { mergeDiagnostics } from './capture.js';\n\nexport type {\n SemanticSnapshot,\n SemanticNode,\n NodePath,\n StyleProvenanceEntry,\n IgnoreSite,\n ComponentHash,\n} from './snapshot.js';\n\nexport { documentDigest, identityDigest, occupiesPixels, pictured, picture } from './document.js';\nexport type {\n RenderDocument,\n RenderFrame,\n FrameElement,\n RenderResource,\n RenderIdentity,\n Raster,\n Pictured,\n} from './document.js';\n"]}
|
|
@@ -247,6 +247,21 @@ export interface ComponentHash {
|
|
|
247
247
|
* kept in place so the list stays parallel to the instances it counts.
|
|
248
248
|
*/
|
|
249
249
|
readonly boxes?: readonly (Rect | null)[];
|
|
250
|
+
/**
|
|
251
|
+
* Every distinct value the component's own nodes declared, per property, in
|
|
252
|
+
* code-unit order; a custom property appears under its `--name` with the
|
|
253
|
+
* values it resolved to. Never hashed.
|
|
254
|
+
*
|
|
255
|
+
* `boxes` answers *by how much*; this answers *which declaration*. `style`
|
|
256
|
+
* moved says a `Button` is not styled as it was, and a reviewer holding only
|
|
257
|
+
* that opens the file and diffs by eye. With both revisions' values the run
|
|
258
|
+
* says `padding-left 8px → 14px`, which is the line somebody edited.
|
|
259
|
+
*
|
|
260
|
+
* Absent for a baseline written before this existed. That means *nobody
|
|
261
|
+
* recorded it*, and the comparison says nothing about properties rather than
|
|
262
|
+
* reporting every one of them as new.
|
|
263
|
+
*/
|
|
264
|
+
readonly values?: Readonly<Record<string, readonly string[]>>;
|
|
250
265
|
}
|
|
251
266
|
export interface StyleProvenanceEntry {
|
|
252
267
|
readonly path: NodePath;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../../src/format/snapshot.ts"],"names":[],"mappings":"","sourcesContent":["import type { SubjectRef, Rect, Diagnostic } from './capture.js';\nimport type { EnvironmentKey } from './environment.js';\nimport type { Digest } from './hash.js';\nimport type { ObservationProfile } from './profile.js';\nimport type { Provenance } from './provenance.js';\nimport type { Wiring } from './wiring.js';\nimport type { Holding } from './holding.js';\n\n/**\n * The normalized semantic snapshot: the verdict's input, and the thing a render\n * hash addresses.\n *\n * Everything volatile is gone by this point. Ids have become structural aliases,\n * class attributes have been dropped entirely, inapplicable CSS has been pruned,\n * and the cascade has been resolved to winning values (ADR-0003). What remains is\n * meant to be *read*: a snapshot a reviewer refuses to open is a snapshot that\n * gets rubber-stamped.\n */\nexport interface SemanticSnapshot {\n readonly formatVersion: 1;\n readonly subject: SubjectRef;\n readonly profile: ObservationProfile;\n readonly environment: EnvironmentKey;\n\n /** Content hash of `root` under `environment`. The identity of this state. */\n readonly renderHash: Digest;\n\n /**\n * Structure and applicable style, hashed separately.\n *\n * These back the tier below full semantic comparison (ADR-0003): matching both\n * settles a subject without resolving layout or taking a screenshot. Split\n * rather than combined because the docket wants to say *which* held — \"the DOM\n * is identical, only styling moved\" is the sentence that turns a diff into a\n * token-band root.\n */\n readonly structureHash: Digest;\n readonly styleHash: Digest;\n\n readonly root: SemanticNode;\n\n /**\n * Where each winning declaration came from. Deliberately outside the hash.\n *\n * Pruning discards exactly what attribution needs — *which rule set this?* — so\n * it is preserved here instead. Keeping it out of the hash is the point: moving\n * a rule between files renames a source without changing a render, and must not\n * invalidate a baseline.\n */\n readonly styleProvenance: readonly StyleProvenanceEntry[];\n\n /**\n * Subtrees the operator declared out of scope, resolved against *this* tree.\n *\n * Deliberately outside the hash, for the reason `styleProvenance` is: an ignore\n * changes what a run says about a render, never the render. Folding it into the\n * identity would invalidate every baseline in the repository the first time\n * somebody masked a clock, which is the surest way to make a safety feature the\n * thing people turn off.\n *\n * Present only when something was excluded, so a snapshot from a run with no\n * ignores is byte-identical to one produced before this field existed.\n */\n readonly ignoreSites?: readonly IgnoreSite[];\n\n /**\n * What the collector or the normalizer could not do, carried forward.\n *\n * Outside the hash: a diagnostic describes the *observation*, not the render,\n * and a snapshot that hashed its own warnings would invalidate whenever the\n * warning text was reworded. But it must survive to the verdict — a subject\n * whose capture half-failed is not a subject that legitimately `unchanged`,\n * and silently discarding that fact is how a broken collector reads as a clean\n * build.\n */\n readonly diagnostics: readonly Diagnostic[];\n}\n\n/**\n * A node's stable address: child indices from the root, e.g. `0/2/1`.\n *\n * Positional because nothing else survives. Ids are volatile (that is why they\n * were aliased away), and class names have been dropped. Position is honest about\n * what it costs: inserting a sibling renumbers everything after it, so the differ\n * matches nodes by shape and provenance before falling back to path (see\n * `diff.ts`), and treats a path change alone as evidence of nothing.\n */\nexport type NodePath = string;\n\nexport interface SemanticNode {\n readonly path: NodePath;\n readonly tag: string;\n\n /** Structural alias, e.g. `#a0`. Absent when the node carried no id. */\n readonly alias?: string;\n\n readonly role?: string;\n readonly name?: string;\n\n /**\n * Accessible description, resolved. See `RawAria.description` for why this is\n * a field of its own rather than an attribute.\n */\n readonly description?: string;\n\n readonly state?: Readonly<Record<string, string | boolean | number>>;\n\n /** Semantic attributes surviving normalization. `class` is never among them. */\n readonly attributes: Readonly<Record<string, string>>;\n\n /** Winning values for allowlisted properties, canonicalized. */\n readonly style: Readonly<Record<string, string>>;\n\n /**\n * Custom properties this node's winning declarations resolved through, as\n * name → resolved value.\n *\n * Recording the name alongside the value is what lets a design-token change\n * surface as one root with counted collateral instead of hundreds of unrelated\n * colour diffs (spec §5, `token` band).\n */\n readonly tokens?: Readonly<Record<string, string>>;\n\n /**\n * Which token each styled property resolved through, including by inheritance.\n *\n * Outside the hash, like `styleProvenance` and for the same reason: swapping a\n * literal for a token of the same value renames a source without changing a\n * render. It exists so the differ can tell *which* property a token edit\n * explains — `tokens` alone says a node uses tokens, not which of its values\n * one drove.\n */\n readonly styleTokens?: Readonly<Record<string, string>>;\n\n /** Present only under a profile with layout. Absent, never zeroed (ADR-0002). */\n readonly rect?: Rect;\n\n /** Text content, or its digest when policy declares the region volatile. */\n readonly text?: string;\n\n readonly provenance?: Provenance;\n\n /**\n * How the framework holds this node's component, when a framework adapter\n * supplied it. See {@link Wiring}.\n *\n * On the node rather than on `Provenance`, though both come from the same\n * adapter and the same fiber, because they answer to different readers.\n * Provenance exists so a diff arrives carrying a name; wiring is a *dimension\n * of the subject*, hashed into a band of its own, and a band's inputs belong\n * beside the other bands' inputs — next to `style` and `attributes`, which is\n * where `shapeOf` goes looking.\n */\n readonly wiring?: Wiring;\n\n /**\n * What that component was holding. **Outside every hash**, always.\n *\n * The field beside it is the contrast worth reading: `wiring` is a band and\n * this is evidence. `Wiring` refuses state values because a hook's value is\n * exactly what may legitimately differ between two readings of one page, and\n * that refusal is right and is not softened here — a `holding` reaches the\n * snapshot and reaches no digest, so a subject whose clock ticked has the same\n * `renderHash` it had before this field existed.\n *\n * It rides outside on the same terms as `styleProvenance`, `styleTokens` and\n * `ignoredBy`, by the same mechanism: `structureOf`, `styleOf` and `shapeOf`\n * project the fields they hash **by name**, so a field none of them names\n * cannot reach an identity. `holding.hash.test.ts` asserts that rather than\n * trusting it, because the cost of being wrong is every baseline in a\n * repository invalidating the first time a component held a timestamp.\n */\n readonly holding?: Holding;\n\n /**\n * `true` on the root of a subtree rendered through a portal.\n *\n * Recorded in the structure hash so that \"the dialog moved from inline to\n * portalled\" is a change rather than a coincidence of identical content, and\n * so the docket can say *where* something rendered, not only that it exists.\n */\n readonly portalled?: boolean;\n\n /**\n * Ignore rules this node was found under. Outside every hash, like its origin\n * on `RawNode`. See {@link SemanticSnapshot.ignoreSites}.\n */\n readonly ignoredBy?: readonly string[];\n\n readonly children: readonly SemanticNode[];\n}\n\n/**\n * A subtree the operator excluded, resolved to a path and a box (spec 0024).\n *\n * Lives in `format` rather than beside the rules that consume it, because it is\n * part of what a snapshot *is*: the collector produced it, it travels with the\n * document over every wire this project has, and `core/judge` is only its first\n * reader. A type the wire format needs cannot live in the policy layer.\n */\nexport interface IgnoreSite {\n /** Root of the excluded subtree, in this snapshot's own path space. */\n readonly path: NodePath;\n\n /** Which rule put it here, matched exactly against `IgnoreRule.id`. */\n readonly rule: string;\n\n /**\n * The box it occupied, for the raster tier.\n *\n * Absent under a profile with no layout — which is ADR-0002 rather than a\n * failure: a profile that cannot measure a box cannot decide pixels either, so\n * there is nothing for the box to subtract from.\n */\n readonly rect?: Rect;\n}\n\n/**\n * A component's own content, hashed per band (ADR-0018).\n *\n * In `format` rather than beside `hashComponents`, which computes it, because a\n * baseline carries these: a stored image plus the component hashes of the\n * document that painted it is what lets a later run tell the component that\n * *caused* a change from the components the change merely moved. That makes this\n * part of the wire format every store and every transport has to preserve, and a\n * type the wire needs cannot live in the layer that derives it.\n */\nexport interface ComponentHash {\n readonly component: string;\n\n /** Boundaries of this component in the subject, counted in document order. */\n readonly instances: number;\n\n /**\n * Tree shape: tags, structural aliases, portalling, allowlisted attributes,\n * and which child boundaries sit where. The `geometry` band's structural half.\n *\n * **Split out of a single `structure` digest on 2026-08-06.** It used to carry\n * the accessible semantics and the text too, and the fusion was invisible\n * until something needed to ask *which band moved* — at which point a baseline\n * could say \"this component changed\" and never say whether a heading was\n * renamed, a paragraph reworded, or a node inserted. Those are three different\n * bands, three different readers, and one of them is the band a route-level\n * test exists to ignore.\n */\n readonly structure: Digest;\n\n /**\n * Role, accessible name, and ARIA state. The `a11y` band.\n *\n * Its own digest because it is the band that must never be absorbed. Every\n * sensitivity level asserts on it, including the ones that ignore everything\n * else — and a level cannot assert on a band that is fused into another one.\n */\n readonly semantics: Digest;\n\n /** Text runs, in document order. The `content` band. */\n readonly text: Digest;\n\n readonly style: Digest;\n\n /**\n * Absent under a profile without layout — absent, never empty (ADR-0002).\n *\n * An empty geometry digest would compare equal between a run that observed no\n * movement and a run that could not observe movement at all, which is the\n * false `unchanged` this system must never produce.\n */\n readonly geometry?: Digest;\n\n /**\n * The box each instance occupies, in document order. Never hashed.\n *\n * A digest can say a component is not what it was and can never say by how\n * much. That is the gap a reviewer falls into: `Button — geometry, token` is\n * true of a control that lost a shadow and of one that grew eight pixels and\n * pushed the page down, and only the second explains why the subject is a\n * different height. Four numbers per instance close it.\n *\n * Absent for a baseline written before this existed and for a profile with no\n * layout — the same absence, and both mean *nobody measured*, never *it did\n * not move*. A `null` entry is a boundary the browser laid out nothing for,\n * kept in place so the list stays parallel to the instances it counts.\n */\n readonly boxes?: readonly (Rect | null)[];\n}\n\nexport interface StyleProvenanceEntry {\n readonly path: NodePath;\n readonly property: string;\n readonly sheet: string;\n /** Generated segments replaced by placeholders, so it can name a root safely. */\n readonly selector: string;\n readonly tokenName?: string;\n readonly source?: { readonly file: string; readonly line: number };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../../src/format/snapshot.ts"],"names":[],"mappings":"","sourcesContent":["import type { SubjectRef, Rect, Diagnostic } from './capture.js';\nimport type { EnvironmentKey } from './environment.js';\nimport type { Digest } from './hash.js';\nimport type { ObservationProfile } from './profile.js';\nimport type { Provenance } from './provenance.js';\nimport type { Wiring } from './wiring.js';\nimport type { Holding } from './holding.js';\n\n/**\n * The normalized semantic snapshot: the verdict's input, and the thing a render\n * hash addresses.\n *\n * Everything volatile is gone by this point. Ids have become structural aliases,\n * class attributes have been dropped entirely, inapplicable CSS has been pruned,\n * and the cascade has been resolved to winning values (ADR-0003). What remains is\n * meant to be *read*: a snapshot a reviewer refuses to open is a snapshot that\n * gets rubber-stamped.\n */\nexport interface SemanticSnapshot {\n readonly formatVersion: 1;\n readonly subject: SubjectRef;\n readonly profile: ObservationProfile;\n readonly environment: EnvironmentKey;\n\n /** Content hash of `root` under `environment`. The identity of this state. */\n readonly renderHash: Digest;\n\n /**\n * Structure and applicable style, hashed separately.\n *\n * These back the tier below full semantic comparison (ADR-0003): matching both\n * settles a subject without resolving layout or taking a screenshot. Split\n * rather than combined because the docket wants to say *which* held — \"the DOM\n * is identical, only styling moved\" is the sentence that turns a diff into a\n * token-band root.\n */\n readonly structureHash: Digest;\n readonly styleHash: Digest;\n\n readonly root: SemanticNode;\n\n /**\n * Where each winning declaration came from. Deliberately outside the hash.\n *\n * Pruning discards exactly what attribution needs — *which rule set this?* — so\n * it is preserved here instead. Keeping it out of the hash is the point: moving\n * a rule between files renames a source without changing a render, and must not\n * invalidate a baseline.\n */\n readonly styleProvenance: readonly StyleProvenanceEntry[];\n\n /**\n * Subtrees the operator declared out of scope, resolved against *this* tree.\n *\n * Deliberately outside the hash, for the reason `styleProvenance` is: an ignore\n * changes what a run says about a render, never the render. Folding it into the\n * identity would invalidate every baseline in the repository the first time\n * somebody masked a clock, which is the surest way to make a safety feature the\n * thing people turn off.\n *\n * Present only when something was excluded, so a snapshot from a run with no\n * ignores is byte-identical to one produced before this field existed.\n */\n readonly ignoreSites?: readonly IgnoreSite[];\n\n /**\n * What the collector or the normalizer could not do, carried forward.\n *\n * Outside the hash: a diagnostic describes the *observation*, not the render,\n * and a snapshot that hashed its own warnings would invalidate whenever the\n * warning text was reworded. But it must survive to the verdict — a subject\n * whose capture half-failed is not a subject that legitimately `unchanged`,\n * and silently discarding that fact is how a broken collector reads as a clean\n * build.\n */\n readonly diagnostics: readonly Diagnostic[];\n}\n\n/**\n * A node's stable address: child indices from the root, e.g. `0/2/1`.\n *\n * Positional because nothing else survives. Ids are volatile (that is why they\n * were aliased away), and class names have been dropped. Position is honest about\n * what it costs: inserting a sibling renumbers everything after it, so the differ\n * matches nodes by shape and provenance before falling back to path (see\n * `diff.ts`), and treats a path change alone as evidence of nothing.\n */\nexport type NodePath = string;\n\nexport interface SemanticNode {\n readonly path: NodePath;\n readonly tag: string;\n\n /** Structural alias, e.g. `#a0`. Absent when the node carried no id. */\n readonly alias?: string;\n\n readonly role?: string;\n readonly name?: string;\n\n /**\n * Accessible description, resolved. See `RawAria.description` for why this is\n * a field of its own rather than an attribute.\n */\n readonly description?: string;\n\n readonly state?: Readonly<Record<string, string | boolean | number>>;\n\n /** Semantic attributes surviving normalization. `class` is never among them. */\n readonly attributes: Readonly<Record<string, string>>;\n\n /** Winning values for allowlisted properties, canonicalized. */\n readonly style: Readonly<Record<string, string>>;\n\n /**\n * Custom properties this node's winning declarations resolved through, as\n * name → resolved value.\n *\n * Recording the name alongside the value is what lets a design-token change\n * surface as one root with counted collateral instead of hundreds of unrelated\n * colour diffs (spec §5, `token` band).\n */\n readonly tokens?: Readonly<Record<string, string>>;\n\n /**\n * Which token each styled property resolved through, including by inheritance.\n *\n * Outside the hash, like `styleProvenance` and for the same reason: swapping a\n * literal for a token of the same value renames a source without changing a\n * render. It exists so the differ can tell *which* property a token edit\n * explains — `tokens` alone says a node uses tokens, not which of its values\n * one drove.\n */\n readonly styleTokens?: Readonly<Record<string, string>>;\n\n /** Present only under a profile with layout. Absent, never zeroed (ADR-0002). */\n readonly rect?: Rect;\n\n /** Text content, or its digest when policy declares the region volatile. */\n readonly text?: string;\n\n readonly provenance?: Provenance;\n\n /**\n * How the framework holds this node's component, when a framework adapter\n * supplied it. See {@link Wiring}.\n *\n * On the node rather than on `Provenance`, though both come from the same\n * adapter and the same fiber, because they answer to different readers.\n * Provenance exists so a diff arrives carrying a name; wiring is a *dimension\n * of the subject*, hashed into a band of its own, and a band's inputs belong\n * beside the other bands' inputs — next to `style` and `attributes`, which is\n * where `shapeOf` goes looking.\n */\n readonly wiring?: Wiring;\n\n /**\n * What that component was holding. **Outside every hash**, always.\n *\n * The field beside it is the contrast worth reading: `wiring` is a band and\n * this is evidence. `Wiring` refuses state values because a hook's value is\n * exactly what may legitimately differ between two readings of one page, and\n * that refusal is right and is not softened here — a `holding` reaches the\n * snapshot and reaches no digest, so a subject whose clock ticked has the same\n * `renderHash` it had before this field existed.\n *\n * It rides outside on the same terms as `styleProvenance`, `styleTokens` and\n * `ignoredBy`, by the same mechanism: `structureOf`, `styleOf` and `shapeOf`\n * project the fields they hash **by name**, so a field none of them names\n * cannot reach an identity. `holding.hash.test.ts` asserts that rather than\n * trusting it, because the cost of being wrong is every baseline in a\n * repository invalidating the first time a component held a timestamp.\n */\n readonly holding?: Holding;\n\n /**\n * `true` on the root of a subtree rendered through a portal.\n *\n * Recorded in the structure hash so that \"the dialog moved from inline to\n * portalled\" is a change rather than a coincidence of identical content, and\n * so the docket can say *where* something rendered, not only that it exists.\n */\n readonly portalled?: boolean;\n\n /**\n * Ignore rules this node was found under. Outside every hash, like its origin\n * on `RawNode`. See {@link SemanticSnapshot.ignoreSites}.\n */\n readonly ignoredBy?: readonly string[];\n\n readonly children: readonly SemanticNode[];\n}\n\n/**\n * A subtree the operator excluded, resolved to a path and a box (spec 0024).\n *\n * Lives in `format` rather than beside the rules that consume it, because it is\n * part of what a snapshot *is*: the collector produced it, it travels with the\n * document over every wire this project has, and `core/judge` is only its first\n * reader. A type the wire format needs cannot live in the policy layer.\n */\nexport interface IgnoreSite {\n /** Root of the excluded subtree, in this snapshot's own path space. */\n readonly path: NodePath;\n\n /** Which rule put it here, matched exactly against `IgnoreRule.id`. */\n readonly rule: string;\n\n /**\n * The box it occupied, for the raster tier.\n *\n * Absent under a profile with no layout — which is ADR-0002 rather than a\n * failure: a profile that cannot measure a box cannot decide pixels either, so\n * there is nothing for the box to subtract from.\n */\n readonly rect?: Rect;\n}\n\n/**\n * A component's own content, hashed per band (ADR-0018).\n *\n * In `format` rather than beside `hashComponents`, which computes it, because a\n * baseline carries these: a stored image plus the component hashes of the\n * document that painted it is what lets a later run tell the component that\n * *caused* a change from the components the change merely moved. That makes this\n * part of the wire format every store and every transport has to preserve, and a\n * type the wire needs cannot live in the layer that derives it.\n */\nexport interface ComponentHash {\n readonly component: string;\n\n /** Boundaries of this component in the subject, counted in document order. */\n readonly instances: number;\n\n /**\n * Tree shape: tags, structural aliases, portalling, allowlisted attributes,\n * and which child boundaries sit where. The `geometry` band's structural half.\n *\n * **Split out of a single `structure` digest on 2026-08-06.** It used to carry\n * the accessible semantics and the text too, and the fusion was invisible\n * until something needed to ask *which band moved* — at which point a baseline\n * could say \"this component changed\" and never say whether a heading was\n * renamed, a paragraph reworded, or a node inserted. Those are three different\n * bands, three different readers, and one of them is the band a route-level\n * test exists to ignore.\n */\n readonly structure: Digest;\n\n /**\n * Role, accessible name, and ARIA state. The `a11y` band.\n *\n * Its own digest because it is the band that must never be absorbed. Every\n * sensitivity level asserts on it, including the ones that ignore everything\n * else — and a level cannot assert on a band that is fused into another one.\n */\n readonly semantics: Digest;\n\n /** Text runs, in document order. The `content` band. */\n readonly text: Digest;\n\n readonly style: Digest;\n\n /**\n * Absent under a profile without layout — absent, never empty (ADR-0002).\n *\n * An empty geometry digest would compare equal between a run that observed no\n * movement and a run that could not observe movement at all, which is the\n * false `unchanged` this system must never produce.\n */\n readonly geometry?: Digest;\n\n /**\n * The box each instance occupies, in document order. Never hashed.\n *\n * A digest can say a component is not what it was and can never say by how\n * much. That is the gap a reviewer falls into: `Button — geometry, token` is\n * true of a control that lost a shadow and of one that grew eight pixels and\n * pushed the page down, and only the second explains why the subject is a\n * different height. Four numbers per instance close it.\n *\n * Absent for a baseline written before this existed and for a profile with no\n * layout — the same absence, and both mean *nobody measured*, never *it did\n * not move*. A `null` entry is a boundary the browser laid out nothing for,\n * kept in place so the list stays parallel to the instances it counts.\n */\n readonly boxes?: readonly (Rect | null)[];\n\n /**\n * Every distinct value the component's own nodes declared, per property, in\n * code-unit order; a custom property appears under its `--name` with the\n * values it resolved to. Never hashed.\n *\n * `boxes` answers *by how much*; this answers *which declaration*. `style`\n * moved says a `Button` is not styled as it was, and a reviewer holding only\n * that opens the file and diffs by eye. With both revisions' values the run\n * says `padding-left 8px → 14px`, which is the line somebody edited.\n *\n * Absent for a baseline written before this existed. That means *nobody\n * recorded it*, and the comparison says nothing about properties rather than\n * reporting every one of them as new.\n */\n readonly values?: Readonly<Record<string, readonly string[]>>;\n}\n\nexport interface StyleProvenanceEntry {\n readonly path: NodePath;\n readonly property: string;\n readonly sheet: string;\n /** Generated segments replaced by placeholders, so it can name a root safely. */\n readonly selector: string;\n readonly tokenName?: string;\n readonly source?: { readonly file: string; readonly line: number };\n}\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A line over HTTP: a bucket, a signed base, a tribunal deployment.
|
|
3
|
+
*
|
|
4
|
+
* The manifest is replaced with `If-Match` on the `ETag` it was read with, or
|
|
5
|
+
* `If-None-Match: *` when there was none, which is the conditional write S3,
|
|
6
|
+
* GCS, R2 and tribunal all honour. Blobs are addressed by digest and written
|
|
7
|
+
* without a condition, first, so a manifest never names a blob that is not
|
|
8
|
+
* there yet.
|
|
9
|
+
*/
|
|
10
|
+
import { type LineCell } from './line.js';
|
|
11
|
+
export interface HttpLineOptions {
|
|
12
|
+
/** Base URL. Every path is appended to it, with exactly one `/` between. */
|
|
13
|
+
readonly endpoint: string;
|
|
14
|
+
/** Sent on every request: a bucket's `Authorization`, a deployment's token. */
|
|
15
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
16
|
+
/** The verb a write uses. `PUT` for a bucket, `POST` for a deployment that routes on it. */
|
|
17
|
+
readonly method?: 'PUT' | 'POST';
|
|
18
|
+
/**
|
|
19
|
+
* Milliseconds one request may take, from sending it to the last byte of its
|
|
20
|
+
* body, before the store is `unreachable`. 60 000 when absent.
|
|
21
|
+
*/
|
|
22
|
+
readonly timeoutMs?: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* `<endpoint>/<kind>/<name>/manifest.json`, the line's entries beside it, and
|
|
26
|
+
* `<endpoint>/images/<digest>` shared by every line, so an image two lines name
|
|
27
|
+
* is stored once.
|
|
28
|
+
*/
|
|
29
|
+
export declare function httpLineCell(options: HttpLineOptions): LineCell;
|
|
30
|
+
//# sourceMappingURL=http-line.d.ts.map
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A line over HTTP: a bucket, a signed base, a tribunal deployment.
|
|
3
|
+
*
|
|
4
|
+
* The manifest is replaced with `If-Match` on the `ETag` it was read with, or
|
|
5
|
+
* `If-None-Match: *` when there was none, which is the conditional write S3,
|
|
6
|
+
* GCS, R2 and tribunal all honour. Blobs are addressed by digest and written
|
|
7
|
+
* without a condition, first, so a manifest never names a blob that is not
|
|
8
|
+
* there yet.
|
|
9
|
+
*/
|
|
10
|
+
import { linePath } from './line.js';
|
|
11
|
+
/** The most of a store's reason one miss carries, so a long body does not become the whole message. */
|
|
12
|
+
const REASON_LIMIT = 1000;
|
|
13
|
+
/**
|
|
14
|
+
* How long one request may take, its body included, when the options do not
|
|
15
|
+
* say. A store that accepts a connection and never answers would otherwise
|
|
16
|
+
* hold `ask`, `serve` and a publish open with nothing said.
|
|
17
|
+
*/
|
|
18
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
19
|
+
/**
|
|
20
|
+
* `<endpoint>/<kind>/<name>/manifest.json`, the line's entries beside it, and
|
|
21
|
+
* `<endpoint>/images/<digest>` shared by every line, so an image two lines name
|
|
22
|
+
* is stored once.
|
|
23
|
+
*/
|
|
24
|
+
export function httpLineCell(options) {
|
|
25
|
+
const base = options.endpoint.replace(/\/+$/, '');
|
|
26
|
+
const headers = options.headers ?? {};
|
|
27
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
28
|
+
const where = (line, path) => path.startsWith('images/') ? `${base}/${path}` : `${base}/${linePath(line)}/${path}`;
|
|
29
|
+
// The signal is made per request and covers reading the body, so a store
|
|
30
|
+
// that sends its headers and then stalls times out like one that never
|
|
31
|
+
// answers at all.
|
|
32
|
+
async function get(url) {
|
|
33
|
+
try {
|
|
34
|
+
const response = await fetch(url, { headers: { ...headers }, signal: AbortSignal.timeout(timeoutMs) });
|
|
35
|
+
if (!response.ok)
|
|
36
|
+
return await missOf(url, response);
|
|
37
|
+
return { bytes: new Uint8Array(await response.arrayBuffer()), etag: response.headers.get('etag') };
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
return thrown(url, error, timeoutMs);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function put(url, body, condition) {
|
|
44
|
+
let response;
|
|
45
|
+
try {
|
|
46
|
+
response = await fetch(url, {
|
|
47
|
+
method: options.method ?? 'PUT',
|
|
48
|
+
headers: { 'content-type': 'application/octet-stream', ...headers, ...condition },
|
|
49
|
+
body,
|
|
50
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
return thrown(url, error, timeoutMs);
|
|
55
|
+
}
|
|
56
|
+
if (response.ok)
|
|
57
|
+
return 'written';
|
|
58
|
+
if (response.status === 412 || response.status === 409)
|
|
59
|
+
return 'conflict';
|
|
60
|
+
// FIXME: a write answered 404 is read as `absent`, so the publish prints
|
|
61
|
+
// "nothing is published there" for a store that would not take the write.
|
|
62
|
+
return missOf(url, response);
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
async load(line) {
|
|
66
|
+
const held = await get(where(line, 'manifest.json'));
|
|
67
|
+
if (!('bytes' in held))
|
|
68
|
+
return held;
|
|
69
|
+
// FIXME: a store that sends no ETag cannot be written conditionally, so
|
|
70
|
+
// two publishers racing on it can lose an entry. The empty version makes
|
|
71
|
+
// the next write unconditional rather than refused.
|
|
72
|
+
return { manifest: held.bytes, version: held.etag ?? '' };
|
|
73
|
+
},
|
|
74
|
+
async blob(line, path) {
|
|
75
|
+
const held = await get(where(line, path));
|
|
76
|
+
return 'bytes' in held ? held.bytes : held;
|
|
77
|
+
},
|
|
78
|
+
async store(line, write) {
|
|
79
|
+
// TODO: an entry or image no manifest names any more stays in the store,
|
|
80
|
+
// and a branch's line outlives the branch; nothing here deletes, and a
|
|
81
|
+
// bucket's lifecycle rule is the collector.
|
|
82
|
+
for (const [path, bytes] of write.blobs) {
|
|
83
|
+
const outcome = await put(where(line, path), bytes, {});
|
|
84
|
+
if (outcome === 'conflict') {
|
|
85
|
+
return { kind: 'unreachable', detail: `${path}: the store answered an unconditional write with a conflict` };
|
|
86
|
+
}
|
|
87
|
+
if (outcome !== 'written')
|
|
88
|
+
return outcome;
|
|
89
|
+
}
|
|
90
|
+
const condition = write.expected === undefined
|
|
91
|
+
? { 'if-none-match': '*' }
|
|
92
|
+
: write.expected === ''
|
|
93
|
+
? {}
|
|
94
|
+
: { 'if-match': write.expected };
|
|
95
|
+
return put(where(line, 'manifest.json'), write.manifest, condition);
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* A request that threw, as the miss it is. A timeout says how long it waited;
|
|
101
|
+
* anything else says the platform's message and, when it wraps one, the
|
|
102
|
+
* reason underneath, which is where a refused connection or an unknown host
|
|
103
|
+
* is named.
|
|
104
|
+
*/
|
|
105
|
+
function thrown(url, error, timeoutMs) {
|
|
106
|
+
const failed = error;
|
|
107
|
+
if (failed.name === 'TimeoutError') {
|
|
108
|
+
return { kind: 'unreachable', detail: `${url}: timed out after ${String(timeoutMs / 1000)} s` };
|
|
109
|
+
}
|
|
110
|
+
const cause = typeof failed.cause?.message === 'string' ? `: ${failed.cause.message}` : '';
|
|
111
|
+
return { kind: 'unreachable', detail: `${url}: ${String(failed.message)}${cause}` };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* A status the store answered with, as the miss it is. 404 is nothing
|
|
115
|
+
* published. 408, 429 and a 5xx say the store could not take the request now,
|
|
116
|
+
* so they are `unreachable`. Every other 4xx is the store saying no — a token
|
|
117
|
+
* it does not take, a key it will not hold, a path it does not route — so it
|
|
118
|
+
* is `refused`, and the detail carries the store's own reason when it sent one.
|
|
119
|
+
*/
|
|
120
|
+
async function missOf(url, response) {
|
|
121
|
+
const { status } = response;
|
|
122
|
+
if (status === 404)
|
|
123
|
+
return { kind: 'absent' };
|
|
124
|
+
const reason = await reasonOf(response);
|
|
125
|
+
const detail = `${url}: HTTP ${String(status)}${reason === undefined ? '' : `: ${reason}`}`;
|
|
126
|
+
if (status >= 400 && status < 500 && status !== 408 && status !== 429)
|
|
127
|
+
return { kind: 'refused', detail };
|
|
128
|
+
return { kind: 'unreachable', detail };
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The reason a store gave with a status: the `error` of a JSON body, as
|
|
132
|
+
* tribunal and most APIs send it, or a plain-text body, on one line. Undefined
|
|
133
|
+
* for any other body, which is a page for a person rather than a reason, and
|
|
134
|
+
* for a body that does not read.
|
|
135
|
+
*/
|
|
136
|
+
async function reasonOf(response) {
|
|
137
|
+
const type = response.headers.get('content-type')?.split(';')[0]?.trim().toLowerCase();
|
|
138
|
+
if (type !== 'application/json' && type !== 'text/plain')
|
|
139
|
+
return undefined;
|
|
140
|
+
let body;
|
|
141
|
+
try {
|
|
142
|
+
body = await response.text();
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
let reason = body;
|
|
148
|
+
if (type === 'application/json') {
|
|
149
|
+
try {
|
|
150
|
+
const parsed = JSON.parse(body);
|
|
151
|
+
if (typeof parsed.error !== 'string')
|
|
152
|
+
return undefined;
|
|
153
|
+
reason = parsed.error;
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const line = reason.replace(/\s+/g, ' ').trim();
|
|
160
|
+
if (line === '')
|
|
161
|
+
return undefined;
|
|
162
|
+
return line.length > REASON_LIMIT ? `${line.slice(0, REASON_LIMIT)}…` : line;
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=http-line.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-line.js","sourceRoot":"","sources":["../../src/share/http-line.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A line over HTTP: a bucket, a signed base, a tribunal deployment.\n *\n * The manifest is replaced with `If-Match` on the `ETag` it was read with, or\n * `If-None-Match: *` when there was none, which is the conditional write S3,\n * GCS, R2 and tribunal all honour. Blobs are addressed by digest and written\n * without a condition, first, so a manifest never names a blob that is not\n * there yet.\n */\n\nimport { linePath, type BlobPath, type LineCell, type ShareLine, type ShareMiss } from './line.js';\n\n// Declared rather than imported, as elsewhere in this package: `core` takes no\n// platform's types. Only the members used are named.\ndeclare const fetch: (\n url: string,\n init?: {\n method?: string;\n headers?: Record<string, string>;\n body?: Uint8Array;\n signal?: unknown;\n },\n) => Promise<{\n readonly ok: boolean;\n readonly status: number;\n readonly headers: { get(name: string): string | null };\n arrayBuffer(): Promise<ArrayBuffer>;\n text(): Promise<string>;\n}>;\ndeclare const AbortSignal: { timeout(ms: number): unknown };\n\ntype Answer = Awaited<ReturnType<typeof fetch>>;\n\n/** The most of a store's reason one miss carries, so a long body does not become the whole message. */\nconst REASON_LIMIT = 1000;\n\n/**\n * How long one request may take, its body included, when the options do not\n * say. A store that accepts a connection and never answers would otherwise\n * hold `ask`, `serve` and a publish open with nothing said.\n */\nconst DEFAULT_TIMEOUT_MS = 60_000;\n\nexport interface HttpLineOptions {\n /** Base URL. Every path is appended to it, with exactly one `/` between. */\n readonly endpoint: string;\n /** Sent on every request: a bucket's `Authorization`, a deployment's token. */\n readonly headers?: Readonly<Record<string, string>>;\n /** The verb a write uses. `PUT` for a bucket, `POST` for a deployment that routes on it. */\n readonly method?: 'PUT' | 'POST';\n /**\n * Milliseconds one request may take, from sending it to the last byte of its\n * body, before the store is `unreachable`. 60 000 when absent.\n */\n readonly timeoutMs?: number;\n}\n\n/**\n * `<endpoint>/<kind>/<name>/manifest.json`, the line's entries beside it, and\n * `<endpoint>/images/<digest>` shared by every line, so an image two lines name\n * is stored once.\n */\nexport function httpLineCell(options: HttpLineOptions): LineCell {\n const base = options.endpoint.replace(/\\/+$/, '');\n const headers = options.headers ?? {};\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const where = (line: ShareLine, path: 'manifest.json' | BlobPath): string =>\n path.startsWith('images/') ? `${base}/${path}` : `${base}/${linePath(line)}/${path}`;\n\n // The signal is made per request and covers reading the body, so a store\n // that sends its headers and then stalls times out like one that never\n // answers at all.\n async function get(url: string): Promise<{ bytes: Uint8Array; etag: string | null } | ShareMiss> {\n try {\n const response = await fetch(url, { headers: { ...headers }, signal: AbortSignal.timeout(timeoutMs) });\n if (!response.ok) return await missOf(url, response);\n return { bytes: new Uint8Array(await response.arrayBuffer()), etag: response.headers.get('etag') };\n } catch (error) {\n return thrown(url, error, timeoutMs);\n }\n }\n\n async function put(url: string, body: Uint8Array, condition: Record<string, string>): Promise<'written' | 'conflict' | ShareMiss> {\n let response: Answer;\n try {\n response = await fetch(url, {\n method: options.method ?? 'PUT',\n headers: { 'content-type': 'application/octet-stream', ...headers, ...condition },\n body,\n signal: AbortSignal.timeout(timeoutMs),\n });\n } catch (error) {\n return thrown(url, error, timeoutMs);\n }\n if (response.ok) return 'written';\n if (response.status === 412 || response.status === 409) return 'conflict';\n // FIXME: a write answered 404 is read as `absent`, so the publish prints\n // \"nothing is published there\" for a store that would not take the write.\n return missOf(url, response);\n }\n\n return {\n async load(line) {\n const held = await get(where(line, 'manifest.json'));\n if (!('bytes' in held)) return held;\n // FIXME: a store that sends no ETag cannot be written conditionally, so\n // two publishers racing on it can lose an entry. The empty version makes\n // the next write unconditional rather than refused.\n return { manifest: held.bytes, version: held.etag ?? '' };\n },\n async blob(line, path) {\n const held = await get(where(line, path));\n return 'bytes' in held ? held.bytes : held;\n },\n async store(line, write) {\n // TODO: an entry or image no manifest names any more stays in the store,\n // and a branch's line outlives the branch; nothing here deletes, and a\n // bucket's lifecycle rule is the collector.\n for (const [path, bytes] of write.blobs) {\n const outcome = await put(where(line, path), bytes, {});\n if (outcome === 'conflict') {\n return { kind: 'unreachable', detail: `${path}: the store answered an unconditional write with a conflict` };\n }\n if (outcome !== 'written') return outcome;\n }\n const condition =\n write.expected === undefined\n ? { 'if-none-match': '*' }\n : write.expected === ''\n ? {}\n : { 'if-match': write.expected };\n return put(where(line, 'manifest.json'), write.manifest, condition);\n },\n };\n}\n\n/**\n * A request that threw, as the miss it is. A timeout says how long it waited;\n * anything else says the platform's message and, when it wraps one, the\n * reason underneath, which is where a refused connection or an unknown host\n * is named.\n */\nfunction thrown(url: string, error: unknown, timeoutMs: number): ShareMiss {\n const failed = error as { readonly name?: unknown; readonly message?: unknown; readonly cause?: { readonly message?: unknown } };\n if (failed.name === 'TimeoutError') {\n return { kind: 'unreachable', detail: `${url}: timed out after ${String(timeoutMs / 1000)} s` };\n }\n const cause = typeof failed.cause?.message === 'string' ? `: ${failed.cause.message}` : '';\n return { kind: 'unreachable', detail: `${url}: ${String(failed.message)}${cause}` };\n}\n\n/**\n * A status the store answered with, as the miss it is. 404 is nothing\n * published. 408, 429 and a 5xx say the store could not take the request now,\n * so they are `unreachable`. Every other 4xx is the store saying no — a token\n * it does not take, a key it will not hold, a path it does not route — so it\n * is `refused`, and the detail carries the store's own reason when it sent one.\n */\nasync function missOf(url: string, response: Answer): Promise<ShareMiss> {\n const { status } = response;\n if (status === 404) return { kind: 'absent' };\n const reason = await reasonOf(response);\n const detail = `${url}: HTTP ${String(status)}${reason === undefined ? '' : `: ${reason}`}`;\n if (status >= 400 && status < 500 && status !== 408 && status !== 429) return { kind: 'refused', detail };\n\n return { kind: 'unreachable', detail };\n}\n\n/**\n * The reason a store gave with a status: the `error` of a JSON body, as\n * tribunal and most APIs send it, or a plain-text body, on one line. Undefined\n * for any other body, which is a page for a person rather than a reason, and\n * for a body that does not read.\n */\nasync function reasonOf(response: Answer): Promise<string | undefined> {\n const type = response.headers.get('content-type')?.split(';')[0]?.trim().toLowerCase();\n if (type !== 'application/json' && type !== 'text/plain') return undefined;\n let body: string;\n try {\n body = await response.text();\n } catch {\n return undefined;\n }\n let reason = body;\n if (type === 'application/json') {\n try {\n const parsed = JSON.parse(body) as { readonly error?: unknown };\n if (typeof parsed.error !== 'string') return undefined;\n reason = parsed.error;\n } catch {\n return undefined;\n }\n }\n const line = reason.replace(/\\s+/g, ' ').trim();\n if (line === '') return undefined;\n\n return line.length > REASON_LIMIT ? `${line.slice(0, REASON_LIMIT)}…` : line;\n}\n"]}
|
package/dist/share/index.d.ts
CHANGED
|
@@ -10,139 +10,22 @@
|
|
|
10
10
|
* So the second machine should not derive it again. A pull-request runner wants
|
|
11
11
|
* what mainline already knows, and mainline already knew it an hour ago on a
|
|
12
12
|
* runner that has since been destroyed. This module is the seam between those
|
|
13
|
-
* two: a
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* ## The rule, inherited
|
|
17
|
-
*
|
|
18
|
-
* > **A `SharedCache` never throws.**
|
|
19
|
-
*
|
|
20
|
-
* `get` answers `null` for a miss, an outage, a permission error, a truncated
|
|
21
|
-
* body and a response nobody can parse. `put` resolves whether or not anything
|
|
22
|
-
* was written. This is the same rule
|
|
23
|
-
* [`RenderCache`](../../../raster/src/store.ts) is defined by, and it is safe
|
|
24
|
-
* here for the same reason: everything a share holds is derivable from the tree
|
|
25
|
-
* it was derived at. A miss costs the derivation, which is the answer that was
|
|
26
|
-
* always available.
|
|
27
|
-
*
|
|
28
|
-
* What that costs, stated rather than hidden: a cache that is failing looks
|
|
29
|
-
* exactly like a cache that is cold, so a broken share makes CI slow and never
|
|
30
|
-
* makes it red. An operator watching the wall clock has to go looking. The
|
|
31
|
-
* alternative — failing a build because a bucket answered 503 — is worse.
|
|
32
|
-
*
|
|
33
|
-
* ## Why a key is a commit
|
|
34
|
-
*
|
|
35
|
-
* Everything shared here is derived, so the only question a reader has is *of
|
|
36
|
-
* what*, and the answer is a tree. A commit names one; a branch names whichever
|
|
37
|
-
* one it points at this second, and two runners resolving `main` four minutes
|
|
38
|
-
* apart would silently compare against different suites.
|
|
39
|
-
*
|
|
40
|
-
* Mainline moves while a branch is open, though, which is why the lookup is
|
|
41
|
-
* {@link firstShared} over a lineage rather than a single `get`: the runner asks
|
|
42
|
-
* for the commits its branch actually descends from, newest first, and takes the
|
|
43
|
-
* first one anybody published. What it gets back says which commit it is, so a
|
|
44
|
-
* reader is never in doubt about what it is holding.
|
|
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.
|
|
45
16
|
*
|
|
46
17
|
* ## What this is not
|
|
47
18
|
*
|
|
48
19
|
* Not a baseline store. A baseline is the thing a comparison is *against*, it
|
|
49
20
|
* cannot be re-derived, and losing one silently is how a suite records whatever
|
|
50
21
|
* happened to be on screen — which is why
|
|
51
|
-
* [`RasterStore`](../../../raster/src/store.ts) throws on every failure
|
|
52
|
-
*
|
|
22
|
+
* [`RasterStore`](../../../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
|
|
53
25
|
* different questions, and a share is disposable in a way a baseline may never
|
|
54
26
|
* be.
|
|
55
27
|
*/
|
|
56
|
-
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* Deliberately the smallest interface that can carry an evaluation: no listing,
|
|
60
|
-
* no deletion, no expiry. A backend that can do more is welcome to; nothing
|
|
61
|
-
* here may depend on it, because the set of things every one of a directory, an
|
|
62
|
-
* action cache, a bucket and a deployment can do is this.
|
|
63
|
-
*/
|
|
64
|
-
export interface SharedCache {
|
|
65
|
-
get(key: string): Promise<Uint8Array | null>;
|
|
66
|
-
put(key: string, bytes: Uint8Array): Promise<void>;
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Hold an implementation to the rule.
|
|
70
|
-
*
|
|
71
|
-
* Backends are built out of a disk, a socket or a bucket, every one of which
|
|
72
|
-
* throws, and every one of them would otherwise have to remember not to.
|
|
73
|
-
* Wrapping at construction makes the rule a property of the value rather than a
|
|
74
|
-
* promise in a comment.
|
|
75
|
-
*/
|
|
76
|
-
export declare function neverFails(cache: SharedCache): SharedCache;
|
|
77
|
-
/**
|
|
78
|
-
* The key an artifact is published under.
|
|
79
|
-
*
|
|
80
|
-
* `project` is a namespace and not a secret: two suites in one monorepo share a
|
|
81
|
-
* bucket and must not share a key. `artifact` names the format, so a reader that
|
|
82
|
-
* does not understand version 2 of something asks for a key version 2 was never
|
|
83
|
-
* written to rather than parsing bytes it will reject.
|
|
84
|
-
*/
|
|
85
|
-
export declare function shareKey(parts: {
|
|
86
|
-
readonly project: string;
|
|
87
|
-
readonly artifact: string;
|
|
88
|
-
readonly commit: string;
|
|
89
|
-
}): string;
|
|
90
|
-
/** What a lookup over a lineage found, and where. */
|
|
91
|
-
export interface SharedHit {
|
|
92
|
-
/** The commit the bytes were published at — never the one that was asked for first. */
|
|
93
|
-
readonly commit: string;
|
|
94
|
-
/** How many commits back that was, zero being the newest candidate offered. */
|
|
95
|
-
readonly behind: number;
|
|
96
|
-
readonly bytes: Uint8Array;
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* The newest published artifact among commits this tree descends from.
|
|
100
|
-
*
|
|
101
|
-
* Newest-first because a nearer ancestor is a better description of the same
|
|
102
|
-
* suite, and `behind` is returned rather than discarded because it is the one
|
|
103
|
-
* number that tells an operator their publishing is failing: a share that is
|
|
104
|
-
* always forty commits behind is a share nothing is writing to, and it is
|
|
105
|
-
* otherwise indistinguishable from a healthy one.
|
|
106
|
-
*
|
|
107
|
-
* Stops at the first hit rather than collecting all of them. There is no merge
|
|
108
|
-
* across generations to perform — an evaluation describes one tree — and asking
|
|
109
|
-
* a network for forty keys that will be discarded is the cost this exists to
|
|
110
|
-
* avoid.
|
|
111
|
-
*/
|
|
112
|
-
export declare function firstShared(cache: SharedCache, lineage: readonly string[], of: (commit: string) => string): Promise<SharedHit | null>;
|
|
113
|
-
/**
|
|
114
|
-
* A cache in this process, for tests and for a run that shares with itself.
|
|
115
|
-
*
|
|
116
|
-
* Kept here rather than in a test helper because it is also the honest answer
|
|
117
|
-
* to "share, but I have nowhere to put it": the seam is exercised, the code
|
|
118
|
-
* path is the real one, and nothing survives the process.
|
|
119
|
-
*/
|
|
120
|
-
export declare function memoryShare(): SharedCache;
|
|
121
|
-
/** What an HTTP share needs to reach its endpoint. */
|
|
122
|
-
export interface HttpShareOptions {
|
|
123
|
-
/** Base URL. A key is appended to it, with exactly one `/` between. */
|
|
124
|
-
readonly endpoint: string;
|
|
125
|
-
/** Sent on every request. A bucket wants `Authorization`; a signed URL wants nothing. */
|
|
126
|
-
readonly headers?: Readonly<Record<string, string>>;
|
|
127
|
-
/** The verb a write uses. `PUT` for a bucket, `POST` for a deployment that routes on it. */
|
|
128
|
-
readonly method?: 'PUT' | 'POST';
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* A share over HTTP: `GET` to read, `PUT` to write, the key as the path.
|
|
132
|
-
*
|
|
133
|
-
* One backend rather than four because a bucket, a signed URL, a static host
|
|
134
|
-
* and a tribunal deployment differ in exactly two ways — the base and the
|
|
135
|
-
* headers — and every one of them speaks this. S3 with `PUT` object access is
|
|
136
|
-
* this with an `Authorization` header; a presigned base is this with none; a
|
|
137
|
-
* deployment is this with a token. Writing three clients would have produced
|
|
138
|
-
* three sets of error handling for an object whose entire error handling is
|
|
139
|
-
* "return null".
|
|
140
|
-
*
|
|
141
|
-
* Nothing here signs anything. Credentials that must be computed per request —
|
|
142
|
-
* SigV4 against a raw bucket — belong to whatever already holds the operator's
|
|
143
|
-
* credentials, and the shape that reaches this is a base URL it produced. That
|
|
144
|
-
* keeps a secret out of this package and out of the config file it would
|
|
145
|
-
* otherwise have to be written in.
|
|
146
|
-
*/
|
|
147
|
-
export declare function httpShare(options: HttpShareOptions): SharedCache;
|
|
28
|
+
export * from './line.js';
|
|
29
|
+
export * from './publish.js';
|
|
30
|
+
export * from './http-line.js';
|
|
148
31
|
//# sourceMappingURL=index.d.ts.map
|