docs: document event bus and wire app consumers

- Update CHANGELOG.md with event bus, test restructuring, and lint hardening
- Document EditorEvents table and conventions in AGENTS.md
- Wire collab graph sync to editor event bus instead of SceneGraph.onNodeEvents
- Wire collab awareness zoom to viewport:changed instead of Vue watcher
- Wire collab selection broadcasting to selection:changed instead of Vue watcher
This commit is contained in:
Danila Poyarkov 2026-05-06 15:38:45 +03:00
parent 1835903e56
commit 7dce333200
5 changed files with 54 additions and 29 deletions

View file

@ -49,8 +49,8 @@ Runtime `canvaskit-wasm` import exists only in `canvaskit.ts` — all other file
| Module | What |
|---|---|
| `types.ts` | EditorState, EditorOptions, Tool, EditorToolDef, EditorContext |
| `create.ts` | `createEditor()` assembler — wires context + all modules |
| `types.ts` | EditorState, EditorOptions, EditorEvents, Tool, EditorToolDef, EditorContext |
| `create.ts` | `createEditor()` assembler — wires context, event bus + all modules |
| `viewport.ts` | screenToCanvas, applyZoom, pan, zoomToFit/100/Selection |
| `selection.ts` | select, clearSelection, marquee, snap, hover, entered container |
| `pages.ts` | switchPage, addPage, deletePage, renamePage |
@ -66,6 +66,29 @@ Each module exports a factory: `createXxxActions(ctx: EditorContext) => { ... }`
`create.ts` assembles context + all modules, spreads into a flat return object.
`Editor` type = `ReturnType<typeof createEditor>`.
#### Editor event bus
The editor exposes a typed nanoevents emitter for lifecycle events. Defined in `EditorEvents` (`types.ts`), emitted via `emitEditorEvent()` on the context, subscribed via `editor.onEditorEvent(event, handler)` which returns an unbind function.
| Event | Payload | Emitted by |
|---|---|---|
| `render:requested` | `{ renderVersion, sceneVersion }` | `requestRender()` |
| `repaint:requested` | `{ renderVersion, sceneVersion }` | `requestRepaint()` |
| `graph:replaced` | `SceneGraph` | `replaceGraph()` |
| `node:created` | `SceneNode` | SceneGraph emitter → `graph-events.ts` |
| `node:updated` | `id, changes` | SceneGraph emitter → `graph-events.ts` |
| `node:deleted` | `id` | SceneGraph emitter → `graph-events.ts` |
| `node:reparented` | `nodeId, oldParentId, newParentId` | SceneGraph emitter → `graph-events.ts` |
| `node:reordered` | `nodeId, parentId, index` | SceneGraph emitter → `graph-events.ts` |
| `selection:changed` | `selectedIds[], previousIds[]` | `setSelectedIds()` |
| `tool:changed` | `tool, previousTool` | `setActiveTool()` |
| `page:changed` | `pageId, previousPageId` | `switchPage()`, `replaceGraph()` |
| `viewport:changed` | `{ panX, panY, zoom }, previous` | viewport actions |
All selection mutations in core use `ctx.setSelectedIds()` and all tool changes use `ctx.setActiveTool()` so the event bus fires consistently. App-layer code uses `editor.clearSelection()`, `editor.select()`, or `editor.setTool()` — never direct `state.selectedIds =` or `state.activeTool =` assignments.
Vue SDK provides `useEditorEvent(event, handler)` composable (`packages/vue/src/editor/events/use.ts`) that auto-disposes on scope cleanup.
The app editor session (`src/app/editor/session/create.ts`) is a thin Vue wrapper: creates `shallowReactive` state, calls `createEditor()`, and assembles app-specific modules for document I/O, autosave, export, vector edit, pen resume, flashes, profiler, and mobile clipboard. Tabs live in `src/app/tabs/`; active editor access lives in `src/app/editor/active-store/`.
## Commands

View file

@ -2,8 +2,16 @@
## Unreleased
### Added
- Add editor event bus with typed lifecycle events — `selection:changed`, `tool:changed`, `page:changed`, `viewport:changed`, `graph:replaced`, `render:requested`, `repaint:requested`, and all scene graph node events. Subscribe via `editor.onEditorEvent()` in core or `useEditorEvent()` composable in the Vue SDK.
### Changed
- Route all selection and tool mutations through `setSelectedIds()` / `setActiveTool()` in the editor core, replacing direct `state.selectedIds` / `state.activeTool` assignments. This ensures event bus subscribers receive consistent change notifications.
- Split large test files into focused domain folders and remove all `max-lines`, `complexity`, and `no-empty-function` test lint exceptions.
- Enforce `no-sibling-domain-prefixed-files` lint rule project-wide — files with prefixes matching an existing sibling folder must be moved into that folder.
- Refactor the editor architecture across core, app, Vue SDK, CLI, MCP, docs, and desktop into smaller domain modules with structural lint rules to keep package boundaries explicit.
- Add targeted core subpath exports and package-local import aliases for cleaner app, Vue SDK, CLI, and MCP imports.
- Split the canvas into separate scene and overlay render layers so rulers, labels, selections, and input overlays are isolated from scene rendering.

View file

@ -1,5 +1,4 @@
import type { Room } from 'trystero'
import { watch } from 'vue'
import type { Ref } from 'vue'
import { IndexeddbPersistence } from 'y-indexeddb'
import * as awarenessProtocol from 'y-protocols/awareness'
@ -136,19 +135,16 @@ export function createCollabConnectionActions({
}
export function watchAwarenessZoom(store: EditorStore, getAwareness: () => Awareness | null) {
return watch(
() => store.state.zoom,
(zoom) => {
const awareness = getAwareness()
if (!awareness) return
const prev = awareness.getLocalState()?.cursor as
| { x: number; y: number; pageId: string; zoom: number }
| undefined
if (prev) {
awareness.setLocalStateField('cursor', { ...prev, zoom })
}
return store.onEditorEvent('viewport:changed', (viewport) => {
const awareness = getAwareness()
if (!awareness) return
const prev = awareness.getLocalState()?.cursor as
| { x: number; y: number; pageId: string; zoom: number }
| undefined
if (prev) {
awareness.setLocalStateField('cursor', { ...prev, zoom: viewport.zoom })
}
)
})
}
export function connectCollabSession({

View file

@ -76,12 +76,12 @@ export function bindCollabGraphEvents({
}
}
return store.graph.onNodeEvents({
updated: (id) => onGraphMutation(id),
created: (node) => onGraphMutation(node.id),
reparented: (nodeId) => onGraphMutation(nodeId),
reordered: (nodeId) => onGraphMutation(nodeId),
deleted: (id) => {
const unbinds = [
store.onEditorEvent('node:updated', (id) => onGraphMutation(id)),
store.onEditorEvent('node:created', (node) => onGraphMutation(node.id)),
store.onEditorEvent('node:reparented', (nodeId) => onGraphMutation(nodeId)),
store.onEditorEvent('node:reordered', (nodeId) => onGraphMutation(nodeId)),
store.onEditorEvent('node:deleted', (id) => {
const ydoc = getYdoc()
const ynodes = getYnodes()
if (!getSuppressGraphSync() && ydoc && ynodes) {
@ -91,8 +91,11 @@ export function bindCollabGraphEvents({
})
setSuppressYjsEvents(false)
}
}
})
})
]
return () => {
for (const unbind of unbinds) unbind()
}
}
export function registerYjsObservers({

View file

@ -1,5 +1,3 @@
import { watch } from 'vue'
import type { useCollabInjected } from '@/app/collab/use'
import type { EditorStore } from '@/app/editor/active-store'
@ -12,10 +10,7 @@ export function useCanvasCollaborationAwareness(store: EditorStore, collab: Coll
collab?.updateCursor(cx, cy, store.state.currentPageId)
}
watch(
() => [...store.state.selectedIds],
(ids) => collab?.updateSelection(ids)
)
store.onEditorEvent('selection:changed', (ids) => collab?.updateSelection(ids))
return { updateCursor }
}