Skip to content

Commit ce0e754

Browse files
lightwalker-ethCopilottk-o
authored
suggest "generic result" refinements (#1541)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Tomasz Kopacki <tomasz@kopacki.net>
1 parent 200ae6a commit ce0e754

14 files changed

Lines changed: 962 additions & 358 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* Example of a simple client-side application client that calls an operation
3+
* returning Result data model.
4+
*
5+
* In a real-world scenario, this could be part of a frontend application
6+
* calling a client to send a request to a backend service and handle
7+
* the response.
8+
*
9+
* In this example, we show how to handle both successful and error results
10+
* returned by the operation. This includes a retry suggestion for
11+
* certain error cases.
12+
*/
13+
import type { Address } from "viem";
14+
15+
import { ResultCodes } from "../result-code";
16+
import { callExampleOp } from "./op-client";
17+
18+
export const myExampleDXClient = (address: Address): void => {
19+
const result = callExampleOp(address);
20+
21+
if (result.resultCode === ResultCodes.Ok) {
22+
// NOTE: Here the type system knows that `result` is of type `ResultExampleOpOk`
23+
console.log(result.data.name);
24+
} else {
25+
// NOTE: Here the type system knows that `result` has fields for `errorMessage` and `suggestRetry`
26+
console.error(`Error: (${result.resultCode}) - ${result.errorMessage}`);
27+
if (result.suggestRetry) {
28+
console.log("Try again?");
29+
}
30+
}
31+
};
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* Example of a simple client-side DX hook that consumes an operation
3+
* returning Result data model.
4+
*
5+
* In a real-world scenario, this could be part of a React component
6+
* calling a hook to manage async data fetching.
7+
*
8+
* In this example, we show how to handle both successful and error results
9+
* returned by the operation. This includes a retry suggestion for
10+
* certain error cases.
11+
*/
12+
import type { Address } from "viem";
13+
14+
import { ResultCodes } from "../result-code";
15+
import { useExampleOp } from "./op-hook";
16+
17+
export const myExampleDXHook = (address: Address): void => {
18+
const result = useExampleOp(address);
19+
20+
if (result.resultCode === ResultCodes.Loading) {
21+
// NOTE: Here the type system knows that `result` is of type `ResultExampleOpLoading`
22+
console.log("Loading...");
23+
} else if (result.resultCode === ResultCodes.Ok) {
24+
// NOTE: Here the type system knows that `result` is of type `ResultExampleOpOk`
25+
console.log(result.data.name);
26+
} else {
27+
// NOTE: Here the type system knows that `result` has fields for `errorMessage` and `suggestRetry`
28+
console.error(`Error: (${result.resultCode}) - ${result.errorMessage}`);
29+
if (result.suggestRetry) {
30+
console.log("Try again?");
31+
}
32+
}
33+
};
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* Example of a simple client-side operation that calls a server operation
3+
* and returns Result data model.
4+
*
5+
* Note: In a real-world scenario, this would involve making an HTTP request
6+
* to a server endpoint. Here, for simplicity, we directly call the server
7+
* operation function.
8+
*
9+
* We also simulate client-side errors like connection errors and timeouts.
10+
*
11+
* If the server returns a result code that is not recognized by this client
12+
* version, the client handles it by returning a special unrecognized operation
13+
* result.
14+
*/
15+
import type { Address } from "viem";
16+
17+
import {
18+
buildResultClientUnrecognizedOperationResult,
19+
buildResultConnectionError,
20+
buildResultRequestTimeout,
21+
isRecognizedResultCodeForOperation,
22+
type ResultClientError,
23+
} from "../result-common";
24+
import {
25+
EXAMPLE_OP_RECOGNIZED_SERVER_RESULT_CODES,
26+
type ExampleOpServerResult,
27+
exampleOp,
28+
} from "./op-server";
29+
30+
export type ExampleOpClientResult = ExampleOpServerResult | ResultClientError;
31+
32+
export const callExampleOp = (address: Address): ExampleOpClientResult => {
33+
try {
34+
const result = exampleOp(address);
35+
36+
// ensure server result code is recognized by this client version
37+
if (
38+
!isRecognizedResultCodeForOperation(
39+
result.resultCode,
40+
EXAMPLE_OP_RECOGNIZED_SERVER_RESULT_CODES,
41+
)
42+
) {
43+
return buildResultClientUnrecognizedOperationResult(result);
44+
}
45+
46+
// return server result
47+
return result;
48+
} catch (error) {
49+
// handle client-side errors
50+
if (error === "connection-error") {
51+
return buildResultConnectionError();
52+
} else {
53+
return buildResultRequestTimeout();
54+
}
55+
}
56+
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* Example of a simple client-side operation hook that returns
3+
* Result data model with Loading state.
4+
*/
5+
import type { Address } from "viem";
6+
7+
import type { AbstractResultLoading } from "../result-base";
8+
import { ResultCodes } from "../result-code";
9+
import { callExampleOp, type ExampleOpClientResult } from "./op-client";
10+
11+
export interface ExampleOpLoadingData {
12+
address: Address;
13+
}
14+
15+
export interface ResultExampleOpLoading extends AbstractResultLoading<ExampleOpLoadingData> {}
16+
17+
export const buildResultExampleOpLoading = (address: Address): ResultExampleOpLoading => {
18+
return {
19+
resultCode: ResultCodes.Loading,
20+
data: {
21+
address,
22+
},
23+
};
24+
};
25+
26+
export type ExampleOpHookResult = ExampleOpClientResult | ResultExampleOpLoading;
27+
28+
export const useExampleOp = (address: Address): ExampleOpHookResult => {
29+
if (Math.random() < 0.5) {
30+
return buildResultExampleOpLoading(address);
31+
} else {
32+
return callExampleOp(address);
33+
}
34+
};
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* Example of a simple server-side operation that returns Result data model.
3+
*
4+
* In a real-world scenario, this could be part of a backend service
5+
* handling requests and returning structured responses.
6+
*
7+
* In this example, we show how to return both successful and error results
8+
* based on input conditions.
9+
*/
10+
import type { Address } from "viem";
11+
import { zeroAddress } from "viem";
12+
13+
import type { AbstractResultOk } from "../result-base";
14+
import { type AssertResultCodeExact, type ExpectTrue, ResultCodes } from "../result-code";
15+
import {
16+
buildResultInternalServerError,
17+
buildResultInvalidRequest,
18+
type ResultInternalServerError,
19+
type ResultInvalidRequest,
20+
} from "../result-common";
21+
22+
export interface ResultExampleOpOkData {
23+
name: string;
24+
}
25+
26+
export interface ResultExampleOpOk extends AbstractResultOk<ResultExampleOpOkData> {}
27+
28+
export const buildResultExampleOpOk = (name: string): ResultExampleOpOk => {
29+
return {
30+
resultCode: ResultCodes.Ok,
31+
data: {
32+
name,
33+
},
34+
};
35+
};
36+
37+
// NOTE: Here we define a union of all possible results returned by the server for this operation.
38+
// We specifically call these "Server Results" because later we need to add all the possible client error results to get
39+
// the full set of all results a client can receive from this operation.
40+
export type ExampleOpServerResult =
41+
| ResultExampleOpOk
42+
| ResultInternalServerError
43+
| ResultInvalidRequest;
44+
45+
export type ExampleOpServerResultCode = ExampleOpServerResult["resultCode"];
46+
47+
export const EXAMPLE_OP_RECOGNIZED_SERVER_RESULT_CODES = [
48+
ResultCodes.Ok,
49+
ResultCodes.InternalServerError,
50+
ResultCodes.InvalidRequest,
51+
] as const satisfies readonly ExampleOpServerResultCode[];
52+
53+
// Intentionally unused: compile-time assertion that the recognized result codes
54+
// exactly match the union of ExampleOpServerResult["resultCode"].
55+
type _AssertExampleOpServerResultCodesMatch = ExpectTrue<
56+
AssertResultCodeExact<ExampleOpServerResultCode, typeof EXAMPLE_OP_RECOGNIZED_SERVER_RESULT_CODES>
57+
>;
58+
59+
export const exampleOp = (address: Address): ExampleOpServerResult => {
60+
if (address === zeroAddress) {
61+
return buildResultInvalidRequest("Address must not be the zero address");
62+
}
63+
if (Math.random() < 0.5) {
64+
return buildResultExampleOpOk("example.eth");
65+
} else {
66+
return buildResultInternalServerError(
67+
"Invariant violation: random number is not less than 0.5",
68+
);
69+
}
70+
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* Example of a simple server-side router handling requests and
3+
* returning Result data model.
4+
*
5+
* In a real-world scenario, this could be part of a backend service
6+
* using a framework like Hono to route requests and return structured
7+
* responses.
8+
*
9+
* In this example, we show how different results are returned
10+
* based on the request path, including delegating to an operation
11+
* that also returns Result data model.
12+
*/
13+
import type { Address } from "viem";
14+
15+
import type { AbstractResult } from "../result-base";
16+
import type { ResultCode } from "../result-code";
17+
import { buildResultInternalServerError, buildResultNotFound } from "../result-common";
18+
import { exampleOp } from "./op-server";
19+
20+
const _routeRequest = (path: string): AbstractResult<ResultCode> => {
21+
// imagine Hono router logic here
22+
try {
23+
if (path === "/example") {
24+
return exampleOp("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" as Address);
25+
} else {
26+
// guarantee in all cases we return our Result data model
27+
return buildResultNotFound(`Path not found: ${path}`);
28+
}
29+
} catch (error) {
30+
// guarantee in all cases we return our Result data model
31+
const errorMessage = error instanceof Error ? error.message : undefined;
32+
return buildResultInternalServerError(errorMessage);
33+
}
34+
};
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1-
export * from "./types";
2-
export * from "./utils";
1+
export * from "./result-base";
2+
export * from "./result-code";
3+
export * from "./result-common";
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import type {
2+
ResultCode,
3+
ResultCodeClientError,
4+
ResultCodeServerError,
5+
ResultCodes,
6+
} from "./result-code";
7+
8+
/************************************************************
9+
* Abstract results
10+
*
11+
* These are base interfaces that should be extended to
12+
* create concrete result types.
13+
************************************************************/
14+
15+
/**
16+
* Abstract representation of any result.
17+
*/
18+
export interface AbstractResult<TResultCode extends ResultCode> {
19+
/**
20+
* The classification of the result.
21+
*/
22+
resultCode: TResultCode;
23+
}
24+
25+
/**
26+
* Abstract representation of a successful result.
27+
*/
28+
export interface AbstractResultOk<TDataType> extends AbstractResult<typeof ResultCodes.Ok> {
29+
/**
30+
* The data of the result.
31+
*/
32+
data: TDataType;
33+
}
34+
35+
/**
36+
* Abstract representation of an error result.
37+
*/
38+
export interface AbstractResultError<
39+
TResultCode extends ResultCodeServerError | ResultCodeClientError,
40+
TDataType = undefined,
41+
> extends AbstractResult<TResultCode> {
42+
/**
43+
* A description of the error.
44+
*/
45+
errorMessage: string;
46+
47+
/**
48+
* Identifies if it may be relevant to retry the operation.
49+
*
50+
* If `false`, retrying the operation is unlikely to be helpful.
51+
*/
52+
suggestRetry: boolean;
53+
54+
/**
55+
* Optional data associated with the error.
56+
*/
57+
data?: TDataType;
58+
}
59+
60+
/**
61+
* Abstract representation of a loading result.
62+
*/
63+
export interface AbstractResultLoading<TDataType = undefined>
64+
extends AbstractResult<typeof ResultCodes.Loading> {
65+
/**
66+
* Optional data associated with the loading operation.
67+
*/
68+
data?: TDataType;
69+
}

0 commit comments

Comments
 (0)