11/// <reference lib="dom" />
22
33import type { HTTPPath } from '@orpc/contract'
4+ import type { ANY_LAZY_PROCEDURE , ANY_PROCEDURE , Router } from '@orpc/server'
45import type { FetchHandler } from '@orpc/server/fetch'
56import type { Router as HonoRouter } from 'hono/router'
7+ import type { EachContractLeafResultItem , EachLeafOptions } from '../utils'
68import { ORPC_HEADER , standardizeHTTPPath } from '@orpc/contract'
7- import { createProcedureCaller , isProcedure , ORPCError , type Procedure , type Router , type WELL_DEFINED_PROCEDURE } from '@orpc/server'
9+ import { createProcedureCaller , isLazy , isProcedure , LAZY_LOADER_SYMBOL , LAZY_ROUTER_PREFIX_SYMBOL , ORPCError } from '@orpc/server'
810import { isPlainObject , mapValues , trim , value } from '@orpc/shared'
911import { OpenAPIDeserializer , OpenAPISerializer , zodCoerce } from '@orpc/transformer'
12+ import { eachContractProcedureLeaf } from '../utils'
1013
11- export type ResolveRouter = ( router : Router < any > , method : string , pathname : string ) => {
14+ export type ResolveRouter = ( router : Router < any > , method : string , pathname : string ) => Promise < {
1215 path : string [ ]
13- procedure : Procedure < any , any , any , any , any >
16+ procedure : ANY_PROCEDURE | ANY_LAZY_PROCEDURE
1417 params : Record < string , string >
15- } | undefined
18+ } | undefined >
1619
17- type Routing = HonoRouter < [ string [ ] , Procedure < any , any , any , any , any > ] >
20+ type Routing = HonoRouter < string [ ] >
1821
1922export function createOpenAPIHandler ( createHonoRouter : ( ) => Routing ) : FetchHandler {
2023 const resolveRouter = createResolveRouter ( createHonoRouter )
@@ -37,14 +40,21 @@ export function createOpenAPIHandler(createHonoRouter: () => Routing): FetchHand
3740 : undefined
3841 const method = customMethod || options . request . method
3942
40- const match = resolveRouter ( options . router , method , pathname )
43+ const match = await resolveRouter ( options . router , method , pathname )
4144
4245 if ( ! match ) {
4346 throw new ORPCError ( { code : 'NOT_FOUND' , message : 'Not found' } )
4447 }
45- const procedure = match . procedure
48+ const procedure = isLazy ( match . procedure ) ? ( await match . procedure [ LAZY_LOADER_SYMBOL ] ( ) ) . default : match . procedure
4649 const path = match . path
4750
51+ if ( ! isProcedure ( procedure ) ) {
52+ throw new ORPCError ( {
53+ code : 'NOT_FOUND' ,
54+ message : 'Not found' ,
55+ } )
56+ }
57+
4858 const params = procedure . zz$p . contract . zz$cp . InputSchema
4959 ? zodCoerce (
5060 procedure . zz$p . contract . zz$cp . InputSchema ,
@@ -107,37 +117,65 @@ export function createOpenAPIHandler(createHonoRouter: () => Routing): FetchHand
107117}
108118
109119const routingCache = new Map < Router < any > , Routing > ( )
120+ const pendingCache = new Map < Router < any > , { ref : EachContractLeafResultItem [ ] } > ( )
110121
111122export function createResolveRouter ( createHonoRouter : ( ) => Routing ) : ResolveRouter {
112- return ( router : Router < any > , method : string , pathname : string ) => {
113- let routing = routingCache . get ( router )
114-
115- if ( ! routing ) {
116- routing = createHonoRouter ( )
117-
118- const addRouteRecursively = ( routing : Routing , router : Router < any > , basePath : string [ ] ) => {
119- for ( const key in router ) {
120- const currentPath = [ ...basePath , key ]
121- const item = router [ key ] as WELL_DEFINED_PROCEDURE | Router < any >
122-
123- if ( isProcedure ( item ) ) {
124- const method = item . zz$p . contract . zz$cp . method ?? 'POST'
125- const path = item . zz$p . contract . zz$cp . path
126- ? openAPIPathToRouterPath ( item . zz$p . contract . zz$cp . path )
127- : `/${ currentPath . map ( encodeURIComponent ) . join ( '/' ) } `
128-
129- routing . add ( method , path , [ currentPath , item ] )
130- }
131- else {
132- addRouteRecursively ( routing , item , currentPath )
133- }
134- }
123+ const addRoutes = ( routing : Routing , pending : { ref : EachContractLeafResultItem [ ] } , options : EachLeafOptions ) => {
124+ const lazies = eachContractProcedureLeaf ( options , ( { path, contract } ) => {
125+ const method = contract . zz$cp . method ?? 'POST'
126+ const httpPath = contract . zz$cp . path
127+ ? openAPIPathToRouterPath ( contract . zz$cp . path )
128+ : `/${ path . map ( encodeURIComponent ) . join ( '/' ) } `
129+
130+ routing . add ( method , httpPath , path )
131+ } )
132+
133+ pending . ref . push ( ...lazies )
134+ }
135+
136+ return async ( router : Router < any > , method : string , pathname : string ) => {
137+ const pending = ( ( ) => {
138+ let pending = pendingCache . get ( router )
139+ if ( ! pending ) {
140+ pending = { ref : [ ] }
141+ pendingCache . set ( router , pending )
142+ }
143+
144+ return pending
145+ } ) ( )
146+
147+ const routing = ( ( ) => {
148+ let routing = routingCache . get ( router )
149+
150+ if ( ! routing ) {
151+ routing = createHonoRouter ( )
152+ routingCache . set ( router , routing )
153+ addRoutes ( routing , pending , { router, path : [ ] } )
154+ }
155+
156+ return routing
157+ } ) ( )
158+
159+ const newPending = [ ]
160+
161+ for ( const item of pending . ref ) {
162+ if (
163+ ( LAZY_ROUTER_PREFIX_SYMBOL in item . lazy )
164+ && item . lazy [ LAZY_ROUTER_PREFIX_SYMBOL ]
165+ && ! pathname . startsWith ( item . lazy [ LAZY_ROUTER_PREFIX_SYMBOL ] as HTTPPath )
166+ && ! pathname . startsWith ( `/${ item . path . map ( encodeURIComponent ) . join ( '/' ) } ` )
167+ ) {
168+ newPending . push ( item )
169+ continue
135170 }
136171
137- addRouteRecursively ( routing , router , [ ] )
138- routingCache . set ( router , routing )
172+ const router = ( await item . lazy [ LAZY_LOADER_SYMBOL ] ( ) ) . default
173+
174+ addRoutes ( routing , pending , { path : item . path , router } )
139175 }
140176
177+ pending . ref = newPending
178+
141179 const [ matches , params_ ] = routing . match ( method , pathname )
142180
143181 const [ match ] = matches . sort ( ( a , b ) => {
@@ -148,20 +186,31 @@ export function createResolveRouter(createHonoRouter: () => Routing): ResolveRou
148186 return undefined
149187 }
150188
151- const path = match [ 0 ] [ 0 ]
152- const procedure = match [ 0 ] [ 1 ]
189+ const path = match [ 0 ]
153190 const params = params_
154191 ? mapValues (
155192 ( match as any ) [ 1 ] ! ,
156193 v => params_ [ v as number ] ! ,
157194 )
158195 : match [ 1 ] as Record < string , string >
159196
160- return {
161- path,
162- procedure,
163- params : { ...params } , // params from hono not a normal object, so we need spread here
197+ let current : Router < any > | ANY_PROCEDURE | ANY_LAZY_PROCEDURE | undefined = router
198+ for ( const segment of path ) {
199+ if ( ( typeof current !== 'object' && typeof current !== 'function' ) || ! current ) {
200+ current = undefined
201+ break
202+ }
203+
204+ current = ( current as any ) [ segment ]
164205 }
206+
207+ return isProcedure ( current ) || isLazy ( current )
208+ ? {
209+ path,
210+ procedure : current ,
211+ params : { ...params } , // params from hono not a normal object, so we need spread here
212+ }
213+ : undefined
165214 }
166215}
167216
@@ -180,7 +229,7 @@ function mergeParamsAndInput(coercedParams: Record<string, unknown>, input: unkn
180229 }
181230}
182231
183- async function deserializeInput ( request : Request , procedure : Procedure < any , any , any , any , any > ) : Promise < unknown > {
232+ async function deserializeInput ( request : Request , procedure : ANY_PROCEDURE ) : Promise < unknown > {
184233 const deserializer = new OpenAPIDeserializer ( {
185234 schema : procedure . zz$p . contract . zz$cp . InputSchema ,
186235 } )
0 commit comments