-
Notifications
You must be signed in to change notification settings - Fork 15
feat(ensindexer): ENSDb Writer Worker #1406
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
92ad577
43ed0e2
1f362f3
781e855
d362c24
aecaf14
623260f
dd19ce9
e300dd9
58d6115
b21c67d
57fcd2a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "ensindexer": minor | ||
| --- | ||
|
|
||
| Introduces a worker that writes serialized representations of ENSIndexer Public Config and Indexing Status to ENSDb. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@ensnode/ensnode-schema": minor | ||
| --- | ||
|
|
||
| Includes schema for `ENSNodeMetadata`. |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,134 @@ | ||||||||||||
| /** | ||||||||||||
| * This file manages syncing ENSNode metadata: | ||||||||||||
| * - ENSIndexer Public Config | ||||||||||||
| * - Indexing Status | ||||||||||||
| * into the ENSDb. | ||||||||||||
|
Comment on lines
+2
to
+5
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
| */ | ||||||||||||
| import { secondsToMilliseconds } from "date-fns"; | ||||||||||||
| import pRetry from "p-retry"; | ||||||||||||
|
|
||||||||||||
| import { | ||||||||||||
| CrossChainIndexingStatusSnapshot, | ||||||||||||
| type Duration, | ||||||||||||
| ENSIndexerPublicConfig, | ||||||||||||
| IndexingStatusResponseCodes, | ||||||||||||
| OmnichainIndexingStatusIds, | ||||||||||||
| validateENSIndexerPublicConfigCompatibility, | ||||||||||||
| } from "@ensnode/ensnode-sdk"; | ||||||||||||
|
|
||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does this file live under the |
||||||||||||
| import { EnsDbClient } from "@/lib/ensdb"; | ||||||||||||
| import { ensIndexerClient, waitForEnsIndexerToBecomeHealthy } from "@/lib/ensindexer"; | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Do I understand this right? Goal: It is confusing for someone reading the code here why ENSIndexer has its own client here. I appreciate how we're deep into this context now but goal is to optimize for someone reading this code 6 months from now. |
||||||||||||
|
|
||||||||||||
| const INDEXING_STATUS_RECORD_UPDATE_INTERVAL: Duration = 1; | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| * ENSDb Writer Worker | ||||||||||||
| * | ||||||||||||
| * Runs the following tasks: | ||||||||||||
| * 1) On application startup, attempt to upsert serialized representation of | ||||||||||||
| * {@link ENSIndexerPublicConfig} into ENSDb. | ||||||||||||
| * 2) On application startup, and then on recurring basis, | ||||||||||||
| * following the {@link INDEXING_STATUS_RECORD_UPDATE_INTERVAL}, attempt to | ||||||||||||
| * upsert serialized representation of {@link CrossChainIndexingStatusSnapshot} | ||||||||||||
| * into ENSDb. | ||||||||||||
| */ | ||||||||||||
| async function ensDbWriterWorker() { | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Is that fair? Seems much more accurate. Assuming so, please rename all the related things / comments / files / etc. |
||||||||||||
| console.log("ENSDb Writer Worker: waiting for ENSIndexer to become healthy."); | ||||||||||||
lightwalker-eth marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
|
|
||||||||||||
| // 0. Wait for ENSIndexer to become healthy before running the worker's logic | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please take special note of this feedback. When writing comments, you shouldn't essentially just write the same thing as the code is saying, but in English. This violates best practices. Instead, your comments should explain: WHY? Why are we waiting for ENSIndexer to become healthy before running the worker's logic? Why is this valuable? What issue are we trying to avoid by doing this? Please apply this idea across all your work forever in the future. Thanks. |
||||||||||||
| await waitForEnsIndexerToBecomeHealthy; | ||||||||||||
|
|
||||||||||||
| console.log("ENSDb Writer Worker: ENSIndexer is healthy, starting tasks."); | ||||||||||||
|
|
||||||||||||
| // 1. Create ENSDb Client | ||||||||||||
| const ensDbClient = new EnsDbClient(); | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm. I think we need to give more of a detailed focus on the case of a completely fresh We have the existing code path:
Right? Don't we need to wait until step 4 for the tables in Assuming so, this waiting potentially needs to grow a lot if we add logic to wait up to an hour for step 1 above to be complete, waiting for ENSRainbow to become both healthy and ready. And during all of this time, what is ENSApi supposed to do? From its perspective, it's been configured to read from One idea is that maybe we don't want to write at all to One thing that maybe could help here is to fix the situation where ENSIndexer can't initialize Ponder for more than an hour. Maybe we can make it so that ENSRainbow returns its config even before it is ready and as soon as it is healthy (which I imagine should be super fast?). If so then Ponder might be able to initialize the Suggest to take a step back and carefully consider all cases and situations, then to document a plan and share it with the team for review 👍 Please feel welcome to challenge all the ideas I shared above.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Short answer is that all we need to to is to wait for ENSIndexer Here are example logs from ponder application when database schema needs to be initialized: Please note how ponder first create tables, and only then starts returning
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like the idea for ENSApi to use specific ENSApi has to wait for ENSDb to include ENSDb won't have the ENSIndexer won't store any I think we should create a "logical healthcheck" for ENSDb, such that clients connecting to ENSDb could know if the service is ready to be used. For example, we can consider this "logical healthcheck" to ensure that ENSDb includes
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @tk-o Overall agreed, but with a few distinctions:
What do you think? These ideas might also influence how ENSApi implements both its |
||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| * Handle ENSIndexerPublicConfig and ENSDb Version Records | ||||||||||||
| */ | ||||||||||||
| const handleEnsIndexerPublicConfigAndEnsDbVersionRecords = async () => { | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Is that fair?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I note how this function doesn't initialize the metadata field for indexing status. As a result, ENSDb clients have additional complexity in that they need to prepare for a situation where there may be a long duration of time where they can read 2 of the 3 metadata fields, but not the indexing status field. What do you think about this? Appreciate your advice. Should we update this function so that it waits until all 3 metadata fields can be initialized at the same time, and then initializes them all together? |
||||||||||||
| // Read stored config and in-memory config. | ||||||||||||
| // Note: we wrap each operation in pRetry to ensure all of them can be | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not clear or confident on how this pRetry really works in reality. What happens if this repeatedly fails? It just enters an infinite loop where it non-stop repeats these operations without any backoff and just slams them endlessly every nanosecond? And there would be no logging information at all about what's going on? Appreciate if you can please ensure we always approach such operations in a very mature production-ready way. To do this properly we need to optimize both for other devs reading this code as well as ENSNode operators with their own ENSNode instances working to debug some issue via logs. |
||||||||||||
| // completed successfully. | ||||||||||||
| const [storedConfig, inMemoryConfig] = await Promise.all([ | ||||||||||||
| pRetry(() => ensDbClient.getEnsIndexerPublicConfig()), | ||||||||||||
| pRetry(() => ensIndexerClient.config()), | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a special reason why ENSIndexer is getting its own config through an API request to itself rather than just using the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, will document how the special reason is decoupling from dependencies that each of the fetched endpoint requires internally. |
||||||||||||
| ]); | ||||||||||||
|
|
||||||||||||
| // Validate in-memory config object compatibility with the stored one, | ||||||||||||
| // if the stored one is available | ||||||||||||
| if (storedConfig) { | ||||||||||||
| try { | ||||||||||||
| validateENSIndexerPublicConfigCompatibility(storedConfig, inMemoryConfig); | ||||||||||||
| } catch (error) { | ||||||||||||
| const errorMessage = `In-memory ENSIndexerPublicConfig object is not compatible with its counterpart stored in ENSDb.`; | ||||||||||||
|
|
||||||||||||
| // Throw the error to terminate the ENSIndexer process due to | ||||||||||||
| // found config incompatibility | ||||||||||||
| throw new Error(errorMessage, { | ||||||||||||
| cause: error, | ||||||||||||
| }); | ||||||||||||
| } | ||||||||||||
| } else { | ||||||||||||
| // Upsert ENSDb Version into ENSDb. | ||||||||||||
| await ensDbClient.upsertEnsDbVersion(inMemoryConfig.versionInfo.ensDb); | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should change this operation into a pure insert, not an upsert, for safety. Right?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we can leave it as is (upsert) and add the ENSDb version check as part of |
||||||||||||
| // Upsert ENSIndexerPublicConfig into ENSDb. | ||||||||||||
| await ensDbClient.upsertEnsIndexerPublicConfig(inMemoryConfig); | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm... I think we have an issue here. The logic flow here will only upsert the config if there's no stored config. But I don't believe that's right. Whenever the config is confirmed as compatible, we should upsert it. We should note how there may be details in the config that are changing across time that are still compatible. We should always reflect the latest versions of these values in ENSDb via this upsert operation. |
||||||||||||
| } | ||||||||||||
| }; | ||||||||||||
|
|
||||||||||||
| /** | ||||||||||||
| * Handle Indexing Status Record Recursively | ||||||||||||
| */ | ||||||||||||
| const handleIndexingStatusRecordRecursively = async () => { | ||||||||||||
| try { | ||||||||||||
| // Read in-memory Indexing Status. | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wouldn't it be more accurate to say that we are fetching the indexing status locally through ENSIndexer's own API? |
||||||||||||
| const inMemoryIndexingStatus = await ensIndexerClient.indexingStatus(); | ||||||||||||
|
|
||||||||||||
| // Check if Indexing Status is available. | ||||||||||||
| if (inMemoryIndexingStatus.responseCode !== IndexingStatusResponseCodes.Ok) { | ||||||||||||
| throw new Error("Indexing Status must be available."); | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to write better. This should explain why. |
||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| const { snapshot } = inMemoryIndexingStatus.realtimeProjection; | ||||||||||||
| const { omnichainSnapshot } = snapshot; | ||||||||||||
|
|
||||||||||||
| // Check if Indexing Status is in expected status. | ||||||||||||
lightwalker-eth marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
| // The Omnichain Status must indicate that indexing has started already. | ||||||||||||
| // Throw an error if Omnichain Status is "Unstarted". | ||||||||||||
| if (omnichainSnapshot.omnichainStatus === OmnichainIndexingStatusIds.Unstarted) { | ||||||||||||
| throw new Error("Omnichain Status must be different than 'Unstarted'."); | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to explain why. Please see another comment where I gave a detailed suggestion for this. |
||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Upsert ENSIndexerPublicConfig into ENSDb. | ||||||||||||
| await ensDbClient.upsertIndexingStatus(snapshot); | ||||||||||||
| } catch (error) { | ||||||||||||
| // Do nothing about this error, but having it logged. | ||||||||||||
| const errorMessage = error instanceof Error ? error.message : "Unknown error"; | ||||||||||||
| console.error(`Could not upsert Indexing Status record due to: ${errorMessage}`); | ||||||||||||
| } finally { | ||||||||||||
| // Regardless of current iteration result, | ||||||||||||
| // schedule the next callback to handle Indexing Status Record. | ||||||||||||
| setTimeout( | ||||||||||||
| handleIndexingStatusRecordRecursively, | ||||||||||||
| secondsToMilliseconds(INDEXING_STATUS_RECORD_UPDATE_INTERVAL), | ||||||||||||
| ); | ||||||||||||
| } | ||||||||||||
| }; | ||||||||||||
|
|
||||||||||||
| // 4. Handle ENSIndexer Public Config and ENSDb Version just once. | ||||||||||||
| console.log("Task: store ENSIndexer Public Config and ENSDb Version in ENSDb."); | ||||||||||||
| await handleEnsIndexerPublicConfigAndEnsDbVersionRecords(); | ||||||||||||
| console.log("ENSIndexer Public Config and ENSDb Version successfully stored in ENSDb."); | ||||||||||||
|
|
||||||||||||
| // 5. Handle Indexing Status on recurring basis. | ||||||||||||
| console.log("Task: store Indexing Status in ENSDb."); | ||||||||||||
| await handleIndexingStatusRecordRecursively(); | ||||||||||||
| console.log("Indexing Status successfully stored in ENSDb."); | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Run ENSDb Writer Worker in the background. | ||||||||||||
| ensDbWriterWorker().catch((error) => { | ||||||||||||
| console.error("ENSDb Writer Worker failed to perform its tasks", error); | ||||||||||||
| process.exit(1); | ||||||||||||
| }); | ||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| // This file was copied 1-to-1 from ENSApi. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it doesn't seem trivial or possible to deduplicate this into ensnode-sdk, so let's wait until we have an ensnode-internal package or something to dry this up — can you add a TODO here indicating that? like
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. likewise for
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. actually because ensindexer isn't using pino-formatted logs consistently, i think it might be best if we just use console.log in these cases. in which case i'd remove the debug logging in the client and let the consumer of the client handle all of the logging. in-client logging is only useful if we can indicate a log level to ignore it by default. if you decide to keep pino in ensindexer, you'd want to add
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, I'm fine with using native logger from |
||
| // TODO: deduplicate with apps/ensapi/src/lib/handlers/drizzle.ts when ensnode nodejs internal package is created | ||
lightwalker-eth marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| import { setDatabaseSchema } from "@ponder/client"; | ||
| import { drizzle } from "drizzle-orm/node-postgres"; | ||
|
|
||
| type Schema = { [name: string]: unknown }; | ||
|
|
||
| /** | ||
| * Makes a Drizzle DB object. | ||
| */ | ||
| export const makeDrizzle = <SCHEMA extends Schema>({ | ||
| schema, | ||
| databaseUrl, | ||
| databaseSchema, | ||
| }: { | ||
| schema: SCHEMA; | ||
| databaseUrl: string; | ||
| databaseSchema: string; | ||
| }) => { | ||
| // monkeypatch schema onto tables | ||
| setDatabaseSchema(schema, databaseSchema); | ||
|
|
||
| return drizzle(databaseUrl, { schema, casing: "snake_case" }); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,192 @@ | ||
| import config from "@/config"; | ||
|
|
||
tk-o marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| import { eq } from "drizzle-orm/sql"; | ||
|
|
||
| import * as schema from "@ensnode/ensnode-schema"; | ||
| import { | ||
| type CrossChainIndexingStatusSnapshot, | ||
| deserializeCrossChainIndexingStatusSnapshot, | ||
| deserializeENSIndexerPublicConfig, | ||
| type ENSIndexerPublicConfig, | ||
| serializeCrossChainIndexingStatusSnapshotOmnichain, | ||
| serializeENSIndexerPublicConfig, | ||
| } from "@ensnode/ensnode-sdk"; | ||
|
|
||
| import { makeDrizzle } from "./drizzle"; | ||
| import { | ||
| EnsNodeMetadataKeys, | ||
| type SerializedEnsNodeMetadata, | ||
| type SerializedEnsNodeMetadataEnsDbVersion, | ||
| type SerializedEnsNodeMetadataEnsIndexerPublicConfig, | ||
| type SerializedEnsNodeMetadataIndexingStatus, | ||
| } from "./ensnode-metadata"; | ||
|
|
||
| /** | ||
| * ENSDb Client Query | ||
| * | ||
| Includes methods for reading from ENSDb. | ||
| */ | ||
| export interface EnsDbClientQuery { | ||
lightwalker-eth marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| /** | ||
| * Get ENSDb Version | ||
| * | ||
| * @returns the existing record, or `undefined`. | ||
| * @throws if not exactly one record was found. | ||
| */ | ||
| getEnsDbVersion(): Promise<string | undefined>; | ||
|
|
||
| /** | ||
| * Get ENSIndexer Public Config | ||
| * | ||
| * @returns the existing record, or `undefined`. | ||
| * @throws if not exactly one record was found. | ||
| */ | ||
| getEnsIndexerPublicConfig(): Promise<ENSIndexerPublicConfig | undefined>; | ||
|
|
||
| /** | ||
| * Get Indexing Status | ||
| * | ||
| * @returns the existing record, or `undefined`. | ||
| * @throws if not exactly one record was found. | ||
| */ | ||
| getIndexingStatus(): Promise<CrossChainIndexingStatusSnapshot | undefined>; | ||
| } | ||
|
|
||
| /** | ||
| * ENSDb Client Mutation | ||
| * | ||
| * Includes methods for writing into ENSDb. | ||
| */ | ||
| export interface EnsDbClientMutation { | ||
| /** | ||
| * Upsert ENSDb Version | ||
| * | ||
| * @throws when upsert operation failed. | ||
| */ | ||
| upsertEnsDbVersion(ensDbVersion: string): Promise<void>; | ||
|
|
||
| /** | ||
| * Upsert ENSIndexer Public Config | ||
| * | ||
| * @throws when upsert operation failed. | ||
| */ | ||
| upsertEnsIndexerPublicConfig(ensIndexerPublicConfig: ENSIndexerPublicConfig): Promise<void>; | ||
|
|
||
| /** | ||
| * Upsert Indexing Status | ||
| * | ||
| * @throws when upsert operation failed. | ||
| */ | ||
| upsertIndexingStatus(indexingStatus: CrossChainIndexingStatusSnapshot): Promise<void>; | ||
| } | ||
|
|
||
| /** | ||
| * ENSDb Client | ||
| */ | ||
| export class EnsDbClient implements EnsDbClientQuery, EnsDbClientMutation { | ||
| #db = makeDrizzle({ | ||
| databaseSchema: config.databaseSchemaName, | ||
| databaseUrl: config.databaseUrl, | ||
| schema, | ||
| }); | ||
|
|
||
| async getEnsDbVersion(): Promise<string | undefined> { | ||
| const record = await this.getEnsNodeMetadata<SerializedEnsNodeMetadataEnsDbVersion>({ | ||
| key: EnsNodeMetadataKeys.EnsDbVersion, | ||
| }); | ||
|
|
||
| return record; | ||
| } | ||
|
|
||
| async getEnsIndexerPublicConfig(): Promise<ENSIndexerPublicConfig | undefined> { | ||
| const record = await this.getEnsNodeMetadata<SerializedEnsNodeMetadataEnsIndexerPublicConfig>({ | ||
| key: EnsNodeMetadataKeys.EnsIndexerPublicConfig, | ||
| }); | ||
|
|
||
| if (!record) { | ||
| return undefined; | ||
| } | ||
|
|
||
| return deserializeENSIndexerPublicConfig(record); | ||
| } | ||
|
|
||
| async getIndexingStatus(): Promise<CrossChainIndexingStatusSnapshot | undefined> { | ||
| const record = await this.getEnsNodeMetadata<SerializedEnsNodeMetadataIndexingStatus>({ | ||
| key: EnsNodeMetadataKeys.IndexingStatus, | ||
| }); | ||
|
|
||
| if (!record) { | ||
| return undefined; | ||
| } | ||
|
|
||
| return deserializeCrossChainIndexingStatusSnapshot(record); | ||
| } | ||
|
|
||
| async upsertEnsDbVersion(ensDbVersion: string): Promise<void> { | ||
| await this.upsertEnsNodeMetadata({ | ||
| key: EnsNodeMetadataKeys.EnsDbVersion, | ||
| value: ensDbVersion, | ||
| }); | ||
| } | ||
|
|
||
| async upsertEnsIndexerPublicConfig( | ||
| ensIndexerPublicConfig: ENSIndexerPublicConfig, | ||
| ): Promise<void> { | ||
| await this.upsertEnsNodeMetadata({ | ||
| key: EnsNodeMetadataKeys.EnsIndexerPublicConfig, | ||
| value: serializeENSIndexerPublicConfig(ensIndexerPublicConfig), | ||
| }); | ||
| } | ||
|
|
||
| async upsertIndexingStatus(indexingStatus: CrossChainIndexingStatusSnapshot): Promise<void> { | ||
| await this.upsertEnsNodeMetadata({ | ||
| key: EnsNodeMetadataKeys.IndexingStatus, | ||
| value: serializeCrossChainIndexingStatusSnapshotOmnichain(indexingStatus), | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Get ENSNode metadata record | ||
| * | ||
| * @returns selected record in ENSDb. | ||
| * @throws when exactly one matching metadata record was not found | ||
| */ | ||
| private async getEnsNodeMetadata< | ||
| EnsNodeMetadataType extends SerializedEnsNodeMetadata = SerializedEnsNodeMetadata, | ||
| >(metadata: Pick<EnsNodeMetadataType, "key">): Promise<EnsNodeMetadataType["value"] | undefined> { | ||
| const result = await this.#db | ||
| .select() | ||
| .from(schema.ensNodeMetadata) | ||
| .where(eq(schema.ensNodeMetadata.key, metadata.key)); | ||
|
|
||
| if (result.length === 0) { | ||
| return undefined; | ||
| } | ||
|
|
||
| if (result.length === 1 && result[0]) { | ||
| return result[0].value as EnsNodeMetadataType["value"]; | ||
| } | ||
|
|
||
| throw new Error(`There must be exactly one ENSNodeMetadata record for '${metadata.key}' key`); | ||
| } | ||
|
|
||
| /** | ||
| * Upsert ENSNode metadata | ||
| * | ||
| * @throws when upsert operation failed. | ||
| */ | ||
| private async upsertEnsNodeMetadata< | ||
| EnsNodeMetadataType extends SerializedEnsNodeMetadata = SerializedEnsNodeMetadata, | ||
| >(metadata: EnsNodeMetadataType): Promise<void> { | ||
| await this.#db | ||
| .insert(schema.ensNodeMetadata) | ||
| .values({ | ||
| key: metadata.key, | ||
| value: metadata.value, | ||
| }) | ||
| .onConflictDoUpdate({ | ||
| target: schema.ensNodeMetadata.key, | ||
| set: { value: metadata.value }, | ||
| }); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.