spike: Yjs CRDT integration for collaborative editing#2250
Draft
spike: Yjs CRDT integration for collaborative editing#2250
Conversation
Adds a Yjs collaboration layer that syncs Slate operations between editors via a shared Y.Doc instead of patch forwarding. Core implementation: - Slate→Yjs translator (apply-to-yjs.ts) for all operation types - Yjs→Slate translator (apply-to-slate.ts) for remote changes - Bidirectional conversion between Slate nodes and Y.XmlText (convert.ts) - withYjs plugin that buffers local ops and observes remote changes - Playground toggle for Yjs sync mode with shared Y.Doc provider Includes unit tests for basic operations and browser tests for concurrent editing scenarios. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Unit tests for: - Marks: add, remove, and multiple marks on text nodes - Inline objects: insert/remove stock-ticker inside blocks - Block objects: insert image block at root - Styles: h1→normal, normal→blockquote changes - Lists: add/remove listItem and level attributes - Conversion roundtrip for mixed text+inline and list blocks Gherkin scenarios for: - Concurrent typing in different blocks - One editor types while the other presses Enter Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Stage 5 - Y.Doc Tree Viewer:
- New `yjs-tree-viewer.tsx` subscribes to `sharedRoot.observeDeep`
and renders a live, collapsible tree of the Y.XmlText CRDT structure
- Color-coded: block types (blue), keys (gray), attributes (purple/amber),
text content (green)
- Appears as "Y.Doc" tab in inspector when Yjs Sync is enabled
Stage 6 - Operation Log:
- Added `onOperation` callback to `YjsEditorConfig` type
- `with-yjs.ts` calls it in both directions: local→Yjs (after flush)
and Yjs→Slate (after remote apply)
- New `yjs-operation-log.tsx` with provider + display component
- Direction badges ("→ Yjs" blue / "→ Slate" green), expandable op details
- Appears as "Ops" tab in inspector when Yjs Sync is enabled
Infrastructure:
- Moved `YjsProvider` up to wrap both editors and inspector
- Exported `YjsContext` for tree viewer access
- Wrapped with `YjsOperationLogProvider` for shared log state
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Stage 7 - Latency Simulator:
- New `yjs-latency-provider.tsx`: creates per-editor Y.Doc instances
that sync via `Y.Doc.on('update')` → `setTimeout` → `Y.applyUpdate`
- Configurable delay (0–2000ms) via slider in header
- When latency > 0, each editor gets its own Y.Doc with delayed sync
instead of the shared Y.Doc, demonstrating CRDT conflict resolution
- When latency = 0, editors share a single Y.Doc (original behavior)
Changes:
- `feature-flags.ts`: add `yjsLatency: number` to PlaygroundFeatureFlags
- `playground-machine.ts`: add `set yjs latency` event
- `header.tsx`: add latency slider (TimerIcon + range input), visible
only when Yjs Sync is enabled
- `yjs-plugin.tsx`: accept `editorIndex` and `useLatency` props,
switch between shared and per-editor Y.Docs
- `editor.tsx`: pass `editorIndex` and `useLatency` to PlaygroundYjsPlugin
- `editors.tsx`: wrap with LatencyYjsProvider
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two fixes: 1. Latency provider: register cross-sync handlers synchronously during doc creation (useMemo) instead of in useEffect. React fires child effects before parent effects, so editors were connecting and pushing content to independent Y.Docs before any cross-sync handlers existed. Also use a ref for latencyMs to avoid recreating docs when the slider moves. 2. with-yjs: move `editor.normalize()` inside `withoutPatching` and `pluginWithoutHistory` in both `handleYjsObserve` and `connect()`. Nodes created from Yjs deltas lack `_key` properties. Normalization adds keys via `set_node` ops, which previously generated patches targeting undefined values because patching was still active. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a new "CRDT" inspector tab that visualizes Yjs sync between editors: - Status bar showing converged/diverged state with in-flight count - Packet lanes showing animated dots with countdown for in-flight updates - Event log with color-coded send/deliver entries and latency annotations The latency provider now tracks in-flight updates and exposes a subscription API for CRDT events via context. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add offline/online toggle per editor to simulate network disconnection in the Yjs playground. Offline editors stop relaying Y.Doc updates; reconnecting triggers a state-vector sync to merge diverged content. Also simplifies YjsPlugin by removing the redundant YjsProvider/YjsContext in favor of the LatencyYjsProvider, and uses a closure-local `isConnected` flag in `withYjs` instead of the public `isYjsConnected` property. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Spike exploring Yjs as a CRDT collaboration layer for the Portable Text Editor. Instead of forwarding patches between editors, this uses a shared Y.Doc that keeps editors in sync via Yjs's conflict-free merge semantics.
Core implementation
apply-to-yjs.ts): Translates all Slate operation types (insert_text,remove_text,insert_node,remove_node,set_node,split_node,merge_node,move_node) into Y.XmlText mutationsapply-to-slate.ts): Converts YjsobserveDeepevents back into Slate operations for remote changesconvert.ts): Converts between Slate node trees and Y.XmlText delta formatwithYjsplugin (with-yjs.ts): Buffers local ops duringapply, flushes to Y.Doc ononChange, observes remote changes viaobserveDeepWhat works
What's next (this PR)
Test plan
npx vitest --run --project unit src/yjs/pnpm --filter @portabletext/editor test:browser:chromium🤖 Generated with Claude Code