Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions packages/devframe/src/adapters/__tests__/dev.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,50 @@ describe('adapters/dev', () => {
}
})

it('forwards onPeerConnect/onPeerDisconnect to the underlying startHttpAndWs', async () => {
const devframe = defineDevframe({
id: 'devframe-peer-hooks',
name: 'Peer Hooks',
version: '0.0.0',
packageName: 'devframe-test',
homepage: 'https://example.test',
description: 'Test devframe.',
setup: () => {},
})
const host = '127.0.0.1'
const port = await getPort({ port: 19470, host })
const onPeerConnect = vi.fn()
const onPeerDisconnect = vi.fn()
const handle = await createDevServer(devframe, {
host,
port,
openBrowser: false,
auth: false,
onPeerConnect,
onPeerDisconnect,
})

try {
const ws = new WebSocket(`ws://${host}:${port}/__devframe_ws`)
await new Promise<void>((resolve, reject) => {
ws.on('open', () => resolve())
ws.on('error', reject)
})
await vi.waitFor(() => {
expect(onPeerConnect).toHaveBeenCalledTimes(1)
})
expect(onPeerDisconnect).not.toHaveBeenCalled()

ws.close()
await vi.waitFor(() => {
expect(onPeerDisconnect).toHaveBeenCalledTimes(1)
})
}
finally {
await handle.close()
}
})

it('resolveDevServerPort honors def.cli.port as the preferred default', async () => {
const preferred = await getPort({ port: 19500, host: '127.0.0.1' })
const devframe = defineDevframe({
Expand Down
15 changes: 15 additions & 0 deletions packages/devframe/src/adapters/dev.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { Peer } from 'crossws'
import type { DevframeAuthHandler } from '../node/auth/handler'
import type { StartedServer } from '../node/server'
import type { ConnectionMeta } from '../types/context'
import type { DevframeDefinition, DevframeSetupInfo, DevframeWsOptions, McpRouteOptions } from '../types/devframe'
import type { DevframeNodeRpcSession, DevframeNodeRpcSessionMeta } from '../types/rpc'
import process from 'node:process'
import { open } from 'devframe/utils/open'
import { mountStaticHandler } from 'devframe/utils/serve-static'
Expand Down Expand Up @@ -87,6 +89,17 @@ export interface CreateDevServerOptions {
* {@link McpRouteOptions}.
*/
mcp?: boolean | McpRouteOptions
/**
* Called once per new WS connection, right after its session is created.
* Forwarded verbatim to the underlying `startHttpAndWs`.
*/
onPeerConnect?: (peer: Peer, session: DevframeNodeRpcSession) => void
/**
* Called once per closed WS connection, right after its session's
* disconnect bookkeeping runs. Forwarded verbatim to the underlying
* `startHttpAndWs`.
*/
onPeerDisconnect?: (peer: Peer, meta: DevframeNodeRpcSessionMeta) => void
/**
* Called once the WS server is bound. Devframe stays headless
* otherwise — wire this if you want a startup banner.
Expand Down Expand Up @@ -255,6 +268,8 @@ export async function createDevServer(
path: bindPath,
wsPort,
auth: resolvedAuth,
onPeerConnect: options.onPeerConnect,
onPeerDisconnect: options.onPeerDisconnect,
onReady: async (info) => {
// Print the auth banner before the caller's own onReady / browser open
// so the code is on screen by the time a browser lands on the page.
Expand Down
44 changes: 44 additions & 0 deletions packages/devframe/src/node/__tests__/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,50 @@ describe('startHttpAndWs rpcOptions passthrough', () => {
})
})

describe('startHttpAndWs onPeerConnect / onPeerDisconnect', () => {
it('forwards both hooks, symmetrically, for the same peer', async () => {
const context = await createTestContext()
const onPeerConnect = vi.fn()
const onPeerDisconnect = vi.fn()
const host = '127.0.0.1'
const port = await getPort({ port: 0, host })
const server = await startHttpAndWs({
context,
host,
port,
auth: false,
onPeerConnect,
onPeerDisconnect,
})

try {
const raw = new WebSocket(`ws://${host}:${port}`)
await new Promise<void>((resolve, reject) => {
raw.once('open', () => resolve())
raw.once('error', reject)
})

await vi.waitFor(() => {
expect(onPeerConnect).toHaveBeenCalledTimes(1)
})
const [, session] = onPeerConnect.mock.calls[0]!
expect(onPeerDisconnect).not.toHaveBeenCalled()

raw.close()

await vi.waitFor(() => {
expect(onPeerDisconnect).toHaveBeenCalledTimes(1)
})
const [, meta] = onPeerDisconnect.mock.calls[0]!
// Same underlying session — proven by the stable meta id — closing out.
expect(meta.id).toBe(session.meta.id)
}
finally {
await server.close()
}
})
})

describe('startHttpAndWs listen failures', () => {
it('rejects when the port is already taken instead of hanging', async () => {
const host = '127.0.0.1'
Expand Down
10 changes: 9 additions & 1 deletion packages/devframe/src/node/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@ export interface StartHttpAndWsOptions {
* observe — but not override — the connect-time trust decision.
*/
onPeerConnect?: (peer: Peer, session: DevframeNodeRpcSession) => void
/**
* Called once per closed WS connection, right after the transport's own
* disconnect bookkeeping runs. Unlike {@link onPeerConnect} this receives
* the raw session meta, not a wrapped session — by the time a peer
* disconnects there is no live RPC client left to attach.
*/
onPeerDisconnect?: (peer: Peer, meta: DevframeNodeRpcSessionMeta) => void
/**
* Forwarded verbatim to the internal `createRpcServer`'s birpc
* `rpcOptions`, alongside the resolver `startHttpAndWs` installs for
Expand Down Expand Up @@ -224,7 +231,8 @@ export async function startHttpAndWs(options: StartHttpAndWsOptions): Promise<St
options.onPeerConnect?.(peer, session)
}
: undefined,
onDisconnected: (_peer, meta) => {
onDisconnected: (peer, meta) => {
options.onPeerDisconnect?.(peer, meta)
rpcHost._emitSessionDisconnected(meta)
},
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export interface CreateDevServerOptions {
openBrowser?: boolean | string;
auth?: boolean | DevframeAuthHandler;
mcp?: boolean | McpRouteOptions;
onPeerConnect?: (_: Peer, _: DevframeNodeRpcSession) => void;
onPeerDisconnect?: (_: Peer, _: DevframeNodeRpcSessionMeta) => void;
onReady?: (_: {
origin: string;
port: number;
Expand Down
Loading