Skip to content

Commit 1e4a2bb

Browse files
committed
refactor(hub): unify internal event names and document event channels
Rename the internal EventEmitter events on the subsystem hosts to the plural subsystem vocabulary already used across every wire name, so the bus lines up with its RPC/broadcast/shared-state counterparts: - dock:activate -> docks:activate - dock:entry:updated -> docks:entry:updated - terminal:session:updated -> terminals:session:updated - message:* -> messages:* - command:* -> commands:* Add a dedicated Hub Events Reference docs page tabling the three event channels (internal node bus, hub: server RPC, devframe: broadcasts and shared state) and their scope/flow. Wire names (hub:*, devframe:*) are public API and stay unchanged.
1 parent 3672417 commit 1e4a2bb

18 files changed

Lines changed: 118 additions & 59 deletions

File tree

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

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/hub/src/node/__tests__/host-docks.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ describe('devframeDockHost grouping', () => {
8585
it('registers a group entry: stored, projected, and emitted', () => {
8686
const host = new DevframeDocksHost(createContext())
8787
const emitted: string[] = []
88-
host.events.on('dock:entry:updated', entry => emitted.push(entry.id))
88+
host.events.on('docks:entry:updated', entry => emitted.push(entry.id))
8989

9090
host.register({
9191
type: 'group',
@@ -192,12 +192,12 @@ describe('devframeDockHost grouping', () => {
192192
})
193193

194194
describe('devframeDockHost activate', () => {
195-
it('emits a dock:activate event carrying the id and params', () => {
195+
it('emits a docks:activate event carrying the id and params', () => {
196196
const host = new DevframeDocksHost(createContext())
197197
host.register({ type: 'iframe', id: 'terminals', title: 'Terminals', icon: 'ph:terminal-window-duotone', url: '/__terminals/' })
198198

199199
const activations: Array<{ dockId: string, params?: Record<string, unknown> }> = []
200-
host.events.on('dock:activate', a => activations.push(a))
200+
host.events.on('docks:activate', a => activations.push(a))
201201

202202
host.activate('terminals', { sessionId: 'sess-1' })
203203
expect(activations).toEqual([{ dockId: 'terminals', params: { sessionId: 'sess-1' } }])
@@ -208,7 +208,7 @@ describe('devframeDockHost activate', () => {
208208
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
209209
try {
210210
const activations: string[] = []
211-
host.events.on('dock:activate', a => activations.push(a.dockId))
211+
host.events.on('docks:activate', a => activations.push(a.dockId))
212212

213213
host.activate('nope')
214214
expect(activations).toEqual(['nope'])

packages/hub/src/node/__tests__/host-terminals.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ describe('devframeTerminalHost child-process status lifecycle', () => {
261261
it('marks status stopped and emits an update on a clean exit', async () => {
262262
const { host } = createTerminalHost()
263263
const updates: string[] = []
264-
host.events.on('terminal:session:updated', s => updates.push(s.status))
264+
host.events.on('terminals:session:updated', s => updates.push(s.status))
265265

266266
const session = await host.startChildProcess({
267267
command: process.execPath,
@@ -441,7 +441,7 @@ describe('devframeTerminalHost PTY status lifecycle', () => {
441441
itPty('marks status stopped and emits an update on a clean exit', async () => {
442442
const { host } = createTerminalHost()
443443
const updates: string[] = []
444-
host.events.on('terminal:session:updated', s => updates.push(s.status))
444+
host.events.on('terminals:session:updated', s => updates.push(s.status))
445445

446446
const session = await host.startPtySession({
447447
command: NODE,

packages/hub/src/node/context.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ export async function createHubContext(options: CreateHubContextOptions): Promis
155155
const refreshDocks = debounce(() => {
156156
docksSharedState.mutate(() => docks.values())
157157
}, debounceMs)
158-
docks.events.on('dock:entry:updated', refreshDocks)
158+
docks.events.on('docks:entry:updated', refreshDocks)
159159
// A remote iframe dock registered before the WS transport finishes binding
160160
// (the common case: `initHub` installs devframes — and their docks — before
161161
// resolving an async side-car/shared-server port) gets projected without a
@@ -174,7 +174,7 @@ export async function createHubContext(options: CreateHubContextOptions): Promis
174174
'devframe:docks:active',
175175
{ initialValue: { activation: null } },
176176
)
177-
docks.events.on('dock:activate', (activation) => {
177+
docks.events.on('docks:activate', (activation) => {
178178
activeDockSharedState.mutate((state) => {
179179
state.activation = activation
180180
})
@@ -191,7 +191,7 @@ export async function createHubContext(options: CreateHubContextOptions): Promis
191191
})
192192
docksSharedState.mutate(() => docks.values())
193193
}, debounceMs)
194-
terminals.events.on('terminal:session:updated', broadcastTerminals)
194+
terminals.events.on('terminals:session:updated', broadcastTerminals)
195195

196196
const broadcastMessages = debounce(() => {
197197
context.rpc.broadcast({
@@ -200,17 +200,17 @@ export async function createHubContext(options: CreateHubContextOptions): Promis
200200
})
201201
docksSharedState.mutate(() => docks.values())
202202
}, debounceMs)
203-
messages.events.on('message:added', broadcastMessages)
204-
messages.events.on('message:updated', broadcastMessages)
205-
messages.events.on('message:removed', broadcastMessages)
206-
messages.events.on('message:cleared', broadcastMessages)
203+
messages.events.on('messages:added', broadcastMessages)
204+
messages.events.on('messages:updated', broadcastMessages)
205+
messages.events.on('messages:removed', broadcastMessages)
206+
messages.events.on('messages:cleared', broadcastMessages)
207207

208208
const commandsSharedState = await context.rpc.sharedState.get('devframe:commands', { initialValue: [] })
209209
const syncCommands = debounce(() => {
210210
commandsSharedState.mutate(() => commands.list())
211211
}, debounceMs)
212-
commands.events.on('command:registered', syncCommands)
213-
commands.events.on('command:unregistered', syncCommands)
212+
commands.events.on('commands:registered', syncCommands)
213+
commands.events.on('commands:unregistered', syncCommands)
214214

215215
commandsSharedState.mutate(() => commands.list())
216216

packages/hub/src/node/host-commands.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export class DevframeCommandsHost implements DevframeCommandsHostType {
7676
validateCommandIds(this.commands, command)
7777
this.validateAgentExposure(command)
7878
this.commands.set(command.id, command)
79-
this.events.emit('command:registered', this.toSerializable(command))
79+
this.events.emit('commands:registered', this.toSerializable(command))
8080
this.agentProvider?.notifyChanged()
8181

8282
return {
@@ -97,7 +97,7 @@ export class DevframeCommandsHost implements DevframeCommandsHostType {
9797
validateCommandIds(this.commands, next, existing.id)
9898
this.validateAgentExposure(next)
9999
Object.assign(existing, patch)
100-
this.events.emit('command:registered', this.toSerializable(existing))
100+
this.events.emit('commands:registered', this.toSerializable(existing))
101101
this.agentProvider?.notifyChanged()
102102
},
103103
unregister: () => this.unregister(command.id),
@@ -107,7 +107,7 @@ export class DevframeCommandsHost implements DevframeCommandsHostType {
107107
unregister(id: string): boolean {
108108
const deleted = this.commands.delete(id)
109109
if (deleted) {
110-
this.events.emit('command:unregistered', id)
110+
this.events.emit('commands:unregistered', id)
111111
this.agentProvider?.notifyChanged()
112112
}
113113
return deleted

packages/hub/src/node/host-docks.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export class DevframeDocksHost implements DevframeDocksHostType {
9191
this.validateGroupMembership(view)
9292
this.prepareRemoteRegistration(view)
9393
this.views.set(view.id, view)
94-
this.events.emit('dock:entry:updated', view)
94+
this.events.emit('docks:entry:updated', view)
9595

9696
return {
9797
update: (patch) => {
@@ -115,7 +115,7 @@ export class DevframeDocksHost implements DevframeDocksHostType {
115115
this.validateGroupMembership(view)
116116
this.prepareRemoteRegistration(view)
117117
this.views.set(view.id, view)
118-
this.events.emit('dock:entry:updated', view)
118+
this.events.emit('docks:entry:updated', view)
119119
}
120120

121121
activate(dockId: string, params?: Record<string, unknown>): void {
@@ -125,7 +125,7 @@ export class DevframeDocksHost implements DevframeDocksHostType {
125125
// rather than fatal.
126126
if (!this.views.has(dockId))
127127
diagnostics.DF8107({ id: dockId })
128-
this.events.emit('dock:activate', { dockId, params })
128+
this.events.emit('docks:activate', { dockId, params })
129129
}
130130

131131
private validateGroupMembership(view: DevframeDockUserEntry): void {

packages/hub/src/node/host-messages.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export class DevframeMessagesHost implements DevframeMessagesHostType {
6969

7070
this.entries.set(entry.id, entry)
7171
this.lastModified.set(entry.id, this._tick())
72-
this.events.emit('message:added', entry)
72+
this.events.emit('messages:added', entry)
7373

7474
if (entry.autoDelete) {
7575
this._autoDeleteTimers.set(entry.id, setTimeout(() => {
@@ -95,7 +95,7 @@ export class DevframeMessagesHost implements DevframeMessagesHostType {
9595

9696
this.entries.set(id, updated)
9797
this.lastModified.set(id, this._tick())
98-
this.events.emit('message:updated', updated)
98+
this.events.emit('messages:updated', updated)
9999

100100
// Reset autoDelete timer if changed
101101
if (patch.autoDelete !== undefined) {
@@ -123,7 +123,7 @@ export class DevframeMessagesHost implements DevframeMessagesHostType {
123123
this.entries.delete(id)
124124
this.lastModified.delete(id)
125125
this._recordRemoval(id, this._tick())
126-
this.events.emit('message:removed', id)
126+
this.events.emit('messages:removed', id)
127127
}
128128

129129
info(message: string, extra?: DevframeMessageShortcutInput): Promise<DevframeMessageHandle> {
@@ -155,7 +155,7 @@ export class DevframeMessagesHost implements DevframeMessagesHostType {
155155
this._recordRemoval(id, tick)
156156
this.entries.clear()
157157
this.lastModified.clear()
158-
this.events.emit('message:cleared')
158+
this.events.emit('messages:cleared')
159159
}
160160

161161
listSince(since?: number | null): DevframeMessagesListDelta {

packages/hub/src/node/host-terminals.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export class DevframeTerminalsHost implements DevframeTerminalsHostType {
7171
}
7272
this.sessions.set(session.id, session)
7373
this.bindStream(session)
74-
this.events.emit('terminal:session:updated', session)
74+
this.events.emit('terminals:session:updated', session)
7575
return session
7676
}
7777

@@ -83,13 +83,13 @@ export class DevframeTerminalsHost implements DevframeTerminalsHostType {
8383
Object.assign(session, patch)
8484
this.sessions.set(patch.id, session)
8585
this.bindStream(session)
86-
this.events.emit('terminal:session:updated', session)
86+
this.events.emit('terminals:session:updated', session)
8787
}
8888

8989
remove(session: DevframeTerminalSession): void {
9090
this._boundStreams.get(session.id)?.dispose()
9191
this.sessions.delete(session.id)
92-
this.events.emit('terminal:session:updated', session)
92+
this.events.emit('terminals:session:updated', session)
9393
this._boundStreams.delete(session.id)
9494
}
9595

@@ -185,7 +185,7 @@ export class DevframeTerminalsHost implements DevframeTerminalsHostType {
185185
if (session.status === next)
186186
return
187187
session.status = next
188-
this.events.emit('terminal:session:updated', session)
188+
this.events.emit('terminals:session:updated', session)
189189
}
190190

191191
const closeStream = () => {
@@ -376,7 +376,7 @@ export class DevframeTerminalsHost implements DevframeTerminalsHostType {
376376
if (session.status === next)
377377
return
378378
session.status = next
379-
this.events.emit('terminal:session:updated', session)
379+
this.events.emit('terminals:session:updated', session)
380380
}
381381

382382
const closeStream = () => {

0 commit comments

Comments
 (0)