Skip to content

Commit f9577fd

Browse files
authored
refactor: unify event names + centralize them in DEVFRAME_EVENTS / HUB_EVENTS maps (#258)
1 parent bc557f8 commit f9577fd

41 files changed

Lines changed: 468 additions & 132 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎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/.vitepress/config.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ function guideGroups(prefix: string) {
5050
{ text: 'Serve a Hub Anywhere', link: `${prefix}/guide/hub-initiate` },
5151
{ text: 'Cross-Plugin Services', link: `${prefix}/guide/services` },
5252
{ text: 'Deep Linking', link: `${prefix}/guide/deep-linking` },
53+
{ text: 'Events Reference', link: `${prefix}/guide/events` },
5354
],
5455
},
5556
{

‎docs/guide/events.md‎

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# Events Reference
6+
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.
8+
9+
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`.
10+
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
16+
17+
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.
18+
19+
| Event | Emitted by | Consumed by | Payload |
20+
|---|---|---|---|
21+
| `docks:entry:updated` | `DocksHost.register` / `update` | context → `devframe:docks` shared state | `DevframeDockUserEntry` |
22+
| `docks:activate` | `DocksHost.activate()` | context → broadcast + `devframe:docks:active` | `DevframeDockActivation` |
23+
| `terminals:session:updated` | `TerminalsHost` register / update / remove / status change | context → `devframe:terminals:updated`; terminals plugin | `DevframeTerminalSession` |
24+
| `messages:added` / `messages:updated` / `messages:removed` / `messages:cleared` | `MessagesHost` mutations | context → `devframe:messages:updated`; messages plugin | entry / entry / id / — |
25+
| `commands:registered` / `commands:unregistered` | `CommandsHost` register / update / unregister | context → `devframe:commands` shared state | entry / id |
26+
27+
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.
28+
29+
### Server RPC methods — client → server
30+
31+
A connected client (any mounted iframe or panel, on its own RPC client) calls these; the hub node handles them. Carry the `hub:` prefix.
32+
33+
| Method | Signature | Purpose |
34+
|---|---|---|
35+
| `hub:docks:activate` | `({ dockId, params? }) => void` | Ask the viewer to switch its active dock — see [Deep Linking](./deep-linking). |
36+
| `hub:commands:execute` | `(id, ...args) => unknown` | Invoke a registered server command by id. |
37+
| `hub:messages:add` | `(input) => DevframeMessageEntry` | Add a message to the feed (marked `from: 'browser'`). |
38+
| `hub:messages:update` | `(id, patch) => DevframeMessageEntry \| undefined` | Patch a message by id. |
39+
| `hub:messages:remove` | `(id) => void` | Remove a message by id. |
40+
| `hub:messages:clear` | `() => void` | Remove every message. |
41+
| `hub:terminals:write` | `(id, data) => void` | Send input to an interactive PTY session. |
42+
| `hub:terminals:resize` | `(id, cols, rows) => void` | Resize an interactive PTY session. |
43+
| `hub:terminals:terminate` | `(id) => void` | Kill a session's process, keeping it registered. |
44+
| `hub:terminals:restart` | `(id) => void` | Re-run a session's command in place. |
45+
| `hub:terminals:remove` | `(id) => void` | Kill a session's process and drop it from the registry. |
46+
47+
### Broadcasts & shared state — server → client
48+
49+
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.
50+
51+
| Name | Kind | Carries |
52+
|---|---|---|
53+
| `devframe:docks:activate` | broadcast | Live "switch active dock" request — the client host calls its local `switchEntry`. |
54+
| `devframe:terminals:updated` | broadcast | Terminal sessions changed; re-read terminal state. |
55+
| `devframe:messages:updated` | broadcast | Message list changed; re-read message state. |
56+
| `devframe:docks` | shared state | Projected dock entry list (`DevframeDockEntry[]`). |
57+
| `devframe:docks:active` | shared state | Most recent `DevframeDockActivation`, so a dock that mounts in response still converges on it. |
58+
| `devframe:commands` | shared state | Serializable command list, handlers stripped (`DevframeServerCommandEntry[]`). |
59+
| `devframe:user-settings` | shared state | Persisted per-workspace hub settings (`DevframeDocksUserSettings`). |
60+
| `devframe:terminals` | streaming channel | Live terminal output stream, keyed by session id. |
61+
62+
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.

‎docs/guide/hub.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ A hub-aware UI doesn't import any hub classes; it reads three shared-state keys
275275
| `hub:commands:execute` RPC | `(id, ...args) => unknown` | Server-side command dispatch. |
276276
| `hub:docks:activate` RPC | `({ dockId, params? }) => void` | Switch the active dock from any client. |
277277

278-
Plus broadcast notifications (`devframe:docks:activate`, `devframe:terminals:updated`, `devframe:messages:updated`) that a UI can subscribe to via `rpc.client.register(...)`. The client host registers the `devframe:docks:activate` handler for you.
278+
Plus broadcast notifications (`devframe:docks:activate`, `devframe:terminals:updated`, `devframe:messages:updated`) that a UI can subscribe to via `rpc.client.register(...)`. The client host registers the `devframe:docks:activate` handler for you. The [Events Reference](./events) tables every channel across all four subsystems.
279279

280280
## Running plugin code in the host page
281281

‎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()

0 commit comments

Comments
 (0)