@secondlayer/shared 6.12.0 → 6.13.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.
|
@@ -792,5 +792,26 @@ interface Gap {
|
|
|
792
792
|
}
|
|
793
793
|
declare function findGaps(db: Kysely<Database>, limit?: number): Promise<Gap[]>;
|
|
794
794
|
declare function countMissingBlocks(db: Kysely<Database>): Promise<number>;
|
|
795
|
+
interface ChainDataIntegrity {
|
|
796
|
+
ok: boolean;
|
|
797
|
+
maxHeight: number;
|
|
798
|
+
sampleHeight: number | null;
|
|
799
|
+
sampleBlocks: number;
|
|
800
|
+
reason: string | null;
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Cheap sanity check that the `blocks` table actually holds the history its tip
|
|
804
|
+
* implies. Catches a wrong/empty Postgres volume — e.g. a container recreated
|
|
805
|
+
* against a fresh volume — being silently served: if the tip is high but a
|
|
806
|
+
* window of blocks well below it is missing, the data isn't what the tip claims.
|
|
807
|
+
*
|
|
808
|
+
* Below `checkFloor` we can't distinguish a fresh install from an empty volume,
|
|
809
|
+
* so we report ok — a genuinely new DB legitimately has little history. Two
|
|
810
|
+
* indexed lookups, safe to run on every startup / health poll.
|
|
811
|
+
*/
|
|
812
|
+
declare function checkChainDataIntegrity(db: Kysely<Database>, opts?: {
|
|
813
|
+
checkFloor?: number
|
|
814
|
+
lookback?: number
|
|
815
|
+
}): Promise<ChainDataIntegrity>;
|
|
795
816
|
declare function computeContiguousTip(db: Kysely<Database>, fromHeight: number): Promise<number>;
|
|
796
|
-
export { findGaps, countMissingBlocks, computeContiguousTip, Gap };
|
|
817
|
+
export { findGaps, countMissingBlocks, computeContiguousTip, checkChainDataIntegrity, Gap, ChainDataIntegrity };
|
|
@@ -48,6 +48,36 @@ async function countMissingBlocks(db) {
|
|
|
48
48
|
`.execute(db);
|
|
49
49
|
return Number(rows[0]?.total ?? 0);
|
|
50
50
|
}
|
|
51
|
+
async function checkChainDataIntegrity(db, opts) {
|
|
52
|
+
const checkFloor = opts?.checkFloor ?? 1e6;
|
|
53
|
+
const lookback = opts?.lookback ?? 500000;
|
|
54
|
+
const { rows: tipRows } = await sql`
|
|
55
|
+
SELECT MAX(height) AS max FROM blocks WHERE canonical = true
|
|
56
|
+
`.execute(db);
|
|
57
|
+
const maxHeight = Number(tipRows[0]?.max ?? 0);
|
|
58
|
+
if (maxHeight < checkFloor) {
|
|
59
|
+
return {
|
|
60
|
+
ok: true,
|
|
61
|
+
maxHeight,
|
|
62
|
+
sampleHeight: null,
|
|
63
|
+
sampleBlocks: 0,
|
|
64
|
+
reason: null
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const sampleHeight = maxHeight - lookback;
|
|
68
|
+
const { rows: sampleRows } = await sql`
|
|
69
|
+
SELECT count(*) AS n FROM blocks
|
|
70
|
+
WHERE canonical = true AND height >= ${sampleHeight} AND height < ${sampleHeight + 1000}
|
|
71
|
+
`.execute(db);
|
|
72
|
+
const sampleBlocks = Number(sampleRows[0]?.n ?? 0);
|
|
73
|
+
return {
|
|
74
|
+
ok: sampleBlocks > 0,
|
|
75
|
+
maxHeight,
|
|
76
|
+
sampleHeight,
|
|
77
|
+
sampleBlocks,
|
|
78
|
+
reason: sampleBlocks > 0 ? null : `tip is ${maxHeight} but no canonical blocks near ${sampleHeight} — wrong or empty volume?`
|
|
79
|
+
};
|
|
80
|
+
}
|
|
51
81
|
async function computeContiguousTip(db, fromHeight) {
|
|
52
82
|
const { rows } = await sql`
|
|
53
83
|
SELECT COALESCE(MAX(height), ${fromHeight}) AS tip
|
|
@@ -65,8 +95,9 @@ async function computeContiguousTip(db, fromHeight) {
|
|
|
65
95
|
export {
|
|
66
96
|
findGaps,
|
|
67
97
|
countMissingBlocks,
|
|
68
|
-
computeContiguousTip
|
|
98
|
+
computeContiguousTip,
|
|
99
|
+
checkChainDataIntegrity
|
|
69
100
|
};
|
|
70
101
|
|
|
71
|
-
//# debugId=
|
|
102
|
+
//# debugId=EADA898ADC77C59264756E2164756E21
|
|
72
103
|
//# sourceMappingURL=integrity.js.map
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/db/queries/integrity.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import { type Kysely, sql } from \"kysely\";\nimport type { Database } from \"../types.ts\";\n\nexport interface Gap {\n\tgapStart: number;\n\tgapEnd: number;\n\tsize: number;\n}\n\nexport async function findGaps(\n\tdb: Kysely<Database>,\n\tlimit?: number,\n): Promise<Gap[]> {\n\tconst limitClause = limit ? sql`LIMIT ${limit}` : sql``;\n\tconst { rows } = await sql<{\n\t\tgap_start: string;\n\t\tgap_end: string;\n\t\tsize: string;\n\t}>`\n SELECT gap_start, gap_end, gap_end - gap_start + 1 AS size\n FROM (\n SELECT height + 1 AS gap_start, next_height - 1 AS gap_end\n FROM (\n SELECT height, LEAD(height) OVER (ORDER BY height) AS next_height\n FROM blocks WHERE canonical = true\n ) sub\n WHERE next_height - height > 1\n ) gaps\n ORDER BY gap_start\n ${limitClause}\n `.execute(db);\n\n\treturn rows.map((r) => ({\n\t\tgapStart: Number(r.gap_start),\n\t\tgapEnd: Number(r.gap_end),\n\t\tsize: Number(r.size),\n\t}));\n}\n\nexport async function countMissingBlocks(\n\tdb: Kysely<Database>,\n): Promise<number> {\n\tconst { rows } = await sql<{ total: string }>`\n SELECT COALESCE(SUM(next_height - height - 1), 0) AS total\n FROM (\n SELECT height, LEAD(height) OVER (ORDER BY height) AS next_height\n FROM blocks WHERE canonical = true\n ) sub\n WHERE next_height - height > 1\n `.execute(db);\n\n\treturn Number(rows[0]?.total ?? 0);\n}\n\nexport async function computeContiguousTip(\n\tdb: Kysely<Database>,\n\tfromHeight: number,\n): Promise<number> {\n\tconst { rows } = await sql<{ tip: string }>`\n SELECT COALESCE(MAX(height), ${fromHeight}) AS tip\n FROM (\n SELECT height, height - ROW_NUMBER() OVER (ORDER BY height) AS grp\n FROM blocks WHERE canonical = true AND height >= ${fromHeight}\n ) sub\n WHERE grp = (\n SELECT height - ROW_NUMBER() OVER (ORDER BY height)\n FROM blocks WHERE canonical = true AND height = ${fromHeight}\n )\n `.execute(db);\n\n\treturn Number(rows[0]?.tip ?? fromHeight);\n}\n"
|
|
5
|
+
"import { type Kysely, sql } from \"kysely\";\nimport type { Database } from \"../types.ts\";\n\nexport interface Gap {\n\tgapStart: number;\n\tgapEnd: number;\n\tsize: number;\n}\n\nexport async function findGaps(\n\tdb: Kysely<Database>,\n\tlimit?: number,\n): Promise<Gap[]> {\n\tconst limitClause = limit ? sql`LIMIT ${limit}` : sql``;\n\tconst { rows } = await sql<{\n\t\tgap_start: string;\n\t\tgap_end: string;\n\t\tsize: string;\n\t}>`\n SELECT gap_start, gap_end, gap_end - gap_start + 1 AS size\n FROM (\n SELECT height + 1 AS gap_start, next_height - 1 AS gap_end\n FROM (\n SELECT height, LEAD(height) OVER (ORDER BY height) AS next_height\n FROM blocks WHERE canonical = true\n ) sub\n WHERE next_height - height > 1\n ) gaps\n ORDER BY gap_start\n ${limitClause}\n `.execute(db);\n\n\treturn rows.map((r) => ({\n\t\tgapStart: Number(r.gap_start),\n\t\tgapEnd: Number(r.gap_end),\n\t\tsize: Number(r.size),\n\t}));\n}\n\nexport async function countMissingBlocks(\n\tdb: Kysely<Database>,\n): Promise<number> {\n\tconst { rows } = await sql<{ total: string }>`\n SELECT COALESCE(SUM(next_height - height - 1), 0) AS total\n FROM (\n SELECT height, LEAD(height) OVER (ORDER BY height) AS next_height\n FROM blocks WHERE canonical = true\n ) sub\n WHERE next_height - height > 1\n `.execute(db);\n\n\treturn Number(rows[0]?.total ?? 0);\n}\n\nexport interface ChainDataIntegrity {\n\tok: boolean;\n\tmaxHeight: number;\n\tsampleHeight: number | null;\n\tsampleBlocks: number;\n\treason: string | null;\n}\n\n/**\n * Cheap sanity check that the `blocks` table actually holds the history its tip\n * implies. Catches a wrong/empty Postgres volume — e.g. a container recreated\n * against a fresh volume — being silently served: if the tip is high but a\n * window of blocks well below it is missing, the data isn't what the tip claims.\n *\n * Below `checkFloor` we can't distinguish a fresh install from an empty volume,\n * so we report ok — a genuinely new DB legitimately has little history. Two\n * indexed lookups, safe to run on every startup / health poll.\n */\nexport async function checkChainDataIntegrity(\n\tdb: Kysely<Database>,\n\topts?: { checkFloor?: number; lookback?: number },\n): Promise<ChainDataIntegrity> {\n\tconst checkFloor = opts?.checkFloor ?? 1_000_000;\n\tconst lookback = opts?.lookback ?? 500_000;\n\n\tconst { rows: tipRows } = await sql<{ max: string | null }>`\n\t\tSELECT MAX(height) AS max FROM blocks WHERE canonical = true\n\t`.execute(db);\n\tconst maxHeight = Number(tipRows[0]?.max ?? 0);\n\n\tif (maxHeight < checkFloor) {\n\t\treturn {\n\t\t\tok: true,\n\t\t\tmaxHeight,\n\t\t\tsampleHeight: null,\n\t\t\tsampleBlocks: 0,\n\t\t\treason: null,\n\t\t};\n\t}\n\n\tconst sampleHeight = maxHeight - lookback;\n\tconst { rows: sampleRows } = await sql<{ n: string }>`\n\t\tSELECT count(*) AS n FROM blocks\n\t\tWHERE canonical = true AND height >= ${sampleHeight} AND height < ${sampleHeight + 1000}\n\t`.execute(db);\n\tconst sampleBlocks = Number(sampleRows[0]?.n ?? 0);\n\n\treturn {\n\t\tok: sampleBlocks > 0,\n\t\tmaxHeight,\n\t\tsampleHeight,\n\t\tsampleBlocks,\n\t\treason:\n\t\t\tsampleBlocks > 0\n\t\t\t\t? null\n\t\t\t\t: `tip is ${maxHeight} but no canonical blocks near ${sampleHeight} — wrong or empty volume?`,\n\t};\n}\n\nexport async function computeContiguousTip(\n\tdb: Kysely<Database>,\n\tfromHeight: number,\n): Promise<number> {\n\tconst { rows } = await sql<{ tip: string }>`\n SELECT COALESCE(MAX(height), ${fromHeight}) AS tip\n FROM (\n SELECT height, height - ROW_NUMBER() OVER (ORDER BY height) AS grp\n FROM blocks WHERE canonical = true AND height >= ${fromHeight}\n ) sub\n WHERE grp = (\n SELECT height - ROW_NUMBER() OVER (ORDER BY height)\n FROM blocks WHERE canonical = true AND height = ${fromHeight}\n )\n `.execute(db);\n\n\treturn Number(rows[0]?.tip ?? fromHeight);\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";;;;;;;;;;;;;;;;;AAAA;AASA,eAAsB,QAAQ,CAC7B,IACA,OACiB;AAAA,EACjB,MAAM,cAAc,QAAQ,YAAY,UAAU;AAAA,EAClD,QAAQ,SAAS,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAelB;AAAA,IACF,QAAQ,EAAE;AAAA,EAEb,OAAO,KAAK,IAAI,CAAC,OAAO;AAAA,IACvB,UAAU,OAAO,EAAE,SAAS;AAAA,IAC5B,QAAQ,OAAO,EAAE,OAAO;AAAA,IACxB,MAAM,OAAO,EAAE,IAAI;AAAA,EACpB,EAAE;AAAA;AAGH,eAAsB,kBAAkB,CACvC,IACkB;AAAA,EAClB,QAAQ,SAAS,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOpB,QAAQ,EAAE;AAAA,EAEb,OAAO,OAAO,KAAK,IAAI,SAAS,CAAC;AAAA;
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": ";;;;;;;;;;;;;;;;;AAAA;AASA,eAAsB,QAAQ,CAC7B,IACA,OACiB;AAAA,EACjB,MAAM,cAAc,QAAQ,YAAY,UAAU;AAAA,EAClD,QAAQ,SAAS,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAelB;AAAA,IACF,QAAQ,EAAE;AAAA,EAEb,OAAO,KAAK,IAAI,CAAC,OAAO;AAAA,IACvB,UAAU,OAAO,EAAE,SAAS;AAAA,IAC5B,QAAQ,OAAO,EAAE,OAAO;AAAA,IACxB,MAAM,OAAO,EAAE,IAAI;AAAA,EACpB,EAAE;AAAA;AAGH,eAAsB,kBAAkB,CACvC,IACkB;AAAA,EAClB,QAAQ,SAAS,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOpB,QAAQ,EAAE;AAAA,EAEb,OAAO,OAAO,KAAK,IAAI,SAAS,CAAC;AAAA;AAqBlC,eAAsB,uBAAuB,CAC5C,IACA,MAC8B;AAAA,EAC9B,MAAM,aAAa,MAAM,cAAc;AAAA,EACvC,MAAM,WAAW,MAAM,YAAY;AAAA,EAEnC,QAAQ,MAAM,YAAY,MAAM;AAAA;AAAA,GAE9B,QAAQ,EAAE;AAAA,EACZ,MAAM,YAAY,OAAO,QAAQ,IAAI,OAAO,CAAC;AAAA,EAE7C,IAAI,YAAY,YAAY;AAAA,IAC3B,OAAO;AAAA,MACN,IAAI;AAAA,MACJ;AAAA,MACA,cAAc;AAAA,MACd,cAAc;AAAA,MACd,QAAQ;AAAA,IACT;AAAA,EACD;AAAA,EAEA,MAAM,eAAe,YAAY;AAAA,EACjC,QAAQ,MAAM,eAAe,MAAM;AAAA;AAAA,yCAEK,6BAA6B,eAAe;AAAA,GAClF,QAAQ,EAAE;AAAA,EACZ,MAAM,eAAe,OAAO,WAAW,IAAI,KAAK,CAAC;AAAA,EAEjD,OAAO;AAAA,IACN,IAAI,eAAe;AAAA,IACnB;AAAA,IACA;AAAA,IACA;AAAA,IACA,QACC,eAAe,IACZ,OACA,UAAU,0CAA0C;AAAA,EACzD;AAAA;AAGD,eAAsB,oBAAoB,CACzC,IACA,YACkB;AAAA,EAClB,QAAQ,SAAS,MAAM;AAAA,mCACW;AAAA;AAAA;AAAA,yDAGsB;AAAA;AAAA;AAAA;AAAA,wDAID;AAAA;AAAA,IAEpD,QAAQ,EAAE;AAAA,EAEb,OAAO,OAAO,KAAK,IAAI,OAAO,UAAU;AAAA;",
|
|
8
|
+
"debugId": "EADA898ADC77C59264756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -172,7 +172,10 @@ class ArchiveReplayClient {
|
|
|
172
172
|
async replayGaps(gapHeights, indexerUrl, opts) {
|
|
173
173
|
if (gapHeights.size === 0)
|
|
174
174
|
return { replayed: 0, errors: 0 };
|
|
175
|
-
|
|
175
|
+
let maxHeight = 0;
|
|
176
|
+
for (const h of gapHeights)
|
|
177
|
+
if (h > maxHeight)
|
|
178
|
+
maxHeight = h;
|
|
176
179
|
let replayed = 0;
|
|
177
180
|
let errors = 0;
|
|
178
181
|
try {
|
|
@@ -251,9 +254,15 @@ class ArchiveReplayClient {
|
|
|
251
254
|
}
|
|
252
255
|
await proc.exited;
|
|
253
256
|
if (remaining.size > 0) {
|
|
257
|
+
const sample = [];
|
|
258
|
+
for (const h of remaining) {
|
|
259
|
+
sample.push(h);
|
|
260
|
+
if (sample.length === 5)
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
254
263
|
logger.warn("Archive replay: some heights not found in archive", {
|
|
255
264
|
missing: remaining.size,
|
|
256
|
-
sample
|
|
265
|
+
sample
|
|
257
266
|
});
|
|
258
267
|
}
|
|
259
268
|
logger.info("Archive replay: complete", {
|
|
@@ -385,5 +394,5 @@ export {
|
|
|
385
394
|
ArchiveReplayClient
|
|
386
395
|
};
|
|
387
396
|
|
|
388
|
-
//# debugId=
|
|
397
|
+
//# debugId=6397FCF10611814B64756E2164756E21
|
|
389
398
|
//# sourceMappingURL=archive-client.js.map
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"import { z } from \"zod/v4\";\n\n// Parse comma-separated networks\nconst networksSchema = z.string().transform((val) => {\n\tconst networks = val\n\t\t.split(\",\")\n\t\t.map((n) => n.trim())\n\t\t.filter(Boolean);\n\tconst valid = [\"mainnet\", \"testnet\"];\n\tfor (const n of networks) {\n\t\tif (!valid.includes(n)) {\n\t\t\tthrow new Error(\n\t\t\t\t`Invalid network: ${n}. Must be one of: ${valid.join(\", \")}`,\n\t\t\t);\n\t\t}\n\t}\n\treturn networks as (\"mainnet\" | \"testnet\")[];\n});\n\ninterface EnvSchemaOutput {\n\tDATABASE_URL?: string;\n\t/**\n\t * Shared indexer DB (blocks/txs/events). Falls back to DATABASE_URL.\n\t * Set this alongside TARGET_DATABASE_URL to enable dual-DB mode.\n\t */\n\tSOURCE_DATABASE_URL?: string;\n\t/**\n\t * Tenant DB (subgraph schemas + subgraphs table). Falls back to DATABASE_URL.\n\t * Set this alongside SOURCE_DATABASE_URL to enable dual-DB mode.\n\t */\n\tTARGET_DATABASE_URL?: string;\n\tNETWORK?: \"mainnet\" | \"testnet\";\n\tNETWORKS?: (\"mainnet\" | \"testnet\")[];\n\tLOG_LEVEL: \"debug\" | \"info\" | \"warn\" | \"error\";\n\tNODE_ENV: \"development\" | \"production\" | \"test\";\n}\n\n// Cast needed: z.preprocess / z.default create different _input vs _output types\n// that z.ZodType<T> can't represent without explicit input type param\nconst envSchema: z.ZodType<EnvSchemaOutput> = z.object({\n\tDATABASE_URL: z.preprocess(\n\t\t(val) => (typeof val === \"string\" && val.length === 0 ? undefined : val),\n\t\tz.string().url().optional(),\n\t),\n\tSOURCE_DATABASE_URL: z.preprocess(\n\t\t(val) => (typeof val === \"string\" && val.length === 0 ? undefined : val),\n\t\tz.string().url().optional(),\n\t),\n\tTARGET_DATABASE_URL: z.preprocess(\n\t\t(val) => (typeof val === \"string\" && val.length === 0 ? undefined : val),\n\t\tz.string().url().optional(),\n\t),\n\tNETWORK: z.enum([\"mainnet\", \"testnet\"]).optional(),\n\tNETWORKS: networksSchema.optional(),\n\tLOG_LEVEL: z.enum([\"debug\", \"info\", \"warn\", \"error\"]).default(\"info\"),\n\tNODE_ENV: z\n\t\t.enum([\"development\", \"production\", \"test\"])\n\t\t.default(\"development\"),\n}) as unknown as z.ZodType<EnvSchemaOutput>;\n\nexport type Env = EnvSchemaOutput & {\n\tenabledNetworks: (\"mainnet\" | \"testnet\")[];\n};\n\nlet cachedEnv: Env | null = null;\n\nexport function getEnv(): Env {\n\tif (cachedEnv) {\n\t\treturn cachedEnv;\n\t}\n\n\tconst result = envSchema.safeParse(process.env);\n\n\tif (!result.success) {\n\t\tconsole.error(\"❌ Invalid environment configuration:\");\n\t\tconsole.error(z.treeifyError(result.error));\n\t\tthrow new Error(\"Invalid environment configuration\");\n\t}\n\n\t// Compute enabled networks from NETWORKS or NETWORK\n\tlet enabledNetworks: (\"mainnet\" | \"testnet\")[];\n\tif (result.data.NETWORKS && result.data.NETWORKS.length > 0) {\n\t\tenabledNetworks = result.data.NETWORKS;\n\t} else if (result.data.NETWORK) {\n\t\tenabledNetworks = [result.data.NETWORK];\n\t} else {\n\t\tenabledNetworks = [\"mainnet\"]; // Default\n\t}\n\n\tcachedEnv = { ...result.data, enabledNetworks };\n\treturn cachedEnv;\n}\n\n// Export for testing\nexport { envSchema };\n",
|
|
6
6
|
"import { getEnv } from \"./env.ts\";\n\ntype LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\nconst LOG_LEVELS: Record<LogLevel, number> = {\n\tdebug: 0,\n\tinfo: 1,\n\twarn: 2,\n\terror: 3,\n};\n\nclass Logger {\n\tprivate _level?: LogLevel;\n\tprivate _isProduction?: boolean;\n\tprivate _initialized = false;\n\n\tprivate init() {\n\t\tif (this._initialized) return;\n\t\tthis._initialized = true;\n\t\ttry {\n\t\t\tconst env = getEnv();\n\t\t\tthis._level = env.LOG_LEVEL;\n\t\t\tthis._isProduction = env.NODE_ENV === \"production\";\n\t\t} catch {\n\t\t\t// Fallback when env is unavailable (e.g. tests without DATABASE_URL)\n\t\t\tthis._level = \"info\";\n\t\t\tthis._isProduction = false;\n\t\t}\n\t}\n\n\tprivate get level(): LogLevel {\n\t\tthis.init();\n\t\t// biome-ignore lint/style/noNonNullAssertion: value is non-null after preceding check or by construction; TS narrowing limitation\n\t\treturn this._level!;\n\t}\n\n\tprivate get isProduction(): boolean {\n\t\tthis.init();\n\t\t// biome-ignore lint/style/noNonNullAssertion: value is non-null after preceding check or by construction; TS narrowing limitation\n\t\treturn this._isProduction!;\n\t}\n\n\tprivate shouldLog(level: LogLevel): boolean {\n\t\treturn LOG_LEVELS[level] >= LOG_LEVELS[this.level];\n\t}\n\n\tprivate formatMessage(\n\t\tlevel: LogLevel,\n\t\tmessage: string,\n\t\t// biome-ignore lint/suspicious/noExplicitAny: interop boundary or dynamic-shape value where typing adds friction without runtime safety\n\t\tmeta?: Record<string, any>,\n\t) {\n\t\tconst timestamp = new Date().toISOString();\n\n\t\tif (this.isProduction) {\n\t\t\t// JSON output for production\n\t\t\treturn JSON.stringify({\n\t\t\t\ttimestamp,\n\t\t\t\tlevel,\n\t\t\t\tmessage,\n\t\t\t\t...meta,\n\t\t\t});\n\t\t}\n\n\t\t// Human-readable output for development\n\t\tconst metaStr = meta ? ` ${JSON.stringify(meta)}` : \"\";\n\t\treturn `[${timestamp}] ${level.toUpperCase()}: ${message}${metaStr}`;\n\t}\n\n\t// biome-ignore lint/suspicious/noExplicitAny: interop boundary or dynamic-shape value where typing adds friction without runtime safety\n\tdebug(message: string, meta?: Record<string, any>): void {\n\t\tif (this.shouldLog(\"debug\")) {\n\t\t\tconsole.debug(this.formatMessage(\"debug\", message, meta));\n\t\t}\n\t}\n\n\t// biome-ignore lint/suspicious/noExplicitAny: interop boundary or dynamic-shape value where typing adds friction without runtime safety\n\tinfo(message: string, meta?: Record<string, any>): void {\n\t\tif (this.shouldLog(\"info\")) {\n\t\t\tconsole.info(this.formatMessage(\"info\", message, meta));\n\t\t}\n\t}\n\n\t// biome-ignore lint/suspicious/noExplicitAny: interop boundary or dynamic-shape value where typing adds friction without runtime safety\n\twarn(message: string, meta?: Record<string, any>): void {\n\t\tif (this.shouldLog(\"warn\")) {\n\t\t\tconsole.warn(this.formatMessage(\"warn\", message, meta));\n\t\t}\n\t}\n\n\t// biome-ignore lint/suspicious/noExplicitAny: interop boundary or dynamic-shape value where typing adds friction without runtime safety\n\terror(message: string, meta?: Record<string, any>): void {\n\t\tif (this.shouldLog(\"error\")) {\n\t\t\tconsole.error(this.formatMessage(\"error\", message, meta));\n\t\t}\n\t}\n}\n\n// Export singleton instance\nexport const logger: Logger = new Logger();\n",
|
|
7
|
-
"/**\n * Archive Replay Client — backfills blocks from Hiro's daily event observer archive.\n *\n * The archive at archive.hiro.so contains zstd-compressed TSV files of raw\n * /new_block payloads (the exact NewBlockPayload JSON our indexer expects).\n * This client downloads the archive, streams + filters for specific block\n * heights, and POSTs matching payloads directly to the indexer.\n *\n * Caches the archive locally for up to 24h to avoid redundant ~25GB downloads.\n * Zero external API dependency — only needs the static archive file.\n */\n\nimport {\n\texistsSync,\n\treadFileSync,\n\trenameSync,\n\tunlinkSync,\n\twriteFileSync,\n} from \"node:fs\";\nimport { logger } from \"../logger.ts\";\n\nconst DEFAULT_ARCHIVE_URL =\n\t\"https://archive.hiro.so/mainnet/stacks-blockchain-api/mainnet-stacks-blockchain-api-latest.zst\";\n\nconst HEIGHT_REGEX = /\"block_height\":\\s*(\\d+)/;\nconst CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours\n\ninterface ArchiveMeta {\n\tlastModified: string | null;\n\tdownloadedAt: string;\n}\n\nexport interface ReplayResult {\n\treplayed: number;\n\terrors: number;\n}\n\nexport interface ReplayOptions {\n\tonProgress?: (count: number, height: number) => void;\n}\n\nexport class ArchiveReplayClient {\n\tprivate archiveUrl: string;\n\tprivate archiveDir: string;\n\n\tconstructor(opts?: { archiveUrl?: string; archiveDir?: string }) {\n\t\tthis.archiveUrl =\n\t\t\topts?.archiveUrl || process.env.ARCHIVE_URL || DEFAULT_ARCHIVE_URL;\n\t\tthis.archiveDir = opts?.archiveDir || process.env.ARCHIVE_DIR || \"/tmp\";\n\t}\n\n\tprivate get archivePath() {\n\t\treturn `${this.archiveDir}/secondlayer-archive.zst`;\n\t}\n\tprivate get metaPath() {\n\t\treturn `${this.archiveDir}/secondlayer-archive.meta.json`;\n\t}\n\tprivate get partialPath() {\n\t\treturn `${this.archiveDir}/secondlayer-archive.zst.partial`;\n\t}\n\n\t/** HEAD request to archive URL — verify reachable and has content */\n\tasync isAvailable(): Promise<boolean> {\n\t\ttry {\n\t\t\tconst res = await fetch(this.archiveUrl, {\n\t\t\t\tmethod: \"HEAD\",\n\t\t\t\tsignal: AbortSignal.timeout(15_000),\n\t\t\t});\n\t\t\tconst contentLength = Number(res.headers.get(\"content-length\") || 0);\n\t\t\treturn res.ok && contentLength > 0;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Download archive, stream-decompress, replay blocks matching gapHeights\n\t * to the indexer's /new_block endpoint.\n\t */\n\tasync replayGaps(\n\t\tgapHeights: Set<number>,\n\t\tindexerUrl: string,\n\t\topts?: ReplayOptions,\n\t): Promise<ReplayResult> {\n\t\tif (gapHeights.size === 0) return { replayed: 0, errors: 0 };\n\n\t\tconst maxHeight = Math.max(...gapHeights);\n\t\tlet replayed = 0;\n\t\tlet errors = 0;\n\n\t\ttry {\n\t\t\tawait this.ensureArchive();\n\n\t\t\tlogger.info(\"Archive replay: starting decompression + replay\", {\n\t\t\t\ttargetHeights: gapHeights.size,\n\t\t\t\tmaxHeight,\n\t\t\t});\n\n\t\t\t// Decompress via zstd subprocess\n\t\t\tconst proc = Bun.spawn([\"zstd\", \"-d\", this.archivePath, \"--stdout\"], {\n\t\t\t\tstdout: \"pipe\",\n\t\t\t\tstderr: \"ignore\",\n\t\t\t});\n\n\t\t\tconst reader = proc.stdout.getReader();\n\t\t\tconst decoder = new TextDecoder();\n\t\t\tlet buffer = \"\";\n\t\t\tconst remaining = new Set(gapHeights);\n\n\t\t\twhile (true) {\n\t\t\t\tconst { done, value } = await reader.read();\n\t\t\t\tif (done) break;\n\n\t\t\t\tbuffer += decoder.decode(value, { stream: true });\n\t\t\t\tconst lines = buffer.split(\"\\n\");\n\t\t\t\tbuffer = lines.pop() || \"\";\n\n\t\t\t\tfor (const line of lines) {\n\t\t\t\t\tif (remaining.size === 0) break;\n\n\t\t\t\t\t// Quick height check via regex (avoid full JSON parse)\n\t\t\t\t\tconst match = HEIGHT_REGEX.exec(line);\n\t\t\t\t\tif (!match) continue;\n\n\t\t\t\t\tconst height = Number.parseInt(match[1]);\n\n\t\t\t\t\t// Skip blocks we don't need\n\t\t\t\t\tif (!remaining.has(height)) {\n\t\t\t\t\t\t// Early exit if past all gap heights\n\t\t\t\t\t\tif (height > maxHeight) break;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Extract payload from TSV (id \\t timestamp \\t path \\t payload)\n\t\t\t\t\tconst tabIdx3 = nthIndex(line, \"\\t\", 3);\n\t\t\t\t\tif (tabIdx3 === -1) continue;\n\n\t\t\t\t\tconst path = line.substring(nthIndex(line, \"\\t\", 2) + 1, tabIdx3);\n\t\t\t\t\tif (path !== \"/new_block\") continue;\n\n\t\t\t\t\tconst payload = line.substring(tabIdx3 + 1);\n\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst res = await fetch(`${indexerUrl}/new_block`, {\n\t\t\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\t\t\t\t\"X-Source\": \"archive-replay\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tbody: payload,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tif (res.ok) {\n\t\t\t\t\t\t\treplayed++;\n\t\t\t\t\t\t\tremaining.delete(height);\n\t\t\t\t\t\t\topts?.onProgress?.(replayed, height);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\terrors++;\n\t\t\t\t\t\t\tlogger.warn(\"Archive replay: indexer rejected block\", {\n\t\t\t\t\t\t\t\theight,\n\t\t\t\t\t\t\t\tstatus: res.status,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\terrors++;\n\t\t\t\t\t\tlogger.warn(\"Archive replay: POST failed\", {\n\t\t\t\t\t\t\theight,\n\t\t\t\t\t\t\terror: String(err),\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Early exit if all gaps filled or past max height\n\t\t\t\tif (remaining.size === 0) {\n\t\t\t\t\tproc.kill();\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Wait for process to exit\n\t\t\tawait proc.exited;\n\n\t\t\tif (remaining.size > 0) {\n\t\t\t\tlogger.warn(\"Archive replay: some heights not found in archive\", {\n\t\t\t\t\tmissing: remaining.size,\n\t\t\t\t\tsample: [...remaining].slice(0, 5),\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tlogger.info(\"Archive replay: complete\", {\n\t\t\t\treplayed,\n\t\t\t\terrors,\n\t\t\t\tmissing: remaining.size,\n\t\t\t});\n\t\t} catch (err) {\n\t\t\t// Clean up on error (corrupt/partial downloads)\n\t\t\tthis.cleanupFile(this.archivePath);\n\t\t\tthis.cleanupFile(this.metaPath);\n\t\t\tthis.cleanupFile(this.partialPath);\n\t\t\tthrow err;\n\t\t}\n\n\t\treturn { replayed, errors };\n\t}\n\n\t/**\n\t * Ensure a fresh-enough archive exists locally.\n\t * Uses HTTP conditional requests to avoid redundant downloads.\n\t */\n\tprivate async ensureArchive(): Promise<void> {\n\t\tthis.cleanStaleFiles();\n\n\t\tconst meta = this.readMeta();\n\t\tconst cached = existsSync(this.archivePath) && meta !== null;\n\n\t\tif (cached) {\n\t\t\tconst age = Date.now() - new Date(meta.downloadedAt).getTime();\n\t\t\tif (age < CACHE_MAX_AGE_MS) {\n\t\t\t\t// Cache is fresh enough — check if remote has a newer version\n\t\t\t\tconst headers: Record<string, string> = {};\n\t\t\t\tif (meta.lastModified) {\n\t\t\t\t\theaders[\"If-Modified-Since\"] = meta.lastModified;\n\t\t\t\t}\n\n\t\t\t\ttry {\n\t\t\t\t\tconst res = await fetch(this.archiveUrl, {\n\t\t\t\t\t\tmethod: \"HEAD\",\n\t\t\t\t\t\theaders,\n\t\t\t\t\t\tsignal: AbortSignal.timeout(15_000),\n\t\t\t\t\t});\n\n\t\t\t\t\tif (res.status === 304) {\n\t\t\t\t\t\tlogger.info(\"Archive replay: using cached archive\", {\n\t\t\t\t\t\t\tageHrs: (age / 3600000).toFixed(1),\n\t\t\t\t\t\t});\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// 200 = remote is newer, re-download below\n\t\t\t\t\tlogger.info(\n\t\t\t\t\t\t\"Archive replay: remote archive is newer, re-downloading\",\n\t\t\t\t\t);\n\t\t\t\t} catch {\n\t\t\t\t\t// Can't reach remote — use cache anyway\n\t\t\t\t\tlogger.info(\n\t\t\t\t\t\t\"Archive replay: remote unreachable, using cached archive\",\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tlogger.info(\"Archive replay: cache expired, re-downloading\");\n\t\t\t}\n\t\t}\n\n\t\t// Download fresh archive\n\t\tlogger.info(\"Archive replay: downloading archive\", {\n\t\t\turl: this.archiveUrl.split(\"/\").pop(),\n\t\t});\n\n\t\tconst lastModified = await this.download(this.partialPath);\n\n\t\t// Atomic rename: partial → final\n\t\trenameSync(this.partialPath, this.archivePath);\n\n\t\t// Write meta sidecar\n\t\tthis.writeMeta({ lastModified, downloadedAt: new Date().toISOString() });\n\n\t\tlogger.info(\"Archive replay: download complete\");\n\t}\n\n\t/** Remove stale cache (> 24h) and orphaned partial files */\n\tprivate cleanStaleFiles(): void {\n\t\ttry {\n\t\t\t// Clean orphaned partial downloads\n\t\t\tif (existsSync(this.partialPath)) {\n\t\t\t\tunlinkSync(this.partialPath);\n\t\t\t}\n\n\t\t\t// Clean stale cache\n\t\t\tconst meta = this.readMeta();\n\t\t\tif (meta) {\n\t\t\t\tconst age = Date.now() - new Date(meta.downloadedAt).getTime();\n\t\t\t\tif (age > CACHE_MAX_AGE_MS) {\n\t\t\t\t\tthis.cleanupFile(this.archivePath);\n\t\t\t\t\tthis.cleanupFile(this.metaPath);\n\t\t\t\t\tlogger.info(\"Archive replay: cleaned stale cache\");\n\t\t\t\t}\n\t\t\t} else if (existsSync(this.archivePath)) {\n\t\t\t\t// Archive without meta — orphaned, clean up\n\t\t\t\tthis.cleanupFile(this.archivePath);\n\t\t\t}\n\t\t} catch {\n\t\t\t// Best-effort cleanup\n\t\t}\n\t}\n\n\tprivate readMeta(): ArchiveMeta | null {\n\t\ttry {\n\t\t\tif (!existsSync(this.metaPath)) return null;\n\t\t\treturn JSON.parse(readFileSync(this.metaPath, \"utf-8\")) as ArchiveMeta;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprivate writeMeta(meta: ArchiveMeta): void {\n\t\ttry {\n\t\t\twriteFileSync(this.metaPath, JSON.stringify(meta));\n\t\t} catch {\n\t\t\t// Best-effort\n\t\t}\n\t}\n\n\tprivate cleanupFile(path: string): void {\n\t\ttry {\n\t\t\tif (existsSync(path)) unlinkSync(path);\n\t\t} catch {\n\t\t\t// Best-effort\n\t\t}\n\t}\n\n\t/** Download archive to disk with streaming. Returns Last-Modified header. */\n\tprivate async download(destPath: string): Promise<string | null> {\n\t\tconst res = await fetch(this.archiveUrl, {\n\t\t\tsignal: AbortSignal.timeout(30 * 60 * 1000), // 30 min timeout\n\t\t});\n\n\t\tif (!res.ok || !res.body) {\n\t\t\tthrow new Error(`Archive download failed: HTTP ${res.status}`);\n\t\t}\n\n\t\tconst lastModified = res.headers.get(\"last-modified\");\n\t\tconst totalBytes = Number(res.headers.get(\"content-length\") || 0);\n\t\tconst writer = Bun.file(destPath).writer();\n\t\tlet downloaded = 0;\n\t\tlet lastLog = 0;\n\n\t\tfor await (const chunk of res.body) {\n\t\t\twriter.write(chunk);\n\t\t\tdownloaded += chunk.byteLength;\n\n\t\t\t// Log progress every 5GB\n\t\t\tif (totalBytes > 0 && downloaded - lastLog > 5_000_000_000) {\n\t\t\t\tlastLog = downloaded;\n\t\t\t\tconst pct = ((downloaded / totalBytes) * 100).toFixed(0);\n\t\t\t\tlogger.info(\"Archive replay: downloading\", { progress: `${pct}%` });\n\t\t\t}\n\t\t}\n\n\t\tawait writer.end();\n\t\treturn lastModified;\n\t}\n}\n\n/** Find the nth occurrence of a character in a string */\nfunction nthIndex(str: string, char: string, n: number): number {\n\tlet idx = -1;\n\tfor (let i = 0; i < n; i++) {\n\t\tidx = str.indexOf(char, idx + 1);\n\t\tif (idx === -1) return -1;\n\t}\n\treturn idx;\n}\n"
|
|
7
|
+
"/**\n * Archive Replay Client — backfills blocks from Hiro's daily event observer archive.\n *\n * The archive at archive.hiro.so contains zstd-compressed TSV files of raw\n * /new_block payloads (the exact NewBlockPayload JSON our indexer expects).\n * This client downloads the archive, streams + filters for specific block\n * heights, and POSTs matching payloads directly to the indexer.\n *\n * Caches the archive locally for up to 24h to avoid redundant ~25GB downloads.\n * Zero external API dependency — only needs the static archive file.\n */\n\nimport {\n\texistsSync,\n\treadFileSync,\n\trenameSync,\n\tunlinkSync,\n\twriteFileSync,\n} from \"node:fs\";\nimport { logger } from \"../logger.ts\";\n\nconst DEFAULT_ARCHIVE_URL =\n\t\"https://archive.hiro.so/mainnet/stacks-blockchain-api/mainnet-stacks-blockchain-api-latest.zst\";\n\nconst HEIGHT_REGEX = /\"block_height\":\\s*(\\d+)/;\nconst CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours\n\ninterface ArchiveMeta {\n\tlastModified: string | null;\n\tdownloadedAt: string;\n}\n\nexport interface ReplayResult {\n\treplayed: number;\n\terrors: number;\n}\n\nexport interface ReplayOptions {\n\tonProgress?: (count: number, height: number) => void;\n}\n\nexport class ArchiveReplayClient {\n\tprivate archiveUrl: string;\n\tprivate archiveDir: string;\n\n\tconstructor(opts?: { archiveUrl?: string; archiveDir?: string }) {\n\t\tthis.archiveUrl =\n\t\t\topts?.archiveUrl || process.env.ARCHIVE_URL || DEFAULT_ARCHIVE_URL;\n\t\tthis.archiveDir = opts?.archiveDir || process.env.ARCHIVE_DIR || \"/tmp\";\n\t}\n\n\tprivate get archivePath() {\n\t\treturn `${this.archiveDir}/secondlayer-archive.zst`;\n\t}\n\tprivate get metaPath() {\n\t\treturn `${this.archiveDir}/secondlayer-archive.meta.json`;\n\t}\n\tprivate get partialPath() {\n\t\treturn `${this.archiveDir}/secondlayer-archive.zst.partial`;\n\t}\n\n\t/** HEAD request to archive URL — verify reachable and has content */\n\tasync isAvailable(): Promise<boolean> {\n\t\ttry {\n\t\t\tconst res = await fetch(this.archiveUrl, {\n\t\t\t\tmethod: \"HEAD\",\n\t\t\t\tsignal: AbortSignal.timeout(15_000),\n\t\t\t});\n\t\t\tconst contentLength = Number(res.headers.get(\"content-length\") || 0);\n\t\t\treturn res.ok && contentLength > 0;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/**\n\t * Download archive, stream-decompress, replay blocks matching gapHeights\n\t * to the indexer's /new_block endpoint.\n\t */\n\tasync replayGaps(\n\t\tgapHeights: Set<number>,\n\t\tindexerUrl: string,\n\t\topts?: ReplayOptions,\n\t): Promise<ReplayResult> {\n\t\tif (gapHeights.size === 0) return { replayed: 0, errors: 0 };\n\n\t\t// Iterate, don't spread: a full-history gap set is millions of heights,\n\t\t// and `Math.max(...set)` spreads them all as call arguments → instant\n\t\t// RangeError (max call-stack) on any large backfill.\n\t\tlet maxHeight = 0;\n\t\tfor (const h of gapHeights) if (h > maxHeight) maxHeight = h;\n\t\tlet replayed = 0;\n\t\tlet errors = 0;\n\n\t\ttry {\n\t\t\tawait this.ensureArchive();\n\n\t\t\tlogger.info(\"Archive replay: starting decompression + replay\", {\n\t\t\t\ttargetHeights: gapHeights.size,\n\t\t\t\tmaxHeight,\n\t\t\t});\n\n\t\t\t// Decompress via zstd subprocess\n\t\t\tconst proc = Bun.spawn([\"zstd\", \"-d\", this.archivePath, \"--stdout\"], {\n\t\t\t\tstdout: \"pipe\",\n\t\t\t\tstderr: \"ignore\",\n\t\t\t});\n\n\t\t\tconst reader = proc.stdout.getReader();\n\t\t\tconst decoder = new TextDecoder();\n\t\t\tlet buffer = \"\";\n\t\t\tconst remaining = new Set(gapHeights);\n\n\t\t\twhile (true) {\n\t\t\t\tconst { done, value } = await reader.read();\n\t\t\t\tif (done) break;\n\n\t\t\t\tbuffer += decoder.decode(value, { stream: true });\n\t\t\t\tconst lines = buffer.split(\"\\n\");\n\t\t\t\tbuffer = lines.pop() || \"\";\n\n\t\t\t\tfor (const line of lines) {\n\t\t\t\t\tif (remaining.size === 0) break;\n\n\t\t\t\t\t// Quick height check via regex (avoid full JSON parse)\n\t\t\t\t\tconst match = HEIGHT_REGEX.exec(line);\n\t\t\t\t\tif (!match) continue;\n\n\t\t\t\t\tconst height = Number.parseInt(match[1]);\n\n\t\t\t\t\t// Skip blocks we don't need\n\t\t\t\t\tif (!remaining.has(height)) {\n\t\t\t\t\t\t// Early exit if past all gap heights\n\t\t\t\t\t\tif (height > maxHeight) break;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Extract payload from TSV (id \\t timestamp \\t path \\t payload)\n\t\t\t\t\tconst tabIdx3 = nthIndex(line, \"\\t\", 3);\n\t\t\t\t\tif (tabIdx3 === -1) continue;\n\n\t\t\t\t\tconst path = line.substring(nthIndex(line, \"\\t\", 2) + 1, tabIdx3);\n\t\t\t\t\tif (path !== \"/new_block\") continue;\n\n\t\t\t\t\tconst payload = line.substring(tabIdx3 + 1);\n\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst res = await fetch(`${indexerUrl}/new_block`, {\n\t\t\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\t\t\t\t\"X-Source\": \"archive-replay\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tbody: payload,\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tif (res.ok) {\n\t\t\t\t\t\t\treplayed++;\n\t\t\t\t\t\t\tremaining.delete(height);\n\t\t\t\t\t\t\topts?.onProgress?.(replayed, height);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\terrors++;\n\t\t\t\t\t\t\tlogger.warn(\"Archive replay: indexer rejected block\", {\n\t\t\t\t\t\t\t\theight,\n\t\t\t\t\t\t\t\tstatus: res.status,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\terrors++;\n\t\t\t\t\t\tlogger.warn(\"Archive replay: POST failed\", {\n\t\t\t\t\t\t\theight,\n\t\t\t\t\t\t\terror: String(err),\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Early exit if all gaps filled or past max height\n\t\t\t\tif (remaining.size === 0) {\n\t\t\t\t\tproc.kill();\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Wait for process to exit\n\t\t\tawait proc.exited;\n\n\t\t\tif (remaining.size > 0) {\n\t\t\t\t// Don't spread `remaining` (can be millions) just to sample a few.\n\t\t\t\tconst sample: number[] = [];\n\t\t\t\tfor (const h of remaining) {\n\t\t\t\t\tsample.push(h);\n\t\t\t\t\tif (sample.length === 5) break;\n\t\t\t\t}\n\t\t\t\tlogger.warn(\"Archive replay: some heights not found in archive\", {\n\t\t\t\t\tmissing: remaining.size,\n\t\t\t\t\tsample,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tlogger.info(\"Archive replay: complete\", {\n\t\t\t\treplayed,\n\t\t\t\terrors,\n\t\t\t\tmissing: remaining.size,\n\t\t\t});\n\t\t} catch (err) {\n\t\t\t// Clean up on error (corrupt/partial downloads)\n\t\t\tthis.cleanupFile(this.archivePath);\n\t\t\tthis.cleanupFile(this.metaPath);\n\t\t\tthis.cleanupFile(this.partialPath);\n\t\t\tthrow err;\n\t\t}\n\n\t\treturn { replayed, errors };\n\t}\n\n\t/**\n\t * Ensure a fresh-enough archive exists locally.\n\t * Uses HTTP conditional requests to avoid redundant downloads.\n\t */\n\tprivate async ensureArchive(): Promise<void> {\n\t\tthis.cleanStaleFiles();\n\n\t\tconst meta = this.readMeta();\n\t\tconst cached = existsSync(this.archivePath) && meta !== null;\n\n\t\tif (cached) {\n\t\t\tconst age = Date.now() - new Date(meta.downloadedAt).getTime();\n\t\t\tif (age < CACHE_MAX_AGE_MS) {\n\t\t\t\t// Cache is fresh enough — check if remote has a newer version\n\t\t\t\tconst headers: Record<string, string> = {};\n\t\t\t\tif (meta.lastModified) {\n\t\t\t\t\theaders[\"If-Modified-Since\"] = meta.lastModified;\n\t\t\t\t}\n\n\t\t\t\ttry {\n\t\t\t\t\tconst res = await fetch(this.archiveUrl, {\n\t\t\t\t\t\tmethod: \"HEAD\",\n\t\t\t\t\t\theaders,\n\t\t\t\t\t\tsignal: AbortSignal.timeout(15_000),\n\t\t\t\t\t});\n\n\t\t\t\t\tif (res.status === 304) {\n\t\t\t\t\t\tlogger.info(\"Archive replay: using cached archive\", {\n\t\t\t\t\t\t\tageHrs: (age / 3600000).toFixed(1),\n\t\t\t\t\t\t});\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// 200 = remote is newer, re-download below\n\t\t\t\t\tlogger.info(\n\t\t\t\t\t\t\"Archive replay: remote archive is newer, re-downloading\",\n\t\t\t\t\t);\n\t\t\t\t} catch {\n\t\t\t\t\t// Can't reach remote — use cache anyway\n\t\t\t\t\tlogger.info(\n\t\t\t\t\t\t\"Archive replay: remote unreachable, using cached archive\",\n\t\t\t\t\t);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tlogger.info(\"Archive replay: cache expired, re-downloading\");\n\t\t\t}\n\t\t}\n\n\t\t// Download fresh archive\n\t\tlogger.info(\"Archive replay: downloading archive\", {\n\t\t\turl: this.archiveUrl.split(\"/\").pop(),\n\t\t});\n\n\t\tconst lastModified = await this.download(this.partialPath);\n\n\t\t// Atomic rename: partial → final\n\t\trenameSync(this.partialPath, this.archivePath);\n\n\t\t// Write meta sidecar\n\t\tthis.writeMeta({ lastModified, downloadedAt: new Date().toISOString() });\n\n\t\tlogger.info(\"Archive replay: download complete\");\n\t}\n\n\t/** Remove stale cache (> 24h) and orphaned partial files */\n\tprivate cleanStaleFiles(): void {\n\t\ttry {\n\t\t\t// Clean orphaned partial downloads\n\t\t\tif (existsSync(this.partialPath)) {\n\t\t\t\tunlinkSync(this.partialPath);\n\t\t\t}\n\n\t\t\t// Clean stale cache\n\t\t\tconst meta = this.readMeta();\n\t\t\tif (meta) {\n\t\t\t\tconst age = Date.now() - new Date(meta.downloadedAt).getTime();\n\t\t\t\tif (age > CACHE_MAX_AGE_MS) {\n\t\t\t\t\tthis.cleanupFile(this.archivePath);\n\t\t\t\t\tthis.cleanupFile(this.metaPath);\n\t\t\t\t\tlogger.info(\"Archive replay: cleaned stale cache\");\n\t\t\t\t}\n\t\t\t} else if (existsSync(this.archivePath)) {\n\t\t\t\t// Archive without meta — orphaned, clean up\n\t\t\t\tthis.cleanupFile(this.archivePath);\n\t\t\t}\n\t\t} catch {\n\t\t\t// Best-effort cleanup\n\t\t}\n\t}\n\n\tprivate readMeta(): ArchiveMeta | null {\n\t\ttry {\n\t\t\tif (!existsSync(this.metaPath)) return null;\n\t\t\treturn JSON.parse(readFileSync(this.metaPath, \"utf-8\")) as ArchiveMeta;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprivate writeMeta(meta: ArchiveMeta): void {\n\t\ttry {\n\t\t\twriteFileSync(this.metaPath, JSON.stringify(meta));\n\t\t} catch {\n\t\t\t// Best-effort\n\t\t}\n\t}\n\n\tprivate cleanupFile(path: string): void {\n\t\ttry {\n\t\t\tif (existsSync(path)) unlinkSync(path);\n\t\t} catch {\n\t\t\t// Best-effort\n\t\t}\n\t}\n\n\t/** Download archive to disk with streaming. Returns Last-Modified header. */\n\tprivate async download(destPath: string): Promise<string | null> {\n\t\tconst res = await fetch(this.archiveUrl, {\n\t\t\tsignal: AbortSignal.timeout(30 * 60 * 1000), // 30 min timeout\n\t\t});\n\n\t\tif (!res.ok || !res.body) {\n\t\t\tthrow new Error(`Archive download failed: HTTP ${res.status}`);\n\t\t}\n\n\t\tconst lastModified = res.headers.get(\"last-modified\");\n\t\tconst totalBytes = Number(res.headers.get(\"content-length\") || 0);\n\t\tconst writer = Bun.file(destPath).writer();\n\t\tlet downloaded = 0;\n\t\tlet lastLog = 0;\n\n\t\tfor await (const chunk of res.body) {\n\t\t\twriter.write(chunk);\n\t\t\tdownloaded += chunk.byteLength;\n\n\t\t\t// Log progress every 5GB\n\t\t\tif (totalBytes > 0 && downloaded - lastLog > 5_000_000_000) {\n\t\t\t\tlastLog = downloaded;\n\t\t\t\tconst pct = ((downloaded / totalBytes) * 100).toFixed(0);\n\t\t\t\tlogger.info(\"Archive replay: downloading\", { progress: `${pct}%` });\n\t\t\t}\n\t\t}\n\n\t\tawait writer.end();\n\t\treturn lastModified;\n\t}\n}\n\n/** Find the nth occurrence of a character in a string */\nfunction nthIndex(str: string, char: string, n: number): number {\n\tlet idx = -1;\n\tfor (let i = 0; i < n; i++) {\n\t\tidx = str.indexOf(char, idx + 1);\n\t\tif (idx === -1) return -1;\n\t}\n\treturn idx;\n}\n"
|
|
8
8
|
],
|
|
9
|
-
"mappings": "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
|
|
10
|
-
"debugId": "
|
|
9
|
+
"mappings": "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",
|
|
10
|
+
"debugId": "6397FCF10611814B64756E2164756E21",
|
|
11
11
|
"names": []
|
|
12
12
|
}
|