Ver Fonte

update specs

Yuki Takei há 2 semanas atrás
pai
commit
fcf034e2e2

+ 11 - 6
.kiro/specs/collaborative-editor-awareness/design.md

@@ -80,7 +80,7 @@ graph TB
 |-------|------------------|-----------------|-------|
 | Editor extensions | `y-codemirror.next@0.3.5` | `yCollab` for text-sync and undo; `yRemoteSelectionsTheme` for base caret CSS | No version change; `yRemoteSelections` no longer used |
 | Cursor rendering | CodeMirror `ViewPlugin` + `WidgetType` (`@codemirror/view`) | DOM-based cursor widget with avatar `<img>` | No new dependency |
-| Awareness | `y-websocket` `awareness` object | State read (`getStates`) and write (`setLocalStateField`) | Unchanged |
+| Awareness | `y-websocket` `awareness` object | State read (`getStates`) and write (`setLocalStateField`) | `Awareness` type derived via `WebsocketProvider['awareness']` — `y-protocols` is not a direct dependency |
 
 ## System Flows
 
@@ -111,11 +111,15 @@ sequenceDiagram
 
     CM->>RC: update(ViewUpdate)
     RC->>AW: setLocalStateField('cursor', {anchor, head})
-    AW-->>RC: awareness.on('change') fires
+    Note over AW,RC: awareness fires 'change' — but changeListener<br/>ignores events where only the local client changed
+    AW-->>RC: awareness.on('change') for REMOTE client
+    RC->>CM: dispatch with yRichCursorsAnnotation
+    CM->>RC: update(ViewUpdate) — triggered by annotation
     RC->>RC: rebuild decorations from state.editors + state.cursor
-    RC->>CM: dispatch with new DecorationSet
 ```
 
+**Annotation-driven update strategy**: The awareness `change` listener does not call `view.dispatch()` unconditionally — doing so would crash with "Calls to EditorView.update are not allowed while an update is in progress" because `setLocalStateField` in the `update()` method itself triggers an awareness `change` event synchronously. Instead, the listener filters by `clientID`: it dispatches (with a `yRichCursorsAnnotation`) only when at least one **remote** client's state has changed. Local-only awareness changes (from the cursor broadcast in the same `update()` cycle) are silently ignored, and the decoration set is rebuilt in the next `update()` call naturally.
+
 ## Requirements Traceability
 
 | Requirement | Summary | Components | Key Interfaces |
@@ -204,10 +208,11 @@ sequenceDiagram
 - Selection highlight (background color from `state.editors.colorLight`) is rendered alongside the caret widget
 
 **Dependencies**
-- External: `@codemirror/view` `ViewPlugin`, `WidgetType`, `Decoration` (P0)
-- External: `@codemirror/state` `RangeSet`, `Annotation` (P0)
+- External: `@codemirror/view` `ViewPlugin`, `WidgetType`, `Decoration`, `EditorView` (P0)
+- External: `@codemirror/state` `RangeSet`, `Annotation` (P0) — `Annotation.define<number[]>()` used for `yRichCursorsAnnotation`
 - External: `yjs` `createRelativePositionFromTypeIndex`, `createAbsolutePositionFromRelativePosition` (P0)
 - External: `y-codemirror.next` `ySyncFacet` (to access `ytext` for position conversion) (P0)
+- External: `y-websocket` — `Awareness` type derived via `WebsocketProvider['awareness']` (not `y-protocols/awareness`, which is not a direct dependency) (P0)
 - Inbound: `provider.awareness` passed as parameter (P0)
 
 **Contracts**: Service [x]
@@ -234,7 +239,7 @@ Postconditions:
 
 Invariants:
 - Local client's own cursor is never rendered
-- Cursor decorations are invalidated and rebuilt on every awareness `change` event affecting cursor or editors fields
+- Cursor decorations are rebuilt when awareness `change` fires for **remote** clients (dispatched via `yRichCursorsAnnotation`); local-only changes are ignored to prevent recursive `dispatch` during an in-progress update
 - `state.cursor` field is written exclusively by `yRichCursors`; no other plugin or code path may call `awareness.setLocalStateField('cursor', ...)` to avoid data races
 
 ##### Widget DOM Structure

+ 2 - 0
.kiro/specs/collaborative-editor-awareness/research.md

@@ -100,6 +100,8 @@
 - `yRichCursors` broadcasts cursor positions via `awareness.setLocalStateField('cursor', ...)` on every `update` call — same as the original `yRemoteSelections`. Throttle is not needed because Yjs awareness batches broadcasts internally.
 - Avatar `<img>` may fail to load (404, CORS) — mitigate with `onerror` handler that replaces the `<img>` with initials fallback span.
 - `awareness.getStates().delete()` removal: confirm Yjs v13 awareness `update` event fires after removing the client from the internal map (verified in Yjs source: removal happens before the event).
+- **Recursive dispatch crash** (discovered during implementation): `setLocalStateField('cursor', ...)` inside the `update()` method fires an awareness `change` event **synchronously**. If the `change` listener calls `view.dispatch()` unconditionally, CodeMirror throws "Calls to EditorView.update are not allowed while an update is in progress". Mitigated by filtering the `change` listener to dispatch only when at least one **remote** client is in the changed set (`clients.findIndex(id => id !== awareness.doc.clientID) >= 0`). This matches the same pattern used by `y-remote-selections.js` in `y-codemirror.next`.
+- **`y-protocols` not a direct dependency**: `y-protocols/awareness` exports the `Awareness` class, but neither `@growi/editor` nor `apps/app` list `y-protocols` as a direct dependency. `import type { Awareness } from 'y-protocols/awareness'` fails under strict pnpm resolution. Mitigated by deriving the type from the existing `y-websocket` dependency: `type Awareness = WebsocketProvider['awareness']`.
 
 ## References
 

+ 1 - 1
.kiro/specs/collaborative-editor/design.md

@@ -77,7 +77,7 @@ graph TB
 |-------|------------------|------|
 | Client Provider | `y-websocket@^2.x` (WebsocketProvider) | Yjs document sync over WebSocket |
 | Server WebSocket | `ws@^8.x` (WebSocket.Server) | Native WebSocket server, `noServer: true` mode |
-| Server Yjs Utils | `y-websocket@^2.x` (`bin/utils`) | `setupWSConnection`, `getYDoc`, `WSSharedDoc` |
+| Server Yjs Utils | `y-websocket@^2.x` (`bin/utils`) | `setupWSConnection`, `getYDoc`, `WSSharedDoc`. Server-side type declarations (`y-websocket-server.d.ts`) derive the `Awareness` type via `WebsocketProvider['awareness']` instead of importing from `y-protocols/awareness`, because `y-protocols` is not a direct dependency. |
 | Persistence | `y-mongodb-provider` (extended) | Yjs document persistence to `yjs-writings` collection |
 | Event Bridge | Socket.IO `io` instance | Awareness state broadcasting to page rooms |
 | Auth | express-session + passport | WebSocket upgrade authentication via cookie |