Skip to content
Merged
5 changes: 5 additions & 0 deletions .changeset/better-bats-switch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ensindexer": minor
---

Improved developer experience by skipping validation step in ENSDb Writer Worker while in dev mode.
5 changes: 5 additions & 0 deletions .changeset/old-seals-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ensnode/ponder-sdk": minor
---

Introduced `PonderAppContext` data model to capture the internal context of a local Ponder app.
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -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");
Expand All @@ -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");
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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);
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -324,16 +332,14 @@ describe("EnsDbWriterWorker", () => {
.mockRejectedValueOnce(new Error("DB error"))
.mockResolvedValueOnce(undefined),
});
const publicConfigBuilder = createMockPublicConfigBuilder();
const indexingStatusBuilder = {
getOmnichainIndexingStatusSnapshot: vi
.fn()
.mockResolvedValueOnce(snapshot1)
.mockResolvedValueOnce(snapshot2)
.mockResolvedValueOnce(snapshot2),
} as unknown as IndexingStatusBuilder;

const worker = new EnsDbWriterWorker(ensDbClient, publicConfigBuilder, indexingStatusBuilder);
const worker = createMockEnsDbWriterWorker({ ensDbClient, indexingStatusBuilder });

// act
await worker.run();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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<EnsIndexerPublicConfig> {
/**
Expand Down Expand Up @@ -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.
Comment on lines +197 to +201
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method-level docs above say compatibility validation runs whenever a stored config exists, but the implementation now skips validation in dev mode. Please update the docstring/@throws description to reflect the new behavior (i.e., validation is conditional on both storedConfig and not being in dev mode).

Suggested change
// 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 the stored one is available and the local Ponder app is not running
// in dev mode.
// The validation is intentionally skipped in dev mode to improve the
// development experience during ENSIndexer development, by allowing
// overriding the stored config in ENSDb with the current in-memory config
// without having to keep them compatible.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, will update the docs.

if (storedConfig && !this.localPonderClient.isInDevMode) {
try {
validateEnsIndexerPublicConfigCompatibility(storedConfig, inMemoryConfig);
} catch (error) {
Expand Down
2 changes: 2 additions & 0 deletions apps/ensindexer/src/lib/ensdb-writer-worker/singleton.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -24,6 +25,7 @@ export function startEnsDbWriterWorker() {
ensDbClient,
publicConfigBuilder,
indexingStatusBuilder,
localPonderClient,
);

ensDbWriterWorker
Expand Down
Loading
Loading