Skip to content

Commit 591cd68

Browse files
committed
refactor: centralize event names in DEVFRAME_EVENTS / HUB_EVENTS maps
Introduce one source-of-truth event map per package and reference it from every call site, so event/broadcast/shared-state/channel names stop living as scattered string literals: - packages/devframe/src/events.ts (DEVFRAME_EVENTS) — agent host bus events, client connection events, and server->client broadcasts; re-exported from devframe/constants. - packages/hub/src/events.ts (HUB_EVENTS) — the docks/terminals/messages/ commands bus events, hub: RPC methods, devframe: broadcasts, shared-state keys, and channels; re-exported from @devframes/hub/constants. Call sites across both packages (plus hub-ui and the messages dev harness) now reference the maps instead of literals. The unavoidable type-position keys (EventEmitter<...> maps, RPC augmentation interfaces) mirror the maps. Document the core devframe channels in the Events Reference and note that the two maps back the page; add an AGENTS.md rule requiring the maps and events.md to move together and forbidding magic event names. Public constant types are preserved (DOCK_RENDERERS_STATE_KEY, FRAME_NAV_CHANNEL, DEVFRAME_REMOTE_ASSETS_ERROR_MESSAGE_TYPE stay `string`); the snapshot change is purely the additive HUB_EVENTS / DEVFRAME_EVENTS.
1 parent 1e4a2bb commit 591cd68

30 files changed

Lines changed: 383 additions & 106 deletions

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ Ahead-of-time build artifacts that live under `src/` - the shadow-root styleshee
4141
## Conventions
4242

4343
- RPC functions must use `defineRpcFunction`; always namespace IDs `devframes:plugin:<slug>:<fn-name>` (matching the plugin's `@devframes/plugin-<slug>` package name).
44+
- **No magic event names — use the centralized event maps.** Every event, broadcast, shared-state key, and channel name lives in one of two source-of-truth maps: `DEVFRAME_EVENTS` (`packages/devframe/src/events.ts`, re-exported from `devframe/constants`) for the core runtime, and `HUB_EVENTS` (`packages/hub/src/events.ts`, re-exported from `@devframes/hub/constants`) for the hub. Reference `DEVFRAME_EVENTS.*` / `HUB_EVENTS.*` at call sites (`.events.emit`/`.on`, `rpc.broadcast({ method })`, `sharedState.get(key)`, `defineHubRpcFunction({ name })`, `rpc.call`) instead of re-typing a string literal. The two maps and the [`docs/guide/events.md`](docs/guide/events.md) Events Reference are kept in lockstep: adding, renaming, or removing a name means editing the map **and** that page in the same change — every name in the maps appears in the tables, and vice versa. The only literals left are unavoidable type-position keys (the `EventEmitter<…>` maps in `types/*` and the `DevframeRpcClientFunctions`/`DevframeRpcServerFunctions` augmentations), which mirror the maps; a package that deliberately avoids a hub dependency (e.g. `@devframes/plugin-terminals`, which models the hub bridge structurally) keeps a local literal rather than importing `HUB_EVENTS`.
4445
- **Stay validator-neutral.** `devframe` and every `@devframes/*` package must not introduce a preferred schema validator dependency - no `valibot`, `zod`, `arktype`, etc. in their runtime `dependencies`. `args`/`returns`/flag schemas are typed against [Standard Schema](https://standardschema.dev/) (`@standard-schema/spec`, types-only); first-party code that needs to author a schema uses the built-in zero-dep `devframe/utils/simple-schema` builder (deliberately minimal - not a general validator). JSON-schema conversion uses each schema's own Standard JSON Schema converter (`~standard.jsonSchema`, implemented by e.g. zod 4) when present and degrades to a permissive object otherwise - no converter library and no vendor dependency is required. Docs, by contrast, should point *users* at a real validator for their own integrations - recommend **valibot** (lightest) or **zod** (worth reusing if they already pull it via the JSON-render or MCP integrations).
4546
- Shared state via `devframe/utils/shared-state`; keep values serializable.
4647
- Utility imports use the package-path form `devframe/utils/*`, never relative `../utils/*`.

docs/guide/events.md

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22
outline: deep
33
---
44

5-
# Hub Events Reference
5+
# Events Reference
66

7-
The hub carries change notifications across three distinct channels. What separates them is **direction and reach**: an in-process event bus that never leaves the node process, server RPC methods a client calls, and server-pushed broadcasts and shared state a client reads.
7+
Devframe carries change notifications across a few distinct channels. What separates them is **direction and reach**: an in-process event bus that never leaves the node process, server RPC methods a client calls, and server-pushed broadcasts and shared state a client reads.
88

99
Two naming prefixes mark the wire surface: `hub:` for hub-layer server RPC (client → server actions), and `devframe:` for the client-facing devframe protocol (broadcasts, shared state, and streams pushed server → client). The internal event bus mirrors the same plural subsystem vocabulary (`docks`, `terminals`, `messages`, `commands`), so each internal event lines up with its wire counterpart — `docks:activate` fans out to `devframe:docks:activate`.
1010

11-
## Internal node event bus
11+
Every name on this page has one home in code: the [`HUB_EVENTS`](https://github.com/devframes/devframe/blob/main/packages/hub/src/events.ts) map (`@devframes/hub/constants`) backs the hub tables, and the [`DEVFRAME_EVENTS`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/events.ts) map (`devframe/constants`) backs the core tables. Call sites reference `HUB_EVENTS.*` / `DEVFRAME_EVENTS.*` rather than re-typing a literal, and this page and those maps move together — changing one without the other is a bug.
12+
13+
## Hub events
14+
15+
### Internal node event bus
1216

1317
Each subsystem host emits on `ctx.<subsystem>.events`. These fire and are consumed **inside the same node process** — chiefly by `createHubContext`, which fans them out onto the wire. They never cross to the browser.
1418

@@ -22,7 +26,7 @@ Each subsystem host emits on `ctx.<subsystem>.events`. These fire and are consum
2226

2327
The `docks:entry:updated` and `terminals:session:updated` middle nouns (`entry`, `session`) name the specific record type; the messages and commands subsystems imply their record in the subsystem name, so they carry the verb directly.
2428

25-
## Server RPC methods — client → server
29+
### Server RPC methods — client → server
2630

2731
A connected client (any mounted iframe or panel, on its own RPC client) calls these; the hub node handles them. Carry the `hub:` prefix.
2832

@@ -40,7 +44,7 @@ A connected client (any mounted iframe or panel, on its own RPC client) calls th
4044
| `hub:terminals:restart` | `(id) => void` | Re-run a session's command in place. |
4145
| `hub:terminals:remove` | `(id) => void` | Kill a session's process and drop it from the registry. |
4246

43-
## Broadcasts & shared state — server → client
47+
### Broadcasts & shared state — server → client
4448

4549
The server pushes these; a hub-aware client reads or subscribes. Carry the `devframe:` prefix. A UI subscribes to broadcasts via `rpc.client.register(...)`; the [client host](./client-context) registers the `devframe:docks:activate` handler for you.
4650

@@ -56,3 +60,45 @@ The server pushes these; a hub-aware client reads or subscribes. Carry the `devf
5660
| `devframe:terminals` | streaming channel | Live terminal output stream, keyed by session id. |
5761

5862
The [`devframe:docks:active`](./shared-state) mirror pairs with the `devframe:docks:activate` broadcast: the broadcast reaches docks already on screen, while the mirror lets a dock that mounts *because* of the switch converge on the same request instead of missing it.
63+
64+
## Core devframe events
65+
66+
The core `devframe` runtime (below the hub) carries its own notification channels — the agent host's change events, the client connection lifecycle, and the server-pushed broadcasts that power shared state and streaming. These are backed by `DEVFRAME_EVENTS` (`devframe/constants`).
67+
68+
This map covers notifications only. The request/response RPC endpoints of the shared-state, streaming, and auth-handshake protocols (`devframe:rpc:server-state:*`, `devframe:streaming:subscribe`, `anonymous:devframe:auth`, …) are defined at their handlers and typed in `types/rpc-augments.ts` — they aren't events.
69+
70+
### Node host bus
71+
72+
Emitted on `ctx.agent.events` as the agent-exposed tool/resource surface changes; protocol adapters (e.g. the MCP server) subscribe to re-publish their manifest.
73+
74+
| Event | Emitted by | Payload |
75+
|---|---|---|
76+
| `agent:manifest:changed` | any tool/resource/provider change ||
77+
| `agent:tool:registered` / `agent:tool:unregistered` | `registerTool` / `unregisterTool` | `AgentTool` / id |
78+
| `agent:resource:registered` / `agent:resource:unregistered` | `registerResource` / `unregisterResource` | `AgentResource` / id |
79+
80+
### Client connection events
81+
82+
Emitted on the RPC client's `rpc.events` emitter (`RpcClientEvents`) for a UI to track connection lifecycle and surface errors.
83+
84+
| Event | Carries |
85+
|---|---|
86+
| `rpc:is-trusted:updated` | Trust gate flipped (`boolean`). |
87+
| `rpc:error` | An RPC call rejected (`error`, `method`). |
88+
| `connection:status` | Connection status changed (`status`, `previous`). |
89+
| `connection:error` | A connection-level error (WebSocket errored, or trust refused). |
90+
91+
### Broadcasts — server → client
92+
93+
Pushed from the server to subscribed clients over the `devframe:` protocol. Wired by the framework's own hosts; not registered manually.
94+
95+
| Name | Carries |
96+
|---|---|
97+
| `devframe:auth:revoked` | This connection's bearer token was revoked; the client drops to untrusted. |
98+
| `devframe:rpc:client-state:updated` | Full shared-state snapshot for a key. |
99+
| `devframe:rpc:client-state:patch` | Incremental shared-state patch for a key. |
100+
| `devframe:streaming:chunk` | A streaming chunk for a subscribed channel/id. |
101+
| `devframe:streaming:end` | A streaming terminator (optionally an error). |
102+
| `devframe:streaming:upload-cancel` | Server-side cancel of an in-flight upload. |
103+
104+
Plus one `postMessage` channel, `devframe:remote-assets-error`, that the remote-assets fallback page posts to `window.parent` so an embedding viewer can replace the bare 502 page with its own UI.

packages/devframe/src/adapters/mcp/build-server.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Server } from '@modelcontextprotocol/server'
88
import { createHostContext } from 'devframe/node'
99
import { toAgentToolName } from 'devframe/utils/agent-tool-name'
1010
import { join } from 'pathe'
11+
import { DEVFRAME_EVENTS } from '../../events'
1112
import { diagnostics } from '../../node/diagnostics'
1213
import { formatMcpError, stringifyForMcp } from './stringify'
1314
import { argsToJsonSchema, returnToJsonSchema } from './to-json-schema'
@@ -70,7 +71,7 @@ export function buildMcpServerFromContext(
7071
const notify = (method: string): void => {
7172
server.notification({ method }).catch(() => { /* ignore transport errors */ })
7273
}
73-
const offManifest = ctx.agent.events.on('agent:manifest:changed', () => {
74+
const offManifest = ctx.agent.events.on(DEVFRAME_EVENTS.bus.agentManifestChanged, () => {
7475
notify('notifications/tools/list_changed')
7576
notify('notifications/resources/list_changed')
7677
})

packages/devframe/src/client/rpc-live.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { ConnectionMeta, DevframeRpcClientFunctions, DevframeRpcServerFunct
33
import type { DevframeConnectionStatus } from './connection'
44
import type { DevframeClientRpcHost, DevframeRpcClientMode, DevframeRpcClientOptions, RpcClientEvents } from './rpc'
55
import { createRpcClient } from 'devframe/rpc/client'
6+
import { DEVFRAME_EVENTS } from '../events'
67
import { promiseWithResolver } from '../utils/promise'
78
import { DevframeConnectionError } from './connection'
89

@@ -65,7 +66,7 @@ export function createLiveRpcClientMode(
6566
return
6667
const previous = status
6768
status = next
68-
events.emit('connection:status', next, previous)
69+
events.emit(DEVFRAME_EVENTS.client.connectionStatus, next, previous)
6970
}
7071

7172
// Pending calls we can settle proactively — a connection that drops (or a
@@ -99,7 +100,7 @@ export function createLiveRpcClientMode(
99100
if (settled)
100101
return
101102
finish()
102-
events.emit('rpc:error', error, method)
103+
events.emit(DEVFRAME_EVENTS.client.error, error, method)
103104
reject(error)
104105
},
105106
}
@@ -127,7 +128,7 @@ export function createLiveRpcClientMode(
127128
return
128129
finish()
129130
const err = error instanceof Error ? error : new Error(String(error))
130-
events.emit('rpc:error', err, method)
131+
events.emit(DEVFRAME_EVENTS.client.error, err, method)
131132
reject(err)
132133
},
133134
)
@@ -148,7 +149,7 @@ export function createLiveRpcClientMode(
148149
definitions,
149150
onError(error) {
150151
setStatus('error', error)
151-
events.emit('connection:error', error)
152+
events.emit(DEVFRAME_EVENTS.client.connectionError, error)
152153
rejectAllPending(new DevframeConnectionError('connection', '[devframe] Connection to the devframe server failed', { cause: error }))
153154
},
154155
onDisconnected() {
@@ -169,15 +170,15 @@ export function createLiveRpcClientMode(
169170

170171
// Handle server-initiated auth revocation
171172
clientRpc.register({
172-
name: 'devframe:auth:revoked',
173+
name: DEVFRAME_EVENTS.broadcast.authRevoked,
173174
type: 'event',
174175
handler: () => {
175176
isTrusted = false
176177
const authError = new DevframeConnectionError('auth', '[devframe] The devframe server revoked this client\'s trust')
177178
setStatus('unauthorized', authError)
178-
events.emit('connection:error', authError)
179+
events.emit(DEVFRAME_EVENTS.client.connectionError, authError)
179180
rejectAllPending(authError)
180-
events.emit('rpc:is-trusted:updated', false)
181+
events.emit(DEVFRAME_EVENTS.client.isTrustedUpdated, false)
181182
},
182183
})
183184

@@ -209,9 +210,9 @@ export function createLiveRpcClientMode(
209210
// so it never lands here.
210211
const authError = new DevframeConnectionError('auth', '[devframe] The devframe server refused this client\'s credentials')
211212
setStatus('unauthorized', authError)
212-
events.emit('connection:error', authError)
213+
events.emit(DEVFRAME_EVENTS.client.connectionError, authError)
213214
}
214-
events.emit('rpc:is-trusted:updated', isTrusted)
215+
events.emit(DEVFRAME_EVENTS.client.isTrustedUpdated, isTrusted)
215216
return result.isTrusted
216217
}
217218

@@ -228,7 +229,7 @@ export function createLiveRpcClientMode(
228229
isTrusted = true
229230
trustedPromise.resolve(true)
230231
setStatus('connected')
231-
events.emit('rpc:is-trusted:updated', true)
232+
events.emit(DEVFRAME_EVENTS.client.isTrustedUpdated, true)
232233
}
233234
return token
234235
}
@@ -284,7 +285,7 @@ export function createLiveRpcClientMode(
284285
const method = String(args[0])
285286
const failFast = terminalError()
286287
if (failFast) {
287-
events.emit('rpc:error', failFast, method)
288+
events.emit(DEVFRAME_EVENTS.client.error, failFast, method)
288289
return Promise.reject(failFast)
289290
}
290291
return guardCall(
@@ -300,7 +301,7 @@ export function createLiveRpcClientMode(
300301
// to send, so surface the failure and drop it instead of queuing forever.
301302
const failFast = terminalError()
302303
if (failFast) {
303-
events.emit('rpc:error', failFast, String(args[0]))
304+
events.emit(DEVFRAME_EVENTS.client.error, failFast, String(args[0]))
304305
return
305306
}
306307
return serverRpc.$callEvent(
@@ -312,7 +313,7 @@ export function createLiveRpcClientMode(
312313
const method = String(args[0])
313314
const failFast = terminalError()
314315
if (failFast) {
315-
events.emit('rpc:error', failFast, method)
316+
events.emit(DEVFRAME_EVENTS.client.error, failFast, method)
316317
return Promise.reject(failFast)
317318
}
318319
return guardCall(

packages/devframe/src/client/rpc-shared-state.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { RpcSharedStateGetOptions, RpcSharedStateHost } from 'devframe/type
22
import type { SharedState, SharedStatePatch } from 'devframe/utils/shared-state'
33
import type { DevframeRpcClient } from './rpc'
44
import { createSharedState } from 'devframe/utils/shared-state'
5+
import { DEVFRAME_EVENTS } from '../events'
56

67
export function createRpcSharedStateClientHost(rpc: DevframeRpcClient): RpcSharedStateHost {
78
const sharedState = new Map<string, SharedState<any>>()
@@ -20,7 +21,7 @@ export function createRpcSharedStateClientHost(rpc: DevframeRpcClient): RpcShare
2021
}
2122

2223
rpc.client.register({
23-
name: 'devframe:rpc:client-state:updated',
24+
name: DEVFRAME_EVENTS.broadcast.clientStateUpdated,
2425
type: 'event',
2526
handler: (key: string, fullState: any, syncId: string) => {
2627
const state = sharedState.get(key)
@@ -31,7 +32,7 @@ export function createRpcSharedStateClientHost(rpc: DevframeRpcClient): RpcShare
3132
})
3233

3334
rpc.client.register({
34-
name: 'devframe:rpc:client-state:patch',
35+
name: DEVFRAME_EVENTS.broadcast.clientStatePatch,
3536
type: 'event',
3637
handler: (key: string, patches: SharedStatePatch[], syncId: string) => {
3738
const state = sharedState.get(key)
@@ -124,7 +125,7 @@ export function createRpcSharedStateClientHost(rpc: DevframeRpcClient): RpcShare
124125
if (!rpc.isTrusted) {
125126
resolve(state)
126127
let initialized = false
127-
rpc.events.on('rpc:is-trusted:updated', (isTrusted) => {
128+
rpc.events.on(DEVFRAME_EVENTS.client.isTrustedUpdated, (isTrusted) => {
128129
if (isTrusted && !initialized) {
129130
initialized = true
130131
initSharedState()

packages/devframe/src/client/rpc-streaming.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { StreamErrorPayload, StreamReader, StreamSink } from 'devframe/utils/streaming-channel'
22
import type { DevframeRpcClient } from './rpc'
33
import { createStreamReader, createStreamSink } from 'devframe/utils/streaming-channel'
4+
import { DEVFRAME_EVENTS } from '../events'
45

56
const STREAM_KEY_SEPARATOR = '\x1F'
67

@@ -46,7 +47,7 @@ export function createRpcStreamingClientHost(rpc: DevframeRpcClient): RpcStreami
4647
const uploads = new Map<string, StreamSink<any>>()
4748

4849
rpc.client.register({
49-
name: 'devframe:streaming:chunk',
50+
name: DEVFRAME_EVENTS.broadcast.streamingChunk,
5051
type: 'event',
5152
handler(channel: string, id: string, seq: number, chunk: any) {
5253
const reader = readers.get(streamKey(channel, id))
@@ -55,7 +56,7 @@ export function createRpcStreamingClientHost(rpc: DevframeRpcClient): RpcStreami
5556
})
5657

5758
rpc.client.register({
58-
name: 'devframe:streaming:end',
59+
name: DEVFRAME_EVENTS.broadcast.streamingEnd,
5960
type: 'event',
6061
handler(channel: string, id: string, error?: StreamErrorPayload) {
6162
const key = streamKey(channel, id)
@@ -68,7 +69,7 @@ export function createRpcStreamingClientHost(rpc: DevframeRpcClient): RpcStreami
6869
})
6970

7071
rpc.client.register({
71-
name: 'devframe:streaming:upload-cancel',
72+
name: DEVFRAME_EVENTS.broadcast.streamingUploadCancel,
7273
type: 'event',
7374
handler(channel: string, id: string) {
7475
const key = streamKey(channel, id)
@@ -87,7 +88,7 @@ export function createRpcStreamingClientHost(rpc: DevframeRpcClient): RpcStreami
8788
// OR the WS dropped briefly (state intact). Either way, sending `subscribe`
8889
// with `afterSeq: lastSeenSeq` is the right thing: the server replays
8990
// missed chunks if it has them, otherwise starts fresh.
90-
rpc.events.on('rpc:is-trusted:updated', (isTrusted) => {
91+
rpc.events.on(DEVFRAME_EVENTS.client.isTrustedUpdated, (isTrusted) => {
9192
if (!isTrusted)
9293
return
9394
for (const [key, reader] of readers) {
@@ -142,7 +143,7 @@ export function createRpcStreamingClientHost(rpc: DevframeRpcClient): RpcStreami
142143
})
143144
}
144145
else {
145-
const off = rpc.events.on('rpc:is-trusted:updated', (trusted) => {
146+
const off = rpc.events.on(DEVFRAME_EVENTS.client.isTrustedUpdated, (trusted) => {
146147
if (trusted) {
147148
off()
148149
if (readers.has(key) && !reader.cancelled && !reader.done) {

packages/devframe/src/constants.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import { DEVFRAME_EVENTS } from './events'
2+
3+
export { DEVFRAME_EVENTS } from './events'
4+
15
// Devframe runtime routes and static output conventions.
26
export const DEVFRAME_CONNECTION_META_FILENAME = '__connection.json'
37

@@ -88,7 +92,7 @@ export const DEVFRAME_VIEWER_ORIGIN_TOKEN_QUERY_PARAM = 'devframe_viewer_origin_
8892
* (`@devframes/hub-ui` does, in its iframe view). Payload shape:
8993
* `RemoteAssetsErrorMessage` (`devframe/types`).
9094
*/
91-
export const DEVFRAME_REMOTE_ASSETS_ERROR_MESSAGE_TYPE = 'devframe:remote-assets-error'
95+
export const DEVFRAME_REMOTE_ASSETS_ERROR_MESSAGE_TYPE: string = DEVFRAME_EVENTS.postMessage.remoteAssetsError
9296

9397
/**
9498
* Prefix that marks an RPC method as callable before a connection is

0 commit comments

Comments
 (0)