Skip to content
This repository has been archived by the owner on Jan 22, 2025. It is now read-only.

Commit

Permalink
Split RpcResponse into RpcResponse and RpcTransportResponse
Browse files Browse the repository at this point in the history
  • Loading branch information
lorisleiva committed Aug 27, 2024
1 parent 0aa60a6 commit f7a09e2
Show file tree
Hide file tree
Showing 9 changed files with 83 additions and 48 deletions.
6 changes: 6 additions & 0 deletions .changeset/fresh-foxes-jam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@solana/rpc-transport-http': patch
'@solana/rpc-spec': patch
---

Split `RpcResponse` into `RpcResponse` and `RpcTransportResponse`
28 changes: 21 additions & 7 deletions packages/rpc-spec/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,25 +41,25 @@ Calling the `send(options)` method on a `PendingRpcRequest` will trigger the req

An object that exposes all of the functions described by `TRpcMethods`, and fulfils them using `TRpcTransport`. Calling each method returns a `PendingRpcRequest<TResponse>` where `TResponse` is that method's response type.

### `RpcRequest`
### `RpcRequest<TParams>`

An object that describes the elements of a JSON RPC request. It consists of the following properties:

- `methodName: string`: The name of the JSON RPC method to be called.
- `params: unknown`: The parameters to be passed to the JSON RPC method.
- `params: TParams`: The parameters to be passed to the JSON RPC method.
- `toPayload?: (methodName: string, params: unknown) => unknown`: An optional function that defines how the method name and parameters should be transformed into a JSON RPC payload.
- `toText?: (payload: unknown) => string`: An optional function that defines how the JSO RPC payload should be transformed into a JSON string.

### `RpcRequestTransformer`

A function that accepts an `RpcRequest` and returns another `RpcRequest`. This allows the `RpcApi` to transform the request before it is sent to the JSON RPC server.

### `RpcResponse`
### `RpcResponse<TResponse>`

An object that represents the response from a JSON RPC server. It contains two asynchronous methods that can be used to access the response data:
An object that represents the response from a JSON RPC server. It contains two functions:

- `await response.json()`: Returns the data as a JSON object.
- `await response.text()`: Returns the data, unparsed, as a JSON string.
- `json: () => Promise<TResponse>`: This async function returns the data as a JSON object.
- `fromText?: (text: string) => unknown`: An optional function that defines how the raw JSON string should be parsed into a JSON object.

This allows the `RpcApi` to decide whether they want the parsed JSON object or the raw JSON string. Ultimately, the `json` method will be used by the `Rpc` to provide the final response to the caller.

Expand Down Expand Up @@ -91,10 +91,24 @@ A configuration object consisting of the following properties:

### `RpcTransport`

Any function that implements this interface can act as a transport for an `Rpc`. It need only return a promise for a response given the following config:
Any function that implements this interface can act as a transport for an `Rpc`. It need only return a promise for a `RpcTransportResponse` given a `RpcTransportRequest`. See below for the types of these objects.

### `RpcTransportRequest`

Represents a request that is ready to be sent via an RPC transport. It consists of the following properties:

- `payload`: A value of arbitrary type to be sent.
- `signal`: An optional `AbortSignal` on which the `'abort'` event will be fired if the request should be cancelled.
- `toText?: (payload: unknown) => string`: An optional function that defines how the JSON RPC payload should be transformed into a JSON string. When not provided, `JSON.stringify` should be used.

### `RpcTransportResponse`

Represents the response of an RPC transport. It contains two asynchronous methods that can be used to access the response data:

- `await response.json()`: Returns the data as a JSON object.
- `await response.text()`: Returns the data, unparsed, as a JSON string.

This allows the consumer to decide whether they want the parsed JSON object or the raw JSON string.

## Functions

Expand Down
24 changes: 7 additions & 17 deletions packages/rpc-spec/src/__tests__/rpc-shared-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@ describe('createJsonRpcResponseTransformer', () => {

// Given a request and a response that returns a number.
const request = { methodName: 'someMethod', params: [123] };
const response = {
json: () => Promise.resolve(123),
text: () => Promise.resolve('123'),
};
const response = { json: () => Promise.resolve(123) };

// When we create a JSON transformer that doubles the number.
const transformer = createJsonRpcResponseTransformer((json: unknown) => (json as number) * 2);
Expand All @@ -22,30 +19,23 @@ describe('createJsonRpcResponseTransformer', () => {
await expect(transformedResponse.json()).resolves.toBe(246);
});

it('does not alter the value of the text Promise', async () => {
expect.assertions(1);

it('does not alter the value of the `fromText` function', () => {
// Given a request and a response that returns a number.
const request = { methodName: 'someMethod', params: [123] };
const response = {
json: () => Promise.resolve(123),
text: () => Promise.resolve('123'),
};
const fromText = jest.fn();
const response = { fromText, json: () => Promise.resolve(123) };

// When we create a JSON transformer that doubles the number.
const transformer = createJsonRpcResponseTransformer((json: unknown) => (json as number) * 2);

// Then the text should function should return the original string.
// Then the `fromText` function should not be altered.
const transformedResponse = transformer(response, request);
await expect(transformedResponse.text()).resolves.toBe('123');
expect(transformedResponse.fromText).toBe(fromText);
});

it('returns a frozen object as the Reponse', () => {
// Given any response.
const response = {
json: () => Promise.resolve(123),
text: () => Promise.resolve('123'),
};
const response = { json: () => Promise.resolve(123) };

// When we pass it through a JSON transformer.
const transformedResponse = createJsonRpcResponseTransformer(x => x)(response, {} as RpcRequest);
Expand Down
11 changes: 7 additions & 4 deletions packages/rpc-spec/src/__tests__/rpc-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { createRpcMessage } from '@solana/rpc-spec-types';

import { createRpc, Rpc } from '../rpc';
import { RpcApi, RpcApiRequestPlan } from '../rpc-api';
import { createJsonRpcResponseTransformer, RpcResponse } from '../rpc-shared';
import { RpcTransport } from '../rpc-transport';
import { createJsonRpcResponseTransformer } from '../rpc-shared';
import { RpcTransport, RpcTransportResponse } from '../rpc-transport';

interface TestRpcMethods {
someMethod(...args: unknown[]): unknown;
}

function createMockResponse<T>(jsonResponse: T): RpcResponse<T> {
function createMockResponse<T>(jsonResponse: T): RpcTransportResponse<T> {
return {
json: () => Promise.resolve(jsonResponse),
text: () => Promise.resolve(JSON.stringify(jsonResponse)),
Expand Down Expand Up @@ -169,7 +169,10 @@ describe('JSON-RPC 2.0', () => {
const rawResponse = createMockResponse(123);
(makeHttpRequest as jest.Mock).mockResolvedValueOnce(rawResponse);
await rpc.someMethod().send();
expect(responseTransformer).toHaveBeenCalledWith(rawResponse, { methodName: 'someMethod', params: [] });
expect(responseTransformer).toHaveBeenCalledWith(expect.objectContaining({ json: expect.any(Function) }), {
methodName: 'someMethod',
params: [],
});
});
it('returns the processed response', async () => {
expect.assertions(1);
Expand Down
2 changes: 1 addition & 1 deletion packages/rpc-spec/src/rpc-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ export type RpcRequest<TParams = unknown> = {
};

export type RpcResponse<TResponse = unknown> = {
readonly fromText?: (text: string) => unknown;
readonly json: () => Promise<TResponse>;
readonly text: () => Promise<string>;
};

export type RpcRequestTransformer = {
Expand Down
17 changes: 10 additions & 7 deletions packages/rpc-spec/src/rpc-transport.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { RpcResponse } from './rpc-shared';
export type RpcTransportRequest = {
readonly payload: unknown;
readonly signal?: AbortSignal;
readonly toText?: (payload: unknown) => string;
};

type RpcTransportRequest = Readonly<{
payload: unknown;
signal?: AbortSignal;
toText?: (payload: unknown) => string;
}>;
export type RpcTransportResponse<TResponse = unknown> = {
readonly json: () => Promise<TResponse>;
readonly text: () => Promise<string>;
};

export type RpcTransport = {
<TResponse>(request: RpcTransportRequest): Promise<RpcResponse<TResponse>>;
<TResponse>(request: RpcTransportRequest): Promise<RpcTransportResponse<TResponse>>;
};
15 changes: 13 additions & 2 deletions packages/rpc-spec/src/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from '@solana/rpc-spec-types';

import { RpcApi, RpcApiRequestPlan } from './rpc-api';
import { RpcResponse } from './rpc-shared';
import { RpcTransport } from './rpc-transport';

export type RpcConfig<TRpcMethods, TRpcTransport extends RpcTransport> = Readonly<{
Expand Down Expand Up @@ -78,12 +79,22 @@ function createPendingRpcRequest<TRpcMethods, TRpcTransport extends RpcTransport
const payload = request.toPayload
? request.toPayload(request.methodName, request.params)
: createRpcMessage(request.methodName, request.params);
const rawResponse = await rpcConfig.transport<TResponse>({
const transportResponse = await rpcConfig.transport<TResponse>({
payload,
signal: options?.abortSignal,
toText: request.toText,
});
const response = responseTransformer ? responseTransformer(rawResponse, request) : rawResponse;
let response: RpcResponse<TResponse> = { json: () => transportResponse.json() };
const rawResponse: RpcResponse<TResponse> = {
json: async () => {
if (response.fromText) {
return response.fromText(await transportResponse.text()) as TResponse;
} else {
return await transportResponse.json();
}
},
};
response = responseTransformer ? responseTransformer(rawResponse, request) : rawResponse;
return await response.json();
},
};
Expand Down
24 changes: 16 additions & 8 deletions packages/rpc-transport-http/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ Using this core transport, you can implement specialized functionality for lever
Here’s an example of how someone might implement a “round robin” approach to distribute requests to multiple transports:

```ts
import { RpcResponse, RpcTransport } from '@solana/rpc-spec';
import { RpcTransportResponse, RpcTransport } from '@solana/rpc-spec';
import { createHttpTransport } from '@solana/rpc-transport-http';

// Create a transport for each RPC server
Expand All @@ -123,7 +123,9 @@ const transports = [

// Create a wrapper transport that distributes requests to them
let nextTransport = 0;
async function roundRobinTransport<TResponse>(...args: Parameters<RpcTransport>): Promise<RpcResponse<TResponse>> {
async function roundRobinTransport<TResponse>(
...args: Parameters<RpcTransport>
): Promise<RpcTransportResponse<TResponse>> {
const transport = transports[nextTransport];
nextTransport = (nextTransport + 1) % transports.length;
return await transport(...args);
Expand All @@ -137,7 +139,7 @@ Another example of a possible customization for a transport is to shard requests
Perhaps your application needs to make a large number of requests, or needs to fan request for different methods out to different servers. Here’s an example of an implementation that does the latter:

```ts
import { RpcResponse, RpcTransport } from '@solana/rpc-spec';
import { RpcTransportResponse, RpcTransport } from '@solana/rpc-spec';
import { createHttpTransport } from '@solana/rpc-transport-http';

// Create multiple transports
Expand All @@ -162,7 +164,9 @@ function selectShard(method: string): RpcTransport {
}
}

async function shardingTransport<TResponse>(...args: Parameters<RpcTransport>): Promise<RpcResponse<TResponse>> {
async function shardingTransport<TResponse>(
...args: Parameters<RpcTransport>
): Promise<RpcTransportResponse<TResponse>> {
const payload = args[0].payload as { method: string };
const selectedTransport = selectShard(payload.method);
return await selectedTransport(...args);
Expand All @@ -174,7 +178,7 @@ async function shardingTransport<TResponse>(...args: Parameters<RpcTransport>):
The transport library can also be used to implement custom retry logic on any request:

```ts
import { RpcResponse, RpcTransport } from '@solana/rpc-spec';
import { RpcTransportResponse, RpcTransport } from '@solana/rpc-spec';
import { createHttpTransport } from '@solana/rpc-transport-http';

// Set the maximum number of attempts to retry a request
Expand All @@ -195,7 +199,9 @@ function calculateRetryDelay(attempt: number): number {
}

// A retrying transport that will retry up to `MAX_ATTEMPTS` times before failing
async function retryingTransport<TResponse>(...args: Parameters<RpcTransport>): Promise<RpcResponse<TResponse>> {
async function retryingTransport<TResponse>(
...args: Parameters<RpcTransport>
): Promise<RpcTransportResponse<TResponse>> {
let requestError;
for (let attempts = 0; attempts < MAX_ATTEMPTS; attempts++) {
try {
Expand All @@ -218,7 +224,7 @@ async function retryingTransport<TResponse>(...args: Parameters<RpcTransport>):
Here’s an example of some failover logic integrated into a transport:

```ts
import { RpcResponse, RpcTransport } from '@solana/rpc-spec';
import { RpcTransportResponse, RpcTransport } from '@solana/rpc-spec';
import { createHttpTransport } from '@solana/rpc-transport-http';

// Create a transport for each RPC server
Expand All @@ -229,7 +235,9 @@ const transports = [
];

// A failover transport that will try each transport in order until one succeeds before failing
async function failoverTransport<TResponse>(...args: Parameters<RpcTransport>): Promise<RpcResponse<TResponse>> {
async function failoverTransport<TResponse>(
...args: Parameters<RpcTransport>
): Promise<RpcTransportResponse<TResponse>> {
let requestError;

for (const transport of transports) {
Expand Down
4 changes: 2 additions & 2 deletions packages/rpc-transport-http/src/http-transport.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SOLANA_ERROR__RPC__TRANSPORT_HTTP_ERROR, SolanaError } from '@solana/errors';
import { RpcResponse, RpcTransport } from '@solana/rpc-spec';
import { RpcTransport, RpcTransportRequest, RpcTransportResponse } from '@solana/rpc-spec';
import type Dispatcher from 'undici-types/dispatcher';

import {
Expand Down Expand Up @@ -45,7 +45,7 @@ export function createHttpTransport(config: Config): RpcTransport {
payload,
signal,
toText,
}: Parameters<RpcTransport>[0]): Promise<RpcResponse<TResponse>> {
}: RpcTransportRequest): Promise<RpcTransportResponse<TResponse>> {
const body = toText ? toText(payload) : JSON.stringify(payload);
const requestInfo = {
...dispatcherConfig,
Expand Down

0 comments on commit f7a09e2

Please sign in to comment.