@@ -117,15 +117,48 @@ export interface BirpcGroupFn<T> {
117117 asEvent : ( ...args : ArgumentsType < T > ) => Promise < void >
118118}
119119
120- export type BirpcReturn < RemoteFunctions , LocalFunctions = Record < string , never > > = {
121- [ K in keyof RemoteFunctions ] : BirpcFn < RemoteFunctions [ K ] >
122- } & {
120+ export interface BirpcReturnBuiltin <
121+ RemoteFunctions ,
122+ LocalFunctions = Record < string , never > ,
123+ > {
124+ /**
125+ * Raw functions object
126+ */
123127 $functions : LocalFunctions
128+ /**
129+ * Whether the RPC is closed
130+ */
131+ readonly $closed : boolean
132+ /**
133+ * Close the RPC connection
134+ */
124135 $close : ( error ?: Error ) => void
125- $closed : boolean
136+ /**
137+ * Reject pending calls
138+ */
126139 $rejectPendingCalls : ( handler ?: PendingCallHandler ) => Promise < void > [ ]
140+ /**
141+ * Call the remote function and wait for the result.
142+ * An alternative to directly calling the function
143+ */
144+ $call : < K extends keyof RemoteFunctions > ( method : K , ...args : ArgumentsType < RemoteFunctions [ K ] > ) => Promise < Awaited < ReturnType < RemoteFunctions [ K ] > > >
145+ /**
146+ * Same as `$call`, but returns `undefined` if the function is not defined on the remote side.
147+ */
148+ $callOptional : < K extends keyof RemoteFunctions > ( method : K , ...args : ArgumentsType < RemoteFunctions [ K ] > ) => Promise < Awaited < ReturnType < RemoteFunctions [ K ] > | undefined > >
149+ /**
150+ * Send event without asking for response
151+ */
152+ $callEvent : < K extends keyof RemoteFunctions > ( method : K , ...args : ArgumentsType < RemoteFunctions [ K ] > ) => Promise < void >
127153}
128154
155+ export type BirpcReturn <
156+ RemoteFunctions ,
157+ LocalFunctions = Record < string , never > ,
158+ > = {
159+ [ K in keyof RemoteFunctions ] : BirpcFn < RemoteFunctions [ K ] >
160+ } & BirpcReturnBuiltin < RemoteFunctions , LocalFunctions >
161+
129162type PendingCallHandler = ( options : Pick < PromiseEntry , 'method' | 'reject' > ) => void | Promise < void >
130163
131164export type BirpcGroupReturn < RemoteFunctions > = {
@@ -166,6 +199,10 @@ interface Request {
166199 * Arguments
167200 */
168201 a : any [ ]
202+ /**
203+ * Optional
204+ */
205+ o ?: boolean
169206}
170207
171208interface Response {
@@ -201,7 +238,7 @@ const { clearTimeout, setTimeout } = globalThis
201238const random = Math . random . bind ( Math )
202239
203240export function createBirpc < RemoteFunctions = Record < string , never > , LocalFunctions extends object = Record < string , never > > (
204- functions : LocalFunctions ,
241+ $ functions : LocalFunctions ,
205242 options : BirpcOptions < RemoteFunctions > ,
206243) : BirpcReturn < RemoteFunctions , LocalFunctions > {
207244 const {
@@ -216,107 +253,134 @@ export function createBirpc<RemoteFunctions = Record<string, never>, LocalFuncti
216253 timeout = DEFAULT_TIMEOUT ,
217254 } = options
218255
219- const rpcPromiseMap = new Map < string , PromiseEntry > ( )
256+ let $closed = false
220257
258+ const _rpcPromiseMap = new Map < string , PromiseEntry > ( )
221259 let _promiseInit : Promise < any > | any
222- let closed = false
223260
224- const rpc = new Proxy ( { } , {
225- get ( _ , method : string ) {
226- if ( method === '$functions' )
227- return functions
228-
229- if ( method === '$close' )
230- return close
261+ async function _call (
262+ method : string ,
263+ args : unknown [ ] ,
264+ event ?: boolean ,
265+ optional ?: boolean ,
266+ ) {
267+ if ( $closed )
268+ throw new Error ( `[birpc] rpc is closed, cannot call "${ method } "` )
269+
270+ const req : Request = { m : method , a : args , t : TYPE_REQUEST }
271+ if ( optional )
272+ req . o = true
273+
274+ const send = async ( _req : Request ) => post ( serialize ( _req ) )
275+ if ( event ) {
276+ await send ( req )
277+ return
278+ }
231279
232- if ( method === '$rejectPendingCalls' ) {
233- return rejectPendingCalls
280+ if ( _promiseInit ) {
281+ // Wait if `on` is promise
282+ try {
283+ await _promiseInit
234284 }
285+ finally {
286+ // don't keep resolved promise hanging
287+ _promiseInit = undefined
288+ }
289+ }
235290
236- if ( method === '$closed' )
237- return closed
291+ // eslint-disable-next-line prefer-const
292+ let { promise , resolve , reject } = createPromiseWithResolvers < any > ( )
238293
239- // catch if "createBirpc" is returned from async function
240- if ( method === 'then' && ! eventNames . includes ( 'then' as any ) && ! ( 'then' in functions ) )
241- return undefined
294+ const id = nanoid ( )
295+ req . i = id
296+ let timeoutId : ReturnType < typeof setTimeout > | undefined
242297
243- const sendEvent = async ( ...args : any [ ] ) => {
244- await post ( serialize ( < Request > { m : method , a : args , t : TYPE_REQUEST } ) )
245- }
246- if ( eventNames . includes ( method as any ) ) {
247- sendEvent . asEvent = sendEvent
248- return sendEvent
249- }
250- const sendCall = async ( ...args : any [ ] ) => {
251- if ( closed )
252- throw new Error ( `[birpc] rpc is closed, cannot call "${ method } "` )
253- if ( _promiseInit ) {
254- // Wait if `on` is promise
298+ async function handler ( newReq : Request = req ) {
299+ if ( timeout >= 0 ) {
300+ timeoutId = setTimeout ( ( ) => {
255301 try {
256- await _promiseInit
302+ // Custom onTimeoutError handler can throw its own error too
303+ const handleResult = options . onTimeoutError ?.( method , args )
304+ if ( handleResult !== true )
305+ throw new Error ( `[birpc] timeout on calling "${ method } "` )
257306 }
258- finally {
259- // don't keep resolved promise hanging
260- _promiseInit = undefined
307+ catch ( e ) {
308+ reject ( e )
261309 }
262- }
310+ _rpcPromiseMap . delete ( id )
311+ } , timeout )
263312
264- // eslint-disable-next-line prefer-const
265- let { promise, resolve, reject } = createPromiseWithResolvers < any > ( )
266-
267- const id = nanoid ( )
268- let timeoutId : ReturnType < typeof setTimeout > | undefined
269- const _req : Request = { m : method , a : args , i : id , t : TYPE_REQUEST }
270-
271- async function handler ( req : Request = _req ) {
272- if ( timeout >= 0 ) {
273- timeoutId = setTimeout ( ( ) => {
274- try {
275- // Custom onTimeoutError handler can throw its own error too
276- const handleResult = options . onTimeoutError ?.( method , args )
277- if ( handleResult !== true )
278- throw new Error ( `[birpc] timeout on calling "${ method } "` )
279- }
280- catch ( e ) {
281- reject ( e )
282- }
283- rpcPromiseMap . delete ( id )
284- } , timeout )
285-
286- // For node.js, `unref` is not available in browser-like environments
287- if ( typeof timeoutId === 'object' )
288- timeoutId = timeoutId . unref ?.( )
289- }
313+ // For node.js, `unref` is not available in browser-like environments
314+ if ( typeof timeoutId === 'object' )
315+ timeoutId = timeoutId . unref ?.( )
316+ }
290317
291- rpcPromiseMap . set ( id , { resolve, reject, timeoutId, method } )
292- await post ( serialize ( req ) )
293- return promise
294- }
318+ _rpcPromiseMap . set ( id , { resolve, reject, timeoutId, method } )
319+ await send ( newReq )
320+ return promise
321+ }
295322
296- try {
297- if ( options . onRequest )
298- await options . onRequest ( _req , handler , resolve )
299- else
300- await handler ( )
301- }
302- catch ( e ) {
303- clearTimeout ( timeoutId )
304- rpcPromiseMap . delete ( id )
305- if ( options . onGeneralError ?.( e as Error ) !== true )
306- throw e
307- return
308- }
323+ try {
324+ if ( options . onRequest )
325+ await options . onRequest ( req , handler , resolve )
326+ else
327+ await handler ( )
328+ }
329+ catch ( e ) {
330+ if ( options . onGeneralError ?.( e as Error ) !== true )
331+ throw e
332+ return
333+ }
334+ finally {
335+ clearTimeout ( timeoutId )
336+ _rpcPromiseMap . delete ( id )
337+ }
309338
310- return promise
339+ return promise
340+ }
341+
342+ const $call = < K extends keyof RemoteFunctions > ( method : K , ...args : ArgumentsType < RemoteFunctions [ K ] > ) =>
343+ _call ( method as string , args , false )
344+ const $callOptional = < K extends keyof RemoteFunctions > ( method : K , ...args : ArgumentsType < RemoteFunctions [ K ] > ) =>
345+ _call ( method as string , args , false , true )
346+ const $callEvent = < K extends keyof RemoteFunctions > ( method : K , ...args : ArgumentsType < RemoteFunctions [ K ] > ) =>
347+ _call ( method as string , args , true )
348+
349+ const specialMethods = {
350+ $call,
351+ $callOptional,
352+ $callEvent,
353+ $rejectPendingCalls,
354+ get $closed ( ) {
355+ return $closed
356+ } ,
357+ $close,
358+ $functions,
359+ }
360+
361+ const rpc = new Proxy ( { } , {
362+ get ( _ , method : string ) {
363+ if ( Object . prototype . hasOwnProperty . call ( specialMethods , method ) )
364+ return ( specialMethods as any ) [ method ]
365+
366+ // catch if "createBirpc" is returned from async function
367+ if ( method === 'then' && ! eventNames . includes ( 'then' as any ) && ! ( 'then' in $functions ) )
368+ return undefined
369+
370+ const sendEvent = ( ...args : any [ ] ) => _call ( method , args , true )
371+ if ( eventNames . includes ( method as any ) ) {
372+ sendEvent . asEvent = sendEvent
373+ return sendEvent
311374 }
375+ const sendCall = ( ...args : any [ ] ) => _call ( method , args , false )
312376 sendCall . asEvent = sendEvent
313377 return sendCall
314378 } ,
315379 } ) as BirpcReturn < RemoteFunctions , LocalFunctions >
316380
317- function close ( customError ?: Error ) {
318- closed = true
319- rpcPromiseMap . forEach ( ( { reject, method } ) => {
381+ function $ close( customError ?: Error ) {
382+ $ closed = true
383+ _rpcPromiseMap . forEach ( ( { reject, method } ) => {
320384 const error = new Error ( `[birpc] rpc is closed, cannot call "${ method } "` )
321385
322386 if ( customError ) {
@@ -326,12 +390,12 @@ export function createBirpc<RemoteFunctions = Record<string, never>, LocalFuncti
326390
327391 reject ( error )
328392 } )
329- rpcPromiseMap . clear ( )
393+ _rpcPromiseMap . clear ( )
330394 off ( onMessage )
331395 }
332396
333- function rejectPendingCalls ( handler ?: PendingCallHandler ) {
334- const entries = Array . from ( rpcPromiseMap . values ( ) )
397+ function $ rejectPendingCalls( handler ?: PendingCallHandler ) {
398+ const entries = Array . from ( _rpcPromiseMap . values ( ) )
335399
336400 const handlerResults = entries . map ( ( { method, reject } ) => {
337401 if ( ! handler ) {
@@ -341,7 +405,7 @@ export function createBirpc<RemoteFunctions = Record<string, never>, LocalFuncti
341405 return handler ( { method, reject } )
342406 } )
343407
344- rpcPromiseMap . clear ( )
408+ _rpcPromiseMap . clear ( )
345409
346410 return handlerResults
347411 }
@@ -359,18 +423,21 @@ export function createBirpc<RemoteFunctions = Record<string, never>, LocalFuncti
359423 }
360424
361425 if ( msg . t === TYPE_REQUEST ) {
362- const { m : method , a : args } = msg
426+ const { m : method , a : args , o : optional } = msg
363427 let result , error : any
364- const fn = await ( resolver
365- ? resolver ( method , ( functions as any ) [ method ] )
366- : ( functions as any ) [ method ] )
428+ let fn = await ( resolver
429+ ? resolver ( method , ( $functions as any ) [ method ] )
430+ : ( $functions as any ) [ method ] )
431+
432+ if ( optional )
433+ fn ||= ( ) => undefined
367434
368435 if ( ! fn ) {
369436 error = new Error ( `[birpc] function "${ method } " not found` )
370437 }
371438 else {
372439 try {
373- result = await fn . apply ( bind === 'rpc' ? rpc : functions , args )
440+ result = await fn . apply ( bind === 'rpc' ? rpc : $ functions, args )
374441 }
375442 catch ( e ) {
376443 error = e
@@ -410,7 +477,7 @@ export function createBirpc<RemoteFunctions = Record<string, never>, LocalFuncti
410477 }
411478 else {
412479 const { i : ack , r : result , e : error } = msg
413- const promise = rpcPromiseMap . get ( ack )
480+ const promise = _rpcPromiseMap . get ( ack )
414481 if ( promise ) {
415482 clearTimeout ( promise . timeoutId )
416483
@@ -419,7 +486,7 @@ export function createBirpc<RemoteFunctions = Record<string, never>, LocalFuncti
419486 else
420487 promise . resolve ( result )
421488 }
422- rpcPromiseMap . delete ( ack )
489+ _rpcPromiseMap . delete ( ack )
423490 }
424491 }
425492
0 commit comments