Skip to content

Commit eb84560

Browse files
committed
feat: add signal to EIP1193RequestOptions
1 parent 25c50c0 commit eb84560

File tree

7 files changed

+186
-18
lines changed

7 files changed

+186
-18
lines changed

src/actions/public/call.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
import type { ErrorType } from '../../errors/utils.js'
2828
import type { BlockTag } from '../../types/block.js'
2929
import type { Chain } from '../../types/chain.js'
30+
import type { EIP1193RequestOptions } from '../../types/eip1193.js'
3031
import type { Hex } from '../../types/misc.js'
3132
import type { RpcTransactionRequest } from '../../types/rpc.js'
3233
import type { StateOverride } from '../../types/stateOverride.js'
@@ -92,6 +93,8 @@ export type CallParameters<
9293
factory?: Address | undefined
9394
/** Calldata to execute on the factory to deploy the contract. */
9495
factoryData?: Hex | undefined
96+
/** Request options. */
97+
requestOptions?: EIP1193RequestOptions | undefined
9598
/** State overrides for the call. */
9699
stateOverride?: StateOverride | undefined
97100
} & (
@@ -174,6 +177,7 @@ export async function call<chain extends Chain | undefined>(
174177
maxFeePerGas,
175178
maxPriorityFeePerGas,
176179
nonce,
180+
requestOptions,
177181
to,
178182
value,
179183
stateOverride,
@@ -276,10 +280,13 @@ export async function call<chain extends Chain | undefined>(
276280
return base
277281
})()
278282

279-
const response = await client.request({
280-
method: 'eth_call',
281-
params,
282-
})
283+
const response = await client.request(
284+
{
285+
method: 'eth_call',
286+
params,
287+
},
288+
requestOptions,
289+
)
283290
if (response === '0x') return { data: undefined }
284291
return { data: response }
285292
} catch (err) {
@@ -428,10 +435,7 @@ type ToDeploylessCallViaBytecodeDataErrorType =
428435
| EncodeDeployDataErrorType
429436
| ErrorType
430437

431-
function toDeploylessCallViaBytecodeData(parameters: {
432-
code: Hex
433-
data: Hex
434-
}) {
438+
function toDeploylessCallViaBytecodeData(parameters: { code: Hex; data: Hex }) {
435439
const { code, data } = parameters
436440
return encodeDeployData({
437441
abi: parseAbi(['constructor(bytes, bytes)']),

src/actions/public/getBlock.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
import type { ErrorType } from '../../errors/utils.js'
99
import type { BlockTag } from '../../types/block.js'
1010
import type { Chain } from '../../types/chain.js'
11+
import type { EIP1193RequestOptions } from '../../types/eip1193.js'
1112
import type { Hash } from '../../types/misc.js'
1213
import type { RpcBlock } from '../../types/rpc.js'
1314
import type { Prettify } from '../../types/utils.js'
@@ -27,6 +28,8 @@ export type GetBlockParameters<
2728
> = {
2829
/** Whether or not to include transaction data in the response. */
2930
includeTransactions?: includeTransactions | undefined
31+
/** Request options. */
32+
requestOptions?: EIP1193RequestOptions | undefined
3033
} & (
3134
| {
3235
/** Hash of the block. */
@@ -99,6 +102,7 @@ export async function getBlock<
99102
blockNumber,
100103
blockTag = client.experimental_blockTag ?? 'latest',
101104
includeTransactions: includeTransactions_,
105+
requestOptions,
102106
}: GetBlockParameters<includeTransactions, blockTag> = {},
103107
): Promise<GetBlockReturnType<chain, includeTransactions, blockTag>> {
104108
const includeTransactions = includeTransactions_ ?? false
@@ -113,15 +117,15 @@ export async function getBlock<
113117
method: 'eth_getBlockByHash',
114118
params: [blockHash, includeTransactions],
115119
},
116-
{ dedupe: true },
120+
{ dedupe: true, ...requestOptions },
117121
)
118122
} else {
119123
block = await client.request(
120124
{
121125
method: 'eth_getBlockByNumber',
122126
params: [blockNumberHex || blockTag, includeTransactions],
123127
},
124-
{ dedupe: Boolean(blockNumberHex) },
128+
{ dedupe: Boolean(blockNumberHex), ...requestOptions },
125129
)
126130
}
127131

src/clients/transports/http.test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,3 +612,81 @@ test('no url', () => {
612612
`,
613613
)
614614
})
615+
616+
describe('request cancellation', () => {
617+
test('cancels request with AbortSignal', async () => {
618+
const server = await createHttpServer((_, res) => {
619+
// Delay response to allow time for cancellation
620+
setTimeout(() => {
621+
res.writeHead(200, { 'Content-Type': 'application/json' })
622+
res.end(JSON.stringify({ jsonrpc: '2.0', result: '0x1', id: 0 }))
623+
}, 100)
624+
})
625+
626+
const controller = new AbortController()
627+
const transport = http(server.url)({})
628+
629+
// Cancel after 50ms (before server responds at 100ms)
630+
setTimeout(() => controller.abort(), 50)
631+
632+
await expect(
633+
transport.request(
634+
{ method: 'eth_blockNumber' },
635+
{ signal: controller.signal },
636+
),
637+
).rejects.toThrow()
638+
639+
await server.close()
640+
})
641+
642+
test('successful request with signal', async () => {
643+
const server = await createHttpServer((_, res) => {
644+
res.writeHead(200, { 'Content-Type': 'application/json' })
645+
res.end(JSON.stringify({ jsonrpc: '2.0', result: '0x1', id: 0 }))
646+
})
647+
648+
const controller = new AbortController()
649+
const transport = http(server.url)({})
650+
651+
const result = await transport.request(
652+
{ method: 'eth_blockNumber' },
653+
{ signal: controller.signal },
654+
)
655+
656+
expect(result).toBe('0x1')
657+
await server.close()
658+
})
659+
660+
test('multiple requests with same controller', async () => {
661+
const server = await createHttpServer((_, res) => {
662+
setTimeout(() => {
663+
res.writeHead(200, { 'Content-Type': 'application/json' })
664+
res.end(JSON.stringify({ jsonrpc: '2.0', result: '0x1', id: 0 }))
665+
}, 100)
666+
})
667+
668+
const controller = new AbortController()
669+
const transport = http(server.url)({})
670+
671+
// Start multiple requests
672+
const promise1 = transport.request(
673+
{ method: 'eth_blockNumber' },
674+
{ signal: controller.signal },
675+
)
676+
const promise2 = transport.request(
677+
{
678+
method: 'eth_getBalance',
679+
params: ['0x0000000000000000000000000000000000000000'],
680+
},
681+
{ signal: controller.signal },
682+
)
683+
684+
// Cancel after 50ms
685+
setTimeout(() => controller.abort(), 50)
686+
687+
await expect(promise1).rejects.toThrow()
688+
await expect(promise2).rejects.toThrow()
689+
690+
await server.close()
691+
})
692+
})

src/clients/transports/http.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ export function http<
122122
key,
123123
methods,
124124
name,
125-
async request({ method, params }) {
125+
async request({ method, params }, options) {
126126
const body = { method, params }
127127

128128
const { schedule } = createBatchScheduler({
@@ -134,6 +134,9 @@ export function http<
134134
fn: (body: RpcRequest[]) =>
135135
rpcClient.request({
136136
body,
137+
fetchOptions: options?.signal
138+
? { signal: options.signal }
139+
: undefined,
137140
}),
138141
sort: (a, b) => a.id - b.id,
139142
})
@@ -144,6 +147,9 @@ export function http<
144147
: [
145148
await rpcClient.request({
146149
body,
150+
fetchOptions: options?.signal
151+
? { signal: options.signal }
152+
: undefined,
147153
}),
148154
]
149155

src/types/eip1193.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2031,6 +2031,8 @@ export type EIP1193RequestOptions = {
20312031
retryDelay?: number | undefined
20322032
/** The max number of times to retry. */
20332033
retryCount?: number | undefined
2034+
/** AbortSignal to cancel the request. */
2035+
signal?: AbortSignal | undefined
20342036
/** Unique identifier for the request. */
20352037
uid?: string | undefined
20362038
}

src/utils/buildRequest.test.ts

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,13 @@ import { getHttpRpcClient } from './rpc/http.js'
4141

4242
function request(url: string) {
4343
const httpClient = getHttpRpcClient(url)
44-
return async ({ method, params }: any) => {
44+
return async ({ method, params }: any, options?: any) => {
4545
const { error, result } = await httpClient.request({
4646
body: {
4747
method,
4848
params,
4949
},
50+
fetchOptions: options?.signal ? { signal: options.signal } : undefined,
5051
})
5152
if (error)
5253
throw new RpcRequestError({
@@ -254,6 +255,79 @@ describe('args', () => {
254255
})
255256
})
256257

258+
describe('options', () => {
259+
test('passes signal to underlying request', async () => {
260+
let receivedSignal: AbortSignal | undefined
261+
const mockRequest = async (_args: any, options?: any) => {
262+
receivedSignal = options?.signal
263+
return 'success'
264+
}
265+
266+
const controller = new AbortController()
267+
const request_ = buildRequest(mockRequest)
268+
269+
await request_({ method: 'eth_blockNumber' }, { signal: controller.signal })
270+
271+
expect(receivedSignal).toBe(controller.signal)
272+
})
273+
274+
test('passes other options alongside signal', async () => {
275+
let receivedOptions: any
276+
const mockRequest = async (_args: any, options?: any) => {
277+
receivedOptions = options
278+
return 'success'
279+
}
280+
281+
const controller = new AbortController()
282+
const request_ = buildRequest(mockRequest)
283+
284+
await request_(
285+
{ method: 'eth_blockNumber' },
286+
{
287+
signal: controller.signal,
288+
retryCount: 1,
289+
dedupe: true,
290+
},
291+
)
292+
293+
expect(receivedOptions).toEqual({ signal: controller.signal })
294+
})
295+
296+
test('works without signal', async () => {
297+
let receivedOptions: any
298+
const mockRequest = async (_args: any, options?: any) => {
299+
receivedOptions = options
300+
return 'success'
301+
}
302+
303+
const request_ = buildRequest(mockRequest)
304+
305+
await request_({ method: 'eth_blockNumber' }, { dedupe: true })
306+
307+
expect(receivedOptions).toBeUndefined()
308+
})
309+
310+
test('prioritizes override options over initial options', async () => {
311+
let receivedSignal: AbortSignal | undefined
312+
const mockRequest = async (_args: any, options?: any) => {
313+
receivedSignal = options?.signal
314+
return 'success'
315+
}
316+
317+
const controller1 = new AbortController()
318+
const controller2 = new AbortController()
319+
320+
const request_ = buildRequest(mockRequest, { signal: controller1.signal })
321+
322+
await request_(
323+
{ method: 'eth_blockNumber' },
324+
{ signal: controller2.signal },
325+
)
326+
327+
expect(receivedSignal).toBe(controller2.signal)
328+
})
329+
})
330+
257331
describe('behavior', () => {
258332
describe('error types', () => {
259333
test('BaseError', async () => {

src/utils/buildRequest.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,16 +113,16 @@ export type RequestErrorType =
113113
| WithRetryErrorType
114114
| ErrorType
115115

116-
export function buildRequest<request extends (args: any) => Promise<any>>(
117-
request: request,
118-
options: EIP1193RequestOptions = {},
119-
): EIP1193RequestFn {
120-
return async (args, overrideOptions = {}) => {
116+
export function buildRequest<
117+
request extends (args: any, options?: EIP1193RequestOptions) => Promise<any>,
118+
>(request: request, options: EIP1193RequestOptions = {}): EIP1193RequestFn {
119+
return async (args, overrideOptions: EIP1193RequestOptions = {}) => {
121120
const {
122121
dedupe = false,
123122
methods,
124123
retryDelay = 150,
125124
retryCount = 3,
125+
signal,
126126
uid,
127127
} = {
128128
...options,
@@ -147,7 +147,7 @@ export function buildRequest<request extends (args: any) => Promise<any>>(
147147
withRetry(
148148
async () => {
149149
try {
150-
return await request(args)
150+
return await request(args, signal ? { signal } : undefined)
151151
} catch (err_) {
152152
const err = err_ as unknown as RpcError<
153153
RpcErrorCode | ProviderRpcErrorCode

0 commit comments

Comments
 (0)