From 3310a7dce1ce935fd8f8e557d8cdfbaaf208492b Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Sat, 17 Jan 2026 01:18:56 +0400 Subject: [PATCH 01/14] suggest refinements --- .../src/shared/result/example-op-1.ts | 33 +++++++ .../src/shared/result/generic-errors.ts | 18 ++++ .../ensnode-sdk/src/shared/result/types.ts | 53 ++++------- .../ensnode-sdk/src/shared/result/utils.ts | 93 +++---------------- 4 files changed, 79 insertions(+), 118 deletions(-) create mode 100644 packages/ensnode-sdk/src/shared/result/example-op-1.ts create mode 100644 packages/ensnode-sdk/src/shared/result/generic-errors.ts diff --git a/packages/ensnode-sdk/src/shared/result/example-op-1.ts b/packages/ensnode-sdk/src/shared/result/example-op-1.ts new file mode 100644 index 000000000..491c1d763 --- /dev/null +++ b/packages/ensnode-sdk/src/shared/result/example-op-1.ts @@ -0,0 +1,33 @@ +import { buildServerErrorResult, type ServerErrorResult } from "./generic-errors"; +import { type AbstractResultOk, ResultCodes } from "./types"; + +export interface ExampleOp1ResultOkData { + name: string; +} + +export interface ExampleOp1ResultOk extends AbstractResultOk { + resultCode: typeof ResultCodes.Ok; + data: ExampleOp1ResultOkData; +} + +export function buildExampleOp1ResultOk(name: string): ExampleOp1ResultOk { + return { + resultCode: ResultCodes.Ok, + data: { + name, + }, + } satisfies ExampleOp1ResultOk; +} + +// NOTE: Here we define a union of all possible results returned by the server for this operation. +// We specifically call these "Server Results" because later we need to add all the possible client error results to get +// the full set of all results a client can receive from this operation. +export type ExampleOp1ServerResult = ExampleOp1ResultOk | ServerErrorResult; + +export const exampleOperation = async (): Promise => { + if (Math.random() < 0.5) { + return buildExampleOp1ResultOk("example.eth"); + } else { + return buildServerErrorResult(); + } +}; diff --git a/packages/ensnode-sdk/src/shared/result/generic-errors.ts b/packages/ensnode-sdk/src/shared/result/generic-errors.ts new file mode 100644 index 000000000..fae371f48 --- /dev/null +++ b/packages/ensnode-sdk/src/shared/result/generic-errors.ts @@ -0,0 +1,18 @@ +// in this file we define errors that are common across multiple operations + +import { type AbstractResultError, ResultCodes } from "./types"; + +// NOTE: the following is just one example of a non-abstract error. +export interface ServerErrorResult extends AbstractResultError { + resultCode: typeof ResultCodes.ServerError; + errorMessage: string; + transient: true; // server errors are always transient +} + +export function buildServerErrorResult(customErrorMessage?: string): ServerErrorResult { + return { + resultCode: ResultCodes.ServerError, + errorMessage: customErrorMessage ?? "Server error", + transient: true, + } satisfies ServerErrorResult; +} diff --git a/packages/ensnode-sdk/src/shared/result/types.ts b/packages/ensnode-sdk/src/shared/result/types.ts index 6473f3c86..625fa3891 100644 --- a/packages/ensnode-sdk/src/shared/result/types.ts +++ b/packages/ensnode-sdk/src/shared/result/types.ts @@ -8,56 +8,35 @@ */ export const ResultCodes = { Ok: "ok", - Error: "error", + ServerError: "server-error", } as const; export type ResultCode = (typeof ResultCodes)[keyof typeof ResultCodes]; +export type ErrorResultCode = Exclude; + /** - * Value type useful for `ResultOk` type. + * Abstract representation of any result. */ -export interface ResultOkValue { - valueCode: ValueCodeType; +export interface AbstractResult { + resultCode: ResultCode; } /** - * Result Ok returned by a successful operation call. + * Abstract representation of a successful result. */ -export interface ResultOk { +export interface AbstractResultOk extends AbstractResult { resultCode: typeof ResultCodes.Ok; - value: ValueType; + data: DataType; } -/** - * Value type useful for `ResultError` type. - */ -export interface ResultErrorValue { - errorCode: ErrorCodeType; +export interface AbstractResultError + extends AbstractResult { + errorMessage: string; + transient: boolean; } -/** - * Result Error returned by a failed operation call. - */ -export interface ResultError { - resultCode: typeof ResultCodes.Error; - value: ErrorType; +export interface AbstractResultErrorData + extends AbstractResultError { + data: DataType; } - -/** - * Result returned by an operation. - * - * Guarantees: - * - `resultCode` indicates if operation succeeded or failed. - * - `value` describes the outcome of the operation, for example - * - {@link ResultOkValue} for successful operation call. - * - {@link ResultErrorValue} for failed operation call. - */ -export type Result = ResultOk | ResultError; - -/** - * Type for marking error as a transient one. - * - * It's useful for downstream consumers to know, so they can attempt fetching - * the result once again. - */ -export type ErrorTransient = ErrorType & { transient: true }; diff --git a/packages/ensnode-sdk/src/shared/result/utils.ts b/packages/ensnode-sdk/src/shared/result/utils.ts index 105f59175..495fb4374 100644 --- a/packages/ensnode-sdk/src/shared/result/utils.ts +++ b/packages/ensnode-sdk/src/shared/result/utils.ts @@ -5,91 +5,22 @@ */ import { - type ErrorTransient, - type Result, + type AbstractResult, + type AbstractResultError, + type AbstractResultOk, + type ErrorResultCode, + type ResultCode, ResultCodes, - type ResultError, - type ResultErrorValue, - type ResultOk, - type ResultOkValue, } from "./types"; -/** - * Build a Result Ok from provided `data`. - * - * Requires `data` to include the `valueCode` property - * It enables the consumer of a Result object to identify `data` the result hold. - */ -export function resultOk>( - value: OkType, -): ResultOk { - return { - resultCode: ResultCodes.Ok, - value, - }; -} - -/** - * Is a result an instance of ResultOk? - */ -export function isResultOk( - result: Pick, "resultCode">, -): result is ResultOk { +export function isResultOk>( + result: AbstractResult, +): result is AbstractResultOk { return result.resultCode === ResultCodes.Ok; } -/** - * Build a Result Error from provided `error`. - * - * Requires `error` to include the `errorCode` property - * It enables the consumer of a Result object to identify `error` the result hold. - */ -export function resultError< - const ErrorValueType, - ErrorType extends ResultErrorValue, ->(value: ErrorType): ResultError { - return { - resultCode: ResultCodes.Error, - value, - }; -} - -/** - * Is a result error? - */ -export function isResultError( - result: Pick, "resultCode">, -): result is ResultError { - return result.resultCode === ResultCodes.Error; -} - -/** - * Is value an instance of a result type? - */ -export function isResult(value: unknown): value is Result { - return ( - typeof value === "object" && - value !== null && - "resultCode" in value && - (value.resultCode === ResultCodes.Ok || value.resultCode === ResultCodes.Error) - ); -} - -/** - * Build a new instance of `error` and mark it as transient. - * - * This "mark" informs downstream consumer about the transient nature of - * the error. - */ -export function errorTransient(error: ErrorType): ErrorTransient { - return { ...error, transient: true }; -} - -/** - * Is error a transient one? - */ -export function isErrorTransient(error: ErrorType): error is ErrorTransient { - return ( - typeof error === "object" && error !== null && "transient" in error && error.transient === true - ); +export function isResultError( + result: AbstractResult, +): result is AbstractResultError { + return !isResultOk(result); } From b49599e285811ccead7ee18389f0000c6edce297 Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Sat, 17 Jan 2026 09:11:09 +0400 Subject: [PATCH 02/14] refinements --- .../src/shared/result/example-dx-client.ts | 19 ++ .../src/shared/result/example-dx-hook.ts | 22 ++ .../src/shared/result/example-op-1.ts | 33 --- .../src/shared/result/example-op-client.ts | 37 ++++ .../src/shared/result/example-op-hook.ts | 30 +++ .../src/shared/result/example-op-server.ts | 59 ++++++ .../shared/result/example-server-router.ts | 22 ++ .../src/shared/result/generic-errors.ts | 18 -- .../ensnode-sdk/src/shared/result/index.ts | 5 +- .../src/shared/result/result-base.ts | 62 ++++++ .../src/shared/result/result-code.ts | 137 ++++++++++++ .../src/shared/result/result-common.ts | 141 +++++++++++++ .../ensnode-sdk/src/shared/result/types.ts | 42 ---- .../src/shared/result/utils.test.ts | 198 ------------------ .../ensnode-sdk/src/shared/result/utils.ts | 26 --- 15 files changed, 532 insertions(+), 319 deletions(-) create mode 100644 packages/ensnode-sdk/src/shared/result/example-dx-client.ts create mode 100644 packages/ensnode-sdk/src/shared/result/example-dx-hook.ts delete mode 100644 packages/ensnode-sdk/src/shared/result/example-op-1.ts create mode 100644 packages/ensnode-sdk/src/shared/result/example-op-client.ts create mode 100644 packages/ensnode-sdk/src/shared/result/example-op-hook.ts create mode 100644 packages/ensnode-sdk/src/shared/result/example-op-server.ts create mode 100644 packages/ensnode-sdk/src/shared/result/example-server-router.ts delete mode 100644 packages/ensnode-sdk/src/shared/result/generic-errors.ts create mode 100644 packages/ensnode-sdk/src/shared/result/result-base.ts create mode 100644 packages/ensnode-sdk/src/shared/result/result-code.ts create mode 100644 packages/ensnode-sdk/src/shared/result/result-common.ts delete mode 100644 packages/ensnode-sdk/src/shared/result/types.ts delete mode 100644 packages/ensnode-sdk/src/shared/result/utils.test.ts delete mode 100644 packages/ensnode-sdk/src/shared/result/utils.ts diff --git a/packages/ensnode-sdk/src/shared/result/example-dx-client.ts b/packages/ensnode-sdk/src/shared/result/example-dx-client.ts new file mode 100644 index 000000000..3ddbd999b --- /dev/null +++ b/packages/ensnode-sdk/src/shared/result/example-dx-client.ts @@ -0,0 +1,19 @@ +import type { Address } from "viem"; + +import { callExampleOp } from "./example-op-client"; +import { ResultCodes } from "./result-code"; + +export const myExampleDXClient = (address: Address): void => { + const result = callExampleOp(address); + + if (result.resultCode === ResultCodes.Ok) { + // NOTE: Here the type system knows that `result` is of type `ResultExampleOpOk` + console.log(result.data.name); + } else { + // NOTE: Here the type system knows that `result` has fields for `errorMessage` and `suggestRetry` + console.error(`Error: (${result.resultCode}) - ${result.errorMessage}`); + if (result.suggestRetry) { + console.log("Try again?"); + } + } +}; diff --git a/packages/ensnode-sdk/src/shared/result/example-dx-hook.ts b/packages/ensnode-sdk/src/shared/result/example-dx-hook.ts new file mode 100644 index 000000000..40d8152d5 --- /dev/null +++ b/packages/ensnode-sdk/src/shared/result/example-dx-hook.ts @@ -0,0 +1,22 @@ +import type { Address } from "viem"; + +import { useExampleOp } from "./example-op-hook"; +import { ResultCodes } from "./result-code"; + +export const myExampleDXHook = (address: Address): void => { + const result = useExampleOp(address); + + if (result.resultCode === ResultCodes.Loading) { + // NOTE: Here the type system knows that `result` is of type `ResultExampleOpLoading` + console.log("Loading..."); + } else if (result.resultCode === ResultCodes.Ok) { + // NOTE: Here the type system knows that `result` is of type `ResultExampleOpOk` + console.log(result.data.name); + } else { + // NOTE: Here the type system knows that `result` has fields for `errorMessage` and `suggestRetry` + console.error(`Error: (${result.resultCode}) - ${result.errorMessage}`); + if (result.suggestRetry) { + console.log("Try again?"); + } + } +}; diff --git a/packages/ensnode-sdk/src/shared/result/example-op-1.ts b/packages/ensnode-sdk/src/shared/result/example-op-1.ts deleted file mode 100644 index 491c1d763..000000000 --- a/packages/ensnode-sdk/src/shared/result/example-op-1.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { buildServerErrorResult, type ServerErrorResult } from "./generic-errors"; -import { type AbstractResultOk, ResultCodes } from "./types"; - -export interface ExampleOp1ResultOkData { - name: string; -} - -export interface ExampleOp1ResultOk extends AbstractResultOk { - resultCode: typeof ResultCodes.Ok; - data: ExampleOp1ResultOkData; -} - -export function buildExampleOp1ResultOk(name: string): ExampleOp1ResultOk { - return { - resultCode: ResultCodes.Ok, - data: { - name, - }, - } satisfies ExampleOp1ResultOk; -} - -// NOTE: Here we define a union of all possible results returned by the server for this operation. -// We specifically call these "Server Results" because later we need to add all the possible client error results to get -// the full set of all results a client can receive from this operation. -export type ExampleOp1ServerResult = ExampleOp1ResultOk | ServerErrorResult; - -export const exampleOperation = async (): Promise => { - if (Math.random() < 0.5) { - return buildExampleOp1ResultOk("example.eth"); - } else { - return buildServerErrorResult(); - } -}; diff --git a/packages/ensnode-sdk/src/shared/result/example-op-client.ts b/packages/ensnode-sdk/src/shared/result/example-op-client.ts new file mode 100644 index 000000000..9e5c10cd7 --- /dev/null +++ b/packages/ensnode-sdk/src/shared/result/example-op-client.ts @@ -0,0 +1,37 @@ +import type { Address } from "viem"; + +import { + EXAMPLE_OP_RECOGNIZED_SERVER_RESULT_CODES, + type ExampleOpServerResult, + exampleOp, +} from "./example-op-server"; +import { + buildResultConnectionError, + buildResultRequestTimeout, + buildResultUnknownError, + isUnrecognizedResult, + type ResultClientError, +} from "./result-common"; + +export type ExampleOpClientResult = ExampleOpServerResult | ResultClientError; + +export const callExampleOp = (address: Address): ExampleOpClientResult => { + try { + const result = exampleOp(address); + + // ensure server result is recognized by client version + if (isUnrecognizedResult(result, EXAMPLE_OP_RECOGNIZED_SERVER_RESULT_CODES)) { + return buildResultUnknownError(result); + } + + // return server result + return result; + } catch (error) { + // handle client-side errors + if (error === "connection-error") { + return buildResultConnectionError(); + } else { + return buildResultRequestTimeout(); + } + } +}; diff --git a/packages/ensnode-sdk/src/shared/result/example-op-hook.ts b/packages/ensnode-sdk/src/shared/result/example-op-hook.ts new file mode 100644 index 000000000..1d34adeb0 --- /dev/null +++ b/packages/ensnode-sdk/src/shared/result/example-op-hook.ts @@ -0,0 +1,30 @@ +import type { Address } from "viem"; + +import { callExampleOp, type ExampleOpClientResult } from "./example-op-client"; +import type { AbstractResultLoading } from "./result-base"; +import { ResultCodes } from "./result-code"; + +export interface ExampleOpLoadingData { + address: Address; +} + +export interface ResultExampleOpLoading extends AbstractResultLoading {} + +export const buildResultExampleOpLoading = (address: Address): ResultExampleOpLoading => { + return { + resultCode: ResultCodes.Loading, + data: { + address: address, + }, + }; +}; + +export type ExampleOpHookResult = ExampleOpClientResult | ResultExampleOpLoading; + +export const useExampleOp = (address: Address): ExampleOpHookResult => { + if (Math.random() < 0.5) { + return buildResultExampleOpLoading(address); + } else { + return callExampleOp(address); + } +}; diff --git a/packages/ensnode-sdk/src/shared/result/example-op-server.ts b/packages/ensnode-sdk/src/shared/result/example-op-server.ts new file mode 100644 index 000000000..44072228e --- /dev/null +++ b/packages/ensnode-sdk/src/shared/result/example-op-server.ts @@ -0,0 +1,59 @@ +import type { Address } from "viem"; +import { zeroAddress } from "viem"; + +import type { AbstractResultOk } from "./result-base"; +import { type AssertResultCodeExact, type ExpectTrue, ResultCodes } from "./result-code"; +import { + buildResultInternalServerError, + buildResultInvalidRequest, + type ResultInternalServerError, + type ResultInvalidRequest, +} from "./result-common"; + +export interface ExampleOpResultOkData { + name: string; +} + +export interface ResultExampleOpOk extends AbstractResultOk {} + +export const buildResultExampleOpOk = (name: string): ResultExampleOpOk => { + return { + resultCode: ResultCodes.Ok, + data: { + name, + }, + }; +}; + +// NOTE: Here we define a union of all possible results returned by the server for this operation. +// We specifically call these "Server Results" because later we need to add all the possible client error results to get +// the full set of all results a client can receive from this operation. +export type ExampleOpServerResult = + | ResultExampleOpOk + | ResultInternalServerError + | ResultInvalidRequest; + +export type ExampleOpServerResultCode = ExampleOpServerResult["resultCode"]; + +export const EXAMPLE_OP_RECOGNIZED_SERVER_RESULT_CODES = [ + ResultCodes.Ok, + ResultCodes.InternalServerError, + ResultCodes.InvalidRequest, +] as const satisfies readonly ExampleOpServerResultCode[]; + +type _CompileTimeAlignmentCheck = ExpectTrue< + AssertResultCodeExact +>; + +export const exampleOp = (address: Address): ExampleOpServerResult => { + if (address === zeroAddress) { + return buildResultInvalidRequest("Address must not be the zero address"); + } + if (Math.random() < 0.5) { + return buildResultExampleOpOk("example.eth"); + } else { + return buildResultInternalServerError( + "Invariant violation: random number is not less than 0.5", + ); + } +}; diff --git a/packages/ensnode-sdk/src/shared/result/example-server-router.ts b/packages/ensnode-sdk/src/shared/result/example-server-router.ts new file mode 100644 index 000000000..5ab2c9752 --- /dev/null +++ b/packages/ensnode-sdk/src/shared/result/example-server-router.ts @@ -0,0 +1,22 @@ +import type { Address } from "viem"; + +import { exampleOp } from "./example-op-server"; +import type { AbstractResult } from "./result-base"; +import type { ResultCode } from "./result-code"; +import { buildResultInternalServerError, buildResultNotFound } from "./result-common"; + +const routeRequest = (path: string): AbstractResult => { + // imagine Hono router logic here + try { + if (path === "/example") { + return exampleOp("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" as Address); + } else { + // guarantee in all cases we return our Result data model + return buildResultNotFound(`Path not found: ${path}`); + } + } catch (error) { + // guarantee in all cases we return our Result data model + const errorMessage = error instanceof Error ? error.message : undefined; + return buildResultInternalServerError(errorMessage); + } +}; diff --git a/packages/ensnode-sdk/src/shared/result/generic-errors.ts b/packages/ensnode-sdk/src/shared/result/generic-errors.ts deleted file mode 100644 index fae371f48..000000000 --- a/packages/ensnode-sdk/src/shared/result/generic-errors.ts +++ /dev/null @@ -1,18 +0,0 @@ -// in this file we define errors that are common across multiple operations - -import { type AbstractResultError, ResultCodes } from "./types"; - -// NOTE: the following is just one example of a non-abstract error. -export interface ServerErrorResult extends AbstractResultError { - resultCode: typeof ResultCodes.ServerError; - errorMessage: string; - transient: true; // server errors are always transient -} - -export function buildServerErrorResult(customErrorMessage?: string): ServerErrorResult { - return { - resultCode: ResultCodes.ServerError, - errorMessage: customErrorMessage ?? "Server error", - transient: true, - } satisfies ServerErrorResult; -} diff --git a/packages/ensnode-sdk/src/shared/result/index.ts b/packages/ensnode-sdk/src/shared/result/index.ts index 9e034905e..70fbbd582 100644 --- a/packages/ensnode-sdk/src/shared/result/index.ts +++ b/packages/ensnode-sdk/src/shared/result/index.ts @@ -1,2 +1,3 @@ -export * from "./types"; -export * from "./utils"; +export * from "./result-base"; +export * from "./result-code"; +export * from "./result-common"; diff --git a/packages/ensnode-sdk/src/shared/result/result-base.ts b/packages/ensnode-sdk/src/shared/result/result-base.ts new file mode 100644 index 000000000..a7eb8afc7 --- /dev/null +++ b/packages/ensnode-sdk/src/shared/result/result-base.ts @@ -0,0 +1,62 @@ +import type { ResultCode, ResultCodeError, ResultCodes } from "./result-code"; + +/************************************************************ + * Abstract results + * + * These are base interfaces that should be extended to + * create concrete result types. + ************************************************************/ + +/** + * Abstract representation of any result. + */ +export interface AbstractResult { + /** + * The classification of the result. + */ + resultCode: TResultCode; +} + +/** + * Abstract representation of a successful result. + */ +export interface AbstractResultOk extends AbstractResult { + /** + * The data of the result. + */ + data: TDataType; +} + +/** + * Abstract representation of an error result. + */ +export interface AbstractResultError + extends AbstractResult { + /** + * A description of the error. + */ + errorMessage: string; + + /** + * Identifies if it may be relevant to retry the operation. + * + * If `false`, retrying the operation is unlikely tobe helpful. + */ + suggestRetry: boolean; + + /** + * Optional data associated with the error. + */ + data?: TDataType; +} + +/** + * Abstract representation of a loading result. + */ +export interface AbstractResultLoading + extends AbstractResult { + /** + * Optional data associated with the loading operation. + */ + data?: TDataType; +} diff --git a/packages/ensnode-sdk/src/shared/result/result-code.ts b/packages/ensnode-sdk/src/shared/result/result-code.ts new file mode 100644 index 000000000..2ad3eb666 --- /dev/null +++ b/packages/ensnode-sdk/src/shared/result/result-code.ts @@ -0,0 +1,137 @@ +/** + * Classifies a result returned by an operation. + */ +export const ResultCodes = { + /** + * Indefinite result. + */ + Loading: "loading", + + /** + * Successful result. + */ + Ok: "ok", + + /** + * Server error: the operation failed due to an unexpected error internally within the server. + */ + InternalServerError: "internal-server-error", + + /** + * Server error: the requested resource was not found. + */ + NotFound: "not-found", + + /** + * Server and client error: the request was invalid. + */ + InvalidRequest: "invalid-request", + + /** + * Client error: the connection to the server failed. + */ + ConnectionError: "connection-error", + + /** + * Client error: the request timed out. + */ + RequestTimeout: "request-timeout", + + /** + * Client error: received an unexpected result code from the server. + */ + UnknownError: "unknown-error", +} as const; + +export const RESULT_CODE_SERVER_AND_CLIENT_ERROR_CODES = [ResultCodes.InvalidRequest] as const; + +export const RESULT_CODE_SERVER_ERROR_CODES = [ + ResultCodes.InternalServerError, + ResultCodes.NotFound, + ...RESULT_CODE_SERVER_AND_CLIENT_ERROR_CODES, +] as const; + +export const RESULT_CODE_CLIENT_ERROR_CODES = [ + ResultCodes.ConnectionError, + ResultCodes.RequestTimeout, + ResultCodes.UnknownError, + ...RESULT_CODE_SERVER_AND_CLIENT_ERROR_CODES, +] as const; + +const RESULT_CODE_ERROR_CODES = [ + ...RESULT_CODE_SERVER_ERROR_CODES, + ...RESULT_CODE_CLIENT_ERROR_CODES, +] as const; + +const RESULT_CODE_ALL_CODES = [ + ResultCodes.Loading, + ResultCodes.Ok, + ...RESULT_CODE_ERROR_CODES, +] as const; + +/** + * Classifies a result returned by an operation. + */ +export type ResultCode = (typeof ResultCodes)[keyof typeof ResultCodes]; + +/** + * ResultCode for a result that is not yet determined. + */ +export type ResultCodeIndefinite = typeof ResultCodes.Loading; + +/** + * ResultCode for a result that has been determined. + */ +export type ResultCodeDefinite = Exclude; + +/** + * ResultCode for an error result that may be determined by the server. + */ +export type ResultCodeServerError = (typeof RESULT_CODE_SERVER_ERROR_CODES)[number]; + +/** + * ResultCode for an error result that may be determined by the client. + */ +export type ResultCodeClientError = (typeof RESULT_CODE_CLIENT_ERROR_CODES)[number]; + +/** + * ResultCode for a result that is an error. + */ +export type ResultCodeError = (typeof RESULT_CODE_ERROR_CODES)[number]; + +/************************************************************ + * Compile-time helpers to ensure invariants expected of + * definitions above are maintained and don't become + * out of sync. + ************************************************************/ + +export type ExpectTrue = T; + +export type ResultCodesFromList = List[number]; + +export type AssertResultCodeSuperset< + Union extends ResultCode, + List extends readonly ResultCode[], +> = Union extends ResultCodesFromList ? true : false; + +export type AssertResultCodeSubset< + Union extends ResultCode, + List extends readonly ResultCode[], +> = ResultCodesFromList extends Union ? true : false; + +export type AssertResultCodeExact< + Union extends ResultCode, + List extends readonly ResultCode[], +> = AssertResultCodeSuperset extends true + ? AssertResultCodeSubset extends true + ? true + : false + : false; + +type _CompileTimeCheck_ResultCodeErrorMatchesUnion = ExpectTrue< + AssertResultCodeExact +>; + +type _CompileTimeCheck_ResultCodeMatchesUnion = ExpectTrue< + AssertResultCodeExact +>; diff --git a/packages/ensnode-sdk/src/shared/result/result-common.ts b/packages/ensnode-sdk/src/shared/result/result-common.ts new file mode 100644 index 000000000..9615fb87c --- /dev/null +++ b/packages/ensnode-sdk/src/shared/result/result-common.ts @@ -0,0 +1,141 @@ +/************************************************************ + * Internal Server Error + ************************************************************/ + +import type { AbstractResult, AbstractResultError } from "./result-base"; +import { type ResultCode, type ResultCodeError, ResultCodes } from "./result-code"; + +export interface ResultInternalServerError + extends AbstractResultError {} + +export const buildResultInternalServerError = ( + errorMessage?: string, + suggestRetry: boolean = true, +): ResultInternalServerError => { + return { + resultCode: ResultCodes.InternalServerError, + errorMessage: errorMessage ?? "An unknown internal server error occurred.", + suggestRetry, + }; +}; + +/************************************************************ + * Not Found + ************************************************************/ + +export interface ResultNotFound extends AbstractResultError {} + +export const buildResultNotFound = ( + errorMessage?: string, + suggestRetry: boolean = false, +): ResultNotFound => { + return { + resultCode: ResultCodes.NotFound, + errorMessage: errorMessage ?? "Requested resource not found.", + suggestRetry, + }; +}; + +/************************************************************ + * Invalid Request + ************************************************************/ + +export interface ResultInvalidRequest + extends AbstractResultError {} + +export const buildResultInvalidRequest = ( + errorMessage?: string, + suggestRetry: boolean = false, +): ResultInvalidRequest => { + return { + resultCode: ResultCodes.InvalidRequest, + errorMessage: errorMessage ?? "Invalid request.", + suggestRetry, + }; +}; + +/************************************************************ + * Connection Error + ************************************************************/ + +export interface ResultConnectionError + extends AbstractResultError {} + +export const buildResultConnectionError = ( + errorMessage?: string, + suggestRetry: boolean = true, +): ResultConnectionError => { + return { + resultCode: ResultCodes.ConnectionError, + errorMessage: errorMessage ?? "Connection error.", + suggestRetry, + }; +}; + +/************************************************************ + * Request Timeout + ************************************************************/ + +export interface ResultRequestTimeout + extends AbstractResultError {} + +export const buildResultRequestTimeout = ( + errorMessage?: string, + suggestRetry: boolean = true, +): ResultRequestTimeout => { + return { + resultCode: ResultCodes.RequestTimeout, + errorMessage: errorMessage ?? "Request timed out.", + suggestRetry, + }; +}; + +/************************************************************ + * Unknown Error + ************************************************************/ + +/** + * Represents an error result that is not recognized by the SDK. + * + * Relevant for cases where a client is running version X while the server + * is running version X+N and the server returns a result code that is not + * recognized by a client because the result code exist in the version X+N + * but not in the version X and therefore needs transformation into a + * fallback result code that will be recognized in version X. + */ +export interface ResultErrorUnrecognized + extends Omit, "resultCode"> { + /** + * The result code that is not recognized by the SDK but was returned by the server. + */ + resultCode: string; +} + +export interface ResultUnknownError extends AbstractResultError {} + +export const buildResultUnknownError = ( + unrecognizedError: ResultErrorUnrecognized, +): ResultUnknownError => { + return { + resultCode: ResultCodes.UnknownError, + errorMessage: unrecognizedError.errorMessage, + suggestRetry: unrecognizedError.suggestRetry, + }; +}; + +export const isUnrecognizedResult = ( + result: AbstractResult, + recognizedResultCodes: readonly ResultCode[], +): result is ResultErrorUnrecognized => { + // Checks if result.resultCode is not one of the recognized ResultCodes for an operation + return ( + typeof result.resultCode === "string" && + !recognizedResultCodes.includes(result.resultCode as ResultCode) + ); +}; + +/************************************************************ + * All common client errors + ************************************************************/ + +export type ResultClientError = ResultConnectionError | ResultRequestTimeout | ResultUnknownError; diff --git a/packages/ensnode-sdk/src/shared/result/types.ts b/packages/ensnode-sdk/src/shared/result/types.ts deleted file mode 100644 index 625fa3891..000000000 --- a/packages/ensnode-sdk/src/shared/result/types.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * This module defines a standardized way to represent the outcome of operations, - * encapsulating both successful results and error results. - */ - -/** - * Possible Result Codes. - */ -export const ResultCodes = { - Ok: "ok", - ServerError: "server-error", -} as const; - -export type ResultCode = (typeof ResultCodes)[keyof typeof ResultCodes]; - -export type ErrorResultCode = Exclude; - -/** - * Abstract representation of any result. - */ -export interface AbstractResult { - resultCode: ResultCode; -} - -/** - * Abstract representation of a successful result. - */ -export interface AbstractResultOk extends AbstractResult { - resultCode: typeof ResultCodes.Ok; - data: DataType; -} - -export interface AbstractResultError - extends AbstractResult { - errorMessage: string; - transient: boolean; -} - -export interface AbstractResultErrorData - extends AbstractResultError { - data: DataType; -} diff --git a/packages/ensnode-sdk/src/shared/result/utils.test.ts b/packages/ensnode-sdk/src/shared/result/utils.test.ts deleted file mode 100644 index 008ca377d..000000000 --- a/packages/ensnode-sdk/src/shared/result/utils.test.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import type { ErrorTransient, Result, ResultError, ResultOk } from "./types"; -import { - errorTransient, - isErrorTransient, - isResult, - isResultError, - isResultOk, - resultError, - resultOk, -} from "./utils"; - -describe("Result type", () => { - describe("Developer Experience", () => { - // Correct value codes that the test operation can return. - const TestOpValueCodes = { Found: "FOUND", NotFound: "NOT_FOUND" } as const; - - // Example of result ok: no records were found for the given request - interface TestOpResultOkNotFound { - valueCode: typeof TestOpValueCodes.NotFound; - } - - // Example of result ok: some records were found for the given request - interface TestOpResultFound { - valueCode: typeof TestOpValueCodes.Found; - records: string[]; - } - - // Union type collecting all ResultOk subtypes - type TestOpResultOk = TestOpResultOkNotFound | TestOpResultFound; - - // Error codes that the test operation can return. - const TestOpErrorCodes = { - InvalidRequest: "INVALID_REQUEST", - TransientIssue: "TRANSIENT_ISSUE", - } as const; - - // Example of result error: invalid request - interface TestOpResultErrorInvalidRequest { - errorCode: typeof TestOpErrorCodes.InvalidRequest; - message: string; - } - - // Example of result error: transient issue, simulates ie. indexing status not ready - type TestOpResultErrorTransientIssue = ErrorTransient<{ - errorCode: typeof TestOpErrorCodes.TransientIssue; - }>; - - // Union type collecting all ResultError subtypes - type TestOpError = TestOpResultErrorInvalidRequest | TestOpResultErrorTransientIssue; - - // Result type for test operation - type TestOpResult = Result; - - interface TestOperationParams { - name: string; - simulate?: { - transientIssue?: boolean; - }; - } - - // An example of operation returning a Result object - function testOperation(params: TestOperationParams): TestOpResult { - // Check if need to simulate transient server issue - if (params.simulate?.transientIssue) { - return resultError( - errorTransient({ - errorCode: TestOpErrorCodes.TransientIssue, - }), - ) satisfies ResultError; - } - - // Check if request is valid - if (params.name.endsWith(".eth") === false) { - return resultError({ - errorCode: TestOpErrorCodes.InvalidRequest, - message: `Invalid request, 'name' must end with '.eth'. Provided name: '${params.name}'.`, - }) satisfies ResultError; - } - - // Check if requested name has any records indexed - if (params.name !== "vitalik.eth") { - return resultOk({ - valueCode: TestOpValueCodes.NotFound, - }) satisfies ResultOk; - } - - // Return records found for the requested name - return resultOk({ - valueCode: TestOpValueCodes.Found, - records: ["a", "b", "c"], - }) satisfies ResultOk; - } - - // Example ResultOk values - const testOperationResultOkFound = testOperation({ - name: "vitalik.eth", - }); - - const testOperationResultOkNotFound = testOperation({ - name: "test.eth", - }); - - // Example ResultError values - const testOperationResultErrorTransientIssue = testOperation({ - name: "vitalik.eth", - simulate: { - transientIssue: true, - }, - }); - - const testOperationResultErrorInvalidRequest = testOperation({ - name: "test.xyz", - }); - - // Example values that are instances of Result type - const results = [ - testOperationResultOkFound, - testOperationResultOkNotFound, - testOperationResultErrorTransientIssue, - testOperationResultErrorInvalidRequest, - ]; - // Example values that are not instances of Result type - const notResults = [null, undefined, 42, "invalid", {}, { resultCode: "unknown" }]; - - describe("Type Guards", () => { - it("should identify Result types correctly", () => { - for (const maybeResult of results) { - expect(isResult(maybeResult)).toBe(true); - } - - for (const maybeResult of notResults) { - expect(isResult(maybeResult)).toBe(false); - } - }); - - it("should identify ResultOk types correctly", () => { - expect(isResultOk(testOperationResultOkFound)).toBe(true); - expect(isResultOk(testOperationResultOkNotFound)).toBe(true); - expect(isResultOk(testOperationResultErrorTransientIssue)).toBe(false); - expect(isResultOk(testOperationResultErrorInvalidRequest)).toBe(false); - - for (const resultOkExample of results.filter((result) => isResultOk(result))) { - const { value } = resultOkExample; - - switch (value.valueCode) { - case TestOpValueCodes.Found: - expect(value).toStrictEqual({ - valueCode: TestOpValueCodes.Found, - records: ["a", "b", "c"], - } satisfies TestOpResultFound); - break; - - case TestOpValueCodes.NotFound: - expect(value).toStrictEqual({ - valueCode: TestOpValueCodes.NotFound, - } satisfies TestOpResultOkNotFound); - break; - } - } - }); - - it("should identify ResultError types correctly", () => { - expect(isResultError(testOperationResultOkFound)).toBe(false); - expect(isResultError(testOperationResultOkNotFound)).toBe(false); - expect(isResultError(testOperationResultErrorTransientIssue)).toBe(true); - expect(isResultError(testOperationResultErrorInvalidRequest)).toBe(true); - - for (const resultErrorExample of results.filter((result) => isResultError(result))) { - const { value } = resultErrorExample; - - switch (value.errorCode) { - case TestOpErrorCodes.InvalidRequest: - expect(value).toStrictEqual({ - errorCode: TestOpErrorCodes.InvalidRequest, - message: "Invalid request, 'name' must end with '.eth'. Provided name: 'test.xyz'.", - } satisfies TestOpResultErrorInvalidRequest); - break; - - case TestOpErrorCodes.TransientIssue: - expect(value).toMatchObject( - errorTransient({ - errorCode: TestOpErrorCodes.TransientIssue, - }) satisfies TestOpResultErrorTransientIssue, - ); - break; - } - } - }); - - it("should distinguish transient errors correctly", () => { - expect(isErrorTransient(testOperationResultErrorTransientIssue.value)).toBe(true); - expect(isErrorTransient(testOperationResultErrorInvalidRequest.value)).toBe(false); - }); - }); - }); -}); diff --git a/packages/ensnode-sdk/src/shared/result/utils.ts b/packages/ensnode-sdk/src/shared/result/utils.ts deleted file mode 100644 index 495fb4374..000000000 --- a/packages/ensnode-sdk/src/shared/result/utils.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * This file defines utilities for working with the Result generic type. - * Functionalities should be use to enhance developer experience while - * interacting with ENSNode APIs. - */ - -import { - type AbstractResult, - type AbstractResultError, - type AbstractResultOk, - type ErrorResultCode, - type ResultCode, - ResultCodes, -} from "./types"; - -export function isResultOk>( - result: AbstractResult, -): result is AbstractResultOk { - return result.resultCode === ResultCodes.Ok; -} - -export function isResultError( - result: AbstractResult, -): result is AbstractResultError { - return !isResultOk(result); -} From 972fa76a7a9c28537d48ed1dbcaf0c3fa80b80fb Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Sat, 17 Jan 2026 09:32:29 +0400 Subject: [PATCH 03/14] refinements --- .../src/shared/result/example-op-client.ts | 6 +++--- .../ensnode-sdk/src/shared/result/example-op-hook.ts | 2 +- packages/ensnode-sdk/src/shared/result/result-base.ts | 2 +- packages/ensnode-sdk/src/shared/result/result-code.ts | 4 +--- .../ensnode-sdk/src/shared/result/result-common.ts | 11 ++++------- 5 files changed, 10 insertions(+), 15 deletions(-) diff --git a/packages/ensnode-sdk/src/shared/result/example-op-client.ts b/packages/ensnode-sdk/src/shared/result/example-op-client.ts index 9e5c10cd7..70da93ca3 100644 --- a/packages/ensnode-sdk/src/shared/result/example-op-client.ts +++ b/packages/ensnode-sdk/src/shared/result/example-op-client.ts @@ -9,7 +9,7 @@ import { buildResultConnectionError, buildResultRequestTimeout, buildResultUnknownError, - isUnrecognizedResult, + hasUnrecognizedResultCode, type ResultClientError, } from "./result-common"; @@ -19,8 +19,8 @@ export const callExampleOp = (address: Address): ExampleOpClientResult => { try { const result = exampleOp(address); - // ensure server result is recognized by client version - if (isUnrecognizedResult(result, EXAMPLE_OP_RECOGNIZED_SERVER_RESULT_CODES)) { + // ensure server result code is recognized by this client version + if (hasUnrecognizedResultCode(result, EXAMPLE_OP_RECOGNIZED_SERVER_RESULT_CODES)) { return buildResultUnknownError(result); } diff --git a/packages/ensnode-sdk/src/shared/result/example-op-hook.ts b/packages/ensnode-sdk/src/shared/result/example-op-hook.ts index 1d34adeb0..783200e63 100644 --- a/packages/ensnode-sdk/src/shared/result/example-op-hook.ts +++ b/packages/ensnode-sdk/src/shared/result/example-op-hook.ts @@ -14,7 +14,7 @@ export const buildResultExampleOpLoading = (address: Address): ResultExampleOpLo return { resultCode: ResultCodes.Loading, data: { - address: address, + address, }, }; }; diff --git a/packages/ensnode-sdk/src/shared/result/result-base.ts b/packages/ensnode-sdk/src/shared/result/result-base.ts index a7eb8afc7..5c924fab9 100644 --- a/packages/ensnode-sdk/src/shared/result/result-base.ts +++ b/packages/ensnode-sdk/src/shared/result/result-base.ts @@ -40,7 +40,7 @@ export interface AbstractResultError, +export const hasUnrecognizedResultCode = ( + result: { resultCode: ResultCode | string }, recognizedResultCodes: readonly ResultCode[], ): result is ResultErrorUnrecognized => { // Checks if result.resultCode is not one of the recognized ResultCodes for an operation - return ( - typeof result.resultCode === "string" && - !recognizedResultCodes.includes(result.resultCode as ResultCode) - ); + return !recognizedResultCodes.includes(result.resultCode as ResultCode); }; /************************************************************ From eb0d84a8abd9a0c297a2de8462c428432f03d323 Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Sat, 17 Jan 2026 10:18:02 +0400 Subject: [PATCH 04/14] refinements --- .../src/shared/result/example-op-client.ts | 13 +++- .../src/shared/result/result-code.ts | 15 ++-- .../src/shared/result/result-common.ts | 74 +++++++++++-------- 3 files changed, 61 insertions(+), 41 deletions(-) diff --git a/packages/ensnode-sdk/src/shared/result/example-op-client.ts b/packages/ensnode-sdk/src/shared/result/example-op-client.ts index 70da93ca3..07d6ee8cd 100644 --- a/packages/ensnode-sdk/src/shared/result/example-op-client.ts +++ b/packages/ensnode-sdk/src/shared/result/example-op-client.ts @@ -6,10 +6,10 @@ import { exampleOp, } from "./example-op-server"; import { + buildResultClientUnrecognizedOperationResult, buildResultConnectionError, buildResultRequestTimeout, - buildResultUnknownError, - hasUnrecognizedResultCode, + isRecognizedResultCodeForOperation, type ResultClientError, } from "./result-common"; @@ -20,8 +20,13 @@ export const callExampleOp = (address: Address): ExampleOpClientResult => { const result = exampleOp(address); // ensure server result code is recognized by this client version - if (hasUnrecognizedResultCode(result, EXAMPLE_OP_RECOGNIZED_SERVER_RESULT_CODES)) { - return buildResultUnknownError(result); + if ( + isRecognizedResultCodeForOperation( + result.resultCode, + EXAMPLE_OP_RECOGNIZED_SERVER_RESULT_CODES, + ) + ) { + return buildResultClientUnrecognizedOperationResult(result); } // return server result diff --git a/packages/ensnode-sdk/src/shared/result/result-code.ts b/packages/ensnode-sdk/src/shared/result/result-code.ts index 18cd7844f..c54317460 100644 --- a/packages/ensnode-sdk/src/shared/result/result-code.ts +++ b/packages/ensnode-sdk/src/shared/result/result-code.ts @@ -38,28 +38,27 @@ export const ResultCodes = { RequestTimeout: "request-timeout", /** - * Client error: received an unexpected result code from the server. + * Client error: received an unrecognized result from the server for an operation. */ - UnknownError: "unknown-error", + ClientUnrecognizedOperationResult: "client-unrecognized-operation-result", } as const; -export const RESULT_CODE_SERVER_AND_CLIENT_ERROR_CODES = [ResultCodes.InvalidRequest] as const; - export const RESULT_CODE_SERVER_ERROR_CODES = [ ResultCodes.InternalServerError, ResultCodes.NotFound, - ...RESULT_CODE_SERVER_AND_CLIENT_ERROR_CODES, + ResultCodes.InvalidRequest, ] as const; export const RESULT_CODE_CLIENT_ERROR_CODES = [ ResultCodes.ConnectionError, ResultCodes.RequestTimeout, - ResultCodes.UnknownError, - ...RESULT_CODE_SERVER_AND_CLIENT_ERROR_CODES, + ResultCodes.ClientUnrecognizedOperationResult, + ...RESULT_CODE_SERVER_ERROR_CODES, ] as const; const RESULT_CODE_ERROR_CODES = [ - ...new Set([...RESULT_CODE_SERVER_ERROR_CODES, ...RESULT_CODE_CLIENT_ERROR_CODES]), + ...RESULT_CODE_SERVER_ERROR_CODES, + ...RESULT_CODE_CLIENT_ERROR_CODES, ] as const; const RESULT_CODE_ALL_CODES = [ ResultCodes.Loading, diff --git a/packages/ensnode-sdk/src/shared/result/result-common.ts b/packages/ensnode-sdk/src/shared/result/result-common.ts index ac6fc7bc7..daf5407b8 100644 --- a/packages/ensnode-sdk/src/shared/result/result-common.ts +++ b/packages/ensnode-sdk/src/shared/result/result-common.ts @@ -3,7 +3,7 @@ ************************************************************/ import type { AbstractResultError } from "./result-base"; -import { type ResultCode, type ResultCodeError, ResultCodes } from "./result-code"; +import { type ResultCode, ResultCodes } from "./result-code"; export interface ResultInternalServerError extends AbstractResultError {} @@ -91,48 +91,64 @@ export const buildResultRequestTimeout = ( }; /************************************************************ - * Unknown Error + * Client-Unrecognized Operation Result ************************************************************/ /** - * Represents an error result that is not recognized by the SDK. + * Represents a result for an operation that was unrecognized by the client. * * Relevant for cases where a client is running version X while the server * is running version X+N and the server returns a result code that is not - * recognized by a client because the result code exist in the version X+N - * but not in the version X and therefore needs transformation into a - * fallback result code that will be recognized in version X. + * recognized by a client for a specific operation because the result code + * exists in version X+N for the operation on the server but not in the + * version X for the operation on the client and therefore needs + * transformation into a fallback result code for the client that is safe + * for recognition by clients that are running version X. */ -export interface ResultErrorUnrecognized - extends Omit, "resultCode"> { - /** - * The result code that is not recognized by the SDK but was returned by the server. - */ - resultCode: string; -} - -export interface ResultUnknownError extends AbstractResultError {} - -export const buildResultUnknownError = ( - unrecognizedError: ResultErrorUnrecognized, -): ResultUnknownError => { +export interface ResultClientUnrecognizedOperationResult + extends AbstractResultError {} + +export const buildResultClientUnrecognizedOperationResult = ( + unrecognizedResult: unknown, +): ResultClientUnrecognizedOperationResult => { + let errorMessage = "An unrecognized result for the operation occurred."; + let suggestRetry = true; + + if (typeof unrecognizedResult === "object" && unrecognizedResult !== null) { + if ( + "errorMessage" in unrecognizedResult && + typeof unrecognizedResult.errorMessage === "string" + ) { + errorMessage = unrecognizedResult.errorMessage; + } + if ( + "suggestRetry" in unrecognizedResult && + typeof unrecognizedResult.suggestRetry === "boolean" + ) { + suggestRetry = unrecognizedResult.suggestRetry; + } + } + return { - resultCode: ResultCodes.UnknownError, - errorMessage: unrecognizedError.errorMessage, - suggestRetry: unrecognizedError.suggestRetry, + resultCode: ResultCodes.ClientUnrecognizedOperationResult, + errorMessage, + suggestRetry, }; }; -export const hasUnrecognizedResultCode = ( - result: { resultCode: ResultCode | string }, - recognizedResultCodes: readonly ResultCode[], -): result is ResultErrorUnrecognized => { - // Checks if result.resultCode is not one of the recognized ResultCodes for an operation - return !recognizedResultCodes.includes(result.resultCode as ResultCode); +export const isRecognizedResultCodeForOperation = ( + resultCode: ResultCode | string, + recognizedResultCodesForOperation: readonly ResultCode[], +): boolean => { + // Checks if resultCode is one of the recognizedResultCodes for an operation + return recognizedResultCodesForOperation.includes(resultCode as ResultCode); }; /************************************************************ * All common client errors ************************************************************/ -export type ResultClientError = ResultConnectionError | ResultRequestTimeout | ResultUnknownError; +export type ResultClientError = + | ResultConnectionError + | ResultRequestTimeout + | ResultClientUnrecognizedOperationResult; From 2aec5fb25338967cd2e0daa49a1264a22d1e1546 Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Sat, 17 Jan 2026 10:25:11 +0400 Subject: [PATCH 05/14] refinements --- packages/ensnode-sdk/src/shared/result/example-op-client.ts | 2 +- packages/ensnode-sdk/src/shared/result/result-common.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/ensnode-sdk/src/shared/result/example-op-client.ts b/packages/ensnode-sdk/src/shared/result/example-op-client.ts index 07d6ee8cd..88a64a51c 100644 --- a/packages/ensnode-sdk/src/shared/result/example-op-client.ts +++ b/packages/ensnode-sdk/src/shared/result/example-op-client.ts @@ -21,7 +21,7 @@ export const callExampleOp = (address: Address): ExampleOpClientResult => { // ensure server result code is recognized by this client version if ( - isRecognizedResultCodeForOperation( + !isRecognizedResultCodeForOperation( result.resultCode, EXAMPLE_OP_RECOGNIZED_SERVER_RESULT_CODES, ) diff --git a/packages/ensnode-sdk/src/shared/result/result-common.ts b/packages/ensnode-sdk/src/shared/result/result-common.ts index daf5407b8..65f07ae71 100644 --- a/packages/ensnode-sdk/src/shared/result/result-common.ts +++ b/packages/ensnode-sdk/src/shared/result/result-common.ts @@ -95,7 +95,8 @@ export const buildResultRequestTimeout = ( ************************************************************/ /** - * Represents a result for an operation that was unrecognized by the client. + * Represents an operation result with a result code that is not recognized + * by this client version. * * Relevant for cases where a client is running version X while the server * is running version X+N and the server returns a result code that is not From cbfd0fa413faed36ffda21edd2316635ba50c447 Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Sat, 17 Jan 2026 10:36:48 +0400 Subject: [PATCH 06/14] refinements --- .../src/shared/result/result-base.ts | 40 +++++++++++++++++-- .../src/shared/result/result-code.ts | 15 +------ .../src/shared/result/result-common.ts | 14 +++---- 3 files changed, 44 insertions(+), 25 deletions(-) diff --git a/packages/ensnode-sdk/src/shared/result/result-base.ts b/packages/ensnode-sdk/src/shared/result/result-base.ts index 5c924fab9..996fa425b 100644 --- a/packages/ensnode-sdk/src/shared/result/result-base.ts +++ b/packages/ensnode-sdk/src/shared/result/result-base.ts @@ -1,4 +1,9 @@ -import type { ResultCode, ResultCodeError, ResultCodes } from "./result-code"; +import type { + ResultCode, + ResultCodeClientError, + ResultCodeServerError, + ResultCodes, +} from "./result-code"; /************************************************************ * Abstract results @@ -28,10 +33,37 @@ export interface AbstractResultOk extends AbstractResult - extends AbstractResult { +export interface AbstractResultServerError< + TResultCode extends ResultCodeServerError, + TDataType = undefined, +> extends AbstractResult { + /** + * A description of the error. + */ + errorMessage: string; + + /** + * Identifies if it may be relevant to retry the operation. + * + * If `false`, retrying the operation is unlikely to be helpful. + */ + suggestRetry: boolean; + + /** + * Optional data associated with the error. + */ + data?: TDataType; +} + +/** + * Abstract representation of a client error result. + */ +export interface AbstractResultClientError< + TResultCode extends ResultCodeClientError, + TDataType = undefined, +> extends AbstractResult { /** * A description of the error. */ diff --git a/packages/ensnode-sdk/src/shared/result/result-code.ts b/packages/ensnode-sdk/src/shared/result/result-code.ts index c54317460..e749346cf 100644 --- a/packages/ensnode-sdk/src/shared/result/result-code.ts +++ b/packages/ensnode-sdk/src/shared/result/result-code.ts @@ -56,14 +56,10 @@ export const RESULT_CODE_CLIENT_ERROR_CODES = [ ...RESULT_CODE_SERVER_ERROR_CODES, ] as const; -const RESULT_CODE_ERROR_CODES = [ - ...RESULT_CODE_SERVER_ERROR_CODES, - ...RESULT_CODE_CLIENT_ERROR_CODES, -] as const; const RESULT_CODE_ALL_CODES = [ ResultCodes.Loading, ResultCodes.Ok, - ...RESULT_CODE_ERROR_CODES, + ...RESULT_CODE_CLIENT_ERROR_CODES, ] as const; /** @@ -91,11 +87,6 @@ export type ResultCodeServerError = (typeof RESULT_CODE_SERVER_ERROR_CODES)[numb */ export type ResultCodeClientError = (typeof RESULT_CODE_CLIENT_ERROR_CODES)[number]; -/** - * ResultCode for a result that is an error. - */ -export type ResultCodeError = (typeof RESULT_CODE_ERROR_CODES)[number]; - /************************************************************ * Compile-time helpers to ensure invariants expected of * definitions above are maintained and don't become @@ -125,10 +116,6 @@ export type AssertResultCodeExact< : false : false; -type _CompileTimeCheck_ResultCodeErrorMatchesUnion = ExpectTrue< - AssertResultCodeExact ->; - type _CompileTimeCheck_ResultCodeMatchesUnion = ExpectTrue< AssertResultCodeExact >; diff --git a/packages/ensnode-sdk/src/shared/result/result-common.ts b/packages/ensnode-sdk/src/shared/result/result-common.ts index 65f07ae71..a7c0e49ac 100644 --- a/packages/ensnode-sdk/src/shared/result/result-common.ts +++ b/packages/ensnode-sdk/src/shared/result/result-common.ts @@ -2,11 +2,11 @@ * Internal Server Error ************************************************************/ -import type { AbstractResultError } from "./result-base"; +import type { AbstractResultClientError, AbstractResultServerError } from "./result-base"; import { type ResultCode, ResultCodes } from "./result-code"; export interface ResultInternalServerError - extends AbstractResultError {} + extends AbstractResultServerError {} export const buildResultInternalServerError = ( errorMessage?: string, @@ -23,7 +23,7 @@ export const buildResultInternalServerError = ( * Not Found ************************************************************/ -export interface ResultNotFound extends AbstractResultError {} +export interface ResultNotFound extends AbstractResultServerError {} export const buildResultNotFound = ( errorMessage?: string, @@ -41,7 +41,7 @@ export const buildResultNotFound = ( ************************************************************/ export interface ResultInvalidRequest - extends AbstractResultError {} + extends AbstractResultServerError {} export const buildResultInvalidRequest = ( errorMessage?: string, @@ -59,7 +59,7 @@ export const buildResultInvalidRequest = ( ************************************************************/ export interface ResultConnectionError - extends AbstractResultError {} + extends AbstractResultClientError {} export const buildResultConnectionError = ( errorMessage?: string, @@ -77,7 +77,7 @@ export const buildResultConnectionError = ( ************************************************************/ export interface ResultRequestTimeout - extends AbstractResultError {} + extends AbstractResultClientError {} export const buildResultRequestTimeout = ( errorMessage?: string, @@ -107,7 +107,7 @@ export const buildResultRequestTimeout = ( * for recognition by clients that are running version X. */ export interface ResultClientUnrecognizedOperationResult - extends AbstractResultError {} + extends AbstractResultClientError {} export const buildResultClientUnrecognizedOperationResult = ( unrecognizedResult: unknown, From ab65b459dd346a9cfdd3658a7c5c8d265874ee7e Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Sat, 17 Jan 2026 10:44:14 +0400 Subject: [PATCH 07/14] Update packages/ensnode-sdk/src/shared/result/example-op-server.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/ensnode-sdk/src/shared/result/example-op-server.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/ensnode-sdk/src/shared/result/example-op-server.ts b/packages/ensnode-sdk/src/shared/result/example-op-server.ts index 44072228e..c5f0f643c 100644 --- a/packages/ensnode-sdk/src/shared/result/example-op-server.ts +++ b/packages/ensnode-sdk/src/shared/result/example-op-server.ts @@ -41,7 +41,9 @@ export const EXAMPLE_OP_RECOGNIZED_SERVER_RESULT_CODES = [ ResultCodes.InvalidRequest, ] as const satisfies readonly ExampleOpServerResultCode[]; -type _CompileTimeAlignmentCheck = ExpectTrue< +// Intentionally unused: compile-time assertion that the recognized result codes +// exactly match the union of ExampleOpServerResult["resultCode"]. +type AssertExampleOpServerResultCodesMatch = ExpectTrue< AssertResultCodeExact >; From 7fe561175501bababd52c4934227cca1ca5a099a Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Sat, 17 Jan 2026 10:44:59 +0400 Subject: [PATCH 08/14] Update packages/ensnode-sdk/src/shared/result/example-op-server.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/ensnode-sdk/src/shared/result/example-op-server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ensnode-sdk/src/shared/result/example-op-server.ts b/packages/ensnode-sdk/src/shared/result/example-op-server.ts index c5f0f643c..0d00bf512 100644 --- a/packages/ensnode-sdk/src/shared/result/example-op-server.ts +++ b/packages/ensnode-sdk/src/shared/result/example-op-server.ts @@ -10,11 +10,11 @@ import { type ResultInvalidRequest, } from "./result-common"; -export interface ExampleOpResultOkData { +export interface ResultExampleOpOkData { name: string; } -export interface ResultExampleOpOk extends AbstractResultOk {} +export interface ResultExampleOpOk extends AbstractResultOk {} export const buildResultExampleOpOk = (name: string): ResultExampleOpOk => { return { From 7d8329f17b8b2c422537be5686315048305819be Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Sat, 17 Jan 2026 10:46:28 +0400 Subject: [PATCH 09/14] Update packages/ensnode-sdk/src/shared/result/result-code.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/ensnode-sdk/src/shared/result/result-code.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/ensnode-sdk/src/shared/result/result-code.ts b/packages/ensnode-sdk/src/shared/result/result-code.ts index e749346cf..ffec08f6e 100644 --- a/packages/ensnode-sdk/src/shared/result/result-code.ts +++ b/packages/ensnode-sdk/src/shared/result/result-code.ts @@ -116,6 +116,12 @@ export type AssertResultCodeExact< : false : false; +/** + * Intentionally unused type alias used only for compile-time verification that + * the `ResultCode` union exactly matches the entries in `RESULT_CODE_ALL_CODES`. + * If this type ever fails to compile, it indicates that one of the above + * invariants has been broken and the result code definitions are out of sync. + */ type _CompileTimeCheck_ResultCodeMatchesUnion = ExpectTrue< AssertResultCodeExact >; From 7bcb8819157f87cb32f6c805d2d79dba99cbea42 Mon Sep 17 00:00:00 2001 From: "lightwalker.eth" <126201998+lightwalker-eth@users.noreply.github.com> Date: Sat, 17 Jan 2026 10:52:58 +0400 Subject: [PATCH 10/14] refinements --- .../src/shared/result/result-base.ts | 31 ++----------------- .../src/shared/result/result-code.ts | 2 +- .../src/shared/result/result-common.ts | 14 ++++----- 3 files changed, 11 insertions(+), 36 deletions(-) diff --git a/packages/ensnode-sdk/src/shared/result/result-base.ts b/packages/ensnode-sdk/src/shared/result/result-base.ts index 996fa425b..7f0477ef8 100644 --- a/packages/ensnode-sdk/src/shared/result/result-base.ts +++ b/packages/ensnode-sdk/src/shared/result/result-base.ts @@ -33,35 +33,10 @@ export interface AbstractResultOk extends AbstractResult extends AbstractResult { - /** - * A description of the error. - */ - errorMessage: string; - - /** - * Identifies if it may be relevant to retry the operation. - * - * If `false`, retrying the operation is unlikely to be helpful. - */ - suggestRetry: boolean; - - /** - * Optional data associated with the error. - */ - data?: TDataType; -} - -/** - * Abstract representation of a client error result. - */ -export interface AbstractResultClientError< - TResultCode extends ResultCodeClientError, +export interface AbstractResultError< + TResultCode extends ResultCodeServerError | ResultCodeClientError, TDataType = undefined, > extends AbstractResult { /** diff --git a/packages/ensnode-sdk/src/shared/result/result-code.ts b/packages/ensnode-sdk/src/shared/result/result-code.ts index ffec08f6e..d279013a9 100644 --- a/packages/ensnode-sdk/src/shared/result/result-code.ts +++ b/packages/ensnode-sdk/src/shared/result/result-code.ts @@ -23,7 +23,7 @@ export const ResultCodes = { NotFound: "not-found", /** - * Server and client error: the request was invalid. + * Server error: the request was invalid. */ InvalidRequest: "invalid-request", diff --git a/packages/ensnode-sdk/src/shared/result/result-common.ts b/packages/ensnode-sdk/src/shared/result/result-common.ts index a7c0e49ac..65f07ae71 100644 --- a/packages/ensnode-sdk/src/shared/result/result-common.ts +++ b/packages/ensnode-sdk/src/shared/result/result-common.ts @@ -2,11 +2,11 @@ * Internal Server Error ************************************************************/ -import type { AbstractResultClientError, AbstractResultServerError } from "./result-base"; +import type { AbstractResultError } from "./result-base"; import { type ResultCode, ResultCodes } from "./result-code"; export interface ResultInternalServerError - extends AbstractResultServerError {} + extends AbstractResultError {} export const buildResultInternalServerError = ( errorMessage?: string, @@ -23,7 +23,7 @@ export const buildResultInternalServerError = ( * Not Found ************************************************************/ -export interface ResultNotFound extends AbstractResultServerError {} +export interface ResultNotFound extends AbstractResultError {} export const buildResultNotFound = ( errorMessage?: string, @@ -41,7 +41,7 @@ export const buildResultNotFound = ( ************************************************************/ export interface ResultInvalidRequest - extends AbstractResultServerError {} + extends AbstractResultError {} export const buildResultInvalidRequest = ( errorMessage?: string, @@ -59,7 +59,7 @@ export const buildResultInvalidRequest = ( ************************************************************/ export interface ResultConnectionError - extends AbstractResultClientError {} + extends AbstractResultError {} export const buildResultConnectionError = ( errorMessage?: string, @@ -77,7 +77,7 @@ export const buildResultConnectionError = ( ************************************************************/ export interface ResultRequestTimeout - extends AbstractResultClientError {} + extends AbstractResultError {} export const buildResultRequestTimeout = ( errorMessage?: string, @@ -107,7 +107,7 @@ export const buildResultRequestTimeout = ( * for recognition by clients that are running version X. */ export interface ResultClientUnrecognizedOperationResult - extends AbstractResultClientError {} + extends AbstractResultError {} export const buildResultClientUnrecognizedOperationResult = ( unrecognizedResult: unknown, From 44d8533c42685fca13131dfe0dc55265157d2f5e Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Sat, 17 Jan 2026 11:08:52 +0100 Subject: [PATCH 11/14] Move examples file into `examples` directory --- .../{example-dx-client.ts => examples/dx-client.ts} | 4 ++-- .../{example-dx-hook.ts => examples/dx-hook.ts} | 4 ++-- .../{example-op-client.ts => examples/op-client.ts} | 12 ++++++------ .../{example-op-hook.ts => examples/op-hook.ts} | 6 +++--- .../{example-op-server.ts => examples/op-server.ts} | 6 +++--- .../server-router.ts} | 8 ++++---- 6 files changed, 20 insertions(+), 20 deletions(-) rename packages/ensnode-sdk/src/shared/result/{example-dx-client.ts => examples/dx-client.ts} (85%) rename packages/ensnode-sdk/src/shared/result/{example-dx-hook.ts => examples/dx-hook.ts} (88%) rename packages/ensnode-sdk/src/shared/result/{example-op-client.ts => examples/op-client.ts} (94%) rename packages/ensnode-sdk/src/shared/result/{example-op-hook.ts => examples/op-hook.ts} (78%) rename packages/ensnode-sdk/src/shared/result/{example-op-server.ts => examples/op-server.ts} (94%) rename packages/ensnode-sdk/src/shared/result/{example-server-router.ts => examples/server-router.ts} (79%) diff --git a/packages/ensnode-sdk/src/shared/result/example-dx-client.ts b/packages/ensnode-sdk/src/shared/result/examples/dx-client.ts similarity index 85% rename from packages/ensnode-sdk/src/shared/result/example-dx-client.ts rename to packages/ensnode-sdk/src/shared/result/examples/dx-client.ts index 3ddbd999b..ee0d4ea71 100644 --- a/packages/ensnode-sdk/src/shared/result/example-dx-client.ts +++ b/packages/ensnode-sdk/src/shared/result/examples/dx-client.ts @@ -1,7 +1,7 @@ import type { Address } from "viem"; -import { callExampleOp } from "./example-op-client"; -import { ResultCodes } from "./result-code"; +import { ResultCodes } from "../result-code"; +import { callExampleOp } from "./op-client"; export const myExampleDXClient = (address: Address): void => { const result = callExampleOp(address); diff --git a/packages/ensnode-sdk/src/shared/result/example-dx-hook.ts b/packages/ensnode-sdk/src/shared/result/examples/dx-hook.ts similarity index 88% rename from packages/ensnode-sdk/src/shared/result/example-dx-hook.ts rename to packages/ensnode-sdk/src/shared/result/examples/dx-hook.ts index 40d8152d5..5673702b3 100644 --- a/packages/ensnode-sdk/src/shared/result/example-dx-hook.ts +++ b/packages/ensnode-sdk/src/shared/result/examples/dx-hook.ts @@ -1,7 +1,7 @@ import type { Address } from "viem"; -import { useExampleOp } from "./example-op-hook"; -import { ResultCodes } from "./result-code"; +import { ResultCodes } from "../result-code"; +import { useExampleOp } from "./op-hook"; export const myExampleDXHook = (address: Address): void => { const result = useExampleOp(address); diff --git a/packages/ensnode-sdk/src/shared/result/example-op-client.ts b/packages/ensnode-sdk/src/shared/result/examples/op-client.ts similarity index 94% rename from packages/ensnode-sdk/src/shared/result/example-op-client.ts rename to packages/ensnode-sdk/src/shared/result/examples/op-client.ts index 88a64a51c..df521dccc 100644 --- a/packages/ensnode-sdk/src/shared/result/example-op-client.ts +++ b/packages/ensnode-sdk/src/shared/result/examples/op-client.ts @@ -1,17 +1,17 @@ import type { Address } from "viem"; -import { - EXAMPLE_OP_RECOGNIZED_SERVER_RESULT_CODES, - type ExampleOpServerResult, - exampleOp, -} from "./example-op-server"; import { buildResultClientUnrecognizedOperationResult, buildResultConnectionError, buildResultRequestTimeout, isRecognizedResultCodeForOperation, type ResultClientError, -} from "./result-common"; +} from "../result-common"; +import { + EXAMPLE_OP_RECOGNIZED_SERVER_RESULT_CODES, + type ExampleOpServerResult, + exampleOp, +} from "./op-server"; export type ExampleOpClientResult = ExampleOpServerResult | ResultClientError; diff --git a/packages/ensnode-sdk/src/shared/result/example-op-hook.ts b/packages/ensnode-sdk/src/shared/result/examples/op-hook.ts similarity index 78% rename from packages/ensnode-sdk/src/shared/result/example-op-hook.ts rename to packages/ensnode-sdk/src/shared/result/examples/op-hook.ts index 783200e63..16e85cf7e 100644 --- a/packages/ensnode-sdk/src/shared/result/example-op-hook.ts +++ b/packages/ensnode-sdk/src/shared/result/examples/op-hook.ts @@ -1,8 +1,8 @@ import type { Address } from "viem"; -import { callExampleOp, type ExampleOpClientResult } from "./example-op-client"; -import type { AbstractResultLoading } from "./result-base"; -import { ResultCodes } from "./result-code"; +import type { AbstractResultLoading } from "../result-base"; +import { ResultCodes } from "../result-code"; +import { callExampleOp, type ExampleOpClientResult } from "./op-client"; export interface ExampleOpLoadingData { address: Address; diff --git a/packages/ensnode-sdk/src/shared/result/example-op-server.ts b/packages/ensnode-sdk/src/shared/result/examples/op-server.ts similarity index 94% rename from packages/ensnode-sdk/src/shared/result/example-op-server.ts rename to packages/ensnode-sdk/src/shared/result/examples/op-server.ts index 0d00bf512..9ede217f8 100644 --- a/packages/ensnode-sdk/src/shared/result/example-op-server.ts +++ b/packages/ensnode-sdk/src/shared/result/examples/op-server.ts @@ -1,14 +1,14 @@ import type { Address } from "viem"; import { zeroAddress } from "viem"; -import type { AbstractResultOk } from "./result-base"; -import { type AssertResultCodeExact, type ExpectTrue, ResultCodes } from "./result-code"; +import type { AbstractResultOk } from "../result-base"; +import { type AssertResultCodeExact, type ExpectTrue, ResultCodes } from "../result-code"; import { buildResultInternalServerError, buildResultInvalidRequest, type ResultInternalServerError, type ResultInvalidRequest, -} from "./result-common"; +} from "../result-common"; export interface ResultExampleOpOkData { name: string; diff --git a/packages/ensnode-sdk/src/shared/result/example-server-router.ts b/packages/ensnode-sdk/src/shared/result/examples/server-router.ts similarity index 79% rename from packages/ensnode-sdk/src/shared/result/example-server-router.ts rename to packages/ensnode-sdk/src/shared/result/examples/server-router.ts index 5ab2c9752..9404f36b7 100644 --- a/packages/ensnode-sdk/src/shared/result/example-server-router.ts +++ b/packages/ensnode-sdk/src/shared/result/examples/server-router.ts @@ -1,9 +1,9 @@ import type { Address } from "viem"; -import { exampleOp } from "./example-op-server"; -import type { AbstractResult } from "./result-base"; -import type { ResultCode } from "./result-code"; -import { buildResultInternalServerError, buildResultNotFound } from "./result-common"; +import type { AbstractResult } from "../result-base"; +import type { ResultCode } from "../result-code"; +import { buildResultInternalServerError, buildResultNotFound } from "../result-common"; +import { exampleOp } from "./op-server"; const routeRequest = (path: string): AbstractResult => { // imagine Hono router logic here From 34f438e0d8f8dcc81a110d2d3b005d692aecd6a7 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Sat, 17 Jan 2026 17:44:36 +0100 Subject: [PATCH 12/14] Add JSDocs --- .../src/shared/result/examples/dx-client.ts | 12 +++++++++++ .../src/shared/result/examples/dx-hook.ts | 11 ++++++++++ .../src/shared/result/examples/op-client.ts | 14 +++++++++++++ .../src/shared/result/examples/op-hook.ts | 4 ++++ .../src/shared/result/examples/op-server.ts | 11 +++++++++- .../shared/result/examples/server-router.ts | 14 ++++++++++++- .../src/shared/result/result-code.ts | 9 ++++++++ .../src/shared/result/result-common.ts | 21 +++++++++++++++++++ 8 files changed, 94 insertions(+), 2 deletions(-) diff --git a/packages/ensnode-sdk/src/shared/result/examples/dx-client.ts b/packages/ensnode-sdk/src/shared/result/examples/dx-client.ts index ee0d4ea71..03fa1f445 100644 --- a/packages/ensnode-sdk/src/shared/result/examples/dx-client.ts +++ b/packages/ensnode-sdk/src/shared/result/examples/dx-client.ts @@ -1,3 +1,15 @@ +/** + * Example of a simple client-side application client that calls an operation + * returning Result data model. + * + * In a real-world scenario, this could be part of a frontend application + * calling a client to send a request to a backend service and handle + * the response. + * + * In this example, we show how to handle both successful and error results + * returned by the operation. This includes a retry suggestion for + * certain error cases. + */ import type { Address } from "viem"; import { ResultCodes } from "../result-code"; diff --git a/packages/ensnode-sdk/src/shared/result/examples/dx-hook.ts b/packages/ensnode-sdk/src/shared/result/examples/dx-hook.ts index 5673702b3..c21482d65 100644 --- a/packages/ensnode-sdk/src/shared/result/examples/dx-hook.ts +++ b/packages/ensnode-sdk/src/shared/result/examples/dx-hook.ts @@ -1,3 +1,14 @@ +/** + * Example of a simple client-side DX hook that consumes an operation + * returning Result data model. + * + * In a real-world scenario, this could be part of a React component + * calling a hook to manage async data fetching. + * + * In this example, we show how to handle both successful and error results + * returned by the operation. This includes a retry suggestion for + * certain error cases. + */ import type { Address } from "viem"; import { ResultCodes } from "../result-code"; diff --git a/packages/ensnode-sdk/src/shared/result/examples/op-client.ts b/packages/ensnode-sdk/src/shared/result/examples/op-client.ts index df521dccc..4d0d4be5b 100644 --- a/packages/ensnode-sdk/src/shared/result/examples/op-client.ts +++ b/packages/ensnode-sdk/src/shared/result/examples/op-client.ts @@ -1,3 +1,17 @@ +/** + * Example of a simple client-side operation that calls a server operation + * and returns Result data model. + * + * Note: In a real-world scenario, this would involve making an HTTP request + * to a server endpoint. Here, for simplicity, we directly call the server + * operation function. + * + * We also simulate client-side errors like connection errors and timeouts. + * + * If the server returns a result code that is not recognized by this client + * version, the client handles it by returning a special unrecognized operation + * result. + */ import type { Address } from "viem"; import { diff --git a/packages/ensnode-sdk/src/shared/result/examples/op-hook.ts b/packages/ensnode-sdk/src/shared/result/examples/op-hook.ts index 16e85cf7e..e84b58b86 100644 --- a/packages/ensnode-sdk/src/shared/result/examples/op-hook.ts +++ b/packages/ensnode-sdk/src/shared/result/examples/op-hook.ts @@ -1,3 +1,7 @@ +/** + * Example of a simple client-side operation hook that returns + * Result data model with Loading state. + */ import type { Address } from "viem"; import type { AbstractResultLoading } from "../result-base"; diff --git a/packages/ensnode-sdk/src/shared/result/examples/op-server.ts b/packages/ensnode-sdk/src/shared/result/examples/op-server.ts index 9ede217f8..3f58ceb0f 100644 --- a/packages/ensnode-sdk/src/shared/result/examples/op-server.ts +++ b/packages/ensnode-sdk/src/shared/result/examples/op-server.ts @@ -1,3 +1,12 @@ +/** + * Example of a simple server-side operation that returns Result data model. + * + * In a real-world scenario, this could be part of a backend service + * handling requests and returning structured responses. + * + * In this example, we show how to return both successful and error results + * based on input conditions. + */ import type { Address } from "viem"; import { zeroAddress } from "viem"; @@ -43,7 +52,7 @@ export const EXAMPLE_OP_RECOGNIZED_SERVER_RESULT_CODES = [ // Intentionally unused: compile-time assertion that the recognized result codes // exactly match the union of ExampleOpServerResult["resultCode"]. -type AssertExampleOpServerResultCodesMatch = ExpectTrue< +type _AssertExampleOpServerResultCodesMatch = ExpectTrue< AssertResultCodeExact >; diff --git a/packages/ensnode-sdk/src/shared/result/examples/server-router.ts b/packages/ensnode-sdk/src/shared/result/examples/server-router.ts index 9404f36b7..87a0e880e 100644 --- a/packages/ensnode-sdk/src/shared/result/examples/server-router.ts +++ b/packages/ensnode-sdk/src/shared/result/examples/server-router.ts @@ -1,3 +1,15 @@ +/** + * Example of a simple server-side router handling requests and + * returning Result data model. + * + * In a real-world scenario, this could be part of a backend service + * using a framework like Hono to route requests and return structured + * responses. + * + * In this example, we show how different results are returned + * based on the request path, including delegating to an operation + * that also returns Result data model. + */ import type { Address } from "viem"; import type { AbstractResult } from "../result-base"; @@ -5,7 +17,7 @@ import type { ResultCode } from "../result-code"; import { buildResultInternalServerError, buildResultNotFound } from "../result-common"; import { exampleOp } from "./op-server"; -const routeRequest = (path: string): AbstractResult => { +const _routeRequest = (path: string): AbstractResult => { // imagine Hono router logic here try { if (path === "/example") { diff --git a/packages/ensnode-sdk/src/shared/result/result-code.ts b/packages/ensnode-sdk/src/shared/result/result-code.ts index d279013a9..49c9984e2 100644 --- a/packages/ensnode-sdk/src/shared/result/result-code.ts +++ b/packages/ensnode-sdk/src/shared/result/result-code.ts @@ -43,12 +43,18 @@ export const ResultCodes = { ClientUnrecognizedOperationResult: "client-unrecognized-operation-result", } as const; +/** + * List of ResultCodes that represent server error results. + */ export const RESULT_CODE_SERVER_ERROR_CODES = [ ResultCodes.InternalServerError, ResultCodes.NotFound, ResultCodes.InvalidRequest, ] as const; +/** + * List of ResultCodes that represent client error results. + */ export const RESULT_CODE_CLIENT_ERROR_CODES = [ ResultCodes.ConnectionError, ResultCodes.RequestTimeout, @@ -56,6 +62,9 @@ export const RESULT_CODE_CLIENT_ERROR_CODES = [ ...RESULT_CODE_SERVER_ERROR_CODES, ] as const; +/** + * List of all ResultCodes. + */ const RESULT_CODE_ALL_CODES = [ ResultCodes.Loading, ResultCodes.Ok, diff --git a/packages/ensnode-sdk/src/shared/result/result-common.ts b/packages/ensnode-sdk/src/shared/result/result-common.ts index 65f07ae71..d13ab55a9 100644 --- a/packages/ensnode-sdk/src/shared/result/result-common.ts +++ b/packages/ensnode-sdk/src/shared/result/result-common.ts @@ -8,6 +8,9 @@ import { type ResultCode, ResultCodes } from "./result-code"; export interface ResultInternalServerError extends AbstractResultError {} +/** + * Builds a result object representing an internal server error. + */ export const buildResultInternalServerError = ( errorMessage?: string, suggestRetry: boolean = true, @@ -25,6 +28,9 @@ export const buildResultInternalServerError = ( export interface ResultNotFound extends AbstractResultError {} +/** + * Builds a result object representing a not found error. + */ export const buildResultNotFound = ( errorMessage?: string, suggestRetry: boolean = false, @@ -43,6 +49,9 @@ export const buildResultNotFound = ( export interface ResultInvalidRequest extends AbstractResultError {} +/** + * Builds a result object representing an invalid request error. + */ export const buildResultInvalidRequest = ( errorMessage?: string, suggestRetry: boolean = false, @@ -61,6 +70,9 @@ export const buildResultInvalidRequest = ( export interface ResultConnectionError extends AbstractResultError {} +/** + * Builds a result object representing a connection error. + */ export const buildResultConnectionError = ( errorMessage?: string, suggestRetry: boolean = true, @@ -79,6 +91,9 @@ export const buildResultConnectionError = ( export interface ResultRequestTimeout extends AbstractResultError {} +/** + * Builds a result object representing a request timeout error. + */ export const buildResultRequestTimeout = ( errorMessage?: string, suggestRetry: boolean = true, @@ -109,6 +124,9 @@ export const buildResultRequestTimeout = ( export interface ResultClientUnrecognizedOperationResult extends AbstractResultError {} +/** + * Builds a result object representing an unrecognized operation result. + */ export const buildResultClientUnrecognizedOperationResult = ( unrecognizedResult: unknown, ): ResultClientUnrecognizedOperationResult => { @@ -137,6 +155,9 @@ export const buildResultClientUnrecognizedOperationResult = ( }; }; +/** + * Checks if a result code is recognized for a specific operation. + */ export const isRecognizedResultCodeForOperation = ( resultCode: ResultCode | string, recognizedResultCodesForOperation: readonly ResultCode[], From 9943791952eeec3c899ad538c76cb44e6bf031d2 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Sat, 17 Jan 2026 17:53:06 +0100 Subject: [PATCH 13/14] Add unit tests --- .../src/shared/result/result-common.test.ts | 313 ++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 packages/ensnode-sdk/src/shared/result/result-common.test.ts diff --git a/packages/ensnode-sdk/src/shared/result/result-common.test.ts b/packages/ensnode-sdk/src/shared/result/result-common.test.ts new file mode 100644 index 000000000..d5069326c --- /dev/null +++ b/packages/ensnode-sdk/src/shared/result/result-common.test.ts @@ -0,0 +1,313 @@ +import { describe, expect, it } from "vitest"; + +import { ResultCodes } from "./result-code"; +import { + buildResultClientUnrecognizedOperationResult, + buildResultConnectionError, + buildResultInternalServerError, + buildResultInvalidRequest, + buildResultNotFound, + buildResultRequestTimeout, + isRecognizedResultCodeForOperation, +} from "./result-common"; + +describe("Result Error Builders", () => { + describe("buildResultInternalServerError", () => { + it("should build an internal server error with custom message", () => { + const result = buildResultInternalServerError("Database connection failed"); + + expect(result).toStrictEqual({ + resultCode: ResultCodes.InternalServerError, + errorMessage: "Database connection failed", + suggestRetry: true, + }); + }); + + it("should build an internal server error with default message when not provided", () => { + const result = buildResultInternalServerError(); + + expect(result).toStrictEqual({ + resultCode: ResultCodes.InternalServerError, + errorMessage: "An unknown internal server error occurred.", + suggestRetry: true, + }); + }); + + it("should respect suggestRetry parameter", () => { + const result = buildResultInternalServerError("Error", false); + + expect(result).toStrictEqual({ + resultCode: ResultCodes.InternalServerError, + errorMessage: "Error", + suggestRetry: false, + }); + }); + }); + + describe("buildResultNotFound", () => { + it("should build a not found error with custom message", () => { + const result = buildResultNotFound("User not found"); + + expect(result).toStrictEqual({ + resultCode: ResultCodes.NotFound, + errorMessage: "User not found", + suggestRetry: false, + }); + }); + + it("should build a not found error with default message when not provided", () => { + const result = buildResultNotFound(); + + expect(result).toStrictEqual({ + resultCode: ResultCodes.NotFound, + errorMessage: "Requested resource not found.", + suggestRetry: false, + }); + }); + + it("should allow retry suggestion for not found errors", () => { + const result = buildResultNotFound("Not found", true); + + expect(result).toStrictEqual({ + resultCode: ResultCodes.NotFound, + errorMessage: "Not found", + suggestRetry: true, + }); + }); + }); + + describe("buildResultInvalidRequest", () => { + it("should build an invalid request error with custom message", () => { + const result = buildResultInvalidRequest("Missing required field: email"); + + expect(result).toStrictEqual({ + resultCode: ResultCodes.InvalidRequest, + errorMessage: "Missing required field: email", + suggestRetry: false, + }); + }); + + it("should build an invalid request error with default message when not provided", () => { + const result = buildResultInvalidRequest(); + + expect(result).toStrictEqual({ + resultCode: ResultCodes.InvalidRequest, + errorMessage: "Invalid request.", + suggestRetry: false, + }); + }); + + it("should allow retry suggestion for invalid request errors", () => { + const result = buildResultInvalidRequest("Bad input", true); + + expect(result).toStrictEqual({ + resultCode: ResultCodes.InvalidRequest, + errorMessage: "Bad input", + suggestRetry: true, + }); + }); + }); + + describe("buildResultConnectionError", () => { + it("should build a connection error with custom message", () => { + const result = buildResultConnectionError("Failed to connect to server"); + + expect(result).toStrictEqual({ + resultCode: ResultCodes.ConnectionError, + errorMessage: "Failed to connect to server", + suggestRetry: true, + }); + }); + + it("should build a connection error with default message when not provided", () => { + const result = buildResultConnectionError(); + + expect(result).toStrictEqual({ + resultCode: ResultCodes.ConnectionError, + errorMessage: "Connection error.", + suggestRetry: true, + }); + }); + + it("should respect suggestRetry parameter", () => { + const result = buildResultConnectionError("Connection failed", false); + + expect(result).toStrictEqual({ + resultCode: ResultCodes.ConnectionError, + errorMessage: "Connection failed", + suggestRetry: false, + }); + }); + }); + + describe("buildResultRequestTimeout", () => { + it("should build a request timeout error with custom message", () => { + const result = buildResultRequestTimeout("Request exceeded 30 second limit"); + + expect(result).toStrictEqual({ + resultCode: ResultCodes.RequestTimeout, + errorMessage: "Request exceeded 30 second limit", + suggestRetry: true, + }); + }); + + it("should build a request timeout error with default message when not provided", () => { + const result = buildResultRequestTimeout(); + + expect(result).toStrictEqual({ + resultCode: ResultCodes.RequestTimeout, + errorMessage: "Request timed out.", + suggestRetry: true, + }); + }); + + it("should respect suggestRetry parameter", () => { + const result = buildResultRequestTimeout("Timeout", false); + + expect(result).toStrictEqual({ + resultCode: ResultCodes.RequestTimeout, + errorMessage: "Timeout", + suggestRetry: false, + }); + }); + }); + + describe("buildResultClientUnrecognizedOperationResult", () => { + it("should build unrecognized result with default values for unknown input", () => { + const result = buildResultClientUnrecognizedOperationResult("unknown"); + + expect(result).toStrictEqual({ + resultCode: ResultCodes.ClientUnrecognizedOperationResult, + errorMessage: "An unrecognized result for the operation occurred.", + suggestRetry: true, + }); + }); + + it("should extract errorMessage from object", () => { + const unrecognizedResult = { errorMessage: "Custom error message" }; + const result = buildResultClientUnrecognizedOperationResult(unrecognizedResult); + + expect(result).toStrictEqual({ + resultCode: ResultCodes.ClientUnrecognizedOperationResult, + errorMessage: "Custom error message", + suggestRetry: true, + }); + }); + + it("should extract suggestRetry from object", () => { + const unrecognizedResult = { suggestRetry: false }; + const result = buildResultClientUnrecognizedOperationResult(unrecognizedResult); + + expect(result).toStrictEqual({ + resultCode: ResultCodes.ClientUnrecognizedOperationResult, + errorMessage: "An unrecognized result for the operation occurred.", + suggestRetry: false, + }); + }); + + it("should extract both errorMessage and suggestRetry from object", () => { + const unrecognizedResult = { + errorMessage: "Custom error", + suggestRetry: false, + }; + const result = buildResultClientUnrecognizedOperationResult(unrecognizedResult); + + expect(result).toStrictEqual({ + resultCode: ResultCodes.ClientUnrecognizedOperationResult, + errorMessage: "Custom error", + suggestRetry: false, + }); + }); + + it("should ignore non-string errorMessage", () => { + const unrecognizedResult = { errorMessage: 123 }; + const result = buildResultClientUnrecognizedOperationResult(unrecognizedResult); + + expect(result).toStrictEqual({ + resultCode: ResultCodes.ClientUnrecognizedOperationResult, + errorMessage: "An unrecognized result for the operation occurred.", + suggestRetry: true, + }); + }); + + it("should ignore non-boolean suggestRetry", () => { + const unrecognizedResult = { suggestRetry: "true" }; + const result = buildResultClientUnrecognizedOperationResult(unrecognizedResult); + + expect(result).toStrictEqual({ + resultCode: ResultCodes.ClientUnrecognizedOperationResult, + errorMessage: "An unrecognized result for the operation occurred.", + suggestRetry: true, + }); + }); + + it("should handle null input", () => { + const result = buildResultClientUnrecognizedOperationResult(null); + + expect(result).toStrictEqual({ + resultCode: ResultCodes.ClientUnrecognizedOperationResult, + errorMessage: "An unrecognized result for the operation occurred.", + suggestRetry: true, + }); + }); + + it("should handle object with extra properties", () => { + const unrecognizedResult = { + errorMessage: "Error occurred", + suggestRetry: false, + extraField: "ignored", + nested: { data: "also ignored" }, + }; + const result = buildResultClientUnrecognizedOperationResult(unrecognizedResult); + + expect(result).toStrictEqual({ + resultCode: ResultCodes.ClientUnrecognizedOperationResult, + errorMessage: "Error occurred", + suggestRetry: false, + }); + }); + }); + + describe("isRecognizedResultCodeForOperation", () => { + const recognizedCodes = [ + ResultCodes.InternalServerError, + ResultCodes.NotFound, + ResultCodes.InvalidRequest, + ] as const; + + it("should return true for recognized result code", () => { + expect( + isRecognizedResultCodeForOperation(ResultCodes.InternalServerError, recognizedCodes), + ).toBe(true); + }); + + it("should return false for unrecognized result code", () => { + expect(isRecognizedResultCodeForOperation(ResultCodes.ConnectionError, recognizedCodes)).toBe( + false, + ); + }); + + it("should return false for unknown string result codes", () => { + expect(isRecognizedResultCodeForOperation("UnknownCode", recognizedCodes)).toBe(false); + }); + + it("should handle empty recognized codes array", () => { + expect(isRecognizedResultCodeForOperation(ResultCodes.InternalServerError, [])).toBe(false); + }); + + it("should handle all result codes in recognized list", () => { + const allCodes = [ + ResultCodes.InternalServerError, + ResultCodes.NotFound, + ResultCodes.InvalidRequest, + ResultCodes.ConnectionError, + ResultCodes.RequestTimeout, + ResultCodes.ClientUnrecognizedOperationResult, + ] as const; + + allCodes.forEach((code) => { + expect(isRecognizedResultCodeForOperation(code, allCodes)).toBe(true); + }); + }); + }); +}); From 08d2c7677bc6cde32468b96d6bf5a25041180061 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Sat, 17 Jan 2026 18:08:54 +0100 Subject: [PATCH 14/14] Refine result codes with more precise language --- packages/ensnode-sdk/src/shared/result/result-code.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/ensnode-sdk/src/shared/result/result-code.ts b/packages/ensnode-sdk/src/shared/result/result-code.ts index 49c9984e2..52e376ea9 100644 --- a/packages/ensnode-sdk/src/shared/result/result-code.ts +++ b/packages/ensnode-sdk/src/shared/result/result-code.ts @@ -59,16 +59,23 @@ export const RESULT_CODE_CLIENT_ERROR_CODES = [ ResultCodes.ConnectionError, ResultCodes.RequestTimeout, ResultCodes.ClientUnrecognizedOperationResult, +] as const; + +/** + * List of all error codes the client can return (client-originated + relayed from server). + */ +export const RESULT_CODE_ALL_ERROR_CODES = [ + ...RESULT_CODE_CLIENT_ERROR_CODES, ...RESULT_CODE_SERVER_ERROR_CODES, ] as const; /** * List of all ResultCodes. */ -const RESULT_CODE_ALL_CODES = [ +export const RESULT_CODE_ALL_CODES = [ ResultCodes.Loading, ResultCodes.Ok, - ...RESULT_CODE_CLIENT_ERROR_CODES, + ...RESULT_CODE_ALL_ERROR_CODES, ] as const; /**