@secondlayer/shared 11.4.2 → 11.4.3
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.
|
@@ -10,17 +10,25 @@
|
|
|
10
10
|
*
|
|
11
11
|
* The distinction this file exists to protect:
|
|
12
12
|
*
|
|
13
|
-
* lagging → the archive trails the chain tip
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
13
|
+
* lagging → the archive trails the chain tip. This is CORRECT and permanent.
|
|
14
|
+
* Two expected causes: twice-weekly publish (Wednesday and Sunday)
|
|
15
|
+
* and the finality boundary. Cadence is the bulk of the gap;
|
|
16
|
+
* finality is hours, not days. An archive that reported `stale`
|
|
17
|
+
* for its own expected lag would be crying wolf forever, and
|
|
18
|
+
* consumers would learn to ignore it.
|
|
19
|
+
* stale → publishing stopped. The lag exceeds what cadence and the
|
|
20
|
+
* finality rule can explain, so something is broken.
|
|
19
21
|
*
|
|
20
22
|
* `state` is derived from measurements, never asserted by the publisher, and
|
|
21
23
|
* every unhealthy state is reachable — an archive that can only report health
|
|
22
24
|
* is decoration.
|
|
23
25
|
*/
|
|
26
|
+
/**
|
|
27
|
+
* v1 is additive. `source.decoder_head` joined the object without a bump:
|
|
28
|
+
* it does not change the meaning of any existing field, and readers already
|
|
29
|
+
* ignore unknown keys. Bump only if a field is renamed, removed, or
|
|
30
|
+
* re-interpreted.
|
|
31
|
+
*/
|
|
24
32
|
declare const ARCHIVE_STATUS_SCHEMA_VERSION = 1;
|
|
25
33
|
type ArchiveState = "fresh" | "lagging" | "stale" | "gap" | "failed-audit" | "source-unavailable";
|
|
26
34
|
type ArchiveStatus = {
|
|
@@ -41,6 +49,12 @@ type ArchiveStatus = {
|
|
|
41
49
|
tip_height: number | null
|
|
42
50
|
/** Highest height eligible to publish under the finality rule. */
|
|
43
51
|
finalized_height: number | null
|
|
52
|
+
/**
|
|
53
|
+
* Highest height the Index decoder (`decode.ft_transfer.v1`) has
|
|
54
|
+
* committed. Null when the checkpoint cannot be read. Distinct from
|
|
55
|
+
* `archive.coverage_to_block`, which is the last signed publish.
|
|
56
|
+
*/
|
|
57
|
+
decoder_head: number | null
|
|
44
58
|
}
|
|
45
59
|
lag: {
|
|
46
60
|
/** Behind the FINALIZED height — the number that indicates a problem. */
|
|
@@ -65,6 +79,11 @@ type StatusInputs = {
|
|
|
65
79
|
/** Null when the source could not be reached. */
|
|
66
80
|
sourceTipHeight: number | null
|
|
67
81
|
finalizedHeight: number | null
|
|
82
|
+
/**
|
|
83
|
+
* Index decoder checkpoint height. Null when unread or unreachable.
|
|
84
|
+
* Reported, never used to derive `state`.
|
|
85
|
+
*/
|
|
86
|
+
decoderHead?: number | null
|
|
68
87
|
audit: {
|
|
69
88
|
complete: boolean
|
|
70
89
|
checkedAt: string
|
|
@@ -80,6 +99,12 @@ type StatusInputs = {
|
|
|
80
99
|
maxSecondsSincePromotion?: number
|
|
81
100
|
};
|
|
82
101
|
/**
|
|
102
|
+
* Next twice-weekly publish fire strictly after `now`. Matches
|
|
103
|
+
* `secondlayer-archive-publish.timer`. Export duration means the promotion
|
|
104
|
+
* lands hours later; this is the scheduled start.
|
|
105
|
+
*/
|
|
106
|
+
declare function nextScheduledArchivePublish(now: Date): Date;
|
|
107
|
+
/**
|
|
83
108
|
* Derive status from measurements. Pure, so every state below is reachable in
|
|
84
109
|
* a test rather than only in production at 3am.
|
|
85
110
|
*
|
|
@@ -89,4 +114,4 @@ type StatusInputs = {
|
|
|
89
114
|
declare function deriveArchiveStatus(inputs: StatusInputs): ArchiveStatus;
|
|
90
115
|
/** States a consumer should refuse to restore from. */
|
|
91
116
|
declare function isRestorable(state: ArchiveState): boolean;
|
|
92
|
-
export { isRestorable, deriveArchiveStatus, StatusInputs, ArchiveStatus, ArchiveState, ARCHIVE_STATUS_SCHEMA_VERSION };
|
|
117
|
+
export { nextScheduledArchivePublish, isRestorable, deriveArchiveStatus, StatusInputs, ArchiveStatus, ArchiveState, ARCHIVE_STATUS_SCHEMA_VERSION };
|
|
@@ -18,6 +18,20 @@ var __export = (target, all) => {
|
|
|
18
18
|
var ARCHIVE_STATUS_SCHEMA_VERSION = 1;
|
|
19
19
|
var DEFAULT_MAX_BLOCKS_BEHIND_FINALIZED = 60000;
|
|
20
20
|
var DEFAULT_MAX_SECONDS_SINCE_PROMOTION = 5 * 24 * 3600;
|
|
21
|
+
var PUBLISH_HOUR_UTC = 8;
|
|
22
|
+
var PUBLISH_WEEKDAYS_UTC = new Set([0, 3]);
|
|
23
|
+
function nextScheduledArchivePublish(now) {
|
|
24
|
+
for (let i = 0;i <= 7; i++) {
|
|
25
|
+
const candidate = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + i, PUBLISH_HOUR_UTC, 0, 0, 0));
|
|
26
|
+
if (PUBLISH_WEEKDAYS_UTC.has(candidate.getUTCDay()) && candidate > now) {
|
|
27
|
+
return candidate;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
throw new Error("nextScheduledArchivePublish: no fire in 7 days");
|
|
31
|
+
}
|
|
32
|
+
function publishWeekdayName(date) {
|
|
33
|
+
return date.getUTCDay() === 0 ? "Sunday" : "Wednesday";
|
|
34
|
+
}
|
|
21
35
|
function formatAge(seconds) {
|
|
22
36
|
const hours = seconds / 3600;
|
|
23
37
|
if (hours < 48)
|
|
@@ -44,7 +58,8 @@ function deriveArchiveStatus(inputs) {
|
|
|
44
58
|
},
|
|
45
59
|
source: {
|
|
46
60
|
tip_height: inputs.sourceTipHeight,
|
|
47
|
-
finalized_height: inputs.finalizedHeight
|
|
61
|
+
finalized_height: inputs.finalizedHeight,
|
|
62
|
+
decoder_head: inputs.decoderHead ?? null
|
|
48
63
|
},
|
|
49
64
|
lag: {
|
|
50
65
|
blocks_behind_finalized: behindFinalized,
|
|
@@ -89,10 +104,11 @@ function deriveArchiveStatus(inputs) {
|
|
|
89
104
|
};
|
|
90
105
|
}
|
|
91
106
|
if (behindTip !== null && behindTip > 0) {
|
|
107
|
+
const nextDay = publishWeekdayName(nextScheduledArchivePublish(inputs.now));
|
|
92
108
|
return {
|
|
93
109
|
...base,
|
|
94
110
|
state: "lagging",
|
|
95
|
-
detail: `${behindTip} blocks behind the chain tip, which is expected:
|
|
111
|
+
detail: `${behindTip} blocks behind the chain tip, which is expected: publishing is twice weekly (Wednesday and Sunday, next ${nextDay}) and stops at the finality boundary`
|
|
96
112
|
};
|
|
97
113
|
}
|
|
98
114
|
return {
|
|
@@ -105,10 +121,11 @@ function isRestorable(state) {
|
|
|
105
121
|
return state === "fresh" || state === "lagging";
|
|
106
122
|
}
|
|
107
123
|
export {
|
|
124
|
+
nextScheduledArchivePublish,
|
|
108
125
|
isRestorable,
|
|
109
126
|
deriveArchiveStatus,
|
|
110
127
|
ARCHIVE_STATUS_SCHEMA_VERSION
|
|
111
128
|
};
|
|
112
129
|
|
|
113
|
-
//# debugId=
|
|
130
|
+
//# debugId=C18B5116B1FC55FB64756E2164756E21
|
|
114
131
|
//# sourceMappingURL=status.js.map
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/archive/status.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * `status.json` — the archive's operational truth, published short-cache from\n * verified state only.\n *\n * Everything else in the tree is immutable and signed, which makes it durable\n * but silent: a consumer holding a valid manifest cannot tell whether it is\n * current, three days behind, or the last thing published before an incident.\n * This object answers that, and it is the only object allowed to say something\n * unflattering.\n *\n * The distinction this file exists to protect:\n *\n * lagging → the archive trails the chain tip because it only publishes below\n * the finality boundary. This is CORRECT and permanent. An archive\n * that reported `stale` for its own finality lag would be crying\n * wolf forever, and consumers would learn to ignore it.\n * stale → publishing stopped. The lag exceeds what the finality rule can\n * explain, so something is broken.\n *\n * `state` is derived from measurements, never asserted by the publisher, and\n * every unhealthy state is reachable — an archive that can only report health\n * is decoration.\n */\n\nexport const ARCHIVE_STATUS_SCHEMA_VERSION = 1;\n\nexport type ArchiveState =\n\t| \"fresh\"\n\t| \"lagging\"\n\t| \"stale\"\n\t| \"gap\"\n\t| \"failed-audit\"\n\t| \"source-unavailable\";\n\nexport type ArchiveStatus = {\n\tschema_version: typeof ARCHIVE_STATUS_SCHEMA_VERSION;\n\tnetwork: string;\n\tstate: ArchiveState;\n\t/** Human-facing one-liner explaining the state, including WHY. */\n\tdetail: string;\n\tgenerated_at: string;\n\tarchive: {\n\t\tsnapshot_digest: string | null;\n\t\tcoverage_to_block: number | null;\n\t\tpromoted_at: string | null;\n\t\tsigning_key_id: string | null;\n\t};\n\tsource: {\n\t\t/** Chain tip as the publisher last observed it. Null when unreachable. */\n\t\ttip_height: number | null;\n\t\t/** Highest height eligible to publish under the finality rule. */\n\t\tfinalized_height: number | null;\n\t};\n\tlag: {\n\t\t/** Behind the FINALIZED height — the number that indicates a problem. */\n\t\tblocks_behind_finalized: number | null;\n\t\t/** Behind the raw chain tip — expected to be non-zero forever. */\n\t\tblocks_behind_tip: number | null;\n\t\tseconds_since_promotion: number | null;\n\t};\n\taudit: {\n\t\tcomplete: boolean;\n\t\tchecked_at: string;\n\t} | null;\n\tsignature?: string;\n\tkey_id?: string;\n};\n\nexport type StatusInputs = {\n\tnetwork: string;\n\tsnapshotDigest: string | null;\n\tcoverageToBlock: number | null;\n\tpromotedAt: string | null;\n\tsigningKeyId: string | null;\n\t/** Null when the source could not be reached. */\n\tsourceTipHeight: number | null;\n\tfinalizedHeight: number | null;\n\taudit: { complete: boolean; checkedAt: string } | null;\n\tnow: Date;\n\t/**\n\t * How far behind the FINALIZED height the archive may fall before it is\n\t * stale rather than merely lagging. Publishing one partition covers 50k\n\t * blocks, so a healthy archive sits well inside this.\n\t */\n\tmaxBlocksBehindFinalized?: number;\n\t/** How long since the last promotion before staleness, regardless of height. */\n\tmaxSecondsSincePromotion?: number;\n};\n\nconst DEFAULT_MAX_BLOCKS_BEHIND_FINALIZED = 60_000;\n/**\n * Must exceed the publish cadence, or the archive reports `stale` on every\n * healthy cycle and the signal means nothing.\n *\n * The publisher runs Wed + Sun, so the longest gap between promotions in a\n * healthy week is 4 days. Five days leaves a day of grace for a slow export or\n * timer jitter, and still fires two days before the next scheduled attempt\n * whenever a cycle is actually missed.\n *\n * It also lands just inside the height rule: at the observed ~7–9k blocks/day\n * the archive is ~45k blocks behind finalized at the five-day mark, so age\n * trips first and reports the more diagnostic message (\"publishing stopped\")\n * rather than the symptom.\n */\nconst DEFAULT_MAX_SECONDS_SINCE_PROMOTION = 5 * 24 * 3_600;\n\n/**\n * Durations here run to days, and \"beyond the 120h objective\" makes a reader do\n * arithmetic before they can tell whether that is alarming.\n */\nfunction formatAge(seconds: number): string {\n\tconst hours = seconds / 3_600;\n\tif (hours < 48) return `${Math.round(hours)}h`;\n\tconst days = hours / 24;\n\t// One decimal only when it changes the reading: 4d and 4.5d are different\n\t// facts during an incident, 5.0d is noise.\n\tconst rounded = Math.round(days * 10) / 10;\n\treturn `${Number.isInteger(rounded) ? rounded : rounded.toFixed(1)}d`;\n}\n\n/**\n * Derive status from measurements. Pure, so every state below is reachable in\n * a test rather than only in production at 3am.\n *\n * Order matters: a failing audit outranks freshness, because an archive that\n * is current and wrong is worse than one that is behind and correct.\n */\nexport function deriveArchiveStatus(inputs: StatusInputs): ArchiveStatus {\n\tconst maxBehind =\n\t\tinputs.maxBlocksBehindFinalized ?? DEFAULT_MAX_BLOCKS_BEHIND_FINALIZED;\n\tconst maxAge =\n\t\tinputs.maxSecondsSincePromotion ?? DEFAULT_MAX_SECONDS_SINCE_PROMOTION;\n\n\tconst secondsSincePromotion = inputs.promotedAt\n\t\t? Math.max(\n\t\t\t\t0,\n\t\t\t\tMath.round(\n\t\t\t\t\t(inputs.now.getTime() - Date.parse(inputs.promotedAt)) / 1000,\n\t\t\t\t),\n\t\t\t)\n\t\t: null;\n\n\tconst behindFinalized =\n\t\tinputs.finalizedHeight !== null && inputs.coverageToBlock !== null\n\t\t\t? Math.max(0, inputs.finalizedHeight - inputs.coverageToBlock)\n\t\t\t: null;\n\tconst behindTip =\n\t\tinputs.sourceTipHeight !== null && inputs.coverageToBlock !== null\n\t\t\t? Math.max(0, inputs.sourceTipHeight - inputs.coverageToBlock)\n\t\t\t: null;\n\n\tconst base = {\n\t\tschema_version:\n\t\t\tARCHIVE_STATUS_SCHEMA_VERSION as typeof ARCHIVE_STATUS_SCHEMA_VERSION,\n\t\tnetwork: inputs.network,\n\t\tgenerated_at: inputs.now.toISOString(),\n\t\tarchive: {\n\t\t\tsnapshot_digest: inputs.snapshotDigest,\n\t\t\tcoverage_to_block: inputs.coverageToBlock,\n\t\t\tpromoted_at: inputs.promotedAt,\n\t\t\tsigning_key_id: inputs.signingKeyId,\n\t\t},\n\t\tsource: {\n\t\t\ttip_height: inputs.sourceTipHeight,\n\t\t\tfinalized_height: inputs.finalizedHeight,\n\t\t},\n\t\tlag: {\n\t\t\tblocks_behind_finalized: behindFinalized,\n\t\t\tblocks_behind_tip: behindTip,\n\t\t\tseconds_since_promotion: secondsSincePromotion,\n\t\t},\n\t\taudit: inputs.audit\n\t\t\t? { complete: inputs.audit.complete, checked_at: inputs.audit.checkedAt }\n\t\t\t: null,\n\t};\n\n\t// A failed audit is the loudest signal: current-and-wrong beats behind-and-right.\n\tif (inputs.audit && !inputs.audit.complete) {\n\t\treturn {\n\t\t\t...base,\n\t\t\tstate: \"failed-audit\",\n\t\t\tdetail: `the canonical audit did not pass at ${inputs.audit.checkedAt}; this archive is not safe to restore from`,\n\t\t};\n\t}\n\n\tif (!inputs.snapshotDigest || inputs.coverageToBlock === null) {\n\t\treturn {\n\t\t\t...base,\n\t\t\tstate: \"gap\",\n\t\t\tdetail: \"no snapshot is currently promoted\",\n\t\t};\n\t}\n\n\t// Source unreachable: report the gap in KNOWLEDGE rather than guessing. The\n\t// archive may be perfectly healthy; we cannot currently say so.\n\tif (inputs.sourceTipHeight === null || inputs.finalizedHeight === null) {\n\t\treturn {\n\t\t\t...base,\n\t\t\tstate: \"source-unavailable\",\n\t\t\tdetail:\n\t\t\t\t\"the chain source could not be reached, so freshness cannot be determined\",\n\t\t};\n\t}\n\n\tif (secondsSincePromotion !== null && secondsSincePromotion > maxAge) {\n\t\treturn {\n\t\t\t...base,\n\t\t\tstate: \"stale\",\n\t\t\tdetail: `last promotion was ${formatAge(secondsSincePromotion)} ago, beyond the ${formatAge(maxAge)} freshness objective`,\n\t\t};\n\t}\n\n\tif (behindFinalized !== null && behindFinalized > maxBehind) {\n\t\treturn {\n\t\t\t...base,\n\t\t\tstate: \"stale\",\n\t\t\tdetail: `${behindFinalized} blocks behind the finalized height, beyond the ${maxBehind} threshold`,\n\t\t};\n\t}\n\n\t// Behind the tip but within the finality boundary — correct, permanent, and\n\t// explicitly NOT staleness.\n\tif (behindTip !== null && behindTip > 0) {\n\t\treturn {\n\t\t\t...base,\n\t\t\tstate: \"lagging\",\n\t\t\tdetail: `${behindTip} blocks behind the chain tip, which is expected: only heights below the finality boundary are published`,\n\t\t};\n\t}\n\n\treturn {\n\t\t...base,\n\t\tstate: \"fresh\",\n\t\tdetail: \"published through the finalized height\",\n\t};\n}\n\n/** States a consumer should refuse to restore from. */\nexport function isRestorable(state: ArchiveState): boolean {\n\treturn state === \"fresh\" || state === \"lagging\";\n}\n"
|
|
5
|
+
"/**\n * `status.json` — the archive's operational truth, published short-cache from\n * verified state only.\n *\n * Everything else in the tree is immutable and signed, which makes it durable\n * but silent: a consumer holding a valid manifest cannot tell whether it is\n * current, three days behind, or the last thing published before an incident.\n * This object answers that, and it is the only object allowed to say something\n * unflattering.\n *\n * The distinction this file exists to protect:\n *\n * lagging → the archive trails the chain tip. This is CORRECT and permanent.\n * Two expected causes: twice-weekly publish (Wednesday and Sunday)\n * and the finality boundary. Cadence is the bulk of the gap;\n * finality is hours, not days. An archive that reported `stale`\n * for its own expected lag would be crying wolf forever, and\n * consumers would learn to ignore it.\n * stale → publishing stopped. The lag exceeds what cadence and the\n * finality rule can explain, so something is broken.\n *\n * `state` is derived from measurements, never asserted by the publisher, and\n * every unhealthy state is reachable — an archive that can only report health\n * is decoration.\n */\n\n/**\n * v1 is additive. `source.decoder_head` joined the object without a bump:\n * it does not change the meaning of any existing field, and readers already\n * ignore unknown keys. Bump only if a field is renamed, removed, or\n * re-interpreted.\n */\nexport const ARCHIVE_STATUS_SCHEMA_VERSION = 1;\n\nexport type ArchiveState =\n\t| \"fresh\"\n\t| \"lagging\"\n\t| \"stale\"\n\t| \"gap\"\n\t| \"failed-audit\"\n\t| \"source-unavailable\";\n\nexport type ArchiveStatus = {\n\tschema_version: typeof ARCHIVE_STATUS_SCHEMA_VERSION;\n\tnetwork: string;\n\tstate: ArchiveState;\n\t/** Human-facing one-liner explaining the state, including WHY. */\n\tdetail: string;\n\tgenerated_at: string;\n\tarchive: {\n\t\tsnapshot_digest: string | null;\n\t\tcoverage_to_block: number | null;\n\t\tpromoted_at: string | null;\n\t\tsigning_key_id: string | null;\n\t};\n\tsource: {\n\t\t/** Chain tip as the publisher last observed it. Null when unreachable. */\n\t\ttip_height: number | null;\n\t\t/** Highest height eligible to publish under the finality rule. */\n\t\tfinalized_height: number | null;\n\t\t/**\n\t\t * Highest height the Index decoder (`decode.ft_transfer.v1`) has\n\t\t * committed. Null when the checkpoint cannot be read. Distinct from\n\t\t * `archive.coverage_to_block`, which is the last signed publish.\n\t\t */\n\t\tdecoder_head: number | null;\n\t};\n\tlag: {\n\t\t/** Behind the FINALIZED height — the number that indicates a problem. */\n\t\tblocks_behind_finalized: number | null;\n\t\t/** Behind the raw chain tip — expected to be non-zero forever. */\n\t\tblocks_behind_tip: number | null;\n\t\tseconds_since_promotion: number | null;\n\t};\n\taudit: {\n\t\tcomplete: boolean;\n\t\tchecked_at: string;\n\t} | null;\n\tsignature?: string;\n\tkey_id?: string;\n};\n\nexport type StatusInputs = {\n\tnetwork: string;\n\tsnapshotDigest: string | null;\n\tcoverageToBlock: number | null;\n\tpromotedAt: string | null;\n\tsigningKeyId: string | null;\n\t/** Null when the source could not be reached. */\n\tsourceTipHeight: number | null;\n\tfinalizedHeight: number | null;\n\t/**\n\t * Index decoder checkpoint height. Null when unread or unreachable.\n\t * Reported, never used to derive `state`.\n\t */\n\tdecoderHead?: number | null;\n\taudit: { complete: boolean; checkedAt: string } | null;\n\tnow: Date;\n\t/**\n\t * How far behind the FINALIZED height the archive may fall before it is\n\t * stale rather than merely lagging. Publishing one partition covers 50k\n\t * blocks, so a healthy archive sits well inside this.\n\t */\n\tmaxBlocksBehindFinalized?: number;\n\t/** How long since the last promotion before staleness, regardless of height. */\n\tmaxSecondsSincePromotion?: number;\n};\n\nconst DEFAULT_MAX_BLOCKS_BEHIND_FINALIZED = 60_000;\n/**\n * Must exceed the publish cadence, or the archive reports `stale` on every\n * healthy cycle and the signal means nothing.\n *\n * The publisher runs Wed + Sun, so the longest gap between promotions in a\n * healthy week is 4 days. Five days leaves a day of grace for a slow export or\n * timer jitter, and still fires two days before the next scheduled attempt\n * whenever a cycle is actually missed.\n *\n * It also lands just inside the height rule: at the observed ~7–9k blocks/day\n * the archive is ~45k blocks behind finalized at the five-day mark, so age\n * trips first and reports the more diagnostic message (\"publishing stopped\")\n * rather than the symptom.\n */\nconst DEFAULT_MAX_SECONDS_SINCE_PROMOTION = 5 * 24 * 3_600;\n\n/** systemd `OnCalendar=Wed,Sun *-*-* 08:00:00`. Host is UTC. Job start, not promotion landing. */\nconst PUBLISH_HOUR_UTC = 8;\nconst PUBLISH_WEEKDAYS_UTC = new Set([0, 3]);\n\n/**\n * Next twice-weekly publish fire strictly after `now`. Matches\n * `secondlayer-archive-publish.timer`. Export duration means the promotion\n * lands hours later; this is the scheduled start.\n */\nexport function nextScheduledArchivePublish(now: Date): Date {\n\tfor (let i = 0; i <= 7; i++) {\n\t\tconst candidate = new Date(\n\t\t\tDate.UTC(\n\t\t\t\tnow.getUTCFullYear(),\n\t\t\t\tnow.getUTCMonth(),\n\t\t\t\tnow.getUTCDate() + i,\n\t\t\t\tPUBLISH_HOUR_UTC,\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t),\n\t\t);\n\t\tif (PUBLISH_WEEKDAYS_UTC.has(candidate.getUTCDay()) && candidate > now) {\n\t\t\treturn candidate;\n\t\t}\n\t}\n\tthrow new Error(\"nextScheduledArchivePublish: no fire in 7 days\");\n}\n\nfunction publishWeekdayName(date: Date): \"Sunday\" | \"Wednesday\" {\n\treturn date.getUTCDay() === 0 ? \"Sunday\" : \"Wednesday\";\n}\n\n/**\n * Durations here run to days, and \"beyond the 120h objective\" makes a reader do\n * arithmetic before they can tell whether that is alarming.\n */\nfunction formatAge(seconds: number): string {\n\tconst hours = seconds / 3_600;\n\tif (hours < 48) return `${Math.round(hours)}h`;\n\tconst days = hours / 24;\n\t// One decimal only when it changes the reading: 4d and 4.5d are different\n\t// facts during an incident, 5.0d is noise.\n\tconst rounded = Math.round(days * 10) / 10;\n\treturn `${Number.isInteger(rounded) ? rounded : rounded.toFixed(1)}d`;\n}\n\n/**\n * Derive status from measurements. Pure, so every state below is reachable in\n * a test rather than only in production at 3am.\n *\n * Order matters: a failing audit outranks freshness, because an archive that\n * is current and wrong is worse than one that is behind and correct.\n */\nexport function deriveArchiveStatus(inputs: StatusInputs): ArchiveStatus {\n\tconst maxBehind =\n\t\tinputs.maxBlocksBehindFinalized ?? DEFAULT_MAX_BLOCKS_BEHIND_FINALIZED;\n\tconst maxAge =\n\t\tinputs.maxSecondsSincePromotion ?? DEFAULT_MAX_SECONDS_SINCE_PROMOTION;\n\n\tconst secondsSincePromotion = inputs.promotedAt\n\t\t? Math.max(\n\t\t\t\t0,\n\t\t\t\tMath.round(\n\t\t\t\t\t(inputs.now.getTime() - Date.parse(inputs.promotedAt)) / 1000,\n\t\t\t\t),\n\t\t\t)\n\t\t: null;\n\n\tconst behindFinalized =\n\t\tinputs.finalizedHeight !== null && inputs.coverageToBlock !== null\n\t\t\t? Math.max(0, inputs.finalizedHeight - inputs.coverageToBlock)\n\t\t\t: null;\n\tconst behindTip =\n\t\tinputs.sourceTipHeight !== null && inputs.coverageToBlock !== null\n\t\t\t? Math.max(0, inputs.sourceTipHeight - inputs.coverageToBlock)\n\t\t\t: null;\n\n\tconst base = {\n\t\tschema_version:\n\t\t\tARCHIVE_STATUS_SCHEMA_VERSION as typeof ARCHIVE_STATUS_SCHEMA_VERSION,\n\t\tnetwork: inputs.network,\n\t\tgenerated_at: inputs.now.toISOString(),\n\t\tarchive: {\n\t\t\tsnapshot_digest: inputs.snapshotDigest,\n\t\t\tcoverage_to_block: inputs.coverageToBlock,\n\t\t\tpromoted_at: inputs.promotedAt,\n\t\t\tsigning_key_id: inputs.signingKeyId,\n\t\t},\n\t\tsource: {\n\t\t\ttip_height: inputs.sourceTipHeight,\n\t\t\tfinalized_height: inputs.finalizedHeight,\n\t\t\tdecoder_head: inputs.decoderHead ?? null,\n\t\t},\n\t\tlag: {\n\t\t\tblocks_behind_finalized: behindFinalized,\n\t\t\tblocks_behind_tip: behindTip,\n\t\t\tseconds_since_promotion: secondsSincePromotion,\n\t\t},\n\t\taudit: inputs.audit\n\t\t\t? { complete: inputs.audit.complete, checked_at: inputs.audit.checkedAt }\n\t\t\t: null,\n\t};\n\n\t// A failed audit is the loudest signal: current-and-wrong beats behind-and-right.\n\tif (inputs.audit && !inputs.audit.complete) {\n\t\treturn {\n\t\t\t...base,\n\t\t\tstate: \"failed-audit\",\n\t\t\tdetail: `the canonical audit did not pass at ${inputs.audit.checkedAt}; this archive is not safe to restore from`,\n\t\t};\n\t}\n\n\tif (!inputs.snapshotDigest || inputs.coverageToBlock === null) {\n\t\treturn {\n\t\t\t...base,\n\t\t\tstate: \"gap\",\n\t\t\tdetail: \"no snapshot is currently promoted\",\n\t\t};\n\t}\n\n\t// Source unreachable: report the gap in KNOWLEDGE rather than guessing. The\n\t// archive may be perfectly healthy; we cannot currently say so.\n\tif (inputs.sourceTipHeight === null || inputs.finalizedHeight === null) {\n\t\treturn {\n\t\t\t...base,\n\t\t\tstate: \"source-unavailable\",\n\t\t\tdetail:\n\t\t\t\t\"the chain source could not be reached, so freshness cannot be determined\",\n\t\t};\n\t}\n\n\tif (secondsSincePromotion !== null && secondsSincePromotion > maxAge) {\n\t\treturn {\n\t\t\t...base,\n\t\t\tstate: \"stale\",\n\t\t\tdetail: `last promotion was ${formatAge(secondsSincePromotion)} ago, beyond the ${formatAge(maxAge)} freshness objective`,\n\t\t};\n\t}\n\n\tif (behindFinalized !== null && behindFinalized > maxBehind) {\n\t\treturn {\n\t\t\t...base,\n\t\t\tstate: \"stale\",\n\t\t\tdetail: `${behindFinalized} blocks behind the finalized height, beyond the ${maxBehind} threshold`,\n\t\t};\n\t}\n\n\t// Behind the tip. CORRECT and permanent: twice-weekly publish plus the\n\t// finality boundary. Cadence is the bulk of the gap. Do not call this stale.\n\tif (behindTip !== null && behindTip > 0) {\n\t\tconst nextDay = publishWeekdayName(nextScheduledArchivePublish(inputs.now));\n\t\treturn {\n\t\t\t...base,\n\t\t\tstate: \"lagging\",\n\t\t\tdetail: `${behindTip} blocks behind the chain tip, which is expected: publishing is twice weekly (Wednesday and Sunday, next ${nextDay}) and stops at the finality boundary`,\n\t\t};\n\t}\n\n\treturn {\n\t\t...base,\n\t\tstate: \"fresh\",\n\t\tdetail: \"published through the finalized height\",\n\t};\n}\n\n/** States a consumer should refuse to restore from. */\nexport function isRestorable(state: ArchiveState): boolean {\n\treturn state === \"fresh\" || state === \"lagging\";\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";;;;;;;;;;;;;;;;;
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "C18B5116B1FC55FB64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|