Skip to content

Commit 89c4df2

Browse files
authored
fix(node): export the structural RpcFunctionsHost type from devframe/node (#112)
1 parent 06554be commit 89c4df2

11 files changed

Lines changed: 73 additions & 39 deletions

File tree

‎docs/guide/rpc.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,8 @@ declare module 'devframe' {
231231

232232
For server→client calls invoked via `ctx.rpc.broadcast`, augment `DevframeRpcClientFunctions` the same way.
233233

234+
Augment one of the canonical module specifiers where these interfaces live — `declare module 'devframe'` or `declare module 'devframe/types'` (the form `@devframes/hub` uses). A wrapper package that re-exports the interface under a renamed alias (e.g. `DevToolsRpcServerFunctions`) is a different declaration, so augmenting the alias no longer merges into the base interface.
235+
234236
## Static dumps
235237

236238
For `static` functions, Devframe records the handler's output during `createBuild` and bakes it into the build:

‎packages/devframe/src/node/__tests__/host-agent.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import type { RpcFunctionDefinitionAnyWithContext } from '../../rpc/types'
22
import type { DevframeNodeContext } from '../../types/context'
33
import { describe, expect, it, vi } from 'vitest'
44
import { DevframeAgentHost } from '../host-agent'
5-
import { RpcFunctionsHost } from '../host-functions'
5+
import { RpcFunctionsHostImpl } from '../host-functions'
66

77
function createContext(): DevframeNodeContext {
88
const ctx = {} as DevframeNodeContext
9-
ctx.rpc = new RpcFunctionsHost(ctx)
9+
ctx.rpc = new RpcFunctionsHostImpl(ctx)
1010
ctx.agent = new DevframeAgentHost(ctx)
1111
return ctx
1212
}

‎packages/devframe/src/node/__tests__/host-functions.test.ts‎

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import type { DevframeNodeContext } from 'devframe/types'
2+
import type { RpcFunctionsHost } from '../host-functions'
23
import { defineRpcFunction } from 'devframe'
3-
import { describe, expect, it } from 'vitest'
4-
import { RpcFunctionsHost } from '../host-functions'
4+
import { describe, expect, expectTypeOf, it } from 'vitest'
5+
import { RpcFunctionsHostImpl } from '../host-functions'
56

67
async function emptyHandler() { /* empty */ }
78
const returnFirst = async () => 'first'
@@ -15,7 +16,7 @@ describe('rpcFunctionsHost', () => {
1516

1617
describe('register() collision detection', () => {
1718
it('should register a new RPC function successfully', () => {
18-
const host = new RpcFunctionsHost(mockContext)
19+
const host = new RpcFunctionsHostImpl(mockContext)
1920
const fn = defineRpcFunction({
2021
name: 'test-function',
2122
type: 'action',
@@ -27,7 +28,7 @@ describe('rpcFunctionsHost', () => {
2728
})
2829

2930
it('should throw error when registering duplicate RPC function ID', () => {
30-
const host = new RpcFunctionsHost(mockContext)
31+
const host = new RpcFunctionsHostImpl(mockContext)
3132
const fn1 = defineRpcFunction({
3233
name: 'duplicate-fn',
3334
type: 'action',
@@ -48,7 +49,7 @@ describe('rpcFunctionsHost', () => {
4849
})
4950

5051
it('should include the duplicate ID in error message', () => {
51-
const host = new RpcFunctionsHost(mockContext)
52+
const host = new RpcFunctionsHostImpl(mockContext)
5253
const fn = defineRpcFunction({
5354
name: 'my-special-function',
5455
type: 'query',
@@ -64,7 +65,7 @@ describe('rpcFunctionsHost', () => {
6465

6566
describe('update() existence validation', () => {
6667
it('should throw error when updating non-existent RPC function', () => {
67-
const host = new RpcFunctionsHost(mockContext)
68+
const host = new RpcFunctionsHostImpl(mockContext)
6869
const fn = defineRpcFunction({
6970
name: 'nonexistent',
7071
type: 'action',
@@ -79,7 +80,7 @@ describe('rpcFunctionsHost', () => {
7980
})
8081

8182
it('should update existing RPC function successfully', () => {
82-
const host = new RpcFunctionsHost(mockContext)
83+
const host = new RpcFunctionsHostImpl(mockContext)
8384
const fn1 = defineRpcFunction({
8485
name: 'update-test',
8586
type: 'action',
@@ -100,7 +101,7 @@ describe('rpcFunctionsHost', () => {
100101
})
101102

102103
it('should validate that update only works on existing entries', () => {
103-
const host = new RpcFunctionsHost(mockContext)
104+
const host = new RpcFunctionsHostImpl(mockContext)
104105

105106
// Register one function
106107
host.register(defineRpcFunction({
@@ -131,15 +132,15 @@ describe('rpcFunctionsHost', () => {
131132

132133
describe('broadcast() without rpc group', () => {
133134
it('should not throw in build mode', async () => {
134-
const host = new RpcFunctionsHost({ mode: 'build' } as DevframeNodeContext)
135+
const host = new RpcFunctionsHostImpl({ mode: 'build' } as DevframeNodeContext)
135136
await expect(host.broadcast({
136137
method: 'devframe:auth:revoked',
137138
args: [],
138139
})).resolves.toBeUndefined()
139140
})
140141

141142
it('should not throw in dev mode when rpc group is not yet set', async () => {
142-
const host = new RpcFunctionsHost({ mode: 'dev' } as DevframeNodeContext)
143+
const host = new RpcFunctionsHostImpl({ mode: 'dev' } as DevframeNodeContext)
143144
await expect(host.broadcast({
144145
method: 'devframe:auth:revoked',
145146
args: [],
@@ -149,7 +150,7 @@ describe('rpcFunctionsHost', () => {
149150

150151
describe('invokeLocal()', () => {
151152
it('should invoke a locally registered function', async () => {
152-
const host = new RpcFunctionsHost(mockContext)
153+
const host = new RpcFunctionsHostImpl(mockContext)
153154
host.register(defineRpcFunction({
154155
name: 'test:invoke-local',
155156
type: 'query',
@@ -162,8 +163,29 @@ describe('rpcFunctionsHost', () => {
162163
})
163164

164165
it('should throw when invoking a missing local function', async () => {
165-
const host = new RpcFunctionsHost(mockContext)
166+
const host = new RpcFunctionsHostImpl(mockContext)
166167
await expect(host.invokeLocal('test:missing' as any)).rejects.toThrow('RPC function "test:missing" is not registered')
167168
})
168169
})
170+
171+
describe('public RpcFunctionsHost type', () => {
172+
it('is the structural type that ctx.rpc satisfies', () => {
173+
// The `RpcFunctionsHost` exported from `devframe/node` (this
174+
// re-export) must be the exact same structural type as
175+
// `DevframeNodeContext['rpc']`, so consumers can cast `ctx.rpc`
176+
// to it without a TS2352 conversion error.
177+
expectTypeOf<DevframeNodeContext['rpc']>().toEqualTypeOf<RpcFunctionsHost>()
178+
expectTypeOf<DevframeNodeContext['rpc']>().toMatchTypeOf<RpcFunctionsHost>()
179+
})
180+
181+
it('does not carry the impl-only @internal members', () => {
182+
const rpc = {} as RpcFunctionsHost
183+
// @ts-expect-error `_rpcGroup` is impl-only and must stay off the public type
184+
void rpc._rpcGroup
185+
// @ts-expect-error `_asyncStorage` is impl-only and must stay off the public type
186+
void rpc._asyncStorage
187+
// @ts-expect-error `_emitSessionDisconnected` is impl-only and must stay off the public type
188+
void rpc._emitSessionDisconnected
189+
})
190+
})
169191
})

‎packages/devframe/src/node/__tests__/rpc-streaming.test.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,20 @@ import { getPort } from 'get-port-please'
99
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
1010
import { WebSocket } from 'ws'
1111

12-
import { RpcFunctionsHost } from '../host-functions'
12+
import { RpcFunctionsHostImpl } from '../host-functions'
1313

1414
vi.stubGlobal('WebSocket', WebSocket)
1515

1616
interface Harness {
1717
port: number
18-
rpcHost: RpcFunctionsHost
18+
rpcHost: RpcFunctionsHostImpl
1919
close: () => Promise<void>
2020
}
2121

2222
async function bootHost(): Promise<Harness> {
2323
const port = await getPort({ host: '127.0.0.1', random: true })
2424
const mockContext = {} as DevframeNodeContext
25-
const rpcHost = new RpcFunctionsHost(mockContext)
25+
const rpcHost = new RpcFunctionsHostImpl(mockContext)
2626

2727
const asyncStorage = new AsyncLocalStorage<any>()
2828

‎packages/devframe/src/node/auth/revoke.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { DevframeNodeContext } from 'devframe/types'
22
import type { SharedState } from 'devframe/utils/shared-state'
3-
import type { RpcFunctionsHost } from '../host-functions'
3+
import type { RpcFunctionsHostImpl } from '../host-functions'
44
import type { InternalAnonymousAuthStorage } from '../hub-internals/context'
55

66
/**
@@ -13,7 +13,7 @@ export async function revokeActiveConnectionsForToken(
1313
context: DevframeNodeContext,
1414
token: string,
1515
): Promise<void> {
16-
const rpcHost = context.rpc as unknown as RpcFunctionsHost | undefined
16+
const rpcHost = context.rpc as unknown as RpcFunctionsHostImpl | undefined
1717
if (!rpcHost?._rpcGroup)
1818
return
1919

‎packages/devframe/src/node/context.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { diagnostics as rpcDiagnostics } from '../rpc/diagnostics'
44
import { diagnostics as devframeDiagnostics } from './diagnostics'
55
import { DevframeAgentHost } from './host-agent'
66
import { DevframeDiagnosticsHost } from './host-diagnostics'
7-
import { RpcFunctionsHost } from './host-functions'
7+
import { RpcFunctionsHostImpl } from './host-functions'
88
import { DevframeServicesHostImpl } from './host-services'
99
import { DevframeViewHost } from './host-views'
1010
import { BUILTIN_AGENT_RPC } from './rpc'
@@ -47,7 +47,7 @@ export async function createHostContext(options: CreateHostContextOptions): Prom
4747
scope: undefined!,
4848
} as unknown as DevframeNodeContext
4949

50-
const rpcHost = new RpcFunctionsHost(context)
50+
const rpcHost = new RpcFunctionsHostImpl(context)
5151
const viewsHost = new DevframeViewHost(context)
5252
const diagnosticsHost = new DevframeDiagnosticsHost(context, [devframeDiagnostics, rpcDiagnostics])
5353
context.rpc = rpcHost

‎packages/devframe/src/node/host-functions.ts‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,25 @@ import { createRpcStreamingServerHost } from './rpc-streaming'
99

1010
const debugBroadcast = createDebug('devframe:rpc:broadcast')
1111

12-
export class RpcFunctionsHost extends RpcFunctionsCollectorBase<DevframeRpcServerFunctions, DevframeNodeContext> implements RpcFunctionsHostType {
12+
/**
13+
* The public, structural shape of `ctx.rpc`. Re-exported from
14+
* `devframe/types` so `import { RpcFunctionsHost } from 'devframe/node'`
15+
* resolves to the exact same type as `DevframeNodeContext['rpc']` (and the
16+
* `devframe` main entry) — free of the `@internal` implementation members
17+
* carried by {@link RpcFunctionsHostImpl}.
18+
*/
19+
export type { RpcFunctionsHost } from 'devframe/types'
20+
21+
/**
22+
* Concrete implementation backing `ctx.rpc`. Internal: consumers should
23+
* depend on the structural {@link RpcFunctionsHost} type, never this class.
24+
* Its `@internal` members (`_rpcGroup`, `_asyncStorage`,
25+
* `_emitSessionDisconnected`) are wired by `startHttpAndWs` and must not
26+
* widen the public surface.
27+
*
28+
* @internal
29+
*/
30+
export class RpcFunctionsHostImpl extends RpcFunctionsCollectorBase<DevframeRpcServerFunctions, DevframeNodeContext> implements RpcFunctionsHostType {
1331
/**
1432
* @internal
1533
*/

‎packages/devframe/src/node/index.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
export * from './context'
33
export * from './host-agent'
44
export * from './host-diagnostics'
5-
export * from './host-functions'
5+
// `RpcFunctionsHostImpl` stays internal; expose only the structural
6+
// `RpcFunctionsHost` type so consumers can type/cast `ctx.rpc` without
7+
// pulling in the implementation's `@internal` members.
8+
export type { RpcFunctionsHost } from './host-functions'
69
export * from './host-h3'
710
export * from './host-services'
811
export * from './host-views'

‎packages/devframe/src/node/server.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { NodeAdapter } from 'crossws/adapters/node'
44
import type { ConnectionMeta, DevframeNodeContext, DevframeNodeRpcSession, DevframeNodeRpcSessionMeta, DevframeRpcClientFunctions, DevframeRpcServerFunctions } from 'devframe/types'
55
import type { Server as NodeHttpServer } from 'node:http'
66
import type { DevframeAuthHandler } from './auth'
7-
import type { RpcFunctionsHost } from './host-functions'
7+
import type { RpcFunctionsHostImpl } from './host-functions'
88
import { AsyncLocalStorage } from 'node:async_hooks'
99
import { createServer } from 'node:http'
1010
import { createRpcServer } from 'devframe/rpc/server'
@@ -143,7 +143,7 @@ export async function startHttpAndWs(options: StartHttpAndWsOptions): Promise<St
143143
// otherwise we own a fresh one bound to `app`.
144144
const ownsHttpServer = !options.server
145145
const httpServer = options.server ?? createServer(toNodeHandler(app))
146-
const rpcHost = context.rpc as unknown as RpcFunctionsHost
146+
const rpcHost = context.rpc as unknown as RpcFunctionsHostImpl
147147

148148
const asyncStorage = new AsyncLocalStorage<DevframeNodeRpcSession>()
149149

‎tests/__snapshots__/tsnapi/devframe/node.snapshot.d.ts‎

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -74,25 +74,14 @@ export declare class DevframeViewHost implements DevframeViewHost$1 {
7474
constructor(_: DevframeNodeContext);
7575
hostStatic(_: string, _: string): void;
7676
}
77-
export declare class RpcFunctionsHost extends RpcFunctionsCollectorBase<DevframeRpcServerFunctions, DevframeNodeContext> implements RpcFunctionsHost$1 {
78-
_rpcGroup: BirpcGroup<DevframeRpcClientFunctions, DevframeRpcServerFunctions, false>;
79-
_asyncStorage: AsyncLocalStorage<DevframeNodeRpcSession>;
80-
constructor(_: DevframeNodeContext);
81-
sharedState: RpcSharedStateHost;
82-
streaming: RpcStreamingHost;
83-
_emitSessionDisconnected(_: DevframeNodeRpcSessionMeta): void;
84-
invokeLocal<T extends keyof DevframeRpcServerFunctions, Args extends Parameters<DevframeRpcServerFunctions[T]>>(_: T, ..._: Args): Promise<Awaited<ReturnType<DevframeRpcServerFunctions[T]>>>;
85-
broadcast<T extends keyof DevframeRpcClientFunctions, Args extends Parameters<DevframeRpcClientFunctions[T]>>(_: RpcBroadcastOptions<T, Args>): Promise<void>;
86-
getCurrentRpcSession(): DevframeNodeRpcSession | undefined;
87-
}
8877
// #endregion
8978

9079
// #region Functions
9180
export declare function createH3DevframeHost(_: CreateH3DevframeHostOptions): DevframeHost;
9281
export declare function createHostContext(_: CreateHostContextOptions): Promise<DevframeNodeContext>;
9382
export declare function createNodeSettings<T extends Record<string, any> = Record<string, any>>(_: DevframeNodeContext, _: string): DevframeSettings<T>;
94-
export declare function createRpcSharedStateServerHost(_: RpcFunctionsHost$1): RpcSharedStateHost;
95-
export declare function createRpcStreamingServerHost(_: RpcFunctionsHost$1): RpcStreamingHost;
83+
export declare function createRpcSharedStateServerHost(_: RpcFunctionsHost): RpcSharedStateHost;
84+
export declare function createRpcStreamingServerHost(_: RpcFunctionsHost): RpcStreamingHost;
9685
export declare function createScopedNodeContext<NS extends string = string>(_: DevframeNodeContext, _: NS): DevframeScopedNodeContext<NS>;
9786
export declare function createStorage<T extends object>(_: CreateStorageOptions<T>): SharedState<T>;
9887
export declare function formatHostForUrl(_: string): string;
@@ -102,6 +91,7 @@ export declare function toDialableHost(_: string): string;
10291
// #endregion
10392

10493
// #region Other
94+
export { RpcFunctionsHost }
10595
export { StartedServer }
10696
export { startHttpAndWs }
10797
export { StartHttpAndWsOptions }

0 commit comments

Comments
 (0)