From 7dce3332006506324c76fb24c9370afb80e4a8f3 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Wed, 6 May 2026 15:38:45 +0300 Subject: [PATCH] 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 --- AGENTS.md | 27 +++++++++++++++++-- CHANGELOG.md | 8 ++++++ src/app/collab/session.ts | 22 +++++++-------- src/app/collab/yjs-sync.ts | 19 +++++++------ .../editor/canvas/collaboration-awareness.ts | 7 +---- 5 files changed, 54 insertions(+), 29 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 55519edc8..a808fac53 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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`. +#### 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e35d528a..fedf142f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/app/collab/session.ts b/src/app/collab/session.ts index 3a34487a3..ff2264cf0 100644 --- a/src/app/collab/session.ts +++ b/src/app/collab/session.ts @@ -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({ diff --git a/src/app/collab/yjs-sync.ts b/src/app/collab/yjs-sync.ts index 3d7c0b4fe..65d3fcdf3 100644 --- a/src/app/collab/yjs-sync.ts +++ b/src/app/collab/yjs-sync.ts @@ -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({ diff --git a/src/app/editor/canvas/collaboration-awareness.ts b/src/app/editor/canvas/collaboration-awareness.ts index 0cc1d79c9..4f8547d15 100644 --- a/src/app/editor/canvas/collaboration-awareness.ts +++ b/src/app/editor/canvas/collaboration-awareness.ts @@ -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 } }