Skip to content

Commit 6626220

Browse files
committed
feat: schemas for revolver transports
1 parent 6e05ec8 commit 6626220

File tree

4 files changed

+119
-49
lines changed

4 files changed

+119
-49
lines changed

src/dev/RevolverTransport.test.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,10 @@ function createClient(
5454
url: s3.url,
5555
},
5656
],
57-
cooldown: 100,
58-
retryCount: 2,
57+
defaultCooldown: 100,
58+
defaultHTTPOptions: {
59+
retryCount: 2,
60+
},
5961
...opts,
6062
}),
6163
});
@@ -208,7 +210,7 @@ it("should not overrotate when hadling parallel requests", async () => {
208210
url: s2.url,
209211
},
210212
],
211-
cooldown: 60_000,
213+
defaultCooldown: 60_000,
212214
});
213215
const resp = await Promise.all([
214216
client.request({ method: "eth_blockNumber" }),
@@ -227,7 +229,7 @@ it("request should fail when all transports are broken", async () => {
227229
s1.stop();
228230
s2.stop();
229231
s3.stop();
230-
const client = createClient({ cooldown: 60_000 });
232+
const client = createClient({ defaultCooldown: 60_000 });
231233
await expect(client.getBlockNumber()).rejects.toThrow(
232234
NoAvailableTransportsError,
233235
);
@@ -239,7 +241,7 @@ it("subscription should fail when all transports are broken", async () => {
239241
s3.stop();
240242
const onError = vi.fn();
241243
const onBlockNumber = vi.fn();
242-
const client = createClient({ cooldown: 60_000 });
244+
const client = createClient({ defaultCooldown: 60_000 });
243245
unwatch = client.watchBlockNumber({
244246
onBlockNumber,
245247
onError,

src/dev/RevolverTransport.ts

Lines changed: 62 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import {
22
BaseError,
3-
type ClientConfig,
43
type EIP1193RequestFn,
54
HttpRequestError,
65
type HttpTransport,
@@ -16,14 +15,30 @@ import {
1615
withRetry,
1716
} from "viem";
1817
import type { HttpRpcClientOptions } from "viem/utils";
19-
import type { ILogger, NetworkType } from "../sdk/index.js";
18+
import { z } from "zod/v4";
19+
import { type ILogger, NetworkType } from "../sdk/index.js";
20+
import { httpTransportConfigSchema } from "./transports.js";
2021

21-
export interface ProviderConfig {
22-
name: string;
23-
url: string;
24-
cooldown?: number;
25-
httpClientOptions?: HttpRpcClientOptions | undefined;
26-
}
22+
export const providerConfigSchema = z.object({
23+
/**
24+
* Provider name for display purposes
25+
*/
26+
name: z.string(),
27+
/**
28+
* Provider URL
29+
*/
30+
url: z.url(),
31+
/**
32+
* How long, in milliseconds, to wait before try this provider again
33+
*/
34+
cooldown: z.number().optional(),
35+
/**
36+
* HTTP transport options to use for this provider
37+
*/
38+
httpClientOptions: httpTransportConfigSchema.optional(),
39+
});
40+
41+
export type ProviderConfig = z.infer<typeof providerConfigSchema>;
2742

2843
interface TransportEntry {
2944
name: string;
@@ -49,23 +64,42 @@ type OnResponseFn = (
4964
...args: Parameters<Required<HttpRpcClientOptions>["onResponse"]>
5065
) => ReturnType<Required<HttpRpcClientOptions>["onResponse"]>;
5166

67+
export const SelectionStrategy = z.enum(["simple", "ordered"]);
68+
5269
/**
5370
* How to select the next transport
5471
* - simple: selects next transport after current that is not in cooldown by checking all transports in cyclic order
5572
* - ordered: will select first available transport that is not in cooldown
5673
*/
57-
export type SelectionStrategy = "simple" | "ordered";
74+
export type SelectionStrategy = z.infer<typeof SelectionStrategy>;
75+
76+
export const revolverTransportConfigSchema = z.object({
77+
network: NetworkType,
78+
/**
79+
* Providers to use
80+
*/
81+
providers: z.array(providerConfigSchema),
82+
/**
83+
* How to select the next transport
84+
* Defaults to "simple"
85+
*/
86+
selectionStrategy: SelectionStrategy.optional(),
87+
/** The key of the transport. */
88+
key: z.string().optional(),
89+
/** The name of the transport. */
90+
name: z.string().optional(),
91+
/**
92+
* Default HTTP options to use for all providers, can be overridden by provider config
93+
*/
94+
defaultHTTPOptions: httpTransportConfigSchema.optional(),
95+
/**
96+
* Default cooldown, in milliseconds, to wait before try this transport again
97+
*/
98+
defaultCooldown: z.number().optional(),
99+
});
58100

59-
export interface RevolverTransportConfig {
60-
network: NetworkType;
61-
providers: ProviderConfig[];
101+
export type RevolverTransportConfig = {
62102
logger?: ILogger;
63-
key?: TransportConfig["key"] | undefined;
64-
name?: TransportConfig["name"] | undefined;
65-
pollingInterval?: ClientConfig["pollingInterval"] | undefined;
66-
retryCount?: TransportConfig["retryCount"] | undefined;
67-
retryDelay?: TransportConfig["retryDelay"] | undefined;
68-
timeout?: TransportConfig["timeout"] | undefined;
69103
/**
70104
* When single http transport should retry?
71105
* Defaults to some less strict criteria, so it'll retry "blockNumber from the future" errors
@@ -78,10 +112,6 @@ export interface RevolverTransportConfig {
78112
shouldRetry?:
79113
| ((iter: { count: number; error: Error }) => Promise<boolean> | boolean)
80114
| undefined;
81-
/**
82-
* Allow batching of json-rpc requests
83-
*/
84-
batch?: boolean | undefined;
85115
/**
86116
* Spying function that also returns provider name in additional to the request
87117
*/
@@ -105,16 +135,7 @@ export interface RevolverTransportConfig {
105135
oldTransportName: string,
106136
reason?: BaseError,
107137
) => void | Promise<void>;
108-
/**
109-
* How long, in milliseconds, to wait before try this transport again
110-
*/
111-
cooldown?: number | undefined;
112-
/**
113-
* How to select the next transport
114-
* Defaults to "simple"
115-
*/
116-
selectionStrategy?: SelectionStrategy;
117-
}
138+
} & z.infer<typeof revolverTransportConfigSchema>;
118139

119140
export class NoAvailableTransportsError extends BaseError {
120141
constructor(cause?: Error) {
@@ -184,11 +205,8 @@ export class RevolverTransport
184205
({ url, name, cooldown, httpClientOptions }): TransportEntry => ({
185206
name,
186207
transport: http(url, {
208+
...config.defaultHTTPOptions,
187209
...httpClientOptions,
188-
retryCount: config.retryCount,
189-
retryDelay: config.retryDelay,
190-
timeout: config.timeout,
191-
batch: !!config.batch,
192210
key: name,
193211
name: name,
194212
onFetchRequest: this.#config.onRequest
@@ -209,8 +227,8 @@ export class RevolverTransport
209227
const selectionStrategy = config.selectionStrategy ?? "simple";
210228
this.#selector =
211229
selectionStrategy === "simple"
212-
? new SimpleTransportSelector(transports, config.cooldown)
213-
: new OrderedTransportSelector(transports, config.cooldown);
230+
? new SimpleTransportSelector(transports, config.defaultCooldown)
231+
: new OrderedTransportSelector(transports, config.defaultCooldown);
214232
}
215233

216234
public get value(): RevolverTransportValue {
@@ -241,8 +259,8 @@ export class RevolverTransport
241259

242260
// for explanation, see shouldRetry comment
243261
const resp = await withRetry(() => transport.request(r), {
244-
delay: this.#config.retryDelay,
245-
retryCount: this.#config.retryCount,
262+
delay: this.#config.defaultHTTPOptions?.retryDelay,
263+
retryCount: this.#config.defaultHTTPOptions?.retryCount,
246264
shouldRetry: this.#config.shouldRetry,
247265
});
248266
this.#requests.delete(r);
@@ -275,10 +293,10 @@ export class RevolverTransport
275293
name: "revolver",
276294
type: "revolver",
277295
request: this.request,
278-
retryCount: this.#config.retryCount,
279-
methods: undefined,
280-
retryDelay: this.#config.retryDelay,
281-
timeout: this.#config.timeout,
296+
retryCount: this.#config.defaultHTTPOptions?.retryCount,
297+
methods: this.#config.defaultHTTPOptions?.methods,
298+
retryDelay: this.#config.defaultHTTPOptions?.retryDelay,
299+
timeout: this.#config.defaultHTTPOptions?.timeout,
282300
};
283301
}
284302

src/dev/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ export * from "./mint/index.js";
1212
export * from "./providers.js";
1313
export * from "./RevolverTransport.js";
1414
export * from "./replaceStorage.js";
15+
export * from "./transports.js";
1516
export * from "./types.js";

src/dev/transports.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { z } from "zod/v4";
2+
3+
export const httpTransportConfigSchema = z.object({
4+
/**
5+
* Whether to enable Batch JSON-RPC.
6+
* @link https://www.jsonrpc.org/specification#batch
7+
*/
8+
batch: z
9+
.union([
10+
z.boolean(),
11+
z.object({
12+
/** The maximum number of JSON-RPC requests to send in a batch. @default 1_000 */
13+
batchSize: z.number().optional(),
14+
/** The maximum number of milliseconds to wait before sending a batch. @default 0 */
15+
wait: z.number().optional(),
16+
}),
17+
])
18+
.optional(),
19+
/**
20+
* Request configuration to pass to `fetch`.
21+
* @link https://developer.mozilla.org/en-US/docs/Web/API/fetch
22+
*/
23+
fetchOptions: z.record(z.string(), z.any()).optional(),
24+
/**
25+
* Methods to include or exclude from executing RPC requests.
26+
**/
27+
methods: z
28+
.union([
29+
z.object({
30+
include: z.array(z.string()).optional(),
31+
}),
32+
z.object({
33+
exclude: z.array(z.string()).optional(),
34+
}),
35+
])
36+
.optional(),
37+
/**
38+
* The max number of times to retry.
39+
**/
40+
retryCount: z.number().optional(),
41+
/**
42+
* The base delay (in ms) between retries.
43+
**/
44+
retryDelay: z.number().optional(),
45+
/**
46+
* The timeout (in ms) for the HTTP request. Default: 10_000
47+
**/
48+
timeout: z.number().optional(),
49+
});

0 commit comments

Comments
 (0)