Skip to content

Commit 5cc1df9

Browse files
authored
feat: add $rejectPendingCalls (#26)
1 parent 40ee074 commit 5cc1df9

File tree

2 files changed

+113
-7
lines changed

2 files changed

+113
-7
lines changed

src/index.ts

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,14 @@ export interface BirpcGroupFn<T> {
109109

110110
export type BirpcReturn<RemoteFunctions, LocalFunctions = Record<string, never>> = {
111111
[K in keyof RemoteFunctions]: BirpcFn<RemoteFunctions[K]>
112-
} & { $functions: LocalFunctions, $close: (error?: Error) => void, $closed: boolean }
112+
} & {
113+
$functions: LocalFunctions
114+
$close: (error?: Error) => void
115+
$closed: boolean
116+
$rejectPendingCalls: (handler?: PendingCallHandler) => Promise<void>[]
117+
}
118+
119+
type PendingCallHandler = (options: Pick<PromiseEntry, 'method' | 'reject'>) => void | Promise<void>
113120

114121
export type BirpcGroupReturn<RemoteFunctions> = {
115122
[K in keyof RemoteFunctions]: BirpcGroupFn<RemoteFunctions[K]>
@@ -122,6 +129,13 @@ export interface BirpcGroup<RemoteFunctions, LocalFunctions = Record<string, nev
122129
updateChannels: (fn?: ((channels: ChannelOptions[]) => void)) => BirpcReturn<RemoteFunctions, LocalFunctions>[]
123130
}
124131

132+
interface PromiseEntry {
133+
resolve: (arg: any) => void
134+
reject: (error: any) => void
135+
method: string
136+
timeoutId?: ReturnType<typeof setTimeout>
137+
}
138+
125139
const TYPE_REQUEST = 'q' as const
126140
const TYPE_RESPONSE = 's' as const
127141

@@ -192,12 +206,7 @@ export function createBirpc<RemoteFunctions = Record<string, never>, LocalFuncti
192206
timeout = DEFAULT_TIMEOUT,
193207
} = options
194208

195-
const rpcPromiseMap = new Map<string, {
196-
resolve: (arg: any) => void
197-
reject: (error: any) => void
198-
method: string
199-
timeoutId?: ReturnType<typeof setTimeout>
200-
}>()
209+
const rpcPromiseMap = new Map<string, PromiseEntry>()
201210

202211
let _promise: Promise<any> | any
203212
let closed = false
@@ -210,6 +219,10 @@ export function createBirpc<RemoteFunctions = Record<string, never>, LocalFuncti
210219
if (method === '$close')
211220
return close
212221

222+
if (method === '$rejectPendingCalls') {
223+
return rejectPendingCalls
224+
}
225+
213226
if (method === '$closed')
214227
return closed
215228

@@ -285,6 +298,22 @@ export function createBirpc<RemoteFunctions = Record<string, never>, LocalFuncti
285298
off(onMessage)
286299
}
287300

301+
function rejectPendingCalls(handler?: PendingCallHandler) {
302+
const entries = Array.from(rpcPromiseMap.values())
303+
304+
const handlerResults = entries.map(({ method, reject }) => {
305+
if (!handler) {
306+
return reject(new Error(`[birpc]: rejected pending call "${method}".`))
307+
}
308+
309+
return handler({ method, reject })
310+
})
311+
312+
rpcPromiseMap.clear()
313+
314+
return handlerResults
315+
}
316+
288317
async function onMessage(data: any, ...extra: any[]) {
289318
let msg: RPCMessage
290319

test/reject-pending-calls.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { expect, it } from 'vitest'
2+
import { createBirpc } from '../src'
3+
4+
it('rejects pending calls', async () => {
5+
const rpc = createBirpc<{ first: () => Promise<void>, second: () => Promise<void> }>({}, {
6+
on() {},
7+
post() {},
8+
})
9+
10+
const promises = [
11+
rpc.first()
12+
.then(() => expect.fail('first() should not resolve'))
13+
.catch(error => error),
14+
15+
rpc.second()
16+
.then(() => expect.fail('second() should not resolve'))
17+
.catch(error => error),
18+
]
19+
20+
const rejections = rpc.$rejectPendingCalls()
21+
expect(rejections).toHaveLength(2)
22+
23+
const errors = await Promise.all(promises)
24+
expect(errors).toHaveLength(2)
25+
26+
expect.soft(errors[0].message).toBe('[birpc]: rejected pending call "first".')
27+
expect.soft(errors[1].message).toBe('[birpc]: rejected pending call "second".')
28+
})
29+
30+
it('rejects pending calls with custom handler', async () => {
31+
const rpc = createBirpc<{ first: () => Promise<void>, second: () => Promise<void> }>({}, {
32+
on() {},
33+
post() {},
34+
})
35+
36+
const promises = [
37+
rpc.first()
38+
.then(() => expect.fail('first() should not resolve'))
39+
.catch(error => error),
40+
41+
rpc.second()
42+
.then(() => expect.fail('second() should not resolve'))
43+
.catch(error => error),
44+
]
45+
46+
const rejections = rpc.$rejectPendingCalls(({ method, reject }) =>
47+
reject(new Error(`Rejected call. Method: "${method}".`)),
48+
)
49+
expect(rejections).toHaveLength(2)
50+
51+
const errors = await Promise.all(promises)
52+
expect(errors).toHaveLength(2)
53+
54+
expect.soft(errors[0].message).toBe('Rejected call. Method: "first".')
55+
expect.soft(errors[1].message).toBe('Rejected call. Method: "second".')
56+
})
57+
58+
it('rejected calls are cleared from rpc', async () => {
59+
const rpc = createBirpc<{ stuck: () => Promise<void> }>({}, {
60+
on() {},
61+
post() {},
62+
})
63+
64+
rpc.stuck()
65+
.then(() => expect.fail('stuck() should not resolve'))
66+
.catch(() => undefined)
67+
68+
{
69+
const rejections = rpc.$rejectPendingCalls(({ reject }) => reject(new Error('Rejected')))
70+
expect(rejections).toHaveLength(1)
71+
}
72+
73+
{
74+
const rejections = rpc.$rejectPendingCalls(({ reject }) => reject(new Error('Rejected')))
75+
expect(rejections).toHaveLength(0)
76+
}
77+
})

0 commit comments

Comments
 (0)