@n8n/crdt
v0.2.0
Published
CRDT abstraction layer for n8n collaborative editing
Maintainers
Keywords
Readme
@n8n/crdt
CRDT abstraction layer for n8n collaborative editing. Provides a unified API built on Yjs for real-time document synchronization.
Quick Start
import { createCRDTProvider, CRDTEngine } from '@n8n/crdt';
// Create provider
const provider = createCRDTProvider({ engine: CRDTEngine.yjs });
// Create document
const doc = provider.createDoc('workflow-123');
// Use data structures
const nodes = doc.getMap('nodes');
nodes.set('node-1', { position: { x: 100, y: 200 } });
// Plain objects are returned as-is (no automatic CRDT wrapping)
const node = nodes.get('node-1'); // Returns { position: { x: 100, y: 200 } }
// To update nested data, replace the whole object
nodes.set('node-1', { position: { x: 150, y: 200 } });
// Observe changes
nodes.onDeepChange((changes) => {
for (const change of changes) {
console.log(change.path, change.action, change.value);
// ['node-1'], 'update', { position: { x: 150, y: 200 } }
}
});Plain Objects and Sync
Plain objects stored in CRDT structures do sync across peers, but they sync as atomic values (last-write-wins), not as collaborative structures:
// Both peers start synced
mapA.set('node', { x: 100, y: 200 });
// After sync: mapB.get('node') → { x: 100, y: 200 }
// Concurrent edits to the same key = conflict (one wins)
mapA.set('node', { x: 150, y: 200 }); // Peer A changes x
mapB.set('node', { x: 100, y: 250 }); // Peer B changes y
// After sync: both get { x: 150, y: 200 } OR { x: 100, y: 250 }
// One write wins entirely - changes are NOT merged
// For fine-grained collaborative editing, use explicit CRDT structures:
const nodeX = doc.getMap('node-x'); // Separate CRDT map for x values
const nodeY = doc.getMap('node-y'); // Separate CRDT map for y valuesThis "no magic" design matches raw Yjs behavior and keeps the API predictable.
Sync
import { createSyncProvider, MockTransport } from '@n8n/crdt';
// Create linked transports
const transportA = new MockTransport();
const transportB = new MockTransport();
MockTransport.link(transportA, transportB);
// Create sync providers
const syncA = createSyncProvider(docA, transportA);
const syncB = createSyncProvider(docB, transportB);
// Start sync
await syncA.start();
await syncB.start();
// Changes now propagate automaticallyAPI
Core Types
CRDTProvider- Factory for creating documentsCRDTDoc- Document container withgetMap(),getArray(),transact()CRDTMap<T>- Key-value CRDT structure with deep change observationCRDTArray<T>- Ordered list CRDT structure
Change Events
DeepChangeEvent- Map changes withpath,action,value,oldValueArrayChangeEvent- Array changes in Quill delta format (retain,insert,delete)
Sync
SyncProvider- Manages document synchronizationSyncTransport- Transport interface for moving binary dataMockTransport- In-memory transport for testingMessagePortTransport- SharedWorker/Worker/MessageChannel communicationWebSocketTransport- Server sync with auto-reconnect
Transports
All transports implement the same SyncTransport interface:
interface SyncTransport {
send(data: Uint8Array): void;
onReceive(handler: (data: Uint8Array) => void): Unsubscribe;
connect(): Promise<void>;
disconnect(): void;
readonly connected: boolean;
}WebSocket Transport
import { WebSocketTransport, createSyncProvider } from '@n8n/crdt';
const transport = new WebSocketTransport({
url: 'wss://server/sync',
reconnect: true,
reconnectDelay: 1000,
maxReconnectAttempts: 10,
});
transport.onConnectionChange((connected) => {
console.log('Connection state:', connected);
});
const sync = createSyncProvider(doc, transport);
await sync.start();MessagePort Transport (SharedWorker)
import { MessagePortTransport, createSyncProvider } from '@n8n/crdt';
// In main thread
const worker = new SharedWorker('worker.js');
const transport = new MessagePortTransport(worker.port);
const sync = createSyncProvider(doc, transport);
await sync.start();Not Yet Implemented
The following CRDT types are not yet part of this abstraction:
- Text - For collaborative text editing (rich text, code editors)
- Counter - For conflict-free increment/decrement operations
These can be added in future phases if needed.
