@dabble/patches 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +221 -208
- package/dist/BaseDoc-DkP3tUhT.d.ts +206 -0
- package/dist/algorithms/client/applyCommittedChanges.d.ts +7 -0
- package/dist/algorithms/client/applyCommittedChanges.js +6 -3
- package/dist/algorithms/lww/consolidateOps.d.ts +40 -0
- package/dist/algorithms/lww/consolidateOps.js +103 -0
- package/dist/algorithms/lww/index.d.ts +2 -0
- package/dist/algorithms/lww/index.js +1 -0
- package/dist/algorithms/lww/mergeServerWithLocal.d.ts +22 -0
- package/dist/algorithms/lww/mergeServerWithLocal.js +32 -0
- package/dist/algorithms/server/commitChanges.d.ts +32 -8
- package/dist/algorithms/server/commitChanges.js +20 -5
- package/dist/algorithms/server/createVersion.d.ts +1 -1
- package/dist/algorithms/server/getSnapshotAtRevision.d.ts +1 -1
- package/dist/algorithms/server/getStateAtRevision.d.ts +1 -1
- package/dist/algorithms/server/handleOfflineSessionsAndBatches.d.ts +1 -1
- package/dist/client/BaseDoc.d.ts +6 -0
- package/dist/client/BaseDoc.js +70 -0
- package/dist/client/ClientAlgorithm.d.ts +101 -0
- package/dist/client/ClientAlgorithm.js +0 -0
- package/dist/client/InMemoryStore.d.ts +5 -7
- package/dist/client/InMemoryStore.js +6 -35
- package/dist/client/IndexedDBStore.d.ts +39 -73
- package/dist/client/IndexedDBStore.js +17 -220
- package/dist/client/LWWAlgorithm.d.ts +43 -0
- package/dist/client/LWWAlgorithm.js +87 -0
- package/dist/client/LWWClientStore.d.ts +73 -0
- package/dist/client/LWWClientStore.js +0 -0
- package/dist/client/LWWDoc.d.ts +56 -0
- package/dist/client/LWWDoc.js +84 -0
- package/dist/client/LWWInMemoryStore.d.ts +88 -0
- package/dist/client/LWWInMemoryStore.js +208 -0
- package/dist/client/LWWIndexedDBStore.d.ts +91 -0
- package/dist/client/LWWIndexedDBStore.js +275 -0
- package/dist/client/OTAlgorithm.d.ts +42 -0
- package/dist/client/OTAlgorithm.js +113 -0
- package/dist/client/OTClientStore.d.ts +50 -0
- package/dist/client/OTClientStore.js +0 -0
- package/dist/client/OTDoc.d.ts +6 -0
- package/dist/client/OTDoc.js +97 -0
- package/dist/client/OTIndexedDBStore.d.ts +84 -0
- package/dist/client/OTIndexedDBStore.js +163 -0
- package/dist/client/Patches.d.ts +36 -16
- package/dist/client/Patches.js +60 -27
- package/dist/client/PatchesDoc.d.ts +4 -113
- package/dist/client/PatchesDoc.js +3 -153
- package/dist/client/PatchesStore.d.ts +8 -105
- package/dist/client/factories.d.ts +72 -0
- package/dist/client/factories.js +80 -0
- package/dist/client/index.d.ts +14 -5
- package/dist/client/index.js +9 -0
- package/dist/compression/index.d.ts +1 -1
- package/dist/data/change.js +2 -0
- package/dist/fractionalIndex.d.ts +67 -0
- package/dist/fractionalIndex.js +241 -0
- package/dist/index.d.ts +13 -3
- package/dist/index.js +1 -0
- package/dist/json-patch/types.d.ts +2 -0
- package/dist/net/PatchesClient.js +15 -15
- package/dist/net/PatchesSync.d.ts +24 -12
- package/dist/net/PatchesSync.js +56 -64
- package/dist/net/index.d.ts +6 -10
- package/dist/net/index.js +6 -1
- package/dist/net/protocol/JSONRPCClient.d.ts +4 -4
- package/dist/net/protocol/JSONRPCClient.js +6 -4
- package/dist/net/protocol/JSONRPCServer.d.ts +45 -9
- package/dist/net/protocol/JSONRPCServer.js +63 -8
- package/dist/net/serverContext.d.ts +38 -0
- package/dist/net/serverContext.js +20 -0
- package/dist/net/webrtc/WebRTCTransport.js +1 -1
- package/dist/net/websocket/AuthorizationProvider.d.ts +3 -3
- package/dist/net/websocket/WebSocketServer.d.ts +29 -20
- package/dist/net/websocket/WebSocketServer.js +23 -12
- package/dist/server/BranchManager.d.ts +50 -0
- package/dist/server/BranchManager.js +0 -0
- package/dist/server/CompressedStoreBackend.d.ts +7 -5
- package/dist/server/CompressedStoreBackend.js +3 -9
- package/dist/server/LWWBranchManager.d.ts +82 -0
- package/dist/server/LWWBranchManager.js +99 -0
- package/dist/server/LWWMemoryStoreBackend.d.ts +78 -0
- package/dist/server/LWWMemoryStoreBackend.js +191 -0
- package/dist/server/LWWServer.d.ts +130 -0
- package/dist/server/LWWServer.js +207 -0
- package/dist/server/{PatchesBranchManager.d.ts → OTBranchManager.d.ts} +32 -12
- package/dist/server/{PatchesBranchManager.js → OTBranchManager.js} +25 -40
- package/dist/server/OTServer.d.ts +108 -0
- package/dist/server/OTServer.js +141 -0
- package/dist/server/PatchesHistoryManager.d.ts +20 -7
- package/dist/server/PatchesHistoryManager.js +26 -3
- package/dist/server/PatchesServer.d.ts +70 -81
- package/dist/server/PatchesServer.js +0 -175
- package/dist/server/branchUtils.d.ts +82 -0
- package/dist/server/branchUtils.js +66 -0
- package/dist/server/index.d.ts +17 -6
- package/dist/server/index.js +33 -4
- package/dist/server/tombstone.d.ts +29 -0
- package/dist/server/tombstone.js +32 -0
- package/dist/server/types.d.ts +128 -26
- package/dist/server/utils.d.ts +12 -0
- package/dist/server/utils.js +23 -0
- package/dist/solid/context.d.ts +5 -4
- package/dist/solid/doc-manager.d.ts +3 -3
- package/dist/solid/index.d.ts +5 -4
- package/dist/solid/primitives.d.ts +2 -3
- package/dist/types.d.ts +4 -2
- package/dist/vue/composables.d.ts +2 -3
- package/dist/vue/doc-manager.d.ts +3 -3
- package/dist/vue/index.d.ts +5 -4
- package/dist/vue/provider.d.ts +5 -4
- package/package.json +1 -1
- package/dist/algorithms/client/collapsePendingChanges.d.ts +0 -30
- package/dist/algorithms/client/collapsePendingChanges.js +0 -78
- package/dist/net/websocket/RPCServer.d.ts +0 -141
- package/dist/net/websocket/RPCServer.js +0 -204
package/README.md
CHANGED
|
@@ -1,26 +1,42 @@
|
|
|
1
1
|
# Patches
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
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.
|
|
4
4
|
|
|
5
5
|
<img src="./patches.png" alt="Patches the Dog" style="width: 300px;">
|
|
6
6
|
|
|
7
|
-
## What
|
|
7
|
+
## What Problem Does This Solve?
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
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.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Your document state is just JSON. Change it with a simple callback:
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
```js
|
|
14
|
+
doc.change(state => (state.title = 'New Title'));
|
|
15
|
+
```
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
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.
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
## Two Sync Strategies
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
Patches gives you two conflict resolution approaches. Pick the right tool for the job.
|
|
22
|
+
|
|
23
|
+
**[Operational Transformation (OT)](./docs/operational-transformation.md)** - When users edit the same content simultaneously
|
|
24
|
+
|
|
25
|
+
- Changes get intelligently merged
|
|
26
|
+
- Required for collaborative text editing
|
|
27
|
+
- Example: Google Docs-style collaboration
|
|
28
|
+
|
|
29
|
+
**[Last-Write-Wins (LWW)](./docs/last-write-wins.md)** - When the latest timestamp should win
|
|
30
|
+
|
|
31
|
+
- Simpler, faster, more predictable
|
|
32
|
+
- Perfect for settings, dashboards, canvas objects
|
|
33
|
+
- [Figma uses this approach](https://www.figma.com/blog/how-figmas-multiplayer-technology-works/) for their multiplayer
|
|
34
|
+
|
|
35
|
+
**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.
|
|
36
|
+
|
|
37
|
+
Need ordered lists with LWW? Use [fractional indexing](./docs/fractional-indexing.md) to maintain order without OT.
|
|
22
38
|
|
|
23
|
-
|
|
39
|
+
Most apps use both strategies: OT for document content, LWW for everything else.
|
|
24
40
|
|
|
25
41
|
## Table of Contents
|
|
26
42
|
|
|
@@ -30,69 +46,50 @@ And bam! You get a fresh new state with your changes applied.
|
|
|
30
46
|
- [Getting Started](#getting-started)
|
|
31
47
|
- [Client Example](#client-example)
|
|
32
48
|
- [Server Example](#server-example)
|
|
49
|
+
- [LWW Quick Start](#lww-quick-start)
|
|
33
50
|
- [Core Components](#core-components)
|
|
34
51
|
- [Basic Workflow](#basic-workflow)
|
|
35
52
|
- [Examples](#examples)
|
|
36
53
|
- [Advanced Topics](#advanced-topics)
|
|
37
|
-
- [JSON Patch (Legacy)](#json-patch-legacy)
|
|
38
54
|
- [Contributing](#contributing)
|
|
39
55
|
- [License](#license)
|
|
40
56
|
|
|
41
57
|
## Why Operational Transformations?
|
|
42
58
|
|
|
43
|
-
|
|
59
|
+
"Shouldn't I use CRDTs instead?"
|
|
44
60
|
|
|
45
|
-
|
|
61
|
+
There are [lots](https://thom.ee/blog/crdt-vs-operational-transformation/) of [opinions](https://www.tiny.cloud/blog/real-time-collaboration-ot-vs-crdt/) about [this](https://fiberplane.com/blog/why-we-at-fiberplane-use-operational-transformation-instead-of-crdt/). Here's what we learned at [Dabble Writer](https://www.dabblewriter.com/): CRDTs don't scale for long-lived documents.
|
|
46
62
|
|
|
47
|
-
Some of our users have projects with
|
|
63
|
+
Some of our users have projects with 480,000+ operations. These monsters took hours to rebuild in [Y.js](https://yjs.dev/), ~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.**
|
|
48
64
|
|
|
49
|
-
As
|
|
65
|
+
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.
|
|
50
66
|
|
|
51
67
|
## Key Concepts
|
|
52
68
|
|
|
53
|
-
|
|
54
|
-
- **Rebasing:** Client changes get "rebased" on top of server changes. Like git rebase, but for your real-time edits!
|
|
55
|
-
- **Linear History:** The server keeps one straight timeline of revisions. No timeline branches = no headaches.
|
|
56
|
-
- **Client-Server Dance:** Clients send batches of changes tagged with the server revision they're based on. The server transforms them, applies them, gives them a new revision number, and broadcasts them back.
|
|
69
|
+
**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.
|
|
57
70
|
|
|
58
|
-
**
|
|
71
|
+
**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.
|
|
59
72
|
|
|
60
|
-
|
|
73
|
+
**Linear History** - The server maintains one straight timeline. No branches, no forks, no merge conflicts at the infrastructure level.
|
|
61
74
|
|
|
62
|
-
|
|
75
|
+
**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.
|
|
63
76
|
|
|
64
|
-
**
|
|
77
|
+
**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.
|
|
65
78
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
That's why we snapshot the data every so often. Grab the latest snapshot, add recent changes, and you're good to go! This is how OT maintains consistent performance over time.
|
|
69
|
-
|
|
70
|
-
**Versions as Snapshots**
|
|
71
|
-
|
|
72
|
-
Most collaborative work happens in bursts. We combine snapshots with versions by creating new snapshots when there's a 30+ minute gap between changes. This clever trick turns a technical requirement into a user-facing feature – versioning!
|
|
73
|
-
|
|
74
|
-
**Immutable State**
|
|
75
|
-
|
|
76
|
-
Patches uses gentleman's immutability – each change creates a new object, keeping unchanged objects as-is and only replacing what changed. This brings tons of [benefits](https://www.freecodecamp.org/news/immutable-javascript-improve-application-performance/) for [performance](http://www.cowtowncoder.com/blog/archives/2010/08/entry_409.html) and [code quality](https://medium.com/@mohitgadhavi1/the-power-of-immutability-improving-javascript-performance-and-code-quality-96d82134d8da).
|
|
79
|
+
Read more: [Operational Transformation deep dive](./docs/operational-transformation.md) | [Algorithm functions](./docs/algorithms.md)
|
|
77
80
|
|
|
78
81
|
## Installation
|
|
79
82
|
|
|
80
83
|
```bash
|
|
81
84
|
npm install @dabble/patches
|
|
82
|
-
# or
|
|
83
|
-
yarn add @dabble/patches
|
|
84
85
|
```
|
|
85
86
|
|
|
86
87
|
## Getting Started
|
|
87
88
|
|
|
88
|
-
Let's set up a basic client and server. (These examples are simplified – real-world apps need error handling, proper network communication, auth, and persistence.)
|
|
89
|
-
|
|
90
89
|
### Client Example
|
|
91
90
|
|
|
92
|
-
Here's how to get rolling with Patches on the client:
|
|
93
|
-
|
|
94
91
|
```typescript
|
|
95
|
-
import { Patches, InMemoryStore } from '@dabble/patches';
|
|
92
|
+
import { Patches, OTStrategy, InMemoryStore } from '@dabble/patches';
|
|
96
93
|
import { PatchesSync } from '@dabble/patches/net';
|
|
97
94
|
|
|
98
95
|
interface MyDoc {
|
|
@@ -100,279 +97,295 @@ interface MyDoc {
|
|
|
100
97
|
count: number;
|
|
101
98
|
}
|
|
102
99
|
|
|
103
|
-
// 1. Create a
|
|
104
|
-
const
|
|
100
|
+
// 1. Create a strategy with its store
|
|
101
|
+
const strategy = new OTStrategy(new InMemoryStore());
|
|
105
102
|
|
|
106
|
-
// 2. Create the
|
|
107
|
-
const patches = new Patches({
|
|
103
|
+
// 2. Create the Patches instance
|
|
104
|
+
const patches = new Patches({
|
|
105
|
+
strategies: { ot: strategy },
|
|
106
|
+
defaultStrategy: 'ot',
|
|
107
|
+
});
|
|
108
108
|
|
|
109
|
-
// 3. Set up real-time sync
|
|
110
|
-
const sync = new PatchesSync('wss://your-server-url'
|
|
111
|
-
await sync.connect();
|
|
109
|
+
// 3. Set up real-time sync
|
|
110
|
+
const sync = new PatchesSync(patches, 'wss://your-server-url');
|
|
111
|
+
await sync.connect();
|
|
112
112
|
|
|
113
|
-
// 4. Open
|
|
113
|
+
// 4. Open a document
|
|
114
114
|
const doc = await patches.openDoc<MyDoc>('my-doc-1');
|
|
115
115
|
|
|
116
|
-
// 5. React to updates
|
|
116
|
+
// 5. React to updates
|
|
117
117
|
doc.onUpdate(newState => {
|
|
118
118
|
console.log('Document updated:', newState);
|
|
119
119
|
// Update your UI here
|
|
120
120
|
});
|
|
121
121
|
|
|
122
|
-
// 6. Make
|
|
123
|
-
// (Changes apply immediately locally and sync to the server automatically)
|
|
122
|
+
// 6. Make changes - they sync automatically
|
|
124
123
|
doc.change(draft => {
|
|
125
124
|
draft.text = 'Hello World!';
|
|
126
125
|
draft.count = (draft.count || 0) + 1;
|
|
127
126
|
});
|
|
128
|
-
|
|
129
|
-
// 7. That's it! Changes sync automatically with PatchesSync
|
|
130
127
|
```
|
|
131
128
|
|
|
132
|
-
|
|
129
|
+
See [Patches](./docs/Patches.md), [PatchesDoc](./docs/PatchesDoc.md), and [PatchesSync](./docs/PatchesSync.md) for full API documentation.
|
|
133
130
|
|
|
134
|
-
|
|
131
|
+
### Server Example
|
|
135
132
|
|
|
136
133
|
```typescript
|
|
137
134
|
import express from 'express';
|
|
138
|
-
import {
|
|
135
|
+
import { OTServer } from '@dabble/patches/server';
|
|
136
|
+
|
|
137
|
+
// Your backend store implementation
|
|
138
|
+
const store = new MyOTStoreBackend();
|
|
139
|
+
const server = new OTServer(store);
|
|
139
140
|
|
|
140
|
-
// Server Setup
|
|
141
|
-
const store = new InMemoryStore(); // Use a real database in production!
|
|
142
|
-
const server = new PatchesServer(store);
|
|
143
141
|
const app = express();
|
|
144
142
|
app.use(express.json());
|
|
145
143
|
|
|
146
|
-
//
|
|
147
|
-
app.
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (!Array.isArray(clientChanges)) {
|
|
152
|
-
return res.status(400).json({ error: 'Invalid request' });
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
try {
|
|
156
|
-
// Process incoming changes
|
|
157
|
-
const committedChanges = await server.receiveChanges(docId, clientChanges);
|
|
158
|
-
// Send confirmation back to the sender
|
|
159
|
-
res.json(committedChanges);
|
|
160
|
-
// Broadcast committed changes to other connected clients (via WebSockets, etc.)
|
|
161
|
-
// broadcastChanges(docId, committedChanges, req.headers['x-client-id']);
|
|
162
|
-
} catch (error) {
|
|
163
|
-
console.error(`Error processing changes for ${docId}:`, error);
|
|
164
|
-
const statusCode = error.message.includes('out of sync') ? 409 : 500;
|
|
165
|
-
res.status(statusCode).json({ error: error.message });
|
|
166
|
-
}
|
|
144
|
+
// Get document state
|
|
145
|
+
app.get('/docs/:docId', async (req, res) => {
|
|
146
|
+
const { state, rev } = await server.getDoc(req.params.docId);
|
|
147
|
+
res.json({ state: state ?? {}, rev });
|
|
167
148
|
});
|
|
168
149
|
|
|
169
|
-
//
|
|
170
|
-
app.
|
|
171
|
-
const docId = req.params.docId;
|
|
150
|
+
// Commit changes
|
|
151
|
+
app.post('/docs/:docId/changes', async (req, res) => {
|
|
172
152
|
try {
|
|
173
|
-
const
|
|
174
|
-
res.json(
|
|
153
|
+
const changes = await server.commitChanges(req.params.docId, req.body.changes);
|
|
154
|
+
res.json(changes);
|
|
155
|
+
// Broadcast to other clients via WebSocket
|
|
175
156
|
} catch (error) {
|
|
176
|
-
|
|
177
|
-
res.status(
|
|
157
|
+
const status = error.message.includes('out of sync') ? 409 : 500;
|
|
158
|
+
res.status(status).json({ error: error.message });
|
|
178
159
|
}
|
|
179
160
|
});
|
|
180
161
|
|
|
181
|
-
|
|
182
|
-
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
|
|
162
|
+
app.listen(3000);
|
|
183
163
|
```
|
|
184
164
|
|
|
185
|
-
|
|
165
|
+
See [OTServer](./docs/OTServer.md) for full API documentation.
|
|
186
166
|
|
|
187
|
-
|
|
167
|
+
### LWW Quick Start
|
|
188
168
|
|
|
189
|
-
|
|
169
|
+
For Last-Write-Wins sync, use LWW-specific stores and strategies:
|
|
190
170
|
|
|
191
|
-
|
|
171
|
+
```typescript
|
|
172
|
+
// Client
|
|
173
|
+
import { Patches, LWWStrategy, LWWInMemoryStore } from '@dabble/patches';
|
|
174
|
+
import { PatchesSync } from '@dabble/patches/net';
|
|
192
175
|
|
|
193
|
-
(
|
|
176
|
+
const strategy = new LWWStrategy(new LWWInMemoryStore());
|
|
177
|
+
const patches = new Patches({
|
|
178
|
+
strategies: { lww: strategy },
|
|
179
|
+
defaultStrategy: 'lww',
|
|
180
|
+
});
|
|
194
181
|
|
|
195
|
-
|
|
182
|
+
const sync = new PatchesSync(patches, 'wss://your-server-url');
|
|
183
|
+
await sync.connect();
|
|
196
184
|
|
|
197
|
-
|
|
198
|
-
- **Persistence:** Works with pluggable storage (in-memory, IndexedDB, custom)
|
|
199
|
-
- **Sync Integration:** Pairs with `PatchesSync` for real-time server communication
|
|
200
|
-
- **Event Emitters:** Hooks like `onError` and `onServerCommit` for reacting to events
|
|
185
|
+
const doc = await patches.openDoc<UserPrefs>('user-prefs');
|
|
201
186
|
|
|
202
|
-
|
|
187
|
+
doc.change(draft => {
|
|
188
|
+
draft.theme = 'dark';
|
|
189
|
+
draft.fontSize = 16;
|
|
190
|
+
});
|
|
191
|
+
```
|
|
203
192
|
|
|
204
|
-
|
|
193
|
+
```typescript
|
|
194
|
+
// Server
|
|
195
|
+
import { LWWServer } from '@dabble/patches/server';
|
|
196
|
+
|
|
197
|
+
const store = new MyLWWStoreBackend();
|
|
198
|
+
const server = new LWWServer(store);
|
|
199
|
+
|
|
200
|
+
app.post('/docs/:docId/changes', async (req, res) => {
|
|
201
|
+
const result = await server.commitChanges(req.params.docId, req.body.changes);
|
|
202
|
+
res.json(result);
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
205
|
|
|
206
|
-
|
|
206
|
+
See [LWWServer](./docs/LWWServer.md) and [Last-Write-Wins concepts](./docs/last-write-wins.md) for more details.
|
|
207
207
|
|
|
208
|
-
|
|
209
|
-
- **Optimistic Updates:** Applies local changes immediately for snappy UIs
|
|
210
|
-
- **Synchronization:** Handles client-side OT magic:
|
|
211
|
-
- Sends pending changes to server
|
|
212
|
-
- Applies server confirmations
|
|
213
|
-
- Applies external updates, rebasing local changes as needed
|
|
214
|
-
- **Event Emitters:** Hooks like `onUpdate` and `onChange` to react to state changes
|
|
208
|
+
## Core Components
|
|
215
209
|
|
|
216
|
-
###
|
|
210
|
+
### Client Side
|
|
217
211
|
|
|
218
|
-
|
|
212
|
+
**[Patches](./docs/Patches.md)** - Main entry point. Manages document lifecycle, coordinates strategies, handles persistence.
|
|
219
213
|
|
|
220
|
-
|
|
214
|
+
**[PatchesDoc](./docs/PatchesDoc.md)** - A single collaborative document. Tracks state, applies changes optimistically, emits update events.
|
|
221
215
|
|
|
222
|
-
-
|
|
223
|
-
- **Transformation:** Transforms client changes against concurrent server changes
|
|
224
|
-
- **Applies Changes:** Applies transformed changes to the authoritative document state
|
|
225
|
-
- **Versioning:** Creates version snapshots based on user sessions
|
|
226
|
-
- **Persistence:** Uses `PatchesStoreBackend` to save/load document state and history
|
|
216
|
+
**[PatchesSync](./docs/PatchesSync.md)** - WebSocket connection manager. Handles reconnection, batching, and bidirectional sync.
|
|
227
217
|
|
|
228
|
-
|
|
218
|
+
**Strategies** - Algorithm-specific logic:
|
|
229
219
|
|
|
230
|
-
|
|
220
|
+
- `OTStrategy` - Owns an `OTClientStore`, handles rebasing and change tracking
|
|
221
|
+
- `LWWStrategy` - Owns an `LWWClientStore`, handles timestamp consolidation
|
|
231
222
|
|
|
232
|
-
|
|
223
|
+
**Stores** - Persistence adapters:
|
|
233
224
|
|
|
234
|
-
-
|
|
235
|
-
-
|
|
236
|
-
- **List Server Changes:** Query raw server changes by revision numbers
|
|
225
|
+
- `InMemoryStore` / `LWWInMemoryStore` - For testing and simple apps
|
|
226
|
+
- `OTIndexedDBStore` / `LWWIndexedDBStore` - Browser persistence with offline support
|
|
237
227
|
|
|
238
|
-
###
|
|
228
|
+
### Server Side
|
|
239
229
|
|
|
240
|
-
|
|
230
|
+
**[OTServer](./docs/OTServer.md)** - OT authority. Transforms concurrent changes, assigns revisions, maintains history.
|
|
241
231
|
|
|
242
|
-
|
|
232
|
+
**[LWWServer](./docs/LWWServer.md)** - LWW authority. Compares timestamps, stores current field values, no history.
|
|
243
233
|
|
|
244
|
-
|
|
245
|
-
- **List Branches:** Shows info about existing branches
|
|
246
|
-
- **Merge Branch:** Merges changes back into the source document
|
|
247
|
-
- **Close Branch:** Marks a branch as closed, merged, or abandoned
|
|
234
|
+
**[PatchesHistoryManager](./docs/PatchesHistoryManager.md)** - Query document versions and history.
|
|
248
235
|
|
|
249
|
-
|
|
236
|
+
**[PatchesBranchManager](./docs/PatchesBranchManager.md)** - Create, list, and merge branches.
|
|
250
237
|
|
|
251
|
-
|
|
238
|
+
**Backend Stores** - You implement these interfaces for your database:
|
|
252
239
|
|
|
253
|
-
|
|
240
|
+
- `OTStoreBackend` - For OT: changes, snapshots, versions
|
|
241
|
+
- `LWWStoreBackend` - For LWW: fields with timestamps, snapshots
|
|
254
242
|
|
|
255
|
-
|
|
243
|
+
See [Persistence](./docs/persist.md) for storage patterns and [Backend Store Interface](./docs/operational-transformation.md#backend-store-interface) for implementation details.
|
|
256
244
|
|
|
257
|
-
###
|
|
245
|
+
### Networking
|
|
258
246
|
|
|
259
|
-
|
|
247
|
+
**[WebSocket Transport](./docs/websocket.md)** - Standard server-mediated communication via `PatchesWebSocket`.
|
|
260
248
|
|
|
261
|
-
|
|
262
|
-
- **WebRTC Transport:** For peer-to-peer, use [`WebRTCTransport`](./docs/operational-transformation.md#webrtc) and [`WebRTCAwareness`](./docs/awareness.md)
|
|
249
|
+
**[WebRTC Transport](./docs/net.md)** - Peer-to-peer for awareness features (cursors, presence).
|
|
263
250
|
|
|
264
|
-
**
|
|
251
|
+
**[JSON-RPC Protocol](./docs/json-rpc.md)** - The wire protocol between client and server.
|
|
265
252
|
|
|
266
|
-
|
|
267
|
-
- WebRTC for peer-to-peer or to reduce server load for awareness/presence
|
|
253
|
+
When to use which? WebSocket for document sync. WebRTC for presence/cursors to reduce server load. See [Networking overview](./docs/net.md).
|
|
268
254
|
|
|
269
|
-
### Awareness (Presence
|
|
255
|
+
### Awareness (Presence & Cursors)
|
|
270
256
|
|
|
271
|
-
|
|
257
|
+
Show who's online, where their cursor is, what they're selecting. Works over both WebSocket and WebRTC.
|
|
272
258
|
|
|
273
|
-
|
|
259
|
+
See [Awareness documentation](./docs/awareness.md) for implementation details.
|
|
274
260
|
|
|
275
261
|
## Basic Workflow
|
|
276
262
|
|
|
277
|
-
### Client
|
|
263
|
+
### Client
|
|
278
264
|
|
|
279
|
-
1.
|
|
280
|
-
2.
|
|
281
|
-
3.
|
|
282
|
-
4.
|
|
283
|
-
5.
|
|
265
|
+
1. Create a `Patches` instance with strategies
|
|
266
|
+
2. Connect `PatchesSync` to your server
|
|
267
|
+
3. Open documents with `patches.openDoc(docId)`
|
|
268
|
+
4. Subscribe to updates with `doc.onUpdate()`
|
|
269
|
+
5. Make changes with `doc.change()` - they sync automatically
|
|
284
270
|
|
|
285
|
-
### Server
|
|
271
|
+
### Server
|
|
286
272
|
|
|
287
|
-
1.
|
|
288
|
-
2.
|
|
289
|
-
3.
|
|
273
|
+
1. Create `OTServer` or `LWWServer` with your backend store
|
|
274
|
+
2. Handle `commitChanges()` requests
|
|
275
|
+
3. Broadcast committed changes to other clients
|
|
276
|
+
4. Optionally use `PatchesHistoryManager` for versioning and `PatchesBranchManager` for branching
|
|
290
277
|
|
|
291
278
|
## Examples
|
|
292
279
|
|
|
293
|
-
###
|
|
280
|
+
### Complete Client Setup
|
|
294
281
|
|
|
295
282
|
```typescript
|
|
296
|
-
import { Patches,
|
|
283
|
+
import { Patches, OTStrategy, OTIndexedDBStore } from '@dabble/patches';
|
|
284
|
+
import { PatchesSync } from '@dabble/patches/net';
|
|
297
285
|
|
|
298
286
|
interface MyDoc {
|
|
299
|
-
|
|
300
|
-
|
|
287
|
+
title: string;
|
|
288
|
+
content: string;
|
|
301
289
|
}
|
|
302
290
|
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
291
|
+
// Production setup with IndexedDB for offline support
|
|
292
|
+
const strategy = new OTStrategy(new OTIndexedDBStore('my-app'));
|
|
293
|
+
const patches = new Patches({
|
|
294
|
+
strategies: { ot: strategy },
|
|
295
|
+
});
|
|
308
296
|
|
|
309
|
-
|
|
310
|
-
|
|
297
|
+
const sync = new PatchesSync(patches, 'wss://api.example.com/sync');
|
|
298
|
+
|
|
299
|
+
// Handle connection state
|
|
300
|
+
sync.onStateChange(state => {
|
|
301
|
+
if (state.connected) {
|
|
302
|
+
console.log('Connected and syncing');
|
|
303
|
+
} else if (!state.online) {
|
|
304
|
+
console.log('Offline - changes saved locally');
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Handle errors
|
|
309
|
+
sync.onError((error, context) => {
|
|
310
|
+
console.error(`Sync error for ${context?.docId}:`, error);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
await sync.connect();
|
|
314
|
+
|
|
315
|
+
// Open and use a document
|
|
316
|
+
const doc = await patches.openDoc<MyDoc>('doc-123');
|
|
317
|
+
|
|
318
|
+
doc.onUpdate(state => {
|
|
319
|
+
renderUI(state);
|
|
311
320
|
});
|
|
312
321
|
|
|
313
322
|
doc.change(draft => {
|
|
314
|
-
draft.
|
|
315
|
-
draft.
|
|
323
|
+
draft.title = 'My Document';
|
|
324
|
+
draft.content = 'Hello, world!';
|
|
316
325
|
});
|
|
317
|
-
// With PatchesSync, changes sync automatically
|
|
318
326
|
```
|
|
319
327
|
|
|
320
|
-
###
|
|
328
|
+
### Using Both Strategies
|
|
321
329
|
|
|
322
330
|
```typescript
|
|
323
|
-
import
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
private docs = new Map<string, { state: any; rev: number; changes: Change[]; versions: VersionMetadata[] }>();
|
|
334
|
-
|
|
335
|
-
// Implementation details omitted for brevity...
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// --- Server Setup ---
|
|
339
|
-
const store = new InMemoryStore();
|
|
340
|
-
const server = new PatchesServer(store);
|
|
341
|
-
const app = express();
|
|
342
|
-
app.use(express.json());
|
|
331
|
+
import { Patches, OTStrategy, LWWStrategy, InMemoryStore, LWWInMemoryStore } from '@dabble/patches';
|
|
332
|
+
|
|
333
|
+
// Configure both strategies
|
|
334
|
+
const patches = new Patches({
|
|
335
|
+
strategies: {
|
|
336
|
+
ot: new OTStrategy(new InMemoryStore()),
|
|
337
|
+
lww: new LWWStrategy(new LWWInMemoryStore()),
|
|
338
|
+
},
|
|
339
|
+
defaultStrategy: 'ot',
|
|
340
|
+
});
|
|
343
341
|
|
|
344
|
-
//
|
|
345
|
-
|
|
342
|
+
// OT for collaborative document editing
|
|
343
|
+
const manuscript = await patches.openDoc('manuscript-123'); // Uses default (ot)
|
|
346
344
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
console.log(`Server listening on port ${PORT}`);
|
|
350
|
-
});
|
|
345
|
+
// LWW for user settings
|
|
346
|
+
const settings = await patches.openDoc('settings-user-456', { strategy: 'lww' });
|
|
351
347
|
```
|
|
352
348
|
|
|
353
349
|
## Advanced Topics
|
|
354
350
|
|
|
355
|
-
###
|
|
351
|
+
### Versioning & History
|
|
356
352
|
|
|
357
|
-
|
|
353
|
+
Documents automatically snapshot after 30 minutes of inactivity. Browse versions with `PatchesHistoryManager`.
|
|
358
354
|
|
|
359
|
-
|
|
355
|
+
See [OTServer Versioning](./docs/OTServer.md#versioning) and [PatchesHistoryManager](./docs/PatchesHistoryManager.md).
|
|
360
356
|
|
|
361
|
-
|
|
357
|
+
### Branching
|
|
362
358
|
|
|
363
|
-
|
|
359
|
+
Create document branches, work in isolation, merge back. Useful for "what if" scenarios or staged editing.
|
|
364
360
|
|
|
365
|
-
See [
|
|
361
|
+
See [Branching](./docs/branching.md) and [PatchesBranchManager](./docs/PatchesBranchManager.md).
|
|
366
362
|
|
|
367
|
-
|
|
363
|
+
### SharedWorker
|
|
368
364
|
|
|
369
|
-
|
|
365
|
+
Run Patches in a SharedWorker for cross-tab coordination and reduced memory usage.
|
|
370
366
|
|
|
371
|
-
|
|
367
|
+
See [SharedWorker documentation](./docs/shared-worker.md).
|
|
372
368
|
|
|
373
|
-
|
|
369
|
+
### Framework Integrations
|
|
370
|
+
|
|
371
|
+
- **Vue 3**: See [src/vue/README.md](src/vue/README.md)
|
|
372
|
+
- **Solid.js**: See [src/solid/README.md](src/solid/README.md)
|
|
373
|
+
|
|
374
|
+
### Custom OT Operations
|
|
375
|
+
|
|
376
|
+
Extend the operation handlers for domain-specific transformations.
|
|
377
|
+
|
|
378
|
+
See [Operation Handlers](./docs/operational-transformation.md#operation-handlers).
|
|
379
|
+
|
|
380
|
+
### JSON Patch
|
|
381
|
+
|
|
382
|
+
Patches uses JSON Patch (RFC 6902) under the hood. You rarely need to work with it directly, but it's there.
|
|
383
|
+
|
|
384
|
+
See [JSON Patch documentation](./docs/json-patch.md).
|
|
385
|
+
|
|
386
|
+
## Contributing
|
|
374
387
|
|
|
375
|
-
|
|
388
|
+
Contributions welcome. Open issues or submit pull requests.
|
|
376
389
|
|
|
377
390
|
## License
|
|
378
391
|
|