diff --git a/apps/ensapi/src/handlers/api/explore/name-tokens-api.ts b/apps/ensapi/src/handlers/api/explore/name-tokens-api.ts index 888d9b6459..00910fc66e 100644 --- a/apps/ensapi/src/handlers/api/explore/name-tokens-api.ts +++ b/apps/ensapi/src/handlers/api/explore/name-tokens-api.ts @@ -17,22 +17,18 @@ import { import { createApp } from "@/lib/hono-factory"; import { findRegisteredNameTokensForDomain } from "@/lib/name-tokens/find-name-tokens-for-domain"; import { getIndexedSubregistries } from "@/lib/name-tokens/get-indexed-subregistries"; +import { indexingStatusMiddleware } from "@/middleware/indexing-status.middleware"; import { nameTokensApiMiddleware } from "@/middleware/name-tokens.middleware"; import { getNameTokensRoute } from "./name-tokens-api.routes"; -const app = createApp(); +const app = createApp({ middlewares: [indexingStatusMiddleware, nameTokensApiMiddleware] }); const indexedSubregistries = getIndexedSubregistries( config.namespace, config.ensIndexerPublicConfig.plugins as PluginName[], ); -// Middleware managing access to Name Tokens API route. -// It makes the route available if all prerequisites are met, -// and if not returns the appropriate HTTP 503 (Service Unavailable) error. -app.use(nameTokensApiMiddleware); - /** * Factory function for creating a 404 Name Tokens Not Indexed error response */ @@ -48,21 +44,6 @@ const makeNameTokensNotIndexedResponse = ( }); app.openapi(getNameTokensRoute, async (c) => { - // Invariant: context must be set by the required middleware - if (c.var.indexingStatus === undefined) { - return c.json( - serializeNameTokensResponse({ - responseCode: NameTokensResponseCodes.Error, - errorCode: NameTokensResponseErrorCodes.IndexingStatusUnsupported, - error: { - message: "Name Tokens API is not available yet", - details: "Indexing status middleware is required but not initialized.", - }, - }), - 503, - ); - } - // Check if Indexing Status resolution failed. if (c.var.indexingStatus instanceof Error) { return c.json( diff --git a/apps/ensapi/src/handlers/api/explore/registrar-actions-api.ts b/apps/ensapi/src/handlers/api/explore/registrar-actions-api.ts index df60954088..1e14060ab4 100644 --- a/apps/ensapi/src/handlers/api/explore/registrar-actions-api.ts +++ b/apps/ensapi/src/handlers/api/explore/registrar-actions-api.ts @@ -12,6 +12,7 @@ import { import { createApp } from "@/lib/hono-factory"; import { makeLogger } from "@/lib/logger"; import { findRegistrarActions } from "@/lib/registrar-actions/find-registrar-actions"; +import { indexingStatusMiddleware } from "@/middleware/indexing-status.middleware"; import { registrarActionsApiMiddleware } from "@/middleware/registrar-actions.middleware"; import { @@ -20,14 +21,10 @@ import { type RegistrarActionsQuery, } from "./registrar-actions-api.routes"; -const app = createApp(); +const app = createApp({ middlewares: [indexingStatusMiddleware, registrarActionsApiMiddleware] }); const logger = makeLogger("registrar-actions-api"); -// Middleware managing access to Registrar Actions API routes. -// It makes the routes available if all prerequisites are met. -app.use(registrarActionsApiMiddleware); - // Shared business logic for fetching registrar actions async function fetchRegistrarActions(parentNode: Node | undefined, query: RegistrarActionsQuery) { const { @@ -150,10 +147,8 @@ app.openapi(getRegistrarActionsRoute, async (c) => { */ app.openapi(getRegistrarActionsByParentNodeRoute, async (c) => { try { - // Middleware ensures indexingStatus is available and not an Error - // This check is for TypeScript type safety - if (!c.var.indexingStatus || c.var.indexingStatus instanceof Error) { - throw new Error("Invariant violation: indexingStatus should be validated by middleware"); + if (c.var.indexingStatus instanceof Error) { + throw new Error("Indexing status has not been loaded yet"); } const { parentNode } = c.req.valid("param"); diff --git a/apps/ensapi/src/handlers/api/graphql/ensnode-graphql-api.ts b/apps/ensapi/src/handlers/api/graphql/ensnode-graphql-api.ts index 42cc6b2ccc..04efec0aa1 100644 --- a/apps/ensapi/src/handlers/api/graphql/ensnode-graphql-api.ts +++ b/apps/ensapi/src/handlers/api/graphql/ensnode-graphql-api.ts @@ -2,9 +2,9 @@ import config from "@/config"; import { hasGraphqlApiConfigSupport } from "@ensnode/ensnode-sdk"; -import { factory } from "@/lib/hono-factory"; +import { createApp } from "@/lib/hono-factory"; -const app = factory.createApp(); +const app = createApp(); // 503 if ensv2 plugin not available app.use(async (c, next) => { diff --git a/apps/ensapi/src/handlers/api/meta/realtime-api.ts b/apps/ensapi/src/handlers/api/meta/realtime-api.ts index 6c222ca012..9b57f0bb7b 100644 --- a/apps/ensapi/src/handlers/api/meta/realtime-api.ts +++ b/apps/ensapi/src/handlers/api/meta/realtime-api.ts @@ -1,18 +1,14 @@ import { errorResponse } from "@/lib/handlers/error-response"; import { createApp } from "@/lib/hono-factory"; +import { indexingStatusMiddleware } from "@/middleware/indexing-status.middleware"; import { realtimeGetMeta } from "./realtime-api.routes"; -const app = createApp(); +const app = createApp({ middlewares: [indexingStatusMiddleware] }); // allow performance monitoring clients to read HTTP Status for the provided // `maxWorstCaseDistance` param app.openapi(realtimeGetMeta, async (c) => { - // context must be set by the required middleware - if (c.var.indexingStatus === undefined) { - throw new Error(`Invariant(realtime-api): indexingStatusMiddleware required.`); - } - // return 503 response error with details on prerequisite being unavailable if (c.var.indexingStatus instanceof Error) { return errorResponse( diff --git a/apps/ensapi/src/handlers/api/meta/status-api.ts b/apps/ensapi/src/handlers/api/meta/status-api.ts index b295823b8b..86a7c06692 100644 --- a/apps/ensapi/src/handlers/api/meta/status-api.ts +++ b/apps/ensapi/src/handlers/api/meta/status-api.ts @@ -10,10 +10,11 @@ import { import { buildEnsApiPublicConfig } from "@/config/config.schema"; import { createApp } from "@/lib/hono-factory"; +import { indexingStatusMiddleware } from "@/middleware/indexing-status.middleware"; import { getConfigRoute, getIndexingStatusRoute } from "./status-api.routes"; -const app = createApp(); +const app = createApp({ middlewares: [indexingStatusMiddleware] }); app.openapi(getConfigRoute, async (c) => { const ensApiPublicConfig = buildEnsApiPublicConfig(config); @@ -21,11 +22,6 @@ app.openapi(getConfigRoute, async (c) => { }); app.openapi(getIndexingStatusRoute, async (c) => { - // context must be set by the required middleware - if (c.var.indexingStatus === undefined) { - throw new Error(`Invariant(indexing-status): indexingStatusMiddleware required`); - } - if (c.var.indexingStatus instanceof Error) { return c.json( serializeEnsApiIndexingStatusResponse({ diff --git a/apps/ensapi/src/handlers/api/resolution/resolution-api.ts b/apps/ensapi/src/handlers/api/resolution/resolution-api.ts index aedb45cb18..9aabc9bd81 100644 --- a/apps/ensapi/src/handlers/api/resolution/resolution-api.ts +++ b/apps/ensapi/src/handlers/api/resolution/resolution-api.ts @@ -11,6 +11,7 @@ import { resolvePrimaryNames } from "@/lib/resolution/multichain-primary-name-re import { resolveReverse } from "@/lib/resolution/reverse-resolution"; import { runWithTrace } from "@/lib/tracing/tracing-api"; import { canAccelerateMiddleware } from "@/middleware/can-accelerate.middleware"; +import { indexingStatusMiddleware } from "@/middleware/indexing-status.middleware"; import { makeIsRealtimeMiddleware } from "@/middleware/is-realtime.middleware"; import { @@ -25,12 +26,13 @@ import { */ const MAX_REALTIME_DISTANCE_TO_ACCELERATE: Duration = 60; // 1 minute in seconds -const app = createApp(); - -// inject c.var.isRealtime derived from MAX_REALTIME_DISTANCE_TO_ACCELERATE -app.use(makeIsRealtimeMiddleware("resolution-api", MAX_REALTIME_DISTANCE_TO_ACCELERATE)); -// inject c.var.canAccelerate derived from that c.var.isRealtime -app.use(canAccelerateMiddleware); +const app = createApp({ + middlewares: [ + indexingStatusMiddleware, + makeIsRealtimeMiddleware("resolution-api", MAX_REALTIME_DISTANCE_TO_ACCELERATE), + canAccelerateMiddleware, + ], +}); /** * Example queries for /records: @@ -45,11 +47,6 @@ app.use(canAccelerateMiddleware); * GET /records/example.eth&name=true&addresses=60,0&texts=avatar,com.twitter */ app.openapi(resolveRecordsRoute, async (c) => { - // context must be set by the required middleware - if (c.var.canAccelerate === undefined) { - throw new Error(`Invariant(resolution-api): canAccelerateMiddleware required`); - } - const { name } = c.req.valid("param"); const { selection, trace: showTrace, accelerate } = c.req.valid("query"); const canAccelerate = c.var.canAccelerate; @@ -82,11 +79,6 @@ app.openapi(resolveRecordsRoute, async (c) => { * GET /primary-name/0x1234...abcd/0 */ app.openapi(resolvePrimaryNameRoute, async (c) => { - // context must be set by the required middleware - if (c.var.canAccelerate === undefined) { - throw new Error(`Invariant(resolution-api): canAccelerateMiddleware required`); - } - const { address, chainId } = c.req.valid("param"); const { trace: showTrace, accelerate } = c.req.valid("query"); const canAccelerate = c.var.canAccelerate; @@ -116,11 +108,6 @@ app.openapi(resolvePrimaryNameRoute, async (c) => { * GET /primary-names/0x1234...abcd?chainIds=1,10,8453 */ app.openapi(resolvePrimaryNamesRoute, async (c) => { - // context must be set by the required middleware - if (c.var.canAccelerate === undefined) { - throw new Error(`Invariant(resolution-api): canAccelerateMiddleware required`); - } - const { address } = c.req.valid("param"); const { chainIds, trace: showTrace, accelerate } = c.req.valid("query"); const canAccelerate = c.var.canAccelerate; diff --git a/apps/ensapi/src/handlers/ensanalytics/ensanalytics-api-v1.ts b/apps/ensapi/src/handlers/ensanalytics/ensanalytics-api-v1.ts index a062e3ed40..2bf4e9a30f 100644 --- a/apps/ensapi/src/handlers/ensanalytics/ensanalytics-api-v1.ts +++ b/apps/ensapi/src/handlers/ensanalytics/ensanalytics-api-v1.ts @@ -29,23 +29,15 @@ import { const logger = makeLogger("ensanalytics-api-v1"); -const app = createApp(); - -// Apply referral program edition config set middleware -app.use(referralProgramEditionConfigSetMiddleware); - -// Apply referrer leaderboard cache middleware (depends on edition config set middleware) -app.use(referralLeaderboardEditionsCachesMiddleware); +const app = createApp({ + middlewares: [ + referralProgramEditionConfigSetMiddleware, + referralLeaderboardEditionsCachesMiddleware, + ], +}); // Get a page from the referrer leaderboard for a specific edition app.openapi(getReferralLeaderboardRoute, async (c) => { - // context must be set by the required middleware - if (c.var.referralLeaderboardEditionsCaches === undefined) { - throw new Error( - `Invariant(ensanalytics-api-v1): referralLeaderboardEditionsCachesMiddleware required`, - ); - } - try { const { edition, page, recordsPerPage } = c.req.valid("query"); @@ -122,13 +114,6 @@ app.openapi(getReferralLeaderboardRoute, async (c) => { // Get referrer detail for a specific address for requested editions app.openapi(getReferrerDetailRoute, async (c) => { - // context must be set by the required middleware - if (c.var.referralLeaderboardEditionsCaches === undefined) { - throw new Error( - `Invariant(ensanalytics-api-v1): referralLeaderboardEditionsCachesMiddleware required`, - ); - } - try { const { referrer } = c.req.valid("param"); const { editions } = c.req.valid("query"); @@ -238,19 +223,6 @@ app.openapi(getReferrerDetailRoute, async (c) => { // Get edition summaries app.openapi(getEditionsRoute, async (c) => { - // context must be set by the required middleware - if (c.var.referralProgramEditionConfigSet === undefined) { - throw new Error( - `Invariant(ensanalytics-api-v1): referralProgramEditionConfigSetMiddleware required`, - ); - } - - if (c.var.referralLeaderboardEditionsCaches === undefined) { - throw new Error( - `Invariant(ensanalytics-api-v1): referralLeaderboardEditionsCachesMiddleware required`, - ); - } - try { // Check if edition config set failed to load if (c.var.referralProgramEditionConfigSet instanceof Error) { diff --git a/apps/ensapi/src/handlers/ensanalytics/ensanalytics-api.ts b/apps/ensapi/src/handlers/ensanalytics/ensanalytics-api.ts index 35d9f7e5d5..875f49bfac 100644 --- a/apps/ensapi/src/handlers/ensanalytics/ensanalytics-api.ts +++ b/apps/ensapi/src/handlers/ensanalytics/ensanalytics-api.ts @@ -17,18 +17,10 @@ import { getReferrerDetailRoute, getReferrerLeaderboardRoute } from "./ensanalyt const logger = makeLogger("ensanalytics-api"); -const app = createApp(); - -// Apply referrer leaderboard cache middleware to all routes in this handler -app.use(referrerLeaderboardMiddleware); +const app = createApp({ middlewares: [referrerLeaderboardMiddleware] }); // Get a page from the referrer leaderboard app.openapi(getReferrerLeaderboardRoute, async (c) => { - // context must be set by the required middleware - if (c.var.referrerLeaderboard === undefined) { - throw new Error(`Invariant(ensanalytics-api): referrerLeaderboardMiddleware required`); - } - try { if (c.var.referrerLeaderboard instanceof Error) { return c.json( @@ -72,11 +64,6 @@ app.openapi(getReferrerLeaderboardRoute, async (c) => { // Get referrer detail for a specific address app.openapi(getReferrerDetailRoute, async (c) => { - // context must be set by the required middleware - if (c.var.referrerLeaderboard === undefined) { - throw new Error(`Invariant(ensanalytics-api): referrerLeaderboardMiddleware required`); - } - try { // Check if leaderboard failed to load if (c.var.referrerLeaderboard instanceof Error) { diff --git a/apps/ensapi/src/handlers/subgraph/subgraph-api.ts b/apps/ensapi/src/handlers/subgraph/subgraph-api.ts index fcfb8afdc7..d05e18cff0 100644 --- a/apps/ensapi/src/handlers/subgraph/subgraph-api.ts +++ b/apps/ensapi/src/handlers/subgraph/subgraph-api.ts @@ -6,10 +6,11 @@ import * as schema from "@ensnode/ensdb-sdk"; import { type Duration, hasSubgraphApiConfigSupport } from "@ensnode/ensnode-sdk"; import { subgraphGraphQLMiddleware } from "@ensnode/ponder-subgraph"; -import { factory } from "@/lib/hono-factory"; +import { createApp } from "@/lib/hono-factory"; import { makeSubgraphApiDocumentation } from "@/lib/subgraph/api-documentation"; import { filterSchemaByPrefix } from "@/lib/subgraph/filter-schema-by-prefix"; import { fixContentLengthMiddleware } from "@/middleware/fix-content-length.middleware"; +import { indexingStatusMiddleware } from "@/middleware/indexing-status.middleware"; import { makeIsRealtimeMiddleware } from "@/middleware/is-realtime.middleware"; import { subgraphMetaMiddleware } from "@/middleware/subgraph-meta.middleware"; import { thegraphFallbackMiddleware } from "@/middleware/thegraph-fallback.middleware"; @@ -19,7 +20,7 @@ const MAX_REALTIME_DISTANCE_TO_RESOLVE: Duration = 10 * 60; // 10 minutes in sec // generate a subgraph-specific subset of the schema const subgraphSchema = filterSchemaByPrefix("subgraph_", schema); -const app = factory.createApp(); +const app = createApp(); // 503 if subgraph plugin not available app.use(async (c, next) => { @@ -31,6 +32,9 @@ app.use(async (c, next) => { await next(); }); +// inject c.var.indexingStatus +app.use(indexingStatusMiddleware); + // inject c.var.isRealtime derived from MAX_REALTIME_DISTANCE_TO_RESOLVE app.use(makeIsRealtimeMiddleware("subgraph-api", MAX_REALTIME_DISTANCE_TO_RESOLVE)); diff --git a/apps/ensapi/src/index.ts b/apps/ensapi/src/index.ts index a5db69aeb4..d074adb9a8 100644 --- a/apps/ensapi/src/index.ts +++ b/apps/ensapi/src/index.ts @@ -12,10 +12,9 @@ import { referralProgramEditionConfigSetCache } from "@/cache/referral-program-e import { referrerLeaderboardCache } from "@/cache/referrer-leaderboard.cache"; import { redactEnsApiConfig } from "@/config/redact"; import { errorResponse } from "@/lib/handlers/error-response"; -import { factory } from "@/lib/hono-factory"; +import { createApp } from "@/lib/hono-factory"; import { sdk } from "@/lib/instrumentation"; import logger from "@/lib/logger"; -import { indexingStatusMiddleware } from "@/middleware/indexing-status.middleware"; import { generateOpenApi31Document } from "@/openapi-document"; import realtimeApi from "./handlers/api/meta/realtime-api"; @@ -24,7 +23,7 @@ import ensanalyticsApi from "./handlers/ensanalytics/ensanalytics-api"; import ensanalyticsApiV1 from "./handlers/ensanalytics/ensanalytics-api-v1"; import subgraphApi from "./handlers/subgraph/subgraph-api"; -const app = factory.createApp(); +const app = createApp(); // set the X-ENSNode-Version header to the current version app.use(async (ctx, next) => { @@ -38,9 +37,6 @@ app.use(cors({ origin: "*" })); // include automatic OpenTelemetry instrumentation for incoming requests app.use(otel()); -// add ENSIndexer Indexing Status Middleware to all routes for convenience -app.use(indexingStatusMiddleware); - // host welcome page app.get("/", (c) => c.html(html` diff --git a/apps/ensapi/src/lib/hono-factory.ts b/apps/ensapi/src/lib/hono-factory.ts index fe3a8743f2..cfae1519ae 100644 --- a/apps/ensapi/src/lib/hono-factory.ts +++ b/apps/ensapi/src/lib/hono-factory.ts @@ -1,4 +1,5 @@ import { OpenAPIHono } from "@hono/zod-openapi"; +import type { MiddlewareHandler } from "hono"; import { createFactory } from "hono/factory"; import { errorResponse } from "@/lib/handlers/error-response"; @@ -18,14 +19,114 @@ export type MiddlewareVariables = IndexingStatusMiddlewareVariables & type AppEnv = { Variables: Partial }; +/** + * Produces an env type where the specified keys of MiddlewareVariables are required (non-optional). + * All other middleware variables remain optional. + */ +type RequireVars = Omit< + Partial, + TRequired +> & + Required>; + export const factory = createFactory(); -export function createApp() { - return new OpenAPIHono({ +/** A middleware that declares the context variable keys it produces via `__produces`. */ +export type ProducingMiddleware = + MiddlewareHandler & { readonly __produces: readonly K[] }; + +type ExtractProduced = T extends ProducingMiddleware ? K : never; + +/** + * Tags a middleware with the context variable keys it produces. + * Pass the result to `createApp` to get compile-time + runtime guarantees on `c.var`. + * + * ```ts + * export const indexingStatusMiddleware = producing( + * ["indexingStatus"], + * factory.createMiddleware(async (c, next) => { ... }) + * ); + * ``` + */ +export function producing( + keys: readonly K[], + middleware: MiddlewareHandler, +): ProducingMiddleware { + return Object.assign(middleware, { __produces: keys }); +} + +/** + * Creates an OpenAPIHono sub-app that declares which middleware variables its handlers require. + * + * Pass middlewares in execution order. Producing middlewares (created with `producing()`) give + * two additional guarantees beyond plain `app.use()`: + * + * 1. **Compile-time**: `c.var.` is typed as non-optional inside handlers. + * 2. **Runtime**: each handler asserts the variables are present before executing, + * producing a clear invariant error if the required middleware was never applied. + * + * Plain middlewares (without `__produces`) are applied in order but don't affect typing. + * + * ```ts + * const app = createApp({ + * middlewares: [ + * indexingStatusMiddleware, // producing — c.var.indexingStatus becomes non-optional + * nameTokensApiMiddleware, // plain gate — applied but doesn't affect types + * ], + * }); + * ``` + * + * Without arguments, all variables remain optional (same as a plain OpenAPIHono app). + */ +export function createApp< + const TMiddlewares extends readonly (ProducingMiddleware | MiddlewareHandler)[] = [], +>({ middlewares }: { middlewares?: TMiddlewares } = {}) { + type TRequired = ExtractProduced; + const mws: readonly (ProducingMiddleware | MiddlewareHandler)[] = middlewares ?? []; + const requiredVars = mws + .filter((m): m is ProducingMiddleware => "__produces" in m) + .flatMap((m) => [...m.__produces]) as TRequired[]; + + const app = new OpenAPIHono<{ Variables: RequireVars }>({ defaultHook: (result, c) => { if (!result.success) { return errorResponse(c, result.error); } }, }); + + // Apply the middlewares in order so callers don't need separate app.use() calls. + for (const middleware of mws) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + app.use(middleware as any); + } + + if (requiredVars.length > 0) { + // Bind openapi as any to avoid fighting overload resolution when wrapping. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const _openapi = app.openapi.bind(app) as (...args: any[]) => any; + + // Override app.openapi to inject a runtime invariant check at the top of every handler body. + // Running the check inside the handler (rather than as a middleware) ensures it fires after + // all middleware have had a chance to set vars. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (app as any).openapi = (route: any, handler: any, hook?: any) => + _openapi( + route, + async (c: any) => { + for (const dep of requiredVars) { + if (c.var[dep] === undefined) { + throw new Error( + `Invariant: handler requires "${dep}" but no middleware provided it in c.var. + Probably middleware didn't produce it.`, + ); + } + } + return handler(c); + }, + hook, + ); + } + + return app; } diff --git a/apps/ensapi/src/middleware/can-accelerate.middleware.ts b/apps/ensapi/src/middleware/can-accelerate.middleware.ts index 9aee7188b5..b14d91154e 100644 --- a/apps/ensapi/src/middleware/can-accelerate.middleware.ts +++ b/apps/ensapi/src/middleware/can-accelerate.middleware.ts @@ -2,7 +2,7 @@ import config from "@/config"; import { PluginName } from "@ensnode/ensnode-sdk"; -import { factory } from "@/lib/hono-factory"; +import { factory, producing } from "@/lib/hono-factory"; import { makeLogger } from "@/lib/logger"; const logger = makeLogger("can-accelerate.middleware"); @@ -23,74 +23,77 @@ let prevCanAccelerate = false; * a parent isRealtimeMiddleware. Sets the `canAccelerate` variable on the context for use by * resolution handlers. */ -export const canAccelerateMiddleware = factory.createMiddleware(async (c, next) => { - // context must be set by the required middleware - if (c.var.isRealtime === undefined) { - throw new Error(`Invariant(canAccelerateMiddleware): isRealtime middleware required`); - } - - //////////////////////////// - /// Temporary ENSv2 Bailout - //////////////////////////// - // TODO: re-enable acceleration for ensv2 once implemented - if (config.ensIndexerPublicConfig.plugins.includes(PluginName.ENSv2)) { - if (!didWarnCannotAccelerateENSv2) { +export const canAccelerateMiddleware = producing( + ["canAccelerate"], + factory.createMiddleware(async (c, next) => { + // context must be set by the required middleware + if (c.var.isRealtime === undefined) { + throw new Error(`Invariant(canAccelerateMiddleware): isRealtime middleware required`); + } + + //////////////////////////// + /// Temporary ENSv2 Bailout + //////////////////////////// + // TODO: re-enable acceleration for ensv2 once implemented + if (config.ensIndexerPublicConfig.plugins.includes(PluginName.ENSv2)) { + if (!didWarnCannotAccelerateENSv2) { + logger.warn( + `ENSApi is temporarily unable to accelerate Resolution API requests while indexing ENSv2. Protocol Acceleration is DISABLED.`, + ); + + didWarnCannotAccelerateENSv2 = true; + } + + c.set("canAccelerate", false); + return await next(); + } + + ////////////////////////////////////////////// + /// Protocol Acceleration Plugin Availability + ////////////////////////////////////////////// + + const hasProtocolAccelerationPlugin = config.ensIndexerPublicConfig.plugins.includes( + PluginName.ProtocolAcceleration, + ); + + // log one warning to the console if !hasProtocolAccelerationPlugin + if (!didWarnNoProtocolAccelerationPlugin && !hasProtocolAccelerationPlugin) { logger.warn( - `ENSApi is currently unable to accelerate Resolution API requests while indexing ENSv2. Protocol Acceleration is DISABLED.`, + `ENSApi is connected to an ENSIndexer that does NOT include the ${PluginName.ProtocolAcceleration} plugin: ENSApi will NOT be able to accelerate Resolution API requests, even if ?accelerate=true. Resolution requests will abide by the full Forward/Reverse Resolution specification, including RPC calls and CCIP-Read requests to external CCIP-Read Gateways.`, ); - didWarnCannotAccelerateENSv2 = true; + didWarnNoProtocolAccelerationPlugin = true; } - c.set("canAccelerate", false); - return await next(); - } - - ////////////////////////////////////////////// - /// Protocol Acceleration Plugin Availability - ////////////////////////////////////////////// + ////////////////////////////// + /// Can Accelerate Derivation + ////////////////////////////// + + // the Resolution API can accelerate requests if + // a) ENSIndexer reports that it is within MAX_REALTIME_DISTANCE_TO_ACCELERATE of realtime, and + // b) ENSIndexer reports that it has the ProtocolAcceleration plugin enabled. + const canAccelerate = hasProtocolAccelerationPlugin && c.var.isRealtime; + + // log notice when acceleration begins + if ( + (!didInitialCanAccelerate && canAccelerate) || // first time + (didInitialCanAccelerate && !prevCanAccelerate && canAccelerate) // future change in status + ) { + logger.info(`Protocol Acceleration is now ENABLED.`); + } - const hasProtocolAccelerationPlugin = config.ensIndexerPublicConfig.plugins.includes( - PluginName.ProtocolAcceleration, - ); + // log notice when acceleration ends + if ( + (!didInitialCanAccelerate && !canAccelerate) || // first time + (didInitialCanAccelerate && prevCanAccelerate && !canAccelerate) // future change in status + ) { + logger.info(`Protocol Acceleration is DISABLED.`); + } - // log one warning to the console if !hasProtocolAccelerationPlugin - if (!didWarnNoProtocolAccelerationPlugin && !hasProtocolAccelerationPlugin) { - logger.warn( - `ENSApi is connected to an ENSIndexer that does NOT include the ${PluginName.ProtocolAcceleration} plugin: ENSApi will NOT be able to accelerate Resolution API requests, even if ?accelerate=true. Resolution requests will abide by the full Forward/Reverse Resolution specification, including RPC calls and CCIP-Read requests to external CCIP-Read Gateways.`, - ); + prevCanAccelerate = canAccelerate; + didInitialCanAccelerate = true; - didWarnNoProtocolAccelerationPlugin = true; - } - - ////////////////////////////// - /// Can Accelerate Derivation - ////////////////////////////// - - // the Resolution API can accelerate requests if - // a) ENSIndexer reports that it is within MAX_REALTIME_DISTANCE_TO_ACCELERATE of realtime, and - // b) ENSIndexer reports that it has the ProtocolAcceleration plugin enabled. - const canAccelerate = hasProtocolAccelerationPlugin && c.var.isRealtime; - - // log notice when acceleration begins - if ( - (!didInitialCanAccelerate && canAccelerate) || // first time - (didInitialCanAccelerate && !prevCanAccelerate && canAccelerate) // future change in status - ) { - logger.info(`Protocol Acceleration is now ENABLED.`); - } - - // log notice when acceleration ends - if ( - (!didInitialCanAccelerate && !canAccelerate) || // first time - (didInitialCanAccelerate && prevCanAccelerate && !canAccelerate) // future change in status - ) { - logger.info(`Protocol Acceleration is DISABLED.`); - } - - prevCanAccelerate = canAccelerate; - didInitialCanAccelerate = true; - - c.set("canAccelerate", canAccelerate); - await next(); -}); + c.set("canAccelerate", canAccelerate); + await next(); + }), +); diff --git a/apps/ensapi/src/middleware/indexing-status.middleware.ts b/apps/ensapi/src/middleware/indexing-status.middleware.ts index 372c7d6312..90d986f1d0 100644 --- a/apps/ensapi/src/middleware/indexing-status.middleware.ts +++ b/apps/ensapi/src/middleware/indexing-status.middleware.ts @@ -7,7 +7,7 @@ import { } from "@ensnode/ensnode-sdk"; import { indexingStatusCache } from "@/cache/indexing-status.cache"; -import { factory } from "@/lib/hono-factory"; +import { factory, producing } from "@/lib/hono-factory"; /** * Type definition for the indexing status middleware context passed to downstream middleware and handlers. @@ -39,18 +39,21 @@ export type IndexingStatusMiddlewareVariables = { * continue generating new {@link RealtimeIndexingStatusProjection} containing updated worst-case distances * to downstream middleware and handlers. */ -export const indexingStatusMiddleware = factory.createMiddleware(async (c, next) => { - const indexingStatus = await indexingStatusCache.read(); +export const indexingStatusMiddleware = producing( + ["indexingStatus"], + factory.createMiddleware(async (c, next) => { + const indexingStatus = await indexingStatusCache.read(); - if (indexingStatus instanceof Error) { - // if indexingStatus was never fetched (and cached), propagate error to consumers - c.set("indexingStatus", indexingStatus); - } else { - // otherwise, build realtime indexing status projection - const now = getUnixTime(new Date()); - const realtimeProjection = createRealtimeIndexingStatusProjection(indexingStatus, now); - c.set("indexingStatus", realtimeProjection); - } + if (indexingStatus instanceof Error) { + // if indexingStatus was never fetched (and cached), propagate error to consumers + c.set("indexingStatus", indexingStatus); + } else { + // otherwise, build realtime indexing status projection + const now = getUnixTime(new Date()); + const realtimeProjection = createRealtimeIndexingStatusProjection(indexingStatus, now); + c.set("indexingStatus", realtimeProjection); + } - await next(); -}); + await next(); + }), +); diff --git a/apps/ensapi/src/middleware/is-realtime.middleware.ts b/apps/ensapi/src/middleware/is-realtime.middleware.ts index 89c7531325..7490799db7 100644 --- a/apps/ensapi/src/middleware/is-realtime.middleware.ts +++ b/apps/ensapi/src/middleware/is-realtime.middleware.ts @@ -1,6 +1,6 @@ import type { Duration } from "@ensnode/ensnode-sdk"; -import { factory } from "@/lib/hono-factory"; +import { factory, producing } from "@/lib/hono-factory"; import { makeLogger } from "@/lib/logger"; /** @@ -14,44 +14,47 @@ export const makeIsRealtimeMiddleware = (scope: string, maxWorstCaseDistance: Du let hasLoggedIndexingStatusError = false; let lastLoggedIsRealtime: boolean | null = null; - return factory.createMiddleware(async function isRealtimeMiddleware(c, next) { - // context must be set by the required middleware - if (c.var.indexingStatus === undefined) { - throw new Error(`Invariant(isRealtimeMiddleware): indexingStatusMiddleware required`); - } + return producing( + ["isRealtime"], + factory.createMiddleware(async function isRealtimeMiddleware(c, next) { + // context must be set by the required middleware + if (c.var.indexingStatus === undefined) { + throw new Error(`Invariant(isRealtimeMiddleware): indexingStatusMiddleware required`); + } - if (c.var.indexingStatus instanceof Error) { - // no indexing status available in context - if (!hasLoggedIndexingStatusError) { - logger.warn( - `ENSIndexer is NOT guaranteed to be within ${maxWorstCaseDistance} seconds of realtime. Current indexing status has not been successfully fetched by this ENSApi instance yet and is therefore unknown to this ENSApi instance because: ${c.var.indexingStatus.message}.`, - ); + if (c.var.indexingStatus instanceof Error) { + // no indexing status available in context + if (!hasLoggedIndexingStatusError) { + logger.warn( + `ENSIndexer is NOT guaranteed to be within ${maxWorstCaseDistance} seconds of realtime. Current indexing status has not been successfully fetched by this ENSApi instance yet and is therefore unknown to this ENSApi instance because: ${c.var.indexingStatus.message}.`, + ); - hasLoggedIndexingStatusError = true; - } + hasLoggedIndexingStatusError = true; + } - c.set("isRealtime", false); - return await next(); - } - - // determine if we're within the max worst-case distance to qualify as "realtime". - const isRealtime = c.var.indexingStatus.worstCaseDistance <= maxWorstCaseDistance; - - if (lastLoggedIsRealtime !== isRealtime) { - if (isRealtime) { - logger.info( - `ENSIndexer is guaranteed to be within ${maxWorstCaseDistance} seconds of realtime`, - ); - } else { - logger.warn( - `ENSIndexer is NOT guaranteed to be within ${maxWorstCaseDistance} seconds of realtime. (Worst Case distance: ${c.var.indexingStatus.worstCaseDistance} seconds > ${maxWorstCaseDistance} seconds).`, - ); + c.set("isRealtime", false); + return await next(); } - lastLoggedIsRealtime = isRealtime; - } + // determine if we're within the max worst-case distance to qualify as "realtime". + const isRealtime = c.var.indexingStatus.worstCaseDistance <= maxWorstCaseDistance; + + if (lastLoggedIsRealtime !== isRealtime) { + if (isRealtime) { + logger.info( + `ENSIndexer is guaranteed to be within ${maxWorstCaseDistance} seconds of realtime`, + ); + } else { + logger.warn( + `ENSIndexer is NOT guaranteed to be within ${maxWorstCaseDistance} seconds of realtime. (Worst Case distance: ${c.var.indexingStatus.worstCaseDistance} seconds > ${maxWorstCaseDistance} seconds).`, + ); + } + + lastLoggedIsRealtime = isRealtime; + } - c.set("isRealtime", isRealtime); - return await next(); - }); + c.set("isRealtime", isRealtime); + return await next(); + }), + ); }; diff --git a/apps/ensapi/src/middleware/referral-leaderboard-editions-caches.middleware.ts b/apps/ensapi/src/middleware/referral-leaderboard-editions-caches.middleware.ts index 1ac93a1bbb..791995ddb5 100644 --- a/apps/ensapi/src/middleware/referral-leaderboard-editions-caches.middleware.ts +++ b/apps/ensapi/src/middleware/referral-leaderboard-editions-caches.middleware.ts @@ -2,7 +2,7 @@ import { initializeReferralLeaderboardEditionsCaches, type ReferralLeaderboardEditionsCacheMap, } from "@/cache/referral-leaderboard-editions.cache"; -import { factory } from "@/lib/hono-factory"; +import { factory, producing } from "@/lib/hono-factory"; import type { referralProgramEditionConfigSetMiddleware } from "@/middleware/referral-program-edition-set.middleware"; /** @@ -40,8 +40,9 @@ export type ReferralLeaderboardEditionsCachesMiddlewareVariables = { * Each cache's builder function handles immutability internally - when an edition becomes immutably * closed (past the safety window), the builder returns previously cached data without re-fetching. */ -export const referralLeaderboardEditionsCachesMiddleware = factory.createMiddleware( - async (c, next) => { +export const referralLeaderboardEditionsCachesMiddleware = producing( + ["referralLeaderboardEditionsCaches"], + factory.createMiddleware(async (c, next) => { const editionConfigSet = c.get("referralProgramEditionConfigSet"); // Invariant: referralProgramEditionConfigSetMiddleware must be applied before this middleware @@ -62,5 +63,5 @@ export const referralLeaderboardEditionsCachesMiddleware = factory.createMiddlew const caches = initializeReferralLeaderboardEditionsCaches(editionConfigSet); c.set("referralLeaderboardEditionsCaches", caches); await next(); - }, + }), ); diff --git a/apps/ensapi/src/middleware/referral-program-edition-set.middleware.ts b/apps/ensapi/src/middleware/referral-program-edition-set.middleware.ts index 58410d4765..b5d38856bb 100644 --- a/apps/ensapi/src/middleware/referral-program-edition-set.middleware.ts +++ b/apps/ensapi/src/middleware/referral-program-edition-set.middleware.ts @@ -1,7 +1,7 @@ import type { ReferralProgramEditionConfigSet } from "@namehash/ens-referrals/v1"; import { referralProgramEditionConfigSetCache } from "@/cache/referral-program-edition-set.cache"; -import { factory } from "@/lib/hono-factory"; +import { factory, producing } from "@/lib/hono-factory"; /** * Type definition for the referral program edition config set middleware context. @@ -26,10 +26,11 @@ export type ReferralProgramEditionConfigSetMiddlewareVariables = { * * If the cache fails to load, the JSON fetching will be retried on subsequent requests. */ -export const referralProgramEditionConfigSetMiddleware = factory.createMiddleware( - async (c, next) => { +export const referralProgramEditionConfigSetMiddleware = producing( + ["referralProgramEditionConfigSet"], + factory.createMiddleware(async (c, next) => { const editionConfigSet = await referralProgramEditionConfigSetCache.read(); c.set("referralProgramEditionConfigSet", editionConfigSet); await next(); - }, + }), ); diff --git a/apps/ensapi/src/middleware/referrer-leaderboard.middleware.ts b/apps/ensapi/src/middleware/referrer-leaderboard.middleware.ts index f3c4023485..52830e1767 100644 --- a/apps/ensapi/src/middleware/referrer-leaderboard.middleware.ts +++ b/apps/ensapi/src/middleware/referrer-leaderboard.middleware.ts @@ -1,7 +1,7 @@ import type { ReferrerLeaderboard } from "@namehash/ens-referrals"; import { referrerLeaderboardCache } from "@/cache/referrer-leaderboard.cache"; -import { factory } from "@/lib/hono-factory"; +import { factory, producing } from "@/lib/hono-factory"; /** * Type definition for the referrer leaderboard middleware context passed to downstream middleware and handlers. @@ -25,9 +25,12 @@ export type ReferrerLeaderboardMiddlewareVariables = { * Middleware that provides {@link ReferrerLeaderboardMiddlewareVariables} * to downstream middleware and handlers. */ -export const referrerLeaderboardMiddleware = factory.createMiddleware(async (c, next) => { - const leaderboard = await referrerLeaderboardCache.read(); +export const referrerLeaderboardMiddleware = producing( + ["referrerLeaderboard"], + factory.createMiddleware(async (c, next) => { + const leaderboard = await referrerLeaderboardCache.read(); - c.set("referrerLeaderboard", leaderboard); - await next(); -}); + c.set("referrerLeaderboard", leaderboard); + await next(); + }), +); diff --git a/docs/docs.ensnode.io/ensapi-openapi.json b/docs/docs.ensnode.io/ensapi-openapi.json index d69de2e6f6..a501614dd4 100644 --- a/docs/docs.ensnode.io/ensapi-openapi.json +++ b/docs/docs.ensnode.io/ensapi-openapi.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "ENSApi APIs", - "version": "1.7.0", + "version": "1.8.0", "description": "APIs for ENS resolution, navigating the ENS nameforest, and metadata about an ENSNode" }, "servers": [