yjs-collaboration
Yjs CRDT collaboration patterns for real-time and offline-first editing. Use when building collaborative documents, shared scratchpads, or multi-user editing. Covers Y.Doc, Y.Map, Y.Array, Y.Text, state encoding, structured patches, server-side document manipulation, and conflict-free merge semantics.
- License 许可
- MIT
- 最近更新
- 2026-11-11
- Stars
- ★ 2
- Forks
- 0
01 前言 Front Matter
- name
- yjs-collaboration
- license
- MIT
- metadata
- { "author": "RevealUI Studio", "version": "0.1.0", "website": "https://revealui.com" }
- description
- Yjs CRDT collaboration patterns for real-time and offline-first editing. Use when building collaborative documents, shared scratchpads, or multi-user editing. Covers Y.Doc, Y.Map, Y.Array, Y.Text, state encoding, structured patches, server-side document manipulation, and conflict-free merge semantics.
- allowed-tools
- Read, Grep, Glob
03 SKILL.md 正文
Yjs CRDT Collaboration
Core Concepts
Yjs is a CRDT framework. Multiple peers can edit concurrently and merge without conflicts. Every operation is commutative and idempotent.
import * as Y from 'yjs';
const doc = new Y.Doc();
const root = doc.getMap('root');
Type System
| Type | Use Case | API |
|---|---|---|
Y.Map |
Key-value store | set(key, val), get(key), delete(key) |
Y.Array |
Ordered list | push([item]), insert(idx, [item]), delete(idx, len) |
Y.Text |
Rich/plain text | insert(idx, str), delete(idx, len), toString() |
Nested types: Y.Map values can be other shared types.
const root = doc.getMap('root');
// String values
root.set('title', 'My Document');
// Nested Y.Text
const findings = new Y.Text();
findings.insert(0, 'Initial finding...');
root.set('findings', findings);
// Nested Y.Array
const items = new Y.Array();
items.push(['Item 1', 'Item 2']);
root.set('items', items);
State Encoding & Persistence
// Encode full state as a binary update
const state = Y.encodeStateAsUpdate(doc); // Uint8Array
const vector = Y.encodeStateVector(doc); // Uint8Array
// Persist to database (Postgres bytea column)
await db.update(documents)
.set({
state: Buffer.from(state),
stateVector: Buffer.from(vector),
})
.where(eq(documents.id, docId));
// Restore from database
const doc = new Y.Doc();
Y.applyUpdate(doc, new Uint8Array(storedState));
Server-Side Document Manipulation
For CLI agents or batch processes that cannot use WebSocket:
import * as Y from 'yjs';
function applyStructuredPatch(
existingState: Uint8Array | null,
patchType: string,
path: string,
content: string,
): { state: Uint8Array; stateVector: Uint8Array } {
const doc = new Y.Doc();
if (existingState) {
Y.applyUpdate(doc, existingState);
}
const root = doc.getMap('root');
switch (patchType) {
case 'set_key':
root.set(path, content);
break;
case 'append_section': {
let section = root.get(path);
if (!(section instanceof Y.Text)) {
section = new Y.Text();
root.set(path, section);
}
section.insert(section.length, content);
break;
}
case 'append_item': {
let arr = root.get(path);
if (!(arr instanceof Y.Array)) {
arr = new Y.Array();
root.set(path, arr);
}
arr.push([content]);
break;
}
case 'replace_section': {
const text = new Y.Text();
text.insert(0, content);
root.set(path, text);
break;
}
}
const result = {
state: Y.encodeStateAsUpdate(doc),
stateVector: Y.encodeStateVector(doc),
};
doc.destroy();
return result;
}
Reading Document Content
function readDocument(state: Uint8Array): Record<string, unknown> {
const doc = new Y.Doc();
Y.applyUpdate(doc, state);
const root = doc.getMap('root');
const result: Record<string, unknown> = {};
for (const [key, value] of root.entries()) {
if (value instanceof Y.Text) {
result[key] = value.toString();
} else if (value instanceof Y.Array) {
result[key] = value.toArray();
} else {
result[key] = value;
}
}
doc.destroy();
return result;
}
Merging Two Documents
// Two docs edited independently
const doc1 = new Y.Doc();
const doc2 = new Y.Doc();
// ... edits happen on each ...
// Merge: apply each doc's state to the other
const state1 = Y.encodeStateAsUpdate(doc1);
const state2 = Y.encodeStateAsUpdate(doc2);
Y.applyUpdate(doc1, state2); // doc1 now has both edits
Y.applyUpdate(doc2, state1); // doc2 now has both edits
// Both docs are identical after merge
Database Schema
CREATE TABLE yjs_documents (
id TEXT PRIMARY KEY,
state BYTEA NOT NULL,
state_vector BYTEA,
connected_clients INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
WebSocket Provider (Real-Time)
For browser agents that need live character-by-character collaboration:
import { WebsocketProvider } from 'y-websocket';
const doc = new Y.Doc();
const provider = new WebsocketProvider(
'wss://collab.example.com',
'document-id',
doc,
);
provider.on('status', ({ status }) => {
console.log('Connection:', status); // 'connecting' | 'connected' | 'disconnected'
});
Awareness (Presence)
const awareness = provider.awareness;
// Set local state (cursor position, user info)
awareness.setLocalState({
user: { name: 'Agent A', color: '#ff0000' },
cursor: { index: 42, length: 0 },
});
// Listen for remote state changes
awareness.on('change', () => {
const states = awareness.getStates();
// Map<clientId, { user, cursor }>
});
Best Practices
- Always call
doc.destroy()after server-side operations to free memory - Use
Buffer.from(state)when storing in Postgres bytea columns - Use
new Uint8Array(buffer)when reading from Postgres bytea columns - Prefer
Y.Textover plain strings for content that may be collaboratively edited - Use
Y.Arrayfor ordered lists (append-heavy patterns) - Use
Y.Mapas the root container for structured documents - Server-side patches and WebSocket edits merge cleanly via CRDT semantics
04 同仓库更多 Skills
good-skill
A minimal valid skill fixture used to prove the skills-lint and plugin-lint validators accept well formed input. Covers frontmatter shape only.
bad-skill-missing-field
暂无简介
bad-skill-awk
Fixture skill whose fenced shell block violates lint-skill.awk. skills-lint must reject it.
vitest-testing
Vitest testing patterns for production TypeScript projects. Use when writing tests, fixing test failures, configuring coverage, mocking modules, debugging flaky tests, or setting up Vitest in a monorepo. Covers Vitest 3+, vi.mock, vi.fn, React Testing Library, PGlite in-memory PostgreSQL, pool forks, maxWorkers, and hookTimeout.
tailwind-v4
Tailwind CSS v4 patterns, CSS-first configuration, and v3 migration guide. Use when writing styles, configuring @theme variables, creating components with CVA, migrating from tailwind.config.js, or using container queries, dark mode, and new v4 utility syntax. Covers @import tailwindcss, custom properties, oklch colors, and clsx.
security-hardening
Web application security hardening for OWASP Top 10 vulnerabilities. Use when implementing authentication, HTTP security headers, CORS, rate limiting, input validation, or reviewing code for XSS, SQL injection, CSRF, or SSRF. Covers CSP, HSTS, session cookies, bcrypt, Zod validation, and secrets management.