@dabble/patches

中文文档
0.34.2发布于 2026/10/27MITTypeScriptES Module近 30 天下载 2113

Immutable JSON Patch implementation based on RFC 6902 supporting operational transformation and last-writer-wins

原始使用文档
文档目录31 节
  1. What Problem Does This Solve?
  2. Scope: what Patches is and is not
  3. Two Sync Strategies
  4. Table of Contents
  5. Why Operational Transformations?
  6. Key Concepts
  7. Installation
  8. Getting Started
  9. Client Example
  10. Server Example
  11. LWW Quick Start
  12. Core Components
  13. Client Side
  14. Server Side
  15. Networking
  16. Awareness (Presence & Cursors)
  17. Basic Workflow
  18. Client
  19. Server
  20. Examples
  21. Complete Client Setup
  22. Using Both Strategies
  23. Advanced Topics
  24. Versioning & History
  25. Branching
  26. SharedWorker
  27. Framework Integrations
  28. Custom OT Operations
  29. JSON Patch
  30. Contributing
  31. License

Patches

A TypeScript library for building real-time collaborative applications. You get two sync strategies: Operational Transformation for collaborative text, and Last-Write-Wins for everything else.

Patches the Dog

What Problem Does This Solve?

Building real-time collaborative features is hard. Users edit simultaneously, connections drop mid-change, and conflict resolution gets gnarly fast. Patches handles all of this so you don't have to.

Your document state is just JSON. Change it with a simple callback:

doc.change(state => (state.title = 'New Title'));

Changes apply immediately for snappy UIs, then sync to the server in the background. Offline? No problem. Changes queue up and sync when you're back online.

Scope: what Patches is and is not

Patches is a generic primitives library. It provides the building blocks (OT, LWW, sync transport, doc-store interfaces) for realtime systems. Compose it into your own backend and frontend.

Patches knows about:

  • JSON documents, JSON Patch operations, and snapshots
  • Operational Transformation and Last-Write-Wins conflict resolution
  • A transport surface (WebSocket, SSE+REST, WebRTC) that moves changes between clients and a server
  • HTTP status codes propagated as StatusError (401, 403, 404, 410) — so consuming apps can branch on the response

Patches does not know about:

  • Users, roles, invites, memberships, or any access-control model
  • Apps, projects, books, documents-as-products, or any app-specific terminology
  • Email sending, name/email lookups, or any identity service
  • UX policy for what to do when access is lost (consuming apps decide whether a 403 means "show a banner", "kick out of the doc", or anything else)

If a feature feels like it belongs in Patches but mentions an app concept (e.g. "an invite for a project"), it doesn't belong here — it belongs one level up, in the app's own client SDK or in the consuming app itself. The Patches surface should stay generic enough to back any realtime system.

Two Sync Strategies

Patches gives you two conflict resolution approaches. Pick the right tool for the job.

Operational Transformation (OT) - When users edit the same content simultaneously

  • Changes get intelligently merged
  • Required for collaborative text editing
  • Example: Google Docs-style collaboration

Last-Write-Wins (LWW) - When the latest timestamp should win

  • Simpler, faster, more predictable
  • Perfect for settings, dashboards, canvas objects
  • Figma uses this approach for their multiplayer

The decision is simple: If users aren't editing the same text collaboratively, use LWW. It's faster, easier to debug, and handles most real-time scenarios perfectly.

Need ordered lists with LWW? Use fractional indexing to maintain order without OT.

Most apps use both strategies: OT for document content, LWW for everything else.

Table of Contents

Why Operational Transformations?

"Shouldn't I use CRDTs instead?"

There are lots of opinions about this. Here's what we learned at Dabble Writer: CRDTs don't scale for long-lived documents.

Some of our users have projects with 480,000+ operations. These monsters took hours to rebuild in Y.js, ~4 seconds to load in optimized Y.js, and ~20ms to add a change. With our OT library? 1-2ms to load and 0.2ms to apply a change.

As documents grow larger or live longer, OT performance stays flat while CRDTs slow down. For most use cases, CRDTs work fine. But if you're building for scale or longevity, OT wins.

Key Concepts

Centralized OT - A server acts as the single source of truth. No peer-to-peer complexity, no vector clocks, no distributed consensus headaches. The server sees all changes in order and broadcasts the canonical state.

Rebasing - When the server has new changes your client hasn't seen, your pending changes get "rebased" on top. Think git rebase, but for real-time edits.

Linear History - The server maintains one straight timeline. No branches, no forks, no merge conflicts at the infrastructure level.

Snapshots - OT documents accumulate changes over time. To avoid replaying 480k operations on load, we snapshot periodically. Load the latest snapshot, apply recent changes, done.

Immutable State - Every change creates a new state object. Unchanged parts stay unchanged. This makes React/Vue/Solid rendering trivial and enables cheap equality checks.

Read more: Operational Transformation deep dive | Algorithm functions

Installation

npm install @dabble/patches

Getting Started

Client Example

import { Patches, OTStrategy, InMemoryStore } from '@dabble/patches';
import { PatchesSync } from '@dabble/patches/net';

interface MyDoc {
  text: string;
  count: number;
}

// 1. Create a strategy with its store
const strategy = new OTStrategy(new InMemoryStore());

// 2. Create the Patches instance
const patches = new Patches({
  strategies: { ot: strategy },
  defaultStrategy: 'ot',
});

// 3. Set up real-time sync
const sync = new PatchesSync(patches, 'wss://your-server-url');
await sync.connect();

// 4. Open a document
const doc = await patches.openDoc<MyDoc>('my-doc-1');

// 5. React to updates
doc.subscribe(newState => {
  console.log('Document updated:', newState);
  // Update your UI here
});

// 6. Make changes - they sync automatically
doc.change(draft => {
  draft.text = 'Hello World!';
  draft.count = (draft.count || 0) + 1;
});

See Patches, PatchesDoc, and PatchesSync for full API documentation.

Server Example

import express from 'express';
import { OTServer } from '@dabble/patches/server';

// Your backend store implementation
const store = new MyOTStoreBackend();
const server = new OTServer(store);

const app = express();
app.use(express.json());

// Get document state
app.get('/docs/:docId', async (req, res) => {
  const { state, rev } = await server.getDoc(req.params.docId);
  res.json({ state: state ?? {}, rev });
});

// Commit changes
app.post('/docs/:docId/changes', async (req, res) => {
  try {
    const changes = await server.commitChanges(req.params.docId, req.body.changes);
    res.json(changes);
    // Broadcast to other clients via WebSocket
  } catch (error) {
    const status = error.message.includes('out of sync') ? 409 : 500;
    res.status(status).json({ error: error.message });
  }
});

app.listen(3000);

See OTServer for full API documentation.

LWW Quick Start

For Last-Write-Wins sync, use LWW-specific stores and strategies:

// Client
import { Patches, LWWStrategy, LWWInMemoryStore } from '@dabble/patches';
import { PatchesSync } from '@dabble/patches/net';

const strategy = new LWWStrategy(new LWWInMemoryStore());
const patches = new Patches({
  strategies: { lww: strategy },
  defaultStrategy: 'lww',
});

const sync = new PatchesSync(patches, 'wss://your-server-url');
await sync.connect();

const doc = await patches.openDoc<UserPrefs>('user-prefs');

doc.change(draft => {
  draft.theme = 'dark';
  draft.fontSize = 16;
});
// Server
import { LWWServer } from '@dabble/patches/server';

const store = new MyLWWStoreBackend();
const server = new LWWServer(store);

app.post('/docs/:docId/changes', async (req, res) => {
  const result = await server.commitChanges(req.params.docId, req.body.changes);
  res.json(result);
});

See LWWServer and Last-Write-Wins concepts for more details.

Core Components

Client Side

Patches - Main entry point. Manages document lifecycle, coordinates strategies, handles persistence.

PatchesDoc - A single collaborative document. Tracks state, applies changes optimistically, emits update events.

PatchesSync - WebSocket connection manager. Handles reconnection, batching, and bidirectional sync.

Strategies - Algorithm-specific logic:

  • OTStrategy - Owns an OTClientStore, handles rebasing and change tracking
  • LWWStrategy - Owns an LWWClientStore, handles timestamp consolidation

Stores - Persistence adapters:

  • InMemoryStore / LWWInMemoryStore - For testing and simple apps
  • OTIndexedDBStore / LWWIndexedDBStore - Browser persistence with offline support

Server Side

OTServer - OT authority. Transforms concurrent changes, assigns revisions, maintains history.

LWWServer - LWW authority. Compares timestamps, stores current field values, no history.

PatchesHistoryManager - Query document versions and history.

PatchesBranchManager - Create, list, and merge branches.

Backend Stores - You implement these interfaces for your database:

  • OTStoreBackend - For OT: changes, snapshots, versions
  • LWWStoreBackend - For LWW: fields with timestamps, snapshots

See Persistence for storage patterns and Backend Store Interface for implementation details.

Networking

WebSocket Transport - Standard server-mediated communication via PatchesWebSocket.

WebRTC Transport - Peer-to-peer for awareness features (cursors, presence).

JSON-RPC Protocol - The wire protocol between client and server.

When to use which? WebSocket for document sync. WebRTC for presence/cursors to reduce server load. See Networking overview.

Awareness (Presence & Cursors)

Show who's online, where their cursor is, what they're selecting. WebRTCAwareness carries presence over peer-to-peer WebRTC connections, with the signaling handshake multiplexed over whatever sync channel you already have open — WebSocketTransport or PatchesREST (SSE+REST). No second connection, same clientId.

See Awareness documentation for implementation details.

Basic Workflow

Client

  1. Create a Patches instance with strategies
  2. Connect PatchesSync to your server
  3. Open documents with patches.openDoc(docId)
  4. Subscribe to updates with doc.subscribe()
  5. Make changes with doc.change() - they sync automatically

Server

  1. Create OTServer or LWWServer with your backend store
  2. Handle commitChanges() requests
  3. Broadcast committed changes to other clients
  4. Optionally use PatchesHistoryManager for versioning and PatchesBranchManager for branching

Examples

Complete Client Setup

import { Patches, OTStrategy, OTIndexedDBStore } from '@dabble/patches';
import { PatchesSync } from '@dabble/patches/net';

interface MyDoc {
  title: string;
  content: string;
}

// Production setup with IndexedDB for offline support
const strategy = new OTStrategy(new OTIndexedDBStore('my-app'));
const patches = new Patches({
  strategies: { ot: strategy },
});

const sync = new PatchesSync(patches, 'wss://api.example.com/sync');

// Handle connection state
sync.subscribe(state => {
  if (state.connected) {
    console.log('Connected and syncing');
  } else if (!state.online) {
    console.log('Offline - changes saved locally');
  }
});

// Handle errors
sync.onError((error, context) => {
  console.error(`Sync error for ${context?.docId}:`, error);
});

await sync.connect();

// Open and use a document
const doc = await patches.openDoc<MyDoc>('doc-123');

doc.subscribe(state => {
  renderUI(state);
});

doc.change(draft => {
  draft.title = 'My Document';
  draft.content = 'Hello, world!';
});

Using Both Strategies

import { Patches, OTStrategy, LWWStrategy, InMemoryStore, LWWInMemoryStore } from '@dabble/patches';

// Configure both strategies
const patches = new Patches({
  strategies: {
    ot: new OTStrategy(new InMemoryStore()),
    lww: new LWWStrategy(new LWWInMemoryStore()),
  },
  defaultStrategy: 'ot',
});

// OT for collaborative document editing
const manuscript = await patches.openDoc('manuscript-123'); // Uses default (ot)

// LWW for user settings
const settings = await patches.openDoc('settings-user-456', { strategy: 'lww' });

Advanced Topics

Versioning & History

Documents automatically snapshot after 30 minutes of inactivity. Browse versions with PatchesHistoryManager.

See OTServer Versioning and PatchesHistoryManager.

Branching

Create document branches, work in isolation, merge back. Useful for "what if" scenarios or staged editing.

See Branching and PatchesBranchManager.

SharedWorker

Run Patches in a SharedWorker for cross-tab coordination and reduced memory usage.

See SharedWorker documentation.

Framework Integrations

Custom OT Operations

Extend the operation handlers for domain-specific transformations.

See Operation Handlers.

JSON Patch

Patches uses JSON Patch (RFC 6902) under the hood. You rarely need to work with it directly, but it's there.

See JSON Patch documentation.

Contributing

Contributions welcome. Open issues or submit pull requests.

License

MIT

版本与发布记录

@dabble/patches 共发布 191 个版本,首次发布于 2025/04/19;过去 12 个月发布了 151 个版本。

版本发布日期距今
0.34.2latest2026/10/2718 天前
0.34.12026/10/071 个月前
0.34.02026/10/071 个月前
0.33.02026/10/011 个月前
0.32.32026/09/291 个月前
0.32.22026/09/261 个月前

关于 @dabble/patches 的常见问题

@dabble/patches 是什么?

Immutable JSON Patch implementation based on RFC 6902 supporting operational transformation and last-writer-wins

如何安装 @dabble/patches?

在项目目录运行 npm install @dabble/patches,或使用 pnpm add / yarn add / bun add。

@dabble/patches 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@dabble/patches 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@dabble/patches 可以商用吗?

@dabble/patches 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@dabble/patches 的下载量有多少?

根据 npm 官方下载统计,@dabble/patches 在 2026/10/11 – 2026/11/09 这 30 天内被下载 2,113 次,日均约 70 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@dabble/patches 还在维护吗?

最新版本 0.34.2 发布于 2026/10/27(18 天前);至今共发布 191 个版本,其中过去 12 个月发布了 151 个;首次发布于 2025/04/19。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。