diff --git a/.changeset/better-bats-switch.md b/.changeset/better-bats-switch.md new file mode 100644 index 000000000..dbb472e94 --- /dev/null +++ b/.changeset/better-bats-switch.md @@ -0,0 +1,5 @@ +--- +"ensindexer": minor +--- + +Improved developer experience by skipping validation step in ENSDb Writer Worker while in dev mode. diff --git a/.changeset/old-seals-draw.md b/.changeset/old-seals-draw.md new file mode 100644 index 000000000..26af620a1 --- /dev/null +++ b/.changeset/old-seals-draw.md @@ -0,0 +1,5 @@ +--- +"@ensnode/ponder-sdk": minor +--- + +Introduced `PonderAppContext` data model to capture the internal context of a local Ponder app. diff --git a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.mock.ts b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.mock.ts index 232428743..daaa6968b 100644 --- a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.mock.ts +++ b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.mock.ts @@ -12,7 +12,9 @@ import { type OmnichainIndexingStatusSnapshot, PluginName, } from "@ensnode/ensnode-sdk"; +import type { LocalPonderClient } from "@ensnode/ponder-sdk"; +import { EnsDbWriterWorker } from "@/lib/ensdb-writer-worker/ensdb-writer-worker"; import type { IndexingStatusBuilder } from "@/lib/indexing-status-builder"; import type { PublicConfigBuilder } from "@/lib/public-config-builder"; @@ -103,3 +105,37 @@ export function createMockCrossChainSnapshot( ...overrides, }; } + +export function createMockLocalPonderClient( + overrides: { isInDevMode?: boolean } = {}, +): LocalPonderClient { + const isInDevMode = overrides.isInDevMode ?? false; + + return { + isInDevMode, + } as unknown as LocalPonderClient; +} + +export function createMockEnsDbWriterWorker( + overrides: { + ensDbClient?: EnsDbWriter; + publicConfigBuilder?: PublicConfigBuilder; + indexingStatusBuilder?: IndexingStatusBuilder; + isInDevMode?: boolean; + } = {}, +) { + const ensDbClient = overrides.ensDbClient ?? createMockEnsDbWriter(); + const publicConfigBuilder = overrides.publicConfigBuilder ?? createMockPublicConfigBuilder(); + const indexingStatusBuilder = + overrides.indexingStatusBuilder ?? createMockIndexingStatusBuilder(); + const localPonderClient = createMockLocalPonderClient({ + isInDevMode: overrides.isInDevMode ?? false, + }); + + return new EnsDbWriterWorker( + ensDbClient, + publicConfigBuilder, + indexingStatusBuilder, + localPonderClient, + ); +} diff --git a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.test.ts b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.test.ts index 0883978cd..f55d75247 100644 --- a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.test.ts +++ b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.test.ts @@ -6,13 +6,13 @@ import { validateEnsIndexerPublicConfigCompatibility, } from "@ensnode/ensnode-sdk"; -import { EnsDbWriterWorker } from "@/lib/ensdb-writer-worker/ensdb-writer-worker"; import type { IndexingStatusBuilder } from "@/lib/indexing-status-builder/indexing-status-builder"; import type { PublicConfigBuilder } from "@/lib/public-config-builder/public-config-builder"; import { createMockCrossChainSnapshot, createMockEnsDbWriter, + createMockEnsDbWriterWorker, createMockIndexingStatusBuilder, createMockOmnichainSnapshot, createMockPublicConfigBuilder, @@ -51,10 +51,10 @@ describe("EnsDbWriterWorker", () => { vi.mocked(buildCrossChainIndexingStatusSnapshotOmnichain).mockReturnValue(snapshot); const ensDbClient = createMockEnsDbWriter(); - const publicConfigBuilder = createMockPublicConfigBuilder(); - const indexingStatusBuilder = createMockIndexingStatusBuilder(omnichainSnapshot); - - const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); + const worker = createMockEnsDbWriterWorker({ + ensDbClient, + indexingStatusBuilder: createMockIndexingStatusBuilder(omnichainSnapshot), + }); // act await worker.run(); @@ -81,31 +81,58 @@ describe("EnsDbWriterWorker", () => { it("throws when stored config is incompatible", async () => { // arrange - const incompatibleError = new Error("incompatible"); vi.mocked(validateEnsIndexerPublicConfigCompatibility).mockImplementation(() => { - throw incompatibleError; + throw new Error("incompatible"); }); const ensDbClient = createMockEnsDbWriter({ getEnsIndexerPublicConfig: vi.fn().mockResolvedValue(mockPublicConfig), }); - const publicConfigBuilder = createMockPublicConfigBuilder(mockPublicConfig); - const indexingStatusBuilder = createMockIndexingStatusBuilder(); - - const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); + const worker = createMockEnsDbWriterWorker({ + ensDbClient, + publicConfigBuilder: createMockPublicConfigBuilder(mockPublicConfig), + }); // act & assert await expect(worker.run()).rejects.toThrow("incompatible"); expect(ensDbClient.upsertEnsDbVersion).not.toHaveBeenCalled(); }); - it("throws error when worker is already running", async () => { + it("skips config validation when in dev mode", async () => { // arrange - const ensDbClient = createMockEnsDbWriter(); - const publicConfigBuilder = createMockPublicConfigBuilder(); - const indexingStatusBuilder = createMockIndexingStatusBuilder(); + vi.mocked(validateEnsIndexerPublicConfigCompatibility).mockImplementation(() => { + throw new Error("incompatible"); + }); + + const snapshot = createMockCrossChainSnapshot(); + vi.mocked(buildCrossChainIndexingStatusSnapshotOmnichain).mockReturnValue(snapshot); - const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); + const ensDbClient = createMockEnsDbWriter({ + getEnsIndexerPublicConfig: vi.fn().mockResolvedValue(mockPublicConfig), + }); + const worker = createMockEnsDbWriterWorker({ + ensDbClient, + publicConfigBuilder: createMockPublicConfigBuilder(mockPublicConfig), + isInDevMode: true, + }); + + // act - should not throw even though configs are incompatible + await worker.run(); + + // assert - validation should not have been called + expect(validateEnsIndexerPublicConfigCompatibility).not.toHaveBeenCalled(); + expect(ensDbClient.upsertEnsDbVersion).toHaveBeenCalledWith( + mockPublicConfig.versionInfo.ensDb, + ); + expect(ensDbClient.upsertEnsIndexerPublicConfig).toHaveBeenCalledWith(mockPublicConfig); + + // cleanup + worker.stop(); + }); + + it("throws error when worker is already running", async () => { + // arrange + const worker = createMockEnsDbWriterWorker(); // act - first run await worker.run(); @@ -119,14 +146,11 @@ describe("EnsDbWriterWorker", () => { it("throws error when config fetch fails", async () => { // arrange - const networkError = new Error("Network failure"); - const ensDbClient = createMockEnsDbWriter(); const publicConfigBuilder = { - getPublicConfig: vi.fn().mockRejectedValue(networkError), + getPublicConfig: vi.fn().mockRejectedValue(new Error("Network failure")), } as unknown as PublicConfigBuilder; - const indexingStatusBuilder = createMockIndexingStatusBuilder(); - - const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); + const ensDbClient = createMockEnsDbWriter(); + const worker = createMockEnsDbWriterWorker({ ensDbClient, publicConfigBuilder }); // act & assert await expect(worker.run()).rejects.toThrow("Network failure"); @@ -136,14 +160,10 @@ describe("EnsDbWriterWorker", () => { it("throws error when stored config fetch fails", async () => { // arrange - const dbError = new Error("Database connection lost"); const ensDbClient = createMockEnsDbWriter({ - getEnsIndexerPublicConfig: vi.fn().mockRejectedValue(dbError), + getEnsIndexerPublicConfig: vi.fn().mockRejectedValue(new Error("Database connection lost")), }); - const publicConfigBuilder = createMockPublicConfigBuilder(); - const indexingStatusBuilder = createMockIndexingStatusBuilder(); - - const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); + const worker = createMockEnsDbWriterWorker({ ensDbClient }); // act & assert await expect(worker.run()).rejects.toThrow("Database connection lost"); @@ -152,17 +172,16 @@ describe("EnsDbWriterWorker", () => { it("fetches stored and in-memory configs concurrently", async () => { // arrange - vi.mocked(validateEnsIndexerPublicConfigCompatibility).mockImplementation(() => { - // validation passes - }); + vi.mocked(validateEnsIndexerPublicConfigCompatibility).mockImplementation(() => {}); const ensDbClient = createMockEnsDbWriter({ getEnsIndexerPublicConfig: vi.fn().mockResolvedValue(mockPublicConfig), }); const publicConfigBuilder = createMockPublicConfigBuilder(mockPublicConfig); - const indexingStatusBuilder = createMockIndexingStatusBuilder(); - - const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); + const worker = createMockEnsDbWriterWorker({ + ensDbClient, + publicConfigBuilder, + }); // act await worker.run(); @@ -182,9 +201,7 @@ describe("EnsDbWriterWorker", () => { const ensDbClient = createMockEnsDbWriter(); const publicConfigBuilder = createMockPublicConfigBuilder(); - const indexingStatusBuilder = createMockIndexingStatusBuilder(); - - const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); + const worker = createMockEnsDbWriterWorker({ ensDbClient, publicConfigBuilder }); // act await worker.run(); @@ -203,10 +220,7 @@ describe("EnsDbWriterWorker", () => { // arrange const upsertIndexingStatusSnapshot = vi.fn().mockResolvedValue(undefined); const ensDbClient = createMockEnsDbWriter({ upsertIndexingStatusSnapshot }); - const publicConfigBuilder = createMockPublicConfigBuilder(); - const indexingStatusBuilder = createMockIndexingStatusBuilder(); - - const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); + const worker = createMockEnsDbWriterWorker({ ensDbClient }); // act await worker.run(); @@ -227,11 +241,7 @@ describe("EnsDbWriterWorker", () => { describe("isRunning - worker state", () => { it("indicates isRunning status correctly", async () => { // arrange - const ensDbClient = createMockEnsDbWriter(); - const publicConfigBuilder = createMockPublicConfigBuilder(); - const indexingStatusBuilder = createMockIndexingStatusBuilder(); - - const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); + const worker = createMockEnsDbWriterWorker(); // assert - not running initially expect(worker.isRunning).toBe(false); @@ -268,15 +278,13 @@ describe("EnsDbWriterWorker", () => { vi.mocked(buildCrossChainIndexingStatusSnapshotOmnichain).mockReturnValue(crossChainSnapshot); const ensDbClient = createMockEnsDbWriter(); - const publicConfigBuilder = createMockPublicConfigBuilder(); const indexingStatusBuilder = { getOmnichainIndexingStatusSnapshot: vi .fn() .mockResolvedValueOnce(unstartedSnapshot) .mockResolvedValueOnce(validSnapshot), } as unknown as IndexingStatusBuilder; - - const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); + const worker = createMockEnsDbWriterWorker({ ensDbClient, indexingStatusBuilder }); // act - run returns immediately await worker.run(); @@ -324,7 +332,6 @@ describe("EnsDbWriterWorker", () => { .mockRejectedValueOnce(new Error("DB error")) .mockResolvedValueOnce(undefined), }); - const publicConfigBuilder = createMockPublicConfigBuilder(); const indexingStatusBuilder = { getOmnichainIndexingStatusSnapshot: vi .fn() @@ -332,8 +339,7 @@ describe("EnsDbWriterWorker", () => { .mockResolvedValueOnce(snapshot2) .mockResolvedValueOnce(snapshot2), } as unknown as IndexingStatusBuilder; - - const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder); + const worker = createMockEnsDbWriterWorker({ ensDbClient, indexingStatusBuilder }); // act await worker.run(); diff --git a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.ts b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.ts index 5dc5b4b1e..1645d196a 100644 --- a/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.ts +++ b/apps/ensindexer/src/lib/ensdb-writer-worker/ensdb-writer-worker.ts @@ -11,6 +11,7 @@ import { type OmnichainIndexingStatusSnapshot, validateEnsIndexerPublicConfigCompatibility, } from "@ensnode/ensnode-sdk"; +import type { LocalPonderClient } from "@ensnode/ponder-sdk"; import type { IndexingStatusBuilder } from "@/lib/indexing-status-builder/indexing-status-builder"; import type { PublicConfigBuilder } from "@/lib/public-config-builder/public-config-builder"; @@ -50,19 +51,29 @@ export class EnsDbWriterWorker { */ private publicConfigBuilder: PublicConfigBuilder; + /** + * Local Ponder Client instance + * + * Used to get local Ponder app command. + */ + private localPonderClient: LocalPonderClient; + /** * @param ensDbClient ENSDb Writer instance used by the worker to interact with ENSDb. * @param publicConfigBuilder ENSIndexer Public Config Builder instance used by the worker to read ENSIndexer Public Config. * @param indexingStatusBuilder Indexing Status Builder instance used by the worker to read ENSIndexer Indexing Status. + * @param localPonderClient Local Ponder Client instance, used to get local Ponder app command. */ constructor( ensDbClient: EnsDbWriter, publicConfigBuilder: PublicConfigBuilder, indexingStatusBuilder: IndexingStatusBuilder, + localPonderClient: LocalPonderClient, ) { this.ensDbClient = ensDbClient; this.publicConfigBuilder = publicConfigBuilder; this.indexingStatusBuilder = indexingStatusBuilder; + this.localPonderClient = localPonderClient; } /** @@ -133,14 +144,16 @@ export class EnsDbWriterWorker { * - stored config in ENSDb, if available, and * - in-memory config from ENSIndexer Client. * - * If, and only if, a stored config is available in ENSDb, then the function - * validates the compatibility of the in-memory config object against - * the stored one. Validation criteria are defined in the function body. + * If a stored config exists **and** the local Ponder app is **not** in dev + * mode, the in-memory config is validated for compatibility against the + * stored one. Validation is skipped if the local Ponder app is in dev mode, + * allowing to override the stored config in ENSDb with the current in-memory + * config, without having to keep them compatible. * - * @returns In-memory config object, if the validation is successful or - * if there is no stored config. - * @throws Error if the in-memory config object cannot be fetched or, - * got fetched and is incompatible with the stored config object. + * @returns The in-memory config when validation passes or no stored config + * exists. + * @throws Error if either fetch fails, or if the in-memory config is + * incompatible with the stored config. */ private async getValidatedEnsIndexerPublicConfig(): Promise { /** @@ -181,8 +194,12 @@ export class EnsDbWriterWorker { } // Validate in-memory config object compatibility with the stored one, - // if the stored one is available - if (storedConfig) { + // if the stored one is available. + // The validation is skipped if the local Ponder app is running in dev mode. + // This is to improve the development experience during ENSIndexer + // development, by allowing to override the stored config in ENSDb with + // the current in-memory config, without having to keep them compatible. + if (storedConfig && !this.localPonderClient.isInDevMode) { try { validateEnsIndexerPublicConfigCompatibility(storedConfig, inMemoryConfig); } catch (error) { diff --git a/apps/ensindexer/src/lib/ensdb-writer-worker/singleton.ts b/apps/ensindexer/src/lib/ensdb-writer-worker/singleton.ts index 48ef90dff..5e0a9d9df 100644 --- a/apps/ensindexer/src/lib/ensdb-writer-worker/singleton.ts +++ b/apps/ensindexer/src/lib/ensdb-writer-worker/singleton.ts @@ -1,5 +1,6 @@ import { ensDbClient } from "@/lib/ensdb/singleton"; import { indexingStatusBuilder } from "@/lib/indexing-status-builder/singleton"; +import { localPonderClient } from "@/lib/local-ponder-client"; import { publicConfigBuilder } from "@/lib/public-config-builder/singleton"; import { EnsDbWriterWorker } from "./ensdb-writer-worker"; @@ -24,6 +25,7 @@ export function startEnsDbWriterWorker() { ensDbClient, publicConfigBuilder, indexingStatusBuilder, + localPonderClient, ); ensDbWriterWorker diff --git a/apps/ensindexer/src/lib/local-ponder-client.ts b/apps/ensindexer/src/lib/local-ponder-client.ts index d68b571bb..139d85f22 100644 --- a/apps/ensindexer/src/lib/local-ponder-client.ts +++ b/apps/ensindexer/src/lib/local-ponder-client.ts @@ -3,10 +3,15 @@ import config from "@/config"; import { publicClients } from "ponder:api"; import { buildIndexedBlockranges } from "@ensnode/ensnode-sdk"; -import { LocalPonderClient } from "@ensnode/ponder-sdk"; +import { deserializePonderAppContext, LocalPonderClient } from "@ensnode/ponder-sdk"; import { getPluginsAllDatasourceNames } from "@/lib/plugin-helpers"; +if (!globalThis.PONDER_COMMON) { + throw new Error("PONDER_COMMON must be defined by Ponder at runtime as a global variable."); +} + +const ponderAppContext = deserializePonderAppContext(globalThis.PONDER_COMMON); const pluginsAllDatasourceNames = getPluginsAllDatasourceNames(config.plugins); const indexedBlockranges = buildIndexedBlockranges(config.namespace, pluginsAllDatasourceNames); @@ -15,4 +20,5 @@ export const localPonderClient = new LocalPonderClient( config.indexedChainIds, indexedBlockranges, publicClients, + ponderAppContext, ); diff --git a/apps/ensindexer/types/env.d.ts b/apps/ensindexer/types/env.d.ts index caa8c9c64..b40d0b1b6 100644 --- a/apps/ensindexer/types/env.d.ts +++ b/apps/ensindexer/types/env.d.ts @@ -1,7 +1,12 @@ import type { ENSIndexerEnvironment } from "@/config/environment"; +import type { RawPonderAppContext } from "@ensnode/ponder-sdk"; declare global { namespace NodeJS { interface ProcessEnv extends ENSIndexerEnvironment {} } + /** + * The "raw" context of the local Ponder app. + */ + var PONDER_COMMON: RawPonderAppContext | undefined; } diff --git a/packages/ponder-sdk/src/deserialize/indexing-metrics.mock.ts b/packages/ponder-sdk/src/deserialize/indexing-metrics.mock.ts index 64dff3b20..98567c0da 100644 --- a/packages/ponder-sdk/src/deserialize/indexing-metrics.mock.ts +++ b/packages/ponder-sdk/src/deserialize/indexing-metrics.mock.ts @@ -1,10 +1,10 @@ import { type ChainIndexingMetricsRealtime, ChainIndexingStates, - PonderAppCommands, type PonderIndexingMetrics, PonderIndexingOrderings, } from "../indexing-metrics"; +import { PonderAppCommands } from "../ponder-app-context"; export const indexingMetricsMockValid = { text: ` diff --git a/packages/ponder-sdk/src/deserialize/indexing-metrics.ts b/packages/ponder-sdk/src/deserialize/indexing-metrics.ts index 5ef3ada3d..59a13380a 100644 --- a/packages/ponder-sdk/src/deserialize/indexing-metrics.ts +++ b/packages/ponder-sdk/src/deserialize/indexing-metrics.ts @@ -17,13 +17,12 @@ import { type ChainIndexingMetricsHistorical, type ChainIndexingMetricsRealtime, ChainIndexingStates, - type PonderAppCommand, - PonderAppCommands, type PonderIndexingMetrics, type PonderIndexingOrdering, PonderIndexingOrderings, } from "../indexing-metrics"; import { schemaPositiveInteger } from "../numbers"; +import { type PonderAppCommand, PonderAppCommands } from "../ponder-app-context"; import { schemaChainIdString } from "./chains"; import { deserializePrometheusMetrics, type PrometheusMetrics } from "./prometheus-metrics-text"; import type { Unvalidated } from "./utils"; diff --git a/packages/ponder-sdk/src/deserialize/ponder-app-context.ts b/packages/ponder-sdk/src/deserialize/ponder-app-context.ts new file mode 100644 index 000000000..3a53ce1a0 --- /dev/null +++ b/packages/ponder-sdk/src/deserialize/ponder-app-context.ts @@ -0,0 +1,76 @@ +/** + * This module provides functionality to deserialize the "raw" context of + * a local Ponder app into a validated Ponder App Context. + * + * The "raw" context is injected by Ponder at runtime as + * the `PONDER_COMMON` global variable. + * + * @see https://github.com/ponder-sh/ponder/blob/6fcc15d4234e43862cb6e21c05f3c57f4c2f7464/packages/core/src/internal/common.ts#L7-L15 + */ + +import { prettifyError, z } from "zod/v4"; + +import { + type PonderAppCommand, + PonderAppCommands, + type PonderAppContext, +} from "../ponder-app-context"; +import type { Unvalidated } from "./utils"; + +/** + * Type representing the "raw" context of a local Ponder app. + */ +const schemaRawPonderAppContext = z.object({ + options: z.object({ + command: z.string(), + }), +}); + +/** + * Type representing the "raw" context of a local Ponder app. + */ +export type RawPonderAppContext = z.infer; + +/** + * Schema representing the "deserialized" context of a local Ponder app. + */ +const schemaPonderAppContext = z.object({ + command: z.enum(PonderAppCommands), +}); + +/** + * Build unvalidated Ponder App Context + * + * @param rawPonderAppContext valid raw Ponder App Context from Ponder app. + * @returns Unvalidated Ponder App Context + * to be validated with {@link schemaPonderAppContext}. + */ +function buildUnvalidatedPonderAppContext( + rawPonderAppContext: RawPonderAppContext, +): Unvalidated { + return { + command: rawPonderAppContext.options.command as Unvalidated, + }; +} + +/** + * Deserialize and validate a Raw Ponder App Context. + * + * @param unvalidatedRawPonderAppContext Raw Ponder App Context to be validated. + * @returns Deserialized and validated Ponder App Context. + * @throws Error if data cannot be deserialized into a valid Ponder App Context. + */ +export function deserializePonderAppContext( + unvalidatedRawPonderAppContext: Unvalidated, +): PonderAppContext { + const validation = schemaRawPonderAppContext + .transform(buildUnvalidatedPonderAppContext) + .pipe(schemaPonderAppContext) + .safeParse(unvalidatedRawPonderAppContext); + + if (!validation.success) { + throw new Error(`Invalid raw Ponder App Context: ${prettifyError(validation.error)}`); + } + + return validation.data; +} diff --git a/packages/ponder-sdk/src/index.ts b/packages/ponder-sdk/src/index.ts index 05cfb4bdc..a3be62ccc 100644 --- a/packages/ponder-sdk/src/index.ts +++ b/packages/ponder-sdk/src/index.ts @@ -2,10 +2,12 @@ export * from "./blockrange"; export * from "./blocks"; export * from "./chains"; export * from "./client"; +export * from "./deserialize/ponder-app-context"; export * from "./indexing-config"; export * from "./indexing-metrics"; export * from "./indexing-status"; export * from "./local-indexing-metrics"; export * from "./local-ponder-client"; export * from "./numbers"; +export * from "./ponder-app-context"; export * from "./time"; diff --git a/packages/ponder-sdk/src/indexing-metrics.ts b/packages/ponder-sdk/src/indexing-metrics.ts index 24d0dc31f..fbc407b25 100644 --- a/packages/ponder-sdk/src/indexing-metrics.ts +++ b/packages/ponder-sdk/src/indexing-metrics.ts @@ -1,17 +1,6 @@ import type { BlockRef } from "./blocks"; import type { ChainId } from "./chains"; - -/** - * Ponder Application Commands - * - * Represents the commands that can be used to start a Ponder app. - */ -export const PonderAppCommands = { - Dev: "dev", - Start: "start", -} as const; - -export type PonderAppCommand = (typeof PonderAppCommands)[keyof typeof PonderAppCommands]; +import type { PonderAppCommand } from "./ponder-app-context"; /** * Ponder Indexing Orderings diff --git a/packages/ponder-sdk/src/local-ponder-client.mock.ts b/packages/ponder-sdk/src/local-ponder-client.mock.ts index af4fd66de..90e49c315 100644 --- a/packages/ponder-sdk/src/local-ponder-client.mock.ts +++ b/packages/ponder-sdk/src/local-ponder-client.mock.ts @@ -2,6 +2,7 @@ import { type BlockNumberRangeWithStartBlock, buildBlockNumberRange } from "./bl import type { CachedPublicClient } from "./cached-public-client"; import type { ChainId, ChainIdString } from "./chains"; import { LocalPonderClient } from "./local-ponder-client"; +import { PonderAppCommands, type PonderAppContext } from "./ponder-app-context"; export const chainIds = { Mainnet: 1, @@ -13,9 +14,11 @@ export function createLocalPonderClientMock(overrides?: { indexedChainIds?: Set; indexedBlockranges?: Map; cachedPublicClients?: Record; + ponderAppContext?: PonderAppContext; }): LocalPonderClient { const indexedChainIds = overrides?.indexedChainIds ?? new Set([chainIds.Mainnet, chainIds.Optimism]); + const indexedBlockranges = overrides?.indexedBlockranges ?? new Map([ @@ -23,6 +26,7 @@ export function createLocalPonderClientMock(overrides?: { [chainIds.Optimism, buildBlockNumberRange(200, undefined)], [chainIds.Base, buildBlockNumberRange(500, undefined)], ]); + const cachedPublicClients = overrides?.cachedPublicClients ?? ({ @@ -31,10 +35,17 @@ export function createLocalPonderClientMock(overrides?: { [`${chainIds.Base}`]: {} as CachedPublicClient, } satisfies Record); + const ponderAppContext = + overrides?.ponderAppContext ?? + ({ + command: PonderAppCommands.Start, + } satisfies PonderAppContext); + return new LocalPonderClient( new URL("http://localhost:3000"), indexedChainIds, indexedBlockranges, cachedPublicClients, + ponderAppContext, ); } diff --git a/packages/ponder-sdk/src/local-ponder-client.test.ts b/packages/ponder-sdk/src/local-ponder-client.test.ts index 6c43c3209..4b02d258e 100644 --- a/packages/ponder-sdk/src/local-ponder-client.test.ts +++ b/packages/ponder-sdk/src/local-ponder-client.test.ts @@ -9,11 +9,11 @@ import { type ChainIndexingMetricsHistorical, type ChainIndexingMetricsRealtime, ChainIndexingStates, - PonderAppCommands, type PonderIndexingMetrics, PonderIndexingOrderings, } from "./indexing-metrics"; import { chainIds, createLocalPonderClientMock } from "./local-ponder-client.mock"; +import { PonderAppCommands } from "./ponder-app-context"; describe("LocalPonderClient", () => { afterEach(() => { @@ -206,4 +206,26 @@ describe("LocalPonderClient", () => { ); }); }); + + describe("isInDevMode", () => { + it("returns true when Ponder app command is 'dev'", () => { + // Arrange + const client = createLocalPonderClientMock({ + ponderAppContext: { command: PonderAppCommands.Dev }, + }); + + // Act & Assert + expect(client.isInDevMode).toBe(true); + }); + + it("returns false when Ponder app command is not 'dev'", () => { + // Arrange + const client = createLocalPonderClientMock({ + ponderAppContext: { command: PonderAppCommands.Start }, + }); + + // Act & Assert + expect(client.isInDevMode).toBe(false); + }); + }); }); diff --git a/packages/ponder-sdk/src/local-ponder-client.ts b/packages/ponder-sdk/src/local-ponder-client.ts index b91dea0ab..35a1b246a 100644 --- a/packages/ponder-sdk/src/local-ponder-client.ts +++ b/packages/ponder-sdk/src/local-ponder-client.ts @@ -12,6 +12,7 @@ import type { LocalChainIndexingMetrics, LocalPonderIndexingMetrics, } from "./local-indexing-metrics"; +import { PonderAppCommands, type PonderAppContext } from "./ponder-app-context"; /** * Local Ponder Client @@ -68,18 +69,27 @@ export class LocalPonderClient extends PonderClient { */ private cachedPublicClients: Map; + /** + * Ponder App Context + * + * The internal context of the local Ponder app. + */ + private ponderAppContext: PonderAppContext; + /** * @param localPonderAppUrl URL of the local Ponder app to connect to. * @param indexedChainIds Configured indexed chain IDs which are used to validate and filter the Ponder app metadata to only include entries for indexed chains. * @param indexedBlockranges Configured indexing blockrange for each indexed chain. * @param ponderPublicClients All cached public clients provided by the local Ponder app * (may include non-indexed chains). + * @param ponderAppContext The internal context of the local Ponder app. */ constructor( localPonderAppUrl: URL, indexedChainIds: Set, indexedBlockranges: Map, ponderPublicClients: Record, + ponderAppContext: PonderAppContext, ) { super(localPonderAppUrl); @@ -103,6 +113,8 @@ export class LocalPonderClient extends PonderClient { cachedPublicClients, "Cached Public Clients", ); + + this.ponderAppContext = ponderAppContext; } /** @@ -170,6 +182,13 @@ export class LocalPonderClient extends PonderClient { return localMetrics; } + /** + * Indicates whether the local Ponder app is running in dev mode. + */ + get isInDevMode(): boolean { + return this.ponderAppContext.command === PonderAppCommands.Dev; + } + /** * Builds a map of cached public clients based on the Ponder cached public clients. * diff --git a/packages/ponder-sdk/src/ponder-app-context.ts b/packages/ponder-sdk/src/ponder-app-context.ts new file mode 100644 index 000000000..9964efdee --- /dev/null +++ b/packages/ponder-sdk/src/ponder-app-context.ts @@ -0,0 +1,23 @@ +/** + * Ponder app commands + * + * Represents the commands that can be used to start a Ponder app. + */ +export const PonderAppCommands = { + Dev: "dev", + Start: "start", +} as const; + +export type PonderAppCommand = (typeof PonderAppCommands)[keyof typeof PonderAppCommands]; + +/** + * Ponder app context + * + * Represents the internal context of a local Ponder app. + */ +export interface PonderAppContext { + /** + * Command used to start the Ponder app. + */ + command: PonderAppCommand; +}