Skip to content
Closed
31 changes: 31 additions & 0 deletions packages/ensnode-sdk/src/shared/result/examples/dx-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* 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";
import { callExampleOp } from "./op-client";

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?");
}
}
};
33 changes: 33 additions & 0 deletions packages/ensnode-sdk/src/shared/result/examples/dx-hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* 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";
import { useExampleOp } from "./op-hook";

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?");
}
}
};
56 changes: 56 additions & 0 deletions packages/ensnode-sdk/src/shared/result/examples/op-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* 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 {
buildResultClientUnrecognizedOperationResult,
buildResultConnectionError,
buildResultRequestTimeout,
isRecognizedResultCodeForOperation,
type ResultClientError,
} from "../result-common";
import {
EXAMPLE_OP_RECOGNIZED_SERVER_RESULT_CODES,
type ExampleOpServerResult,
exampleOp,
} from "./op-server";

export type ExampleOpClientResult = ExampleOpServerResult | ResultClientError;

export const callExampleOp = (address: Address): ExampleOpClientResult => {
try {
const result = exampleOp(address);

// ensure server result code is recognized by this client version
if (
!isRecognizedResultCodeForOperation(
result.resultCode,
EXAMPLE_OP_RECOGNIZED_SERVER_RESULT_CODES,
)
) {
return buildResultClientUnrecognizedOperationResult(result);
}

// return server result
return result;
} catch (error) {
// handle client-side errors
if (error === "connection-error") {
return buildResultConnectionError();
} else {
return buildResultRequestTimeout();
}
}
};
34 changes: 34 additions & 0 deletions packages/ensnode-sdk/src/shared/result/examples/op-hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* 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";
import { ResultCodes } from "../result-code";
import { callExampleOp, type ExampleOpClientResult } from "./op-client";

export interface ExampleOpLoadingData {
address: Address;
}

export interface ResultExampleOpLoading extends AbstractResultLoading<ExampleOpLoadingData> {}

export const buildResultExampleOpLoading = (address: Address): ResultExampleOpLoading => {
return {
resultCode: ResultCodes.Loading,
data: {
address,
},
};
};

export type ExampleOpHookResult = ExampleOpClientResult | ResultExampleOpLoading;

export const useExampleOp = (address: Address): ExampleOpHookResult => {
if (Math.random() < 0.5) {
return buildResultExampleOpLoading(address);
} else {
return callExampleOp(address);
}
};
70 changes: 70 additions & 0 deletions packages/ensnode-sdk/src/shared/result/examples/op-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* 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";

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 ResultExampleOpOkData {
name: string;
}

export interface ResultExampleOpOk extends AbstractResultOk<ResultExampleOpOkData> {}

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[];

// Intentionally unused: compile-time assertion that the recognized result codes
// exactly match the union of ExampleOpServerResult["resultCode"].
type _AssertExampleOpServerResultCodesMatch = ExpectTrue<
AssertResultCodeExact<ExampleOpServerResultCode, typeof EXAMPLE_OP_RECOGNIZED_SERVER_RESULT_CODES>
>;

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",
);
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* 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";
import type { ResultCode } from "../result-code";
import { buildResultInternalServerError, buildResultNotFound } from "../result-common";
import { exampleOp } from "./op-server";

const _routeRequest = (path: string): AbstractResult<ResultCode> => {
// 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);
}
};
5 changes: 3 additions & 2 deletions packages/ensnode-sdk/src/shared/result/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./types";
export * from "./utils";
export * from "./result-base";
export * from "./result-code";
export * from "./result-common";
69 changes: 69 additions & 0 deletions packages/ensnode-sdk/src/shared/result/result-base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type {
ResultCode,
ResultCodeClientError,
ResultCodeServerError,
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<TResultCode extends ResultCode> {
/**
* The classification of the result.
*/
resultCode: TResultCode;
}

/**
* Abstract representation of a successful result.
*/
export interface AbstractResultOk<TDataType> extends AbstractResult<typeof ResultCodes.Ok> {
/**
* The data of the result.
*/
data: TDataType;
}

/**
* Abstract representation of an error result.
*/
export interface AbstractResultError<
TResultCode extends ResultCodeServerError | ResultCodeClientError,
TDataType = undefined,
> extends AbstractResult<TResultCode> {
/**
* 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 loading result.
*/
export interface AbstractResultLoading<TDataType = undefined>
extends AbstractResult<typeof ResultCodes.Loading> {
/**
* Optional data associated with the loading operation.
*/
data?: TDataType;
}
Loading