AGENT SKILL

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.Text over plain strings for content that may be collaboratively edited
  • Use Y.Array for ordered lists (append-heavy patterns)
  • Use Y.Map as the root container for structured documents
  • Server-side patches and WebSocket edits merge cleanly via CRDT semantics

04 同仓库更多 Skills

revealui-studio
revealui-studio/revskills

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.

Skill与智能体开发
★ 2
revealui-studio
revealui-studio/revskills

bad-skill-missing-field

暂无简介

Skill与智能体开发
★ 2
revealui-studio
revealui-studio/revskills

bad-skill-awk

Fixture skill whose fenced shell block violates lint-skill.awk. skills-lint must reject it.

Skill与智能体开发
★ 2
revealui-studio
revealui-studio/revskills

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.

内容创作者测试与质量写作与文案
★ 2
revealui-studio
revealui-studio/revskills

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.

★ 2
revealui-studio
revealui-studio/revskills

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.

★ 2