diff --git a/packages/ponder-sdk/src/chains.ts b/packages/ponder-sdk/src/chains.ts index 943a07fba..67aa08d51 100644 --- a/packages/ponder-sdk/src/chains.ts +++ b/packages/ponder-sdk/src/chains.ts @@ -14,3 +14,8 @@ export const schemaChainId = schemaPositiveInteger; * Chain id standards are organized by the Ethereum Community @ https://github.com/ethereum-lists/chains **/ export type ChainId = z.infer; + +/** + * String representation of a valid Chain ID. + */ +export type ChainIdString = string; diff --git a/packages/ponder-sdk/src/client.test.ts b/packages/ponder-sdk/src/client.test.ts index dd074c3fd..3b4bd73d4 100644 --- a/packages/ponder-sdk/src/client.test.ts +++ b/packages/ponder-sdk/src/client.test.ts @@ -124,12 +124,6 @@ describe("Ponder Client", () => { expect(errorMessage).toContain( "Missing required Prometheus metric: ponder_historical_total_blocks", ); - expect(errorMessage).toContain( - "Missing required Prometheus metric: ponder_sync_is_complete", - ); - expect(errorMessage).toContain( - "Missing required Prometheus metric: ponder_sync_is_realtime", - ); } }); @@ -151,11 +145,21 @@ describe("Ponder Client", () => { const errorMessage = error instanceof Error ? error.message : "unknown error"; // Assert expect(errorMessage).toContain("Invalid serialized Ponder Indexing Metrics"); - expect(errorMessage).toContain("'optimism' must be a string representing a chain ID"); - expect(errorMessage).toContain("'mainnet' must be a string representing a chain ID"); - expect(errorMessage).toContain("'base' must be a string representing a chain ID"); - expect(errorMessage).toContain("'scroll' must be a string representing a chain ID"); - expect(errorMessage).toContain("'linea' must be a string representing a chain ID"); + expect(errorMessage).toContain( + "metric must be a string representing a valid ChainId, but got: 'optimism'", + ); + expect(errorMessage).toContain( + "metric must be a string representing a valid ChainId, but got: 'mainnet'", + ); + expect(errorMessage).toContain( + "metric must be a string representing a valid ChainId, but got: 'base'", + ); + expect(errorMessage).toContain( + "metric must be a string representing a valid ChainId, but got: 'scroll'", + ); + expect(errorMessage).toContain( + "metric must be a string representing a valid ChainId, but got: 'linea'", + ); } }); @@ -189,7 +193,7 @@ describe("Ponder Client", () => { // Act & Assert await expect(ponderClient.metrics()).rejects.toThrowError( - /Invalid serialized Ponder Indexing Metrics.*Chain Indexing Metrics cannot have both `indexingCompleted` and `indexingRealtime` as `true`/, + /Invalid serialized Ponder Indexing Metrics.*'ponder_sync_is_complete' and 'ponder_sync_is_realtime' metrics cannot both be 1 at the same time for chain 10/, ); }); }); diff --git a/packages/ponder-sdk/src/deserialize/indexing-metrics.mock.ts b/packages/ponder-sdk/src/deserialize/indexing-metrics.mock.ts index 7e1d0a25e..64dff3b20 100644 --- a/packages/ponder-sdk/src/deserialize/indexing-metrics.mock.ts +++ b/packages/ponder-sdk/src/deserialize/indexing-metrics.mock.ts @@ -1,4 +1,6 @@ import { + type ChainIndexingMetricsRealtime, + ChainIndexingStates, PonderAppCommands, type PonderIndexingMetrics, PonderIndexingOrderings, @@ -55,56 +57,44 @@ ponder_historical_total_blocks{chain="59144"} 21873991 [ 10, { - backfillSyncBlocksTotal: 36827849, - indexingCompleted: false, - indexingRealtime: true, + state: ChainIndexingStates.Realtime, latestSyncedBlock: { number: 147268938, timestamp: 1770136653 }, - }, + } satisfies ChainIndexingMetricsRealtime, ], [ 1, { - backfillSyncBlocksTotal: 21042285, - indexingCompleted: false, - indexingRealtime: true, + state: ChainIndexingStates.Realtime, latestSyncedBlock: { number: 24377568, timestamp: 1770136655 }, - }, + } satisfies ChainIndexingMetricsRealtime, ], [ 8453, { - backfillSyncBlocksTotal: 24103899, - indexingCompleted: false, - indexingRealtime: true, + state: ChainIndexingStates.Realtime, latestSyncedBlock: { number: 41673653, timestamp: 1770136653 }, - }, + } satisfies ChainIndexingMetricsRealtime, ], [ 534352, { - backfillSyncBlocksTotal: 12693186, - indexingCompleted: false, - indexingRealtime: true, + state: ChainIndexingStates.Realtime, latestSyncedBlock: { number: 29373405, timestamp: 1770136654 }, - }, + } satisfies ChainIndexingMetricsRealtime, ], [ 42161, { - backfillSyncBlocksTotal: 78607197, - indexingCompleted: false, - indexingRealtime: true, + state: ChainIndexingStates.Realtime, latestSyncedBlock: { number: 428248999, timestamp: 1770136654 }, - }, + } satisfies ChainIndexingMetricsRealtime, ], [ 59144, { - backfillSyncBlocksTotal: 21873991, - indexingCompleted: false, - indexingRealtime: true, + state: ChainIndexingStates.Realtime, latestSyncedBlock: { number: 28584906, timestamp: 1770136654 }, - }, + } satisfies ChainIndexingMetricsRealtime, ], ]), } satisfies PonderIndexingMetrics, @@ -215,6 +205,8 @@ ponder_sync_is_realtime{chain="1"} 1 ponder_sync_is_realtime{chain="59144"} 1 ponder_sync_is_realtime{chain="8453"} 1 +# HELP ponder_sync_is_complete Boolean (0 or 1) indicating if the sync has synced all blocks +# TYPE ponder_sync_is_complete gauge ponder_sync_is_complete{chain="42161"} 1 ponder_sync_is_complete{chain="534352"} 1 ponder_sync_is_complete{chain="10"} 1 @@ -222,8 +214,6 @@ ponder_sync_is_complete{chain="1"} 1 ponder_sync_is_complete{chain="59144"} 1 ponder_sync_is_complete{chain="8453"} 1 -# HELP ponder_sync_is_complete Boolean (0 or 1) indicating if the sync has synced all blocks -# TYPE ponder_sync_is_complete gauge # HELP ponder_historical_total_blocks Number of blocks required for the historical sync # TYPE ponder_historical_total_blocks gauge diff --git a/packages/ponder-sdk/src/deserialize/indexing-metrics.ts b/packages/ponder-sdk/src/deserialize/indexing-metrics.ts index b4a1c5365..5ef3ada3d 100644 --- a/packages/ponder-sdk/src/deserialize/indexing-metrics.ts +++ b/packages/ponder-sdk/src/deserialize/indexing-metrics.ts @@ -10,97 +10,91 @@ import { prettifyError, z } from "zod/v4"; import type { ParsePayload } from "zod/v4/core"; import { type BlockRef, schemaBlockRef } from "../blocks"; +import { type ChainId, type ChainIdString, schemaChainId } from "../chains"; import { + type ChainIndexingMetrics, + type ChainIndexingMetricsCompleted, + type ChainIndexingMetricsHistorical, + type ChainIndexingMetricsRealtime, + ChainIndexingStates, + type PonderAppCommand, PonderAppCommands, type PonderIndexingMetrics, + type PonderIndexingOrdering, PonderIndexingOrderings, } from "../indexing-metrics"; import { schemaPositiveInteger } from "../numbers"; import { schemaChainIdString } from "./chains"; import { deserializePrometheusMetrics, type PrometheusMetrics } from "./prometheus-metrics-text"; +import type { Unvalidated } from "./utils"; -function invariant_indexingCompletedAndRealtimeAreNotBothTrue( - ctx: ParsePayload, -) { - const data = ctx.value; +const schemaChainIndexingMetricsHistorical = z.object({ + state: z.literal(ChainIndexingStates.Historical), + latestSyncedBlock: schemaBlockRef, + historicalTotalBlocks: schemaPositiveInteger, +}); - if (data.indexingCompleted && data.indexingRealtime) { - ctx.issues.push({ - code: "custom", - input: ctx.value, - message: - "Chain Indexing Metrics cannot have both `indexingCompleted` and `indexingRealtime` as `true`.", - }); - } -} +const schemaChainIndexingMetricsRealtime = z.object({ + state: z.literal(ChainIndexingStates.Realtime), + latestSyncedBlock: schemaBlockRef, +}); + +const schemaChainIndexingMetricsCompleted = z.object({ + state: z.literal(ChainIndexingStates.Completed), + finalIndexedBlock: schemaBlockRef, +}); /** * Schema describing the chain indexing metrics. */ -const schemaSerializedChainIndexingMetrics = z - .object({ - backfillSyncBlocksTotal: schemaPositiveInteger, - latestSyncedBlock: schemaBlockRef, - indexingCompleted: z.boolean(), - indexingRealtime: z.boolean(), - }) - .check(invariant_indexingCompletedAndRealtimeAreNotBothTrue); - -type SerializedChainIndexingMetrics = z.infer; +const schemaChainIndexingMetrics = z.discriminatedUnion("state", [ + schemaChainIndexingMetricsHistorical, + schemaChainIndexingMetricsRealtime, + schemaChainIndexingMetricsCompleted, +]); /** * Schema describing the chains indexing metrics. */ -const schemaSerializedChainsIndexingMetrics = z.map( - schemaChainIdString, - schemaSerializedChainIndexingMetrics, -); +const schemaChainsIndexingMetrics = z.map(schemaChainId, schemaChainIndexingMetrics); -function invariant_includesAtLeastOneIndexedChain( - ctx: ParsePayload, +function invariant_indexingCompletedAndRealtimeAreNotBothTrue( + ctx: ParsePayload, ) { - const { chains } = ctx.value; + const prometheusMetrics = ctx.value; + const chainReferences = prometheusMetrics.getLabels("ponder_sync_block", "chain"); - if (chains.size === 0) { - ctx.issues.push({ - code: "custom", - input: ctx.value, - message: "Ponder Indexing Metrics must include at least one indexed chain.", + for (const maybeChainId of chainReferences) { + const ponderSyncIsComplete = prometheusMetrics.getValue("ponder_sync_is_complete", { + chain: maybeChainId, }); - } -} -/** - * Schema representing settings of a Ponder app. - */ -const schemaSerializedApplicationSettings = z.object({ - command: z.enum(PonderAppCommands), - ordering: z.enum(PonderIndexingOrderings), -}); + const ponderSyncIsRealtime = prometheusMetrics.getValue("ponder_sync_is_realtime", { + chain: maybeChainId, + }); -/** - * Schema describing the Ponder Indexing Metrics. - */ -const schemaPonderIndexingMetrics = z - .object({ - appSettings: schemaSerializedApplicationSettings, - chains: schemaSerializedChainsIndexingMetrics, - }) - .check(invariant_includesAtLeastOneIndexedChain); + if (ponderSyncIsComplete === 1 && ponderSyncIsRealtime === 1) { + ctx.issues.push({ + code: "custom", + input: ctx.value, + message: `'ponder_sync_is_complete' and 'ponder_sync_is_realtime' metrics cannot both be 1 at the same time for chain ${maybeChainId}`, + }); + } + } +} function invariant_includesRequiredMetrics(ctx: ParsePayload) { const prometheusMetrics = ctx.value; const metricNames = prometheusMetrics.getMetricNames(); - const requiredMetricNames = [ - "ponder_settings_info", + const requiredChainMetricNames = [ "ponder_sync_block", "ponder_sync_block_timestamp", "ponder_historical_total_blocks", - "ponder_sync_is_complete", - "ponder_sync_is_realtime", ]; + const requiredMetricNames = ["ponder_settings_info", ...requiredChainMetricNames]; + // Invariant: All required metrics must be present in the Prometheus metrics text. for (const requiredMetricName of requiredMetricNames) { if (!metricNames.includes(requiredMetricName)) { ctx.issues.push({ @@ -110,6 +104,32 @@ function invariant_includesRequiredMetrics(ctx: ParsePayload) }); } } + + // Invariant: All required chain metrics must include a 'chain' label. + for (const requiredChainMetricName of requiredChainMetricNames) { + const metricLabels = prometheusMetrics.getLabels(requiredChainMetricName, "chain"); + + if (metricLabels.length === 0) { + ctx.issues.push({ + code: "custom", + input: ctx.value, + message: `At least one '${requiredChainMetricName}' metric must include a 'chain' label.`, + }); + } + + // Invariant: All values in the 'chain' label of required chain metrics must be valid ChainId strings. + for (const maybeChainId of metricLabels) { + const result = schemaChainIdString.safeParse(maybeChainId); + + if (!result.success) { + ctx.issues.push({ + code: "custom", + input: ctx.value, + message: `Value in 'chain' label of '${requiredChainMetricName}' metric must be a string representing a valid ChainId, but got: '${maybeChainId}'`, + }); + } + } + } } /** @@ -120,80 +140,157 @@ const schemaSerializedPonderIndexingMetrics = z.coerce .nonempty({ error: `Ponder Indexing Metrics must be a non-empty string.` }) .transform(deserializePrometheusMetrics) // deserialize Prometheus metrics text into PrometheusMetrics instance .check(invariant_includesRequiredMetrics) - .transform(buildUnvalidatedPonderIndexingMetrics) - .pipe(schemaPonderIndexingMetrics); + .check(invariant_indexingCompletedAndRealtimeAreNotBothTrue); + +function invariant_includesAtLeastOneIndexedChain(ctx: ParsePayload) { + const { chains } = ctx.value; + + if (chains.size === 0) { + ctx.issues.push({ + code: "custom", + input: ctx.value, + message: "Ponder Indexing Metrics must include at least one indexed chain.", + }); + } +} + +/** + * Schema representing settings of a Ponder app. + */ +const schemaApplicationSettings = z.object({ + command: z.enum(PonderAppCommands), + ordering: z.enum(PonderIndexingOrderings), +}); /** - * Serialized Ponder Indexing Metrics. + * Schema describing Ponder Indexing Metrics. */ -type SerializedPonderIndexingMetrics = z.infer; +const schemaPonderIndexingMetrics = z + .object({ + appSettings: schemaApplicationSettings, + chains: schemaChainsIndexingMetrics, + }) + .check(invariant_includesAtLeastOneIndexedChain); /** - * Build unvalidated (and perhaps partial) Ponder Indexing Metrics + * Build unvalidated Chain Indexing Metrics * + * @param chainId Chain ID * @param prometheusMetrics valid Prometheus Metrics from Ponder app. - * @returns Unvalidated (possibly incomplete) Ponder Indexing Metrics - * to be validated with {@link schemaSerializedPonderIndexingMetrics}. + * @returns Unvalidated Chain Indexing Metrics + * to be validated by {@link schemaChainIndexingMetrics}. */ -function buildUnvalidatedPonderIndexingMetrics(prometheusMetrics: PrometheusMetrics): unknown { - const appSettings = { - command: prometheusMetrics.getLabel("ponder_settings_info", "command"), - ordering: prometheusMetrics.getLabel("ponder_settings_info", "ordering"), - }; +function buildUnvalidatedChainIndexingMetrics( + chainIdString: ChainIdString, + prometheusMetrics: PrometheusMetrics, +): Unvalidated { + const ponderSyncIsComplete = prometheusMetrics.getValue("ponder_sync_is_complete", { + chain: chainIdString, + }); - const chainReferences = prometheusMetrics.getLabels("ponder_sync_block", "chain"); + const ponderSyncIsRealtime = prometheusMetrics.getValue("ponder_sync_is_realtime", { + chain: chainIdString, + }); - const chains = new Map(); + const latestSyncedBlockNumber = prometheusMetrics.getValue("ponder_sync_block", { + chain: chainIdString, + }); - for (const maybeChainId of chainReferences) { - const latestSyncedBlock = { - number: prometheusMetrics.getValue("ponder_sync_block", { - chain: maybeChainId, - }), - timestamp: prometheusMetrics.getValue("ponder_sync_block_timestamp", { - chain: maybeChainId, - }), - } satisfies Partial; - - const backfillSyncBlocksTotal = prometheusMetrics.getValue("ponder_historical_total_blocks", { - chain: maybeChainId, - }); + const latestSyncedBlockTimestamp = prometheusMetrics.getValue("ponder_sync_block_timestamp", { + chain: chainIdString, + }); - const indexingCompleted = - prometheusMetrics.getValue("ponder_sync_is_complete", { - chain: maybeChainId, - }) === 1; + const latestSyncedBlock = { + number: latestSyncedBlockNumber, + timestamp: latestSyncedBlockTimestamp, + } satisfies Unvalidated; - const indexingRealtime = - prometheusMetrics.getValue("ponder_sync_is_realtime", { - chain: maybeChainId, - }) === 1; + // The `ponder_sync_is_complete` metric is set to `1` if, and only if, + // the indexing has been completed for the chain. + if (ponderSyncIsComplete === 1) { + return { + state: ChainIndexingStates.Completed, + finalIndexedBlock: latestSyncedBlock, + } satisfies Unvalidated; + } - chains.set(maybeChainId, { + // The `ponder_sync_is_realtime` metric is set to `1` if, and only if, + // the indexing is currently in realtime for the chain. + if (ponderSyncIsRealtime === 1) { + return { + state: ChainIndexingStates.Realtime, latestSyncedBlock, - backfillSyncBlocksTotal, - indexingCompleted, - indexingRealtime, - }); + } satisfies Unvalidated; } - const unvalidatedPonderIndexingMetrics = { + const historicalTotalBlocks = prometheusMetrics.getValue("ponder_historical_total_blocks", { + chain: chainIdString, + }); + + return { + state: ChainIndexingStates.Historical, + historicalTotalBlocks, + latestSyncedBlock, + } satisfies Unvalidated; +} + +/** + * Build unvalidated Ponder Indexing Metrics + * + * @param prometheusMetrics valid Prometheus Metrics from Ponder app. + * @returns Unvalidated Ponder Indexing Metrics + * to be validated with {@link schemaPonderIndexingMetrics}. + */ +function buildUnvalidatedPonderIndexingMetrics( + prometheusMetrics: PrometheusMetrics, +): Unvalidated { + const appSettings = { + command: prometheusMetrics.getLabel( + "ponder_settings_info", + "command", + ) as Unvalidated, + ordering: prometheusMetrics.getLabel( + "ponder_settings_info", + "ordering", + ) as Unvalidated, + }; + + const chainReferences = prometheusMetrics.getLabels( + "ponder_sync_block", + "chain", + ) satisfies ChainIdString[]; + + const chains = new Map, Unvalidated>(); + + for (const chainIdString of chainReferences) { + const chainIndexingMetrics = buildUnvalidatedChainIndexingMetrics( + chainIdString, + prometheusMetrics, + ); + + const chainId = Number(chainIdString) satisfies Unvalidated; + + chains.set(chainId, chainIndexingMetrics); + } + + return { appSettings, chains, }; - - return unvalidatedPonderIndexingMetrics; } /** * Deserialize and validate a Serialized Ponder Indexing Metrics. * - * @param data Maybe a string representing Ponder Indexing Metrics. + * @param ponderMetricsText Raw text maybe including Prometheus metrics. * @returns Deserialized and validated Ponder Indexing Metrics. * @throws Error if data cannot be deserialized into a valid Ponder Indexing Metrics. */ -export function deserializePonderIndexingMetrics(data: string | unknown): PonderIndexingMetrics { - const validation = schemaSerializedPonderIndexingMetrics.safeParse(data); +export function deserializePonderIndexingMetrics(ponderMetricsText: string): PonderIndexingMetrics { + const validation = schemaSerializedPonderIndexingMetrics + .transform(buildUnvalidatedPonderIndexingMetrics) + .pipe(schemaPonderIndexingMetrics) + .safeParse(ponderMetricsText); if (!validation.success) { throw new Error( diff --git a/packages/ponder-sdk/src/deserialize/indexing-status.ts b/packages/ponder-sdk/src/deserialize/indexing-status.ts index 9f998c160..fa85060e2 100644 --- a/packages/ponder-sdk/src/deserialize/indexing-status.ts +++ b/packages/ponder-sdk/src/deserialize/indexing-status.ts @@ -9,11 +9,10 @@ import { prettifyError, z } from "zod/v4"; import type { ParsePayload } from "zod/v4/core"; -import type { BlockRef } from "../blocks"; import { schemaBlockRef } from "../blocks"; import type { ChainId } from "../chains"; import { schemaChainId } from "../chains"; -import type { PonderIndexingStatus } from "../indexing-status"; +import type { ChainIndexingStatus, PonderIndexingStatus } from "../indexing-status"; const schemaSerializedChainName = z.string(); @@ -54,10 +53,10 @@ export type SerializedPonderIndexingStatus = z.infer(); + const chains = new Map(); for (const [, chainData] of Object.entries(data)) { - chains.set(chainData.id, chainData.block); + chains.set(chainData.id, { checkpointBlock: chainData.block }); } return { diff --git a/packages/ponder-sdk/src/deserialize/utils.ts b/packages/ponder-sdk/src/deserialize/utils.ts new file mode 100644 index 000000000..b99f20a6e --- /dev/null +++ b/packages/ponder-sdk/src/deserialize/utils.ts @@ -0,0 +1,68 @@ +/** + * A utility type that makes all properties of a type optional recursively, + * including nested objects and arrays. + * + * @example + * ```typescript + * type Config = { + * a: string; + * b: { + * x: number; + * y: { z: boolean }; + * }; + * c: { id: string }[]; + * } + * + * type PartialConfig = DeepPartial; + * // Results in: + * // { + * // a?: string; + * // b?: { + * // x?: number; + * // y?: { z?: boolean }; + * // }; + * // c?: { id?: string }[]; + * // } + * + * // Usage: + * const update: PartialConfig = { b: { y: { z: true } } }; + * ``` + */ +export type DeepPartial = { + [P in keyof T]?: T[P] extends (infer U)[] + ? DeepPartial[] + : T[P] extends object + ? DeepPartial + : T[P]; +}; + +/** + * Helper type to represent an unvalidated version of a business layer type `T`, + * where all properties are optional. + * + * This is useful for building a validated object `T` from partial input, + * where the input may be missing required fields or have fields that + * are not yet validated. + * + * For example, transforming serialized representation of type `T` into + * an unvalidated version of `T` that can be later validated against + * defined business rules and constraints. + * + * ```ts + * function buildUnvalidatedValue(serialized: SerializedChainId): Unvalidated { + * // transform serialized chainId into unvalidated number (e.g. parseInt) + * return parseInt(serialized, 10); + * } + * + * // Later, we can validate the unvalidated value against our business rules + * function validateChainId(unvalidatedChainId: Unvalidated): ChainId { + * if (typeof unvalidatedChainId !== "number" || unvalidatedChainId <= 0) { + * throw new Error("Invalid ChainId"); + * } + * + * return unvalidatedChainId as ChainId; + * } + * + * ``` + */ +export type Unvalidated = DeepPartial; diff --git a/packages/ponder-sdk/src/indexing-metrics.ts b/packages/ponder-sdk/src/indexing-metrics.ts index 08b390077..24d0dc31f 100644 --- a/packages/ponder-sdk/src/indexing-metrics.ts +++ b/packages/ponder-sdk/src/indexing-metrics.ts @@ -45,57 +45,101 @@ export interface PonderApplicationSettings { } /** - * Chain Indexing Metrics + * Chain Indexing States * - * Represents the indexing metrics for a specific chain indexed by a Ponder app. + * Represents the indexing state of a chain indexed by a Ponder app. + */ +export const ChainIndexingStates = { + Historical: "historical", + Completed: "completed", + Realtime: "realtime", +} as const; + +export type ChainIndexingState = (typeof ChainIndexingStates)[keyof typeof ChainIndexingStates]; + +/** + * Chain Indexing Metrics Historical * - * Guarantees: - * - `indexingCompleted` and `indexingRealtime` cannot both be `true` - * at the same time. All other combinations are valid. + * Represents the indexing metrics for a chain that is currently queued for + * indexing or in the backfill phase by a Ponder app. */ -export interface ChainIndexingMetrics { +export interface ChainIndexingMetricsHistorical { + state: typeof ChainIndexingStates.Historical; + /** - * Number of blocks required to be synced to complete - * the backfill phase of indexing. - * - * This value is calculated by Ponder at the time - * the backfill starts. It corresponds to the number of blocks between: - * - the first block to be indexed (specified in Ponder config), and - * - the last block to be indexed during backfill. - * The last block to be indexed during backfill is one of: - * - The specified end block (if any) in the Ponder config, or - * - The latest known block at the time the backfill started. - * - * Guarantees: - * - Is a positive integer. + * A {@link BlockRef} to the "highest" block that has been discovered by RPCs + * and stored in the RPC cache as of the time the metric value was captured. */ - backfillSyncBlocksTotal: number; + latestSyncedBlock: BlockRef; /** - * Latest synced block + * Total count of historical blocks. + * + * The count of historical blocks is only reset when a Ponder app + * restarts. If historical blocks have not been fully indexed yet + * (for example, the chain is queued for indexing or in the backfill + * phase), the count will increase as more historical blocks are + * discovered by RPCs and stored in the RPC cache, potentially exceeding + * the count from before the restart. Between restarts, this count + * remains unchanged. * - * Corresponds to the latest block stored in the RPC cache for the chain. + * Guaranteed to be a positive integer. */ - latestSyncedBlock: BlockRef; + historicalTotalBlocks: number; +} + +/** + * Chain Indexing Metrics Realtime + * + * Represents the indexing metrics for a chain that is currently in + * the realtime indexing phase by a Ponder app. It means that + * the backfill phase transitioned to realtime phase, as there was + * no "config end block" specified for the chain. + * + * The indexing continues in realtime, with no "target end block". + * The "latest synced block" is continuously updated as new blocks are + * discovered by RPCs and stored in the RPC cache. + */ +export interface ChainIndexingMetricsRealtime { + state: typeof ChainIndexingStates.Realtime; /** - * Is indexing completed for the chain? - * - * This will be true when the backfill has been completed, - * and a specified end block for the chain has been reached. + * A {@link BlockRef} to the "highest" block that has been discovered by RPCs + * and stored in the RPC cache as of the time the metric value was captured. */ - indexingCompleted: boolean; + latestSyncedBlock: BlockRef; +} + +/** + * Chain Indexing Metrics Completed + * + * Represents the indexing metrics for a chain configured to only index + * a finite range of blocks where all blocks in that finite range + * have been indexed. + */ +export interface ChainIndexingMetricsCompleted { + state: typeof ChainIndexingStates.Completed; /** - * Is indexing following in real-time for the chain? + * Final indexed block * - * This will be true when the backfill has been completed, - * and there was no specified end block for the chain, - * so indexing continues in real-time. + * A {@link BlockRef} to the final block that was the finite target + * for indexing the chain. No more blocks will be indexed for the chain + * after this block. */ - indexingRealtime: boolean; + finalIndexedBlock: BlockRef; } +/** + * Chain Indexing Metrics + * + * Represents the indexing metrics for a specific chain indexed by a Ponder app. + */ +export type ChainIndexingMetrics = + | ChainIndexingMetricsHistorical + | ChainIndexingMetricsCompleted + | ChainIndexingMetricsRealtime; + /** * Ponder Indexing Metrics * diff --git a/packages/ponder-sdk/src/indexing-status.ts b/packages/ponder-sdk/src/indexing-status.ts index 8bd444b4d..d528c227c 100644 --- a/packages/ponder-sdk/src/indexing-status.ts +++ b/packages/ponder-sdk/src/indexing-status.ts @@ -1,20 +1,39 @@ import type { BlockRef } from "./blocks"; import type { ChainId } from "./chains"; +/** + * Chain Indexing Status + * + * Represents the indexing status for a specific chain in a Ponder app. + */ +export interface ChainIndexingStatus { + /** + * Checkpoint Block + * + * During indexing, a Ponder app indexes the chain and + * keeps track of a checkpoint block for each indexed chain. + * + * @see https://ponder.sh/docs/api-reference/ponder/database#checkpoint-table + * + * The `checkpointBlock` is a reference to either: + * - the first block to be indexed for the chain (if indexing is queued), or + * - the last indexed block for the chain (if one or more blocks + * have been indexed for the chain). + */ + checkpointBlock: BlockRef; +} + /** * Ponder Indexing Status * - * Represents the chain indexing status in a Ponder application. + * Represents the indexing status of each chain in a Ponder app. */ export interface PonderIndexingStatus { /** - * Map of indexed chain IDs to their block reference. + * Map of indexed chain IDs to their chain indexing status. * * Guarantees: * - Includes entry for at least one indexed chain. - * - BlockRef corresponds to either: - * - The first block to be indexed (when chain indexing is currently queued). - * - The last indexed block (when chain indexing is currently in progress). */ - chains: Map; + chains: Map; }