@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.
Files changed (114) hide show
  1. package/README.md +221 -208
  2. package/dist/BaseDoc-DkP3tUhT.d.ts +206 -0
  3. package/dist/algorithms/client/applyCommittedChanges.d.ts +7 -0
  4. package/dist/algorithms/client/applyCommittedChanges.js +6 -3
  5. package/dist/algorithms/lww/consolidateOps.d.ts +40 -0
  6. package/dist/algorithms/lww/consolidateOps.js +103 -0
  7. package/dist/algorithms/lww/index.d.ts +2 -0
  8. package/dist/algorithms/lww/index.js +1 -0
  9. package/dist/algorithms/lww/mergeServerWithLocal.d.ts +22 -0
  10. package/dist/algorithms/lww/mergeServerWithLocal.js +32 -0
  11. package/dist/algorithms/server/commitChanges.d.ts +32 -8
  12. package/dist/algorithms/server/commitChanges.js +20 -5
  13. package/dist/algorithms/server/createVersion.d.ts +1 -1
  14. package/dist/algorithms/server/getSnapshotAtRevision.d.ts +1 -1
  15. package/dist/algorithms/server/getStateAtRevision.d.ts +1 -1
  16. package/dist/algorithms/server/handleOfflineSessionsAndBatches.d.ts +1 -1
  17. package/dist/client/BaseDoc.d.ts +6 -0
  18. package/dist/client/BaseDoc.js +70 -0
  19. package/dist/client/ClientAlgorithm.d.ts +101 -0
  20. package/dist/client/ClientAlgorithm.js +0 -0
  21. package/dist/client/InMemoryStore.d.ts +5 -7
  22. package/dist/client/InMemoryStore.js +6 -35
  23. package/dist/client/IndexedDBStore.d.ts +39 -73
  24. package/dist/client/IndexedDBStore.js +17 -220
  25. package/dist/client/LWWAlgorithm.d.ts +43 -0
  26. package/dist/client/LWWAlgorithm.js +87 -0
  27. package/dist/client/LWWClientStore.d.ts +73 -0
  28. package/dist/client/LWWClientStore.js +0 -0
  29. package/dist/client/LWWDoc.d.ts +56 -0
  30. package/dist/client/LWWDoc.js +84 -0
  31. package/dist/client/LWWInMemoryStore.d.ts +88 -0
  32. package/dist/client/LWWInMemoryStore.js +208 -0
  33. package/dist/client/LWWIndexedDBStore.d.ts +91 -0
  34. package/dist/client/LWWIndexedDBStore.js +275 -0
  35. package/dist/client/OTAlgorithm.d.ts +42 -0
  36. package/dist/client/OTAlgorithm.js +113 -0
  37. package/dist/client/OTClientStore.d.ts +50 -0
  38. package/dist/client/OTClientStore.js +0 -0
  39. package/dist/client/OTDoc.d.ts +6 -0
  40. package/dist/client/OTDoc.js +97 -0
  41. package/dist/client/OTIndexedDBStore.d.ts +84 -0
  42. package/dist/client/OTIndexedDBStore.js +163 -0
  43. package/dist/client/Patches.d.ts +36 -16
  44. package/dist/client/Patches.js +60 -27
  45. package/dist/client/PatchesDoc.d.ts +4 -113
  46. package/dist/client/PatchesDoc.js +3 -153
  47. package/dist/client/PatchesStore.d.ts +8 -105
  48. package/dist/client/factories.d.ts +72 -0
  49. package/dist/client/factories.js +80 -0
  50. package/dist/client/index.d.ts +14 -5
  51. package/dist/client/index.js +9 -0
  52. package/dist/compression/index.d.ts +1 -1
  53. package/dist/data/change.js +2 -0
  54. package/dist/fractionalIndex.d.ts +67 -0
  55. package/dist/fractionalIndex.js +241 -0
  56. package/dist/index.d.ts +13 -3
  57. package/dist/index.js +1 -0
  58. package/dist/json-patch/types.d.ts +2 -0
  59. package/dist/net/PatchesClient.js +15 -15
  60. package/dist/net/PatchesSync.d.ts +24 -12
  61. package/dist/net/PatchesSync.js +56 -64
  62. package/dist/net/index.d.ts +6 -10
  63. package/dist/net/index.js +6 -1
  64. package/dist/net/protocol/JSONRPCClient.d.ts +4 -4
  65. package/dist/net/protocol/JSONRPCClient.js +6 -4
  66. package/dist/net/protocol/JSONRPCServer.d.ts +45 -9
  67. package/dist/net/protocol/JSONRPCServer.js +63 -8
  68. package/dist/net/serverContext.d.ts +38 -0
  69. package/dist/net/serverContext.js +20 -0
  70. package/dist/net/webrtc/WebRTCTransport.js +1 -1
  71. package/dist/net/websocket/AuthorizationProvider.d.ts +3 -3
  72. package/dist/net/websocket/WebSocketServer.d.ts +29 -20
  73. package/dist/net/websocket/WebSocketServer.js +23 -12
  74. package/dist/server/BranchManager.d.ts +50 -0
  75. package/dist/server/BranchManager.js +0 -0
  76. package/dist/server/CompressedStoreBackend.d.ts +7 -5
  77. package/dist/server/CompressedStoreBackend.js +3 -9
  78. package/dist/server/LWWBranchManager.d.ts +82 -0
  79. package/dist/server/LWWBranchManager.js +99 -0
  80. package/dist/server/LWWMemoryStoreBackend.d.ts +78 -0
  81. package/dist/server/LWWMemoryStoreBackend.js +191 -0
  82. package/dist/server/LWWServer.d.ts +130 -0
  83. package/dist/server/LWWServer.js +207 -0
  84. package/dist/server/{PatchesBranchManager.d.ts → OTBranchManager.d.ts} +32 -12
  85. package/dist/server/{PatchesBranchManager.js → OTBranchManager.js} +25 -40
  86. package/dist/server/OTServer.d.ts +108 -0
  87. package/dist/server/OTServer.js +141 -0
  88. package/dist/server/PatchesHistoryManager.d.ts +20 -7
  89. package/dist/server/PatchesHistoryManager.js +26 -3
  90. package/dist/server/PatchesServer.d.ts +70 -81
  91. package/dist/server/PatchesServer.js +0 -175
  92. package/dist/server/branchUtils.d.ts +82 -0
  93. package/dist/server/branchUtils.js +66 -0
  94. package/dist/server/index.d.ts +17 -6
  95. package/dist/server/index.js +33 -4
  96. package/dist/server/tombstone.d.ts +29 -0
  97. package/dist/server/tombstone.js +32 -0
  98. package/dist/server/types.d.ts +128 -26
  99. package/dist/server/utils.d.ts +12 -0
  100. package/dist/server/utils.js +23 -0
  101. package/dist/solid/context.d.ts +5 -4
  102. package/dist/solid/doc-manager.d.ts +3 -3
  103. package/dist/solid/index.d.ts +5 -4
  104. package/dist/solid/primitives.d.ts +2 -3
  105. package/dist/types.d.ts +4 -2
  106. package/dist/vue/composables.d.ts +2 -3
  107. package/dist/vue/doc-manager.d.ts +3 -3
  108. package/dist/vue/index.d.ts +5 -4
  109. package/dist/vue/provider.d.ts +5 -4
  110. package/package.json +1 -1
  111. package/dist/algorithms/client/collapsePendingChanges.d.ts +0 -30
  112. package/dist/algorithms/client/collapsePendingChanges.js +0 -78
  113. package/dist/net/websocket/RPCServer.d.ts +0 -141
  114. package/dist/net/websocket/RPCServer.js +0 -204
package/README.md CHANGED
@@ -1,26 +1,42 @@
1
1
  # Patches
2
2
 
3
- **Hello, friend!** Meet your new favorite realtime library. It's based on operational transformations, but don't let that scare you!
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 Is This Thing?
7
+ ## What Problem Does This Solve?
8
8
 
9
- Patches is a TypeScript library that makes building collaborative apps _delightfully_ straightforward. You know, the kind where multiple people can edit the same document at once without everything exploding? Yeah, those!
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
- It uses something called Operational Transformation (fancy, I know) with a centralized server model. Translation: Your users can collaborate without weird conflicts, even when their internet connection gets flaky.
11
+ Your document state is just JSON. Change it with a simple callback:
12
12
 
13
- The BEST part? It handles massive documents with loooong histories. We're talking documents with 480,000+ operations that load in 1-2ms. Not a typo!
13
+ ```js
14
+ doc.change(state => (state.title = 'New Title'));
15
+ ```
14
16
 
15
- ## Why You'll Love It
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
- When working with Patches, you're just using normal JavaScript data. If JSON supports it, Patches supports it. Your document's `state` is immutable (fancy word for "won't change unexpectedly"). When you want to change something, you just do:
19
+ ## Two Sync Strategies
18
20
 
19
- ```js
20
- doc.change(state => (state.prop = 'new value'));
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
- And bam! You get a fresh new state with your changes applied.
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
- **"Wait, shouldn't I be using CRDTs instead?"**
59
+ "Shouldn't I use CRDTs instead?"
44
60
 
45
- Look, 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 the deal: at [Dabble Writer](https://www.dabblewriter.com/), we tried CRDTs. We REALLY wanted them to work. Even the super-optimized [Y.js](https://yjs.dev/) couldn't handle our power users' documents.
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 480k+ operations. 😱 These monsters took hours to re-create 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.
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 projects grow larger or longer-lived, OT performance stays zippy while CRDTs slow down. For most use cases, CRDTs might be perfect! But if you have very large or long-lived documents (especially ones that accumulate tons of changes over time), OT could save your bacon.
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
- - **Centralized OT:** Using a server as the authority makes everything WAY simpler. No complicated peer-to-peer conflict resolution!
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
- **Why Centralized?**
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
- We use an algorithm that only transforms operations in one direction (like git rebase), inspired by [Marijn Haverbeke's article](https://marijnhaverbeke.nl/blog/collaborative-editing.html). Originally, we made the server reject changes if new ones came in before them, forcing clients to transform and resubmit. BUT! This could theoretically make slow clients keep resubmitting forever and never committing.
73
+ **Linear History** - The server maintains one straight timeline. No branches, no forks, no merge conflicts at the infrastructure level.
61
74
 
62
- So we leveled up! Now the server does the transform and commit, sending back both new changes AND the transformed submitted ones. Everyone gets equal time with the server, even the slowpokes!
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
- **Snapshots = Performance Magic**
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
- OT documents are just arrays of changes. To create the current document state, you replay each change from first to last. For looooong documents (like our 480k changes monster), this would be painfully slow.
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 store (just using in-memory for this demo)
104
- const store = new InMemoryStore();
100
+ // 1. Create a strategy with its store
101
+ const strategy = new OTStrategy(new InMemoryStore());
105
102
 
106
- // 2. Create the main Patches client
107
- const patches = new Patches({ store });
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 with your server
110
- const sync = new PatchesSync('wss://your-server-url', patches);
111
- await sync.connect(); // Connect to the server!
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 or create a document by ID
113
+ // 4. Open a document
114
114
  const doc = await patches.openDoc<MyDoc>('my-doc-1');
115
115
 
116
- // 5. React to updates (update your UI here)
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 local changes
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
- ### Server Example
129
+ See [Patches](./docs/Patches.md), [PatchesDoc](./docs/PatchesDoc.md), and [PatchesSync](./docs/PatchesSync.md) for full API documentation.
133
130
 
134
- Here's a basic Express server using `PatchesServer`:
131
+ ### Server Example
135
132
 
136
133
  ```typescript
137
134
  import express from 'express';
138
- import { PatchesServer, PatchesStoreBackend, Change } from '@dabble/patches/server';
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
- // Endpoint to receive changes
147
- app.post('/docs/:docId/changes', async (req, res) => {
148
- const docId = req.params.docId;
149
- const clientChanges = req.body.changes;
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
- // Endpoint to get initial state
170
- app.get('/docs/:docId', async (req, res) => {
171
- const docId = req.params.docId;
150
+ // Commit changes
151
+ app.post('/docs/:docId/changes', async (req, res) => {
172
152
  try {
173
- const { state, rev } = await server.getLatestDocumentStateAndRev(docId);
174
- res.json({ state: state ?? {}, rev }); // Default to empty obj if new
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
- console.error(`Error fetching state for ${docId}:`, error);
177
- res.status(500).json({ error: 'Failed to fetch document state.' });
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
- const PORT = 3000;
182
- app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
162
+ app.listen(3000);
183
163
  ```
184
164
 
185
- For more details and advanced features, check out the rest of the docs!
165
+ See [OTServer](./docs/OTServer.md) for full API documentation.
186
166
 
187
- ## Core Components
167
+ ### LWW Quick Start
188
168
 
189
- Centralized OT has two very different areas: server and client. They do completely different jobs!
169
+ For Last-Write-Wins sync, use LWW-specific stores and strategies:
190
170
 
191
- ### Patches (Main Client)
171
+ ```typescript
172
+ // Client
173
+ import { Patches, LWWStrategy, LWWInMemoryStore } from '@dabble/patches';
174
+ import { PatchesSync } from '@dabble/patches/net';
192
175
 
193
- ([`docs/Patches.md`](./docs/Patches.md))
176
+ const strategy = new LWWStrategy(new LWWInMemoryStore());
177
+ const patches = new Patches({
178
+ strategies: { lww: strategy },
179
+ defaultStrategy: 'lww',
180
+ });
194
181
 
195
- This is your main entry point on the client. It manages document instances and persistence. You get a `PatchesDoc` by calling `patches.openDoc(docId)`.
182
+ const sync = new PatchesSync(patches, 'wss://your-server-url');
183
+ await sync.connect();
196
184
 
197
- - **Document Management:** Opens, tracks, and closes collaborative documents
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
- ### PatchesDoc (Document Instance)
187
+ doc.change(draft => {
188
+ draft.theme = 'dark';
189
+ draft.fontSize = 16;
190
+ });
191
+ ```
203
192
 
204
- ([`docs/PatchesDoc.md`](./docs/PatchesDoc.md))
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
- This represents a single collaborative document. You don't create this directly; use `patches.openDoc(docId)` instead.
206
+ See [LWWServer](./docs/LWWServer.md) and [Last-Write-Wins concepts](./docs/last-write-wins.md) for more details.
207
207
 
208
- - **Local State Management:** Tracks committed state, sending changes, and pending changes
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
- ### PatchesServer
210
+ ### Client Side
217
211
 
218
- ([`docs/PatchesServer.md`](./docs/PatchesServer.md))
212
+ **[Patches](./docs/Patches.md)** - Main entry point. Manages document lifecycle, coordinates strategies, handles persistence.
219
213
 
220
- The heart of server-side logic!
214
+ **[PatchesDoc](./docs/PatchesDoc.md)** - A single collaborative document. Tracks state, applies changes optimistically, emits update events.
221
215
 
222
- - **Receives Changes:** Handles incoming `Change` objects from clients
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
- ### PatchesHistoryManager
218
+ **Strategies** - Algorithm-specific logic:
229
219
 
230
- ([`docs/PatchesHistoryManager.md`](./docs/PatchesHistoryManager.md))
220
+ - `OTStrategy` - Owns an `OTClientStore`, handles rebasing and change tracking
221
+ - `LWWStrategy` - Owns an `LWWClientStore`, handles timestamp consolidation
231
222
 
232
- Helps you query document history.
223
+ **Stores** - Persistence adapters:
233
224
 
234
- - **List Versions:** Get metadata about saved document versions
235
- - **Get Version State/Changes:** Load the full state or specific changes for a version
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
- ### PatchesBranchManager
228
+ ### Server Side
239
229
 
240
- ([`docs/PatchesBranchManager.md`](./docs/PatchesBranchManager.md))
230
+ **[OTServer](./docs/OTServer.md)** - OT authority. Transforms concurrent changes, assigns revisions, maintains history.
241
231
 
242
- Manages branching and merging workflows.
232
+ **[LWWServer](./docs/LWWServer.md)** - LWW authority. Compares timestamps, stores current field values, no history.
243
233
 
244
- - **Create Branch:** Makes a new document branching off from a source doc
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
- ### Backend Store
236
+ **[PatchesBranchManager](./docs/PatchesBranchManager.md)** - Create, list, and merge branches.
250
237
 
251
- ([`docs/operational-transformation.md#backend-store-interface`](./docs/operational-transformation.md#backend-store-interface))
238
+ **Backend Stores** - You implement these interfaces for your database:
252
239
 
253
- This is an interface you implement, not a specific class. It defines how the server components interact with your chosen storage (database, file system, memory).
240
+ - `OTStoreBackend` - For OT: changes, snapshots, versions
241
+ - `LWWStoreBackend` - For LWW: fields with timestamps, snapshots
254
242
 
255
- You're responsible for making it work with your backend!
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
- ### Transport & Networking
245
+ ### Networking
258
246
 
259
- Patches gives you flexible networking options:
247
+ **[WebSocket Transport](./docs/websocket.md)** - Standard server-mediated communication via `PatchesWebSocket`.
260
248
 
261
- - **WebSocket Transport:** For most apps, use [`PatchesWebSocket`](./docs/websocket.md) to connect to a central server
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
- **When to use which?**
251
+ **[JSON-RPC Protocol](./docs/json-rpc.md)** - The wire protocol between client and server.
265
252
 
266
- - WebSocket for most collaborative apps with a central server
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, Cursors, etc.)
255
+ ### Awareness (Presence & Cursors)
270
256
 
271
- "Awareness" lets you show who's online, where their cursor is, and more. Patches supports awareness over both WebSocket and WebRTC.
257
+ Show who's online, where their cursor is, what they're selecting. Works over both WebSocket and WebRTC.
272
258
 
273
- Check the [Awareness documentation](./docs/awareness.md) for how to build collaborative cursors, user lists, and other cool features.
259
+ See [Awareness documentation](./docs/awareness.md) for implementation details.
274
260
 
275
261
  ## Basic Workflow
276
262
 
277
- ### Client-Side
263
+ ### Client
278
264
 
279
- 1. **Initialize `Patches`** with a store
280
- 2. **Track and Open a Document** with `patches.trackDocs([docId])` and `patches.openDoc(docId)`
281
- 3. **Subscribe to Updates** with `doc.onUpdate`
282
- 4. **Make Local Changes** with `doc.change()`
283
- 5. **Sync Changes** automatically with `PatchesSync` or manually with your own logic
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-Side
271
+ ### Server
286
272
 
287
- 1. **Initialize `PatchesServer`** with your backend store
288
- 2. **Receive Client Changes** with `server.receiveChanges()`
289
- 3. **Handle History/Branching** with `PatchesHistoryManager` and `PatchesBranchManager`
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
- ### Simple Client Setup
280
+ ### Complete Client Setup
294
281
 
295
282
  ```typescript
296
- import { Patches, InMemoryStore } from '@dabble/patches';
283
+ import { Patches, OTStrategy, OTIndexedDBStore } from '@dabble/patches';
284
+ import { PatchesSync } from '@dabble/patches/net';
297
285
 
298
286
  interface MyDoc {
299
- text: string;
300
- count: number;
287
+ title: string;
288
+ content: string;
301
289
  }
302
290
 
303
- const store = new InMemoryStore();
304
- const patches = new Patches({ store });
305
- const docId = 'doc123';
306
- await patches.trackDocs([docId]);
307
- const doc = await patches.openDoc<MyDoc>(docId);
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
- doc.onUpdate(newState => {
310
- console.log('Document updated:', newState);
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.text = 'Hello';
315
- draft.count = 0;
323
+ draft.title = 'My Document';
324
+ draft.content = 'Hello, world!';
316
325
  });
317
- // With PatchesSync, changes sync automatically
318
326
  ```
319
327
 
320
- ### Simple Server Setup
328
+ ### Using Both Strategies
321
329
 
322
330
  ```typescript
323
- import express from 'express';
324
- import {
325
- PatchesServer,
326
- PatchesStoreBackend,
327
- Change,
328
- VersionMetadata, //... other types
329
- } from '@dabble/patches/server';
330
-
331
- // --- Basic In-Memory Store (Use a real database!) ---
332
- class InMemoryStore implements PatchesStoreBackend {
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
- // API endpoints for changes and state...
345
- // (see full example in code)
342
+ // OT for collaborative document editing
343
+ const manuscript = await patches.openDoc('manuscript-123'); // Uses default (ot)
346
344
 
347
- const PORT = 3000;
348
- app.listen(PORT, () => {
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
- ### Offline Support & Versioning
351
+ ### Versioning & History
356
352
 
357
- See [`PatchesServer Versioning`](./docs/PatchesServer.md#versioning) and [`PatchesHistoryManager`](./docs/PatchesHistoryManager.md).
353
+ Documents automatically snapshot after 30 minutes of inactivity. Browse versions with `PatchesHistoryManager`.
358
354
 
359
- ### Branching and Merging
355
+ See [OTServer Versioning](./docs/OTServer.md#versioning) and [PatchesHistoryManager](./docs/PatchesHistoryManager.md).
360
356
 
361
- See [`PatchesBranchManager`](./docs/PatchesBranchManager.md).
357
+ ### Branching
362
358
 
363
- ### Custom OT Types
359
+ Create document branches, work in isolation, merge back. Useful for "what if" scenarios or staged editing.
364
360
 
365
- See [`Operational Transformation > Operation Handlers`](./docs/operational-transformation.md#operation-handlers).
361
+ See [Branching](./docs/branching.md) and [PatchesBranchManager](./docs/PatchesBranchManager.md).
366
362
 
367
- ## JSON Patch (Legacy)
363
+ ### SharedWorker
368
364
 
369
- For legacy JSON Patch features, see [`docs/json-patch.md`](./docs/json-patch.md).
365
+ Run Patches in a SharedWorker for cross-tab coordination and reduced memory usage.
370
366
 
371
- ## Contributing
367
+ See [SharedWorker documentation](./docs/shared-worker.md).
372
368
 
373
- Contributions are welcome! Please feel free to open issues or submit pull requests.
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
- _(TODO: Add contribution guidelines)_
388
+ Contributions welcome. Open issues or submit pull requests.
376
389
 
377
390
  ## License
378
391