@@ -2,17 +2,15 @@ import type { BirpcGroup, EventOptions } from 'birpc'
22import type { Peer } from 'crossws'
33import type { NodeAdapter } from 'crossws/adapters/node'
44import type { WsOriginRegistry } from 'devframe/rpc/transports/ws-server'
5- import type { ConnectionMeta , DevframeNodeContext , DevframeNodeRpcSession , DevframeNodeRpcSessionMeta , DevframeRpcClientFunctions , DevframeRpcServerFunctions } from 'devframe/types'
5+ import type { ConnectionMeta , DevframeNodeContext , DevframeNodeRpcSession , DevframeRpcClientFunctions , DevframeRpcServerFunctions } from 'devframe/types'
66import type { Server as NodeHttpServer } from 'node:http'
77import type { DevframeAuthHandler } from './auth'
88import type { RpcFunctionsHostImpl } from './host-functions'
9- import { AsyncLocalStorage } from 'node:async_hooks'
109import { createServer } from 'node:http'
11- import { createRpcServer } from 'devframe/rpc/server'
1210import { attachWsRpcTransport } from 'devframe/rpc/transports/ws-server'
1311import { H3 , toNodeHandler } from 'h3'
14- import { diagnostics } from './diagnostics'
1512import { getInternalContext } from './hub-internals/context'
13+ import { createContextRpcServer } from './rpc-core'
1614import { formatHostForUrl , normalizeHttpServerUrl } from './utils'
1715
1816export interface StartHttpAndWsOptions {
@@ -26,7 +24,7 @@ export interface StartHttpAndWsOptions {
2624 */
2725 app ?: H3
2826 /**
29- * Bind the WS endpoint to a single upgrade route (e.g. `/__devframe_ws `) instead of
27+ * Bind the WS endpoint to a single upgrade route (e.g. `/__ws `) instead of
3028 * claiming every upgrade on the port. This lets the socket share a server
3129 * with other upgrade handlers (Vite HMR, a host framework's own sockets)
3230 * and is what the SPA's `__connection.json` points at. When omitted, the WS
@@ -146,56 +144,15 @@ export async function startHttpAndWs(options: StartHttpAndWsOptions): Promise<St
146144 const httpServer = options . server ?? createServer ( toNodeHandler ( app ) )
147145 const rpcHost = context . rpc as unknown as RpcFunctionsHostImpl
148146
149- const asyncStorage = new AsyncLocalStorage < DevframeNodeRpcSession > ( )
150-
151- // A full auth handler (e.g. from `createInteractiveAuth`) registers its own
152- // RPC functions and supplies both the resolver gate and the connect-time
153- // trust hook. `authorize`/`onPeerConnect` are the lower-level escape
154- // hatches for callers not using a full handler.
155- const authHandler : DevframeAuthHandler | undefined = typeof options . auth === 'object' ? options . auth : undefined
156- const effectiveAuthorize = options . authorize ?? authHandler ?. authorize
157-
158- if ( authHandler ) {
159- for ( const fn of authHandler . rpcFunctions ) {
160- if ( ! rpcHost . definitions . has ( fn . name ) )
161- rpcHost . register ( fn )
162- }
163- }
164-
165- const rpcGroup = createRpcServer < DevframeRpcClientFunctions , DevframeRpcServerFunctions > (
166- rpcHost . functions ,
167- {
168- rpcOptions : {
169- // Forwarded as-is so a host with its own structured diagnostics
170- // keeps seeing RPC failures; see `StartHttpAndWsOptions.rpcOptions`.
171- onFunctionError : options . rpcOptions ?. onFunctionError ,
172- onGeneralError : options . rpcOptions ?. onGeneralError ,
173- // Wrap each RPC handler in an AsyncLocalStorage context so
174- // `ctx.rpc.getCurrentRpcSession()` works inside handlers (used
175- // by streaming subscribe/unsubscribe/cancel and shared-state
176- // sync), and — when an `authorize` gate is configured — reject
177- // the call before it ever reaches the handler. Mirrors
178- // `packages/core/src/node/ws.ts`'s resolver.
179- resolver ( name , fn ) {
180- // eslint-disable-next-line ts/no-this-alias
181- const rpc = this
182- if ( ! fn )
183- return undefined
184- return async function ( this : any , ...args ) {
185- const meta = rpc . $meta as DevframeNodeRpcSessionMeta
186- if ( effectiveAuthorize && ! effectiveAuthorize ( name , { meta, rpc : rpc as any } ) )
187- throw diagnostics . DF0036 ( { name } )
188- return await asyncStorage . run ( {
189- rpc,
190- meta,
191- } , async ( ) => {
192- return ( await fn ) . apply ( this , args )
193- } )
194- }
195- } ,
196- } ,
197- } ,
198- )
147+ // Transport-agnostic RPC core: auth wiring, session resolver, and the
148+ // peer lifecycle handlers the WS transport below plugs into.
149+ const { rpcGroup, onConnected, onDisconnected } = createContextRpcServer ( {
150+ context,
151+ auth : options . auth ,
152+ authorize : options . authorize ,
153+ onPeerConnect : options . onPeerConnect ,
154+ rpcOptions : options . rpcOptions ,
155+ } )
199156
200157 // A dedicated WS port (the "different port" scenario) only applies when we
201158 // own the HTTP server — a shared host server already dictates the port.
@@ -214,44 +171,10 @@ export async function startHttpAndWs(options: StartHttpAndWsOptions): Promise<St
214171 // other sockets, so leave non-matching upgrades for them.
215172 destroyUnmatched : ownsHttpServer ,
216173 allowedOrigins : options . allowedOrigins ,
217- onConnected : ( authHandler || options . onPeerConnect )
218- ? ( peer , meta ) => {
219- const session : DevframeNodeRpcSession = {
220- meta,
221- rpc : rpcGroup . clients . find ( client => ( client as any ) . $meta === meta ) as any ,
222- }
223- authHandler ?. onConnect ( peer , session )
224- options . onPeerConnect ?.( peer , session )
225- }
226- : undefined ,
227- onDisconnected : ( _peer , meta ) => {
228- rpcHost . _emitSessionDisconnected ( meta )
229- } ,
174+ onConnected,
175+ onDisconnected,
230176 } )
231177
232- ; ( rpcHost as any ) . _rpcGroup = rpcGroup
233- ; ( rpcHost as any ) . _asyncStorage = asyncStorage
234- ; ( rpcHost as any ) . _authDisabled = options . auth === false
235-
236- // The browser client unconditionally calls `anonymous:devframe:auth` on
237- // connect (see `client/rpc-ws.ts`). When `auth: false` is set on the
238- // standalone server, register a noop handler that auto-trusts so the
239- // client's hardcoded handshake succeeds. A host passing a full
240- // `DevframeAuthHandler` already registered the real handler above, and
241- // never opts into `auth: false`, so the two paths never overlap.
242- if ( options . auth === false && ! rpcHost . definitions . has ( 'anonymous:devframe:auth' ) ) {
243- rpcHost . register ( {
244- name : 'anonymous:devframe:auth' ,
245- type : 'action' ,
246- handler : ( ) => {
247- const session = rpcHost . getCurrentRpcSession ( )
248- if ( session )
249- session . meta . isTrusted = true
250- return { isTrusted : true }
251- } ,
252- } )
253- }
254-
255178 // Only start listening on a server we created. A shared server is already
256179 // (or about to be) listening under the caller's control.
257180 if ( ownsHttpServer ) {
0 commit comments