@dabble/patches 0.1.1

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 (120) hide show
  1. package/README.md +632 -0
  2. package/dist/client/PatchDoc.d.ts +85 -0
  3. package/dist/client/PatchDoc.js +299 -0
  4. package/dist/client/index.d.ts +2 -0
  5. package/dist/client/index.js +1 -0
  6. package/dist/event-signal.d.ts +31 -0
  7. package/dist/event-signal.js +40 -0
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.js +1 -0
  10. package/dist/json-patch/JSONPatch.d.ts +126 -0
  11. package/dist/json-patch/JSONPatch.js +221 -0
  12. package/dist/json-patch/applyPatch.d.ts +11 -0
  13. package/dist/json-patch/applyPatch.js +37 -0
  14. package/dist/json-patch/composePatch.d.ts +2 -0
  15. package/dist/json-patch/composePatch.js +38 -0
  16. package/dist/json-patch/createJSONPatch.d.ts +35 -0
  17. package/dist/json-patch/createJSONPatch.js +41 -0
  18. package/dist/json-patch/index.d.ts +9 -0
  19. package/dist/json-patch/index.js +8 -0
  20. package/dist/json-patch/invertPatch.d.ts +2 -0
  21. package/dist/json-patch/invertPatch.js +31 -0
  22. package/dist/json-patch/ops/add.d.ts +2 -0
  23. package/dist/json-patch/ops/add.js +52 -0
  24. package/dist/json-patch/ops/bitmask.d.ts +14 -0
  25. package/dist/json-patch/ops/bitmask.js +48 -0
  26. package/dist/json-patch/ops/copy.d.ts +2 -0
  27. package/dist/json-patch/ops/copy.js +34 -0
  28. package/dist/json-patch/ops/increment.d.ts +5 -0
  29. package/dist/json-patch/ops/increment.js +21 -0
  30. package/dist/json-patch/ops/index.d.ts +22 -0
  31. package/dist/json-patch/ops/index.js +25 -0
  32. package/dist/json-patch/ops/move.d.ts +2 -0
  33. package/dist/json-patch/ops/move.js +211 -0
  34. package/dist/json-patch/ops/remove.d.ts +2 -0
  35. package/dist/json-patch/ops/remove.js +31 -0
  36. package/dist/json-patch/ops/replace.d.ts +2 -0
  37. package/dist/json-patch/ops/replace.js +44 -0
  38. package/dist/json-patch/ops/test.d.ts +2 -0
  39. package/dist/json-patch/ops/test.js +22 -0
  40. package/dist/json-patch/ops/text.d.ts +2 -0
  41. package/dist/json-patch/ops/text.js +57 -0
  42. package/dist/json-patch/patchProxy.d.ts +41 -0
  43. package/dist/json-patch/patchProxy.js +125 -0
  44. package/dist/json-patch/state.d.ts +2 -0
  45. package/dist/json-patch/state.js +8 -0
  46. package/dist/json-patch/transformPatch.d.ts +19 -0
  47. package/dist/json-patch/transformPatch.js +37 -0
  48. package/dist/json-patch/types.d.ts +52 -0
  49. package/dist/json-patch/types.js +1 -0
  50. package/dist/json-patch/utils/deepEqual.d.ts +1 -0
  51. package/dist/json-patch/utils/deepEqual.js +33 -0
  52. package/dist/json-patch/utils/exit.d.ts +2 -0
  53. package/dist/json-patch/utils/exit.js +4 -0
  54. package/dist/json-patch/utils/get.d.ts +2 -0
  55. package/dist/json-patch/utils/get.js +6 -0
  56. package/dist/json-patch/utils/getOpData.d.ts +2 -0
  57. package/dist/json-patch/utils/getOpData.js +10 -0
  58. package/dist/json-patch/utils/getType.d.ts +3 -0
  59. package/dist/json-patch/utils/getType.js +6 -0
  60. package/dist/json-patch/utils/index.d.ts +14 -0
  61. package/dist/json-patch/utils/index.js +14 -0
  62. package/dist/json-patch/utils/log.d.ts +2 -0
  63. package/dist/json-patch/utils/log.js +7 -0
  64. package/dist/json-patch/utils/ops.d.ts +14 -0
  65. package/dist/json-patch/utils/ops.js +103 -0
  66. package/dist/json-patch/utils/paths.d.ts +9 -0
  67. package/dist/json-patch/utils/paths.js +53 -0
  68. package/dist/json-patch/utils/pluck.d.ts +5 -0
  69. package/dist/json-patch/utils/pluck.js +30 -0
  70. package/dist/json-patch/utils/shallowCopy.d.ts +1 -0
  71. package/dist/json-patch/utils/shallowCopy.js +20 -0
  72. package/dist/json-patch/utils/softWrites.d.ts +7 -0
  73. package/dist/json-patch/utils/softWrites.js +18 -0
  74. package/dist/json-patch/utils/toArrayIndex.d.ts +1 -0
  75. package/dist/json-patch/utils/toArrayIndex.js +12 -0
  76. package/dist/json-patch/utils/toKeys.d.ts +1 -0
  77. package/dist/json-patch/utils/toKeys.js +15 -0
  78. package/dist/json-patch/utils/updateArrayIndexes.d.ts +5 -0
  79. package/dist/json-patch/utils/updateArrayIndexes.js +38 -0
  80. package/dist/json-patch/utils/updateArrayPath.d.ts +5 -0
  81. package/dist/json-patch/utils/updateArrayPath.js +45 -0
  82. package/dist/net/AbstractTransport.d.ts +47 -0
  83. package/dist/net/AbstractTransport.js +37 -0
  84. package/dist/net/PatchesOfflineFirst.d.ts +3 -0
  85. package/dist/net/PatchesOfflineFirst.js +3 -0
  86. package/dist/net/PatchesRealtime.d.ts +90 -0
  87. package/dist/net/PatchesRealtime.js +257 -0
  88. package/dist/net/index.d.ts +9 -0
  89. package/dist/net/index.js +8 -0
  90. package/dist/net/protocol/JSONRPCClient.d.ts +55 -0
  91. package/dist/net/protocol/JSONRPCClient.js +106 -0
  92. package/dist/net/protocol/types.d.ts +142 -0
  93. package/dist/net/protocol/types.js +1 -0
  94. package/dist/net/webrtc/WebRTCAwareness.d.ts +81 -0
  95. package/dist/net/webrtc/WebRTCAwareness.js +119 -0
  96. package/dist/net/webrtc/WebRTCTransport.d.ts +80 -0
  97. package/dist/net/webrtc/WebRTCTransport.js +157 -0
  98. package/dist/net/websocket/PatchesWebSocket.d.ts +107 -0
  99. package/dist/net/websocket/PatchesWebSocket.js +144 -0
  100. package/dist/net/websocket/SignalingService.d.ts +91 -0
  101. package/dist/net/websocket/SignalingService.js +140 -0
  102. package/dist/net/websocket/WebSocketTransport.d.ts +47 -0
  103. package/dist/net/websocket/WebSocketTransport.js +138 -0
  104. package/dist/persist/IndexedDBStore.d.ts +72 -0
  105. package/dist/persist/IndexedDBStore.js +283 -0
  106. package/dist/persist/index.d.ts +2 -0
  107. package/dist/persist/index.js +1 -0
  108. package/dist/server/BranchManager.d.ts +40 -0
  109. package/dist/server/BranchManager.js +138 -0
  110. package/dist/server/HistoryManager.d.ts +63 -0
  111. package/dist/server/HistoryManager.js +92 -0
  112. package/dist/server/PatchServer.d.ts +129 -0
  113. package/dist/server/PatchServer.js +358 -0
  114. package/dist/server/index.d.ts +4 -0
  115. package/dist/server/index.js +3 -0
  116. package/dist/types.d.ts +158 -0
  117. package/dist/types.js +1 -0
  118. package/dist/utils.d.ts +36 -0
  119. package/dist/utils.js +83 -0
  120. package/package.json +78 -0
package/README.md ADDED
@@ -0,0 +1,632 @@
1
+ # Patches
2
+
3
+ A friendly realtime library based on operational transformations.
4
+
5
+ <img src="./patches.png" alt="Patches the Dog" style="width: 300px;">
6
+
7
+ Patches is a TypeScript library designed for building real-time collaborative applications. It leverages Operational Transformation (OT) with a centralized server model to ensure document consistency across multiple clients. It supports versioning, offline work, branching, and can handle very large and very long-lived documents.
8
+
9
+ When working with a document in Patches, you are working with regular JavaScript data types. If it is supported by JSON, you can have it in your document. The `state` in your `doc.state` is your immutable data. When you modify your document with `doc.change(state => state.prop = 'new value')` the doc will get a new immutable `state` object with those changes applied.
10
+
11
+ ## Table of Contents
12
+
13
+ - [Why Operational Transformations?](#why-operational-transformations)
14
+ - [Key Concepts](#key-concepts)
15
+ - [Installation](#installation)
16
+ - [Getting Started](#getting-started)
17
+ - [Client Example](#client-example)
18
+ - [Server Example](#server-example)
19
+ - [Core Components](#core-components)
20
+ - [PatchDoc](#patchdoc)
21
+ - [PatchServer](#patchserver)
22
+ - [HistoryManager](#historymanager)
23
+ - [BranchManager](#branchmanager)
24
+ - [Backend Store](#backend-store)
25
+ - [Transport & Networking](#transport--networking)
26
+ - [Awareness (Presence, Cursors, etc.)](#awareness-presence-cursors-etc)
27
+ - [Basic Workflow](#basic-workflow)
28
+ - [Client-Side](#client-side)
29
+ - [Server-Side](#server-side)
30
+ - [Examples](#examples)
31
+ - [Simple Client Setup](#simple-client-setup)
32
+ - [Simple Server Setup](#simple-server-setup)
33
+ - [Advanced Topics](#advanced-topics)
34
+ - [Offline Support & Versioning](#offline-support--versioning)
35
+ - [Branching and Merging](#branching-and-merging)
36
+ - [Custom OT Types](#custom-ot-types)
37
+ - [JSON Patch (Legacy)](#json-patch-legacy)
38
+ - [Contributing](#contributing)
39
+ - [License](#license)
40
+
41
+ ## Why Operational Transformations?
42
+
43
+ **OT vs CRDT**
44
+ [Conflict-Free Replicated Datatypes](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type) are the newest and improved algorithm for collaborative editing, so why use [Operational Transformation](https://en.wikipedia.org/wiki/Operational_transformation)? There [are](https://thom.ee/blog/crdt-vs-operational-transformation/) [various](https://www.tiny.cloud/blog/real-time-collaboration-ot-vs-crdt/) [opinions](https://fiberplane.com/blog/why-we-at-fiberplane-use-operational-transformation-instead-of-crdt/) about which to use. We found that at [Dabble Writer](https://www.dabblewriter.com/), the performance of CRDTs was not good enough for some of the extremely large or long-lived documents for our customers. Even the highly optimized [Y.js](https://yjs.dev/) which we really hoped would work for us didn't cut it. And since our service requires a central server anyway, we decided to double-down on our OT library, spruce it up, deck it out, improve the [DX](https://en.wikipedia.org/wiki/User_experience#Developer_experience) for ourselves, but hopefully it is useful for you too.
45
+
46
+ **What about Y.js?**
47
+ For those who may want to let us know that Y.js can handle large documents, we did run tests ourselves. We were impressed with what Y.js offers and were hopeful it would work for us. We prefer to focus on the user experience than on our syncing library. However, it was not to be.
48
+
49
+ Our longest project contains over 480k operations in it. 😳 And considering we save written text in 30-second chunks, not character-by-character, you can start to understand how _extra_ some of our customers are in their writing. That project took a few hours to re-create in Y.js from our OT patches, ~4 seconds to load in an optimized, GCed Y.js doc on a fast Mac Studio, and ~20ms to add a new change to it. Compare that to our (this) OT library which takes 1-2ms to load the doc and 0.2ms to apply a new change to it. As projects grow larger or longer, OT's performance remains constant and CRDT's diminish. For _most_ use-cases CRDTs may be better, but if you have very large—or more importantly long-lived (many changes over time)—documents, you may find OT a better choice.
50
+
51
+ ## Key Concepts
52
+
53
+ - **Centralized OT:** Uses a central authority (the server) to definitively order operations, simplifying conflict resolution compared to fully distributed OT systems. ([Learn more about centralized vs. distributed OT](https://marijnhaverbeke.nl/blog/collaborative-editing.html#centralization)).
54
+ - **Rebasing:** Client changes are "rebased" on top of changes they receive from the server, ensuring local edits are adjusted correctly based on the server's history.
55
+ - **Linear History:** The server maintains a single, linear history of document revisions.
56
+ - **Client-Server Communication:** Clients send batches of changes (`Change` objects) tagged with the server revision they were based on (`baseRev`). The server transforms these changes, applies them, assigns a new revision number, and broadcasts the committed change back to clients.
57
+
58
+ **Why Centralized?**
59
+ There are many papers and algorithms for OT. There are problems—edge-cases—with those that don't rely on a central authority. To simplify, we use an algorithm that only transforms operations in one direction, rather than in 2. It is more like a git _rebase_. This method was inspired by [Marijn Haverbeke's article](https://marijnhaverbeke.nl/blog/collaborative-editing.html) about the topic, and we originally had the server reject changes if new ones came in before them and require the client to transform (rebase) them and resubmit. This comes with a theoretical downside, however. Slow connections and quickly changing documents may keep slower clients resubmitting over and over and never committing. For example, if you had an OT document that tracked all the mouse movements of every client connected to a document, a slow client might have severe jitter while it tries to commit its mouse position. I wouldn't suggest using OT for this use-case, but as I said, it is a theoretical downside. So we have modified our approach to make the server do the transform and commit, sending back any new changes _and_ the transformed submitted ones for the client to apply. This ensures all clients "get equal time with the server", even with slow connections.
60
+
61
+ **Snapshots**
62
+ OT documents are essentially an array of changes. To create the in-memory state of the document, the `doc.state` that you view, you must replay each change from the first to the last. You may recognize a problem here. For long documents (like ones with 480k changes), this could take some time. For this reason, OT will snapshot the data every X number of changes (200, 500, etc). This allows you to grab the latest snapshot and then any changes after it was created and replay those change on top of the snapshot to get the latest state. This is what allows OT to have consistent performance over time.
63
+
64
+ **Versioning as Snapshots**
65
+ Most realtime collaborative documents are accessed and changed in bursts—user sessions—where a person sits down to write, design, edit, whiteboard, etc. Most of these use-cases benefit from "versioning" features where the user can go back in time to see old versions of their project. Patches combines the concept of snapshots and versions. Instead of using X number of changes to decide when to create a snapshot, Patches creates a new versions/snapshots after there is more than 30 minutes between any 2 changes. Some versions or snapshots may only reflect 1 change. Others may reflect 100s. As long as your document isn't being constantly updated, the requirement of snapshots turns into a feature you can provide your users. _If you have an [IoT](https://en.wikipedia.org/wiki/Internet_of_things) use-case or something similar where there is no break to create versions, we'd be happy for a pull request that allows Patches to support both. But we didn't want to make the code more complex for something that may not be used._
66
+
67
+ **Immutable State**
68
+ Patches uses immutable data. That is, it uses gentleman's (and lady's) immutability, meaning, you _shouldn't_ change the structure, but for performance the objects aren't frozen. Each change creates a new object in memory, keeping the old objects that didn't change and replacing only those that did. There are [articles](https://www.freecodecamp.org/news/immutable-javascript-improve-application-performance/) [about](http://www.cowtowncoder.com/blog/archives/2010/08/entry_409.html) the [benefits](https://medium.com/@mohitgadhavi1/the-power-of-immutability-improving-javascript-performance-and-code-quality-96d82134d8da) of using immutable data, but suffice it to say, Patches assumes you won't be changing the state data outside of the `doc.change(stateProxy => {...})` method (which uses a proxy, BTW, and does not operate on the state directly).
69
+
70
+ ## Installation
71
+
72
+ ```bash
73
+ npm install @dabble/patches
74
+ # or
75
+ yarn add @dabble/patches
76
+ ```
77
+
78
+ ## Getting Started
79
+
80
+ Here's a quick overview of how to set up a basic client and server using Patches. These examples assume you have a way to communicate changes between the client and server (e.g., WebSockets, HTTP polling).
81
+
82
+ _(Note: These are simplified examples. Real-world implementations require proper error handling, network communication, authentication, and a persistent backend store.)_
83
+
84
+ ### Client Example
85
+
86
+ This shows how to initialize `PatchDoc`, make local changes, and handle sending/receiving updates.
87
+
88
+ ```typescript
89
+ import { PatchDoc, Change } from '@dabble/patches';
90
+
91
+ interface MyDoc {
92
+ text: string;
93
+ count: number;
94
+ }
95
+
96
+ const patchDoc = new PatchDoc<MyDoc>();
97
+
98
+ // React to Updates (e.g., update UI)
99
+ patchDoc.onUpdate(newState => {
100
+ console.log('Document updated:', newState);
101
+ // Update your UI here
102
+ });
103
+
104
+ // Make Local Changes
105
+ patchDoc.change(draft => {
106
+ draft.text = 'Hello World!';
107
+ draft.count += 1;
108
+ });
109
+
110
+ // Triggered after local changes occur
111
+ patchDoc.onChange(change => {
112
+ syncWithServer();
113
+ });
114
+
115
+ // Or Trigger Sync Periodically or On Events
116
+ // setInterval(syncWithServer, 5000); // Example: sync every 5 seconds
117
+ // syncWithServer(); // Initial sync
118
+
119
+ // Syncing Logic (Simplified)
120
+ async function syncWithServer() {
121
+ // 1. Get local changes to send
122
+ const changesToSend = patchDoc.getUpdatesForServer();
123
+ if (changesToSend.length > 0) {
124
+ try {
125
+ // 2. Send changes via your network layer (e.g., fetch, WebSocket)
126
+ // Replace 'sendChangesToServer' with your actual implementation
127
+ const serverCommit = await sendChangesToServer(initialDocId, changesToSend);
128
+ // 3. Apply server's confirmation
129
+ patchDoc.applyServerConfirmation(serverCommit);
130
+ } catch (error) {
131
+ console.error('Error sending changes:', error);
132
+ // Handle error (e.g., retry, show message)
133
+ }
134
+ }
135
+
136
+ // 4. Receive external changes from server (e.g., via WebSocket listener)
137
+ // Replace 'checkForServerUpdates' with your actual implementation
138
+ const externalChanges = await checkForServerUpdates(initialDocId, patchDoc.rev);
139
+ if (externalChanges && externalChanges.length > 0) {
140
+ try {
141
+ patchDoc.applyExternalServerUpdate(externalChanges);
142
+ } catch (error) {
143
+ console.error('Error applying external changes:', error);
144
+ // Handle error (potentially requires full resync)
145
+ }
146
+ }
147
+ }
148
+ ```
149
+
150
+ ### Server Example
151
+
152
+ This outlines a basic Express server using `PatchServer` with an in-memory store.
153
+
154
+ ```typescript
155
+ import express from 'express';
156
+ import { PatchServer, PatchStoreBackend, Change } from '@dabble/patches';
157
+
158
+ // Server Setup
159
+ const store = new InMemoryStore(); // Fictional in-memory backend, use a database
160
+ const server = new PatchServer(store);
161
+ const app = express();
162
+ app.use(express.json());
163
+
164
+ // Endpoint to receive changes
165
+ app.post('/docs/:docId/changes', async (req, res) => {
166
+ const docId = req.params.docId;
167
+ const clientChanges: Change[] = req.body.changes;
168
+
169
+ if (!Array.isArray(clientChanges)) {
170
+ return res.status(400).json({ error: 'Invalid request' });
171
+ }
172
+
173
+ try {
174
+ // Process incoming changes
175
+ const committedChanges = await server.receiveChanges(docId, clientChanges);
176
+ // Send confirmation back to the sender
177
+ res.json(committedChanges);
178
+ // Broadcast committed changes to other connected clients (via WebSockets, etc.)
179
+ // broadcastChanges(docId, committedChanges, req.headers['x-client-id']);
180
+ } catch (error: any) {
181
+ console.error(`Error processing changes for ${docId}:`, error);
182
+ const statusCode = error.message.includes('out of sync') ? 409 : 500;
183
+ res.status(statusCode).json({ error: error.message });
184
+ }
185
+ });
186
+
187
+ // Endpoint to get initial state
188
+ app.get('/docs/:docId', async (req, res) => {
189
+ const docId = req.params.docId;
190
+ try {
191
+ const { state, rev } = await server.getLatestDocumentStateAndRev(docId);
192
+ res.json({ state: state ?? {}, rev }); // Default to empty obj if new
193
+ } catch (error) {
194
+ console.error(`Error fetching state for ${docId}:`, error);
195
+ res.status(500).json({ error: 'Failed to fetch document state.' });
196
+ }
197
+ });
198
+
199
+ const PORT = 3000;
200
+ app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
201
+ ```
202
+
203
+ For more detailed explanations and advanced features, dive into the [Core Components](#core-components) and [Examples](#examples) sections.
204
+
205
+ ## Core Components
206
+
207
+ Centralized OT has two different areas of focus, the server and the client. They both have very different jobs and interaction patterns.
208
+
209
+ These are the main classes you'll interact with when building a collaborative application with Patches.
210
+
211
+ ### PatchDoc
212
+
213
+ (`PatchDoc` Documentation: [`docs/PatchDoc.md`](./docs/PatchDoc.md))
214
+
215
+ This is what you'll be working with on the client in your app. It is the client-side view of a collaborative document. You can check out [`docs/operational-transformation.md#patchdoc`](./docs/operational-transformation.md#patchdoc) to learn more about its role in the OT flow.
216
+
217
+ - **Local State Management:** Maintains the _committed_ state (last known server state), sending changes (awaiting server confirmation), and pending changes (local edits not yet sent).
218
+ - **Optimistic Updates:** Applies local changes immediately for a responsive UI.
219
+ - **Synchronization:** Implements the client-side OT logic:
220
+ - Sends pending changes to the server (`getUpdatesForServer`).
221
+ - Applies server confirmations (`applyServerConfirmation`).
222
+ - Applies external server updates from other clients (`applyExternalServerUpdate`), rebasing local changes as needed.
223
+ - **Event Emitters:** Provides hooks (`onUpdate`, `onChange`, etc.) to react to state changes.
224
+
225
+ See [`docs/PatchDoc.md`](./docs/PatchDoc.md) for detailed usage and examples.
226
+
227
+ ### PatchServer
228
+
229
+ (`PatchServer` Documentation: [`docs/PatchServer.md`](./docs/PatchServer.md))
230
+
231
+ The heart of the server-side logic. See [`docs/operational-transformation.md#patchserver`](./docs/operational-transformation.md#patchserver) for its role in the OT flow.
232
+
233
+ - **Receives Changes:** Handles incoming `Change` objects from clients.
234
+ - **Transformation:** Transforms client changes against concurrent server changes using the OT algorithm.
235
+ - **Applies Changes:** Applies the final transformed changes to the authoritative document state.
236
+ - **Versioning:** Creates version snapshots based on time-based sessions or explicit triggers (useful for history and offline support).
237
+ - **Persistence:** Uses a `PatchStoreBackend` implementation to save/load document state, changes, and versions.
238
+
239
+ See [`docs/PatchServer.md`](./docs/PatchServer.md) for detailed usage and examples.
240
+
241
+ ### HistoryManager
242
+
243
+ (`HistoryManager` Documentation: [`docs/HistoryManager.md`](./docs/HistoryManager.md))
244
+
245
+ Provides an API for querying the history ([`VersionMetadata`](./docs/types.ts)) of a document.
246
+
247
+ - **List Versions:** Retrieve metadata about saved document versions (snapshots).
248
+ - **Get Version State/Changes:** Load the full state or the specific changes associated with a past version.
249
+ - **List Server Changes:** Query the raw sequence of committed server changes based on revision numbers.
250
+
251
+ See [`docs/HistoryManager.md`](./docs/HistoryManager.md) for detailed usage and examples.
252
+
253
+ ### BranchManager
254
+
255
+ (`BranchManager` Documentation: [`docs/BranchManager.md`](./docs/BranchManager.md))
256
+
257
+ Manages branching ([`Branch`](./docs/types.ts)) and merging workflows.
258
+
259
+ - **Create Branch:** Creates a new document branching off from a source document at a specific revision.
260
+ - **List Branches:** Retrieves information about existing branches.
261
+ - **Merge Branch:** Merges the changes made on a branch back into its source document (requires OT on the server to handle conflicts).
262
+ - **Close Branch:** Marks a branch as closed, merged, or abandoned.
263
+
264
+ See [`docs/BranchManager.md`](./docs/BranchManager.md) for detailed usage and examples.
265
+
266
+ ### Backend Store
267
+
268
+ ([`PatchStoreBackend` / `BranchingStoreBackend`](./docs/operational-transformation.md#backend-store-interface) Documentation: [`docs/operational-transformation.md#backend-store-interface`](./docs/operational-transformation.md#backend-store-interface))
269
+
270
+ This isn't a specific class provided by the library, but rather an _interface_ (`PatchStoreBackend` and `BranchingStoreBackend`) that you need to implement. This interface defines how the `PatchServer`, `HistoryManager`, and `BranchManager` interact with your chosen persistence layer (e.g., a database, file system, in-memory store).
271
+
272
+ You are responsible for providing an implementation that fulfills the methods defined in the interface (e.g., `getLatestRevision`, `saveChange`, `listVersions`, `createBranch`).
273
+
274
+ See [`docs/operational-transformation.md#backend-store-interface`](./docs/operational-transformation.md#backend-store-interface) for the interface definition.
275
+
276
+ ## Transport & Networking
277
+
278
+ Patches provides flexible networking options for real-time collaboration:
279
+
280
+ - **WebSocket Transport:** For most applications, use the high-level [`PatchesWebSocket`](./docs/websocket.md) class to connect to a central Patches server. This handles document updates, awareness, and versioning over a persistent WebSocket connection.
281
+ - **WebRTC Transport:** For peer-to-peer scenarios, use [`WebRTCTransport`](./docs/operational-transformation.md#webrtc) and [`WebRTCAwareness`](./docs/awareness.md) for direct client-to-client communication and awareness.
282
+
283
+ See [WebSocket Transport](./docs/websocket.md) and [Awareness](./docs/awareness.md) for detailed usage and examples.
284
+
285
+ **When to use which?**
286
+
287
+ - Use WebSocket for most collaborative apps with a central server.
288
+ - Use WebRTC for peer-to-peer or hybrid topologies, or to reduce server load for awareness/presence.
289
+
290
+ ---
291
+
292
+ ## Awareness (Presence, Cursors, etc.)
293
+
294
+ "Awareness" lets you show who is online, where their cursor is, and more. Patches supports awareness over both WebSocket (server-mediated) and WebRTC (peer-to-peer). You can use awareness to build collaborative cursors, user lists, and more.
295
+
296
+ See [Awareness documentation](./docs/awareness.md) for how to use awareness features in your app.
297
+
298
+ ## Basic Workflow
299
+
300
+ ### Client-Side (`PatchDoc`)
301
+
302
+ 1. **Initialize `PatchDoc`:** Create an instance. See [`docs/PatchDoc.md#initialization`](./docs/PatchDoc.md#initialization).
303
+ 2. **Subscribe to Updates:** Use [`doc.onUpdate`](./docs/PatchDoc.md#onupdate).
304
+ 3. **Make Local Changes:** Use [`doc.change()`](./docs/PatchDoc.md#update).
305
+ 4. **Send Changes:** Use [`doc.getUpdatesForServer()`](./docs/PatchDoc.md#getupdatesforserver) and [`doc.applyServerConfirmation()`](./docs/PatchDoc.md#applyserverconfirmation).
306
+ 5. **Receive Server Changes:** Use [`doc.applyExternalServerUpdate()`](./docs/PatchDoc.md#applyexternalserverupdate).
307
+
308
+ ### Server-Side (`PatchServer`)
309
+
310
+ 1. **Initialize `PatchServer`:** Create an instance. See [`docs/PatchServer.md#initialization`](./docs/PatchServer.md#initialization).
311
+ 2. **Receive Client Changes:** Use [`server.receiveChanges()`](./docs/PatchServer.md#core-method-receivechanges).
312
+ 3. **Handle History/Branching:** Use [`HistoryManager`](./docs/HistoryManager.md) and [`BranchManager`](./docs/BranchManager.md).
313
+
314
+ ## Examples
315
+
316
+ _(Note: These are simplified examples. Real-world implementations require proper error handling, network communication, authentication, and backend setup.)_
317
+
318
+ ### Simple Client Setup
319
+
320
+ ```typescript
321
+ import { PatchDoc, Change } from '@dabble/patches';
322
+
323
+ interface MyDoc {
324
+ text: string;
325
+ count: number;
326
+ }
327
+
328
+ // Assume these are fetched initially
329
+ const initialDocId = 'doc123';
330
+ const initialServerState: MyDoc = { text: 'Hello', count: 0 };
331
+ const initialServerRev = 5; // Revision corresponding to initialServerState
332
+
333
+ // Your function to send changes and receive the server's commit
334
+ async function sendChangesToServer(docId: string, changes: Change[]): Promise<Change[]> {
335
+ const response = await fetch(`/docs/${docId}/changes`, {
336
+ method: 'POST',
337
+ headers: { 'Content-Type': 'application/json' },
338
+ body: JSON.stringify({ changes }),
339
+ });
340
+ if (!response.ok) {
341
+ const errorData = await response.json();
342
+ throw new Error(errorData.error || `Server error: ${response.status}`);
343
+ }
344
+ return await response.json();
345
+ }
346
+
347
+ // --- Initialize PatchDoc ---
348
+ const patchDoc = new PatchDoc<MyDoc>(initialServerState, initialServerRev);
349
+
350
+ // --- UI Update Logic ---
351
+ patchDoc.onUpdate(newState => {
352
+ console.log('Document updated:', newState);
353
+ // Update your UI element displaying newState.text, newState.count, etc.
354
+ });
355
+
356
+ // --- Making a Local Change ---
357
+ function handleTextInput(newText: string) {
358
+ patchDoc.change(draft => {
359
+ draft.text = newText;
360
+ });
361
+ // Trigger sending changes (e.g., debounced)
362
+ sendLocalChanges();
363
+ }
364
+
365
+ function handleIncrement() {
366
+ patchDoc.change(draft => {
367
+ draft.count = (draft.count || 0) + 1;
368
+ });
369
+ sendLocalChanges();
370
+ }
371
+
372
+ // --- Sending Changes ---
373
+ let isSending = false;
374
+ async function sendLocalChanges() {
375
+ if (isSending || !patchDoc.hasPending) return;
376
+
377
+ isSending = true;
378
+ try {
379
+ const changesToSend = patchDoc.getUpdatesForServer();
380
+ if (changesToSend.length > 0) {
381
+ console.log('Sending changes:', changesToSend);
382
+ const serverCommit = await sendChangesToServer(initialDocId, changesToSend);
383
+ console.log('Received confirmation:', serverCommit);
384
+ patchDoc.applyServerConfirmation(serverCommit);
385
+ }
386
+ } catch (error) {
387
+ console.error('Failed to send changes:', error);
388
+ // Handle error - maybe retry, revert local changes, or force resync
389
+ // For simplicity, just log here. PatchDoc state might be inconsistent.
390
+ } finally {
391
+ isSending = false;
392
+ // Check again in case new changes came in while sending
393
+ if (patchDoc.hasPending) {
394
+ setTimeout(sendLocalChanges, 100); // Basic retry/check again
395
+ }
396
+ }
397
+ }
398
+
399
+ // --- Receiving External Changes (e.g., via WebSocket) ---
400
+ function handleServerBroadcast(externalChanges: Change[]) {
401
+ if (!externalChanges || externalChanges.length === 0) return;
402
+ console.log('Received external changes:', externalChanges);
403
+ try {
404
+ patchDoc.applyExternalServerUpdate(externalChanges);
405
+ } catch (error) {
406
+ console.error('Error applying external server changes:', error);
407
+ // Critical error - likely need to resync the document state
408
+ }
409
+ }
410
+
411
+ // --- Example Usage ---
412
+ // handleTextInput("Hello World!");
413
+ // handleIncrement();
414
+ // Assume setup for receiving broadcasts via `handleServerBroadcast`
415
+ ```
416
+
417
+ ### Simple Server Setup
418
+
419
+ ```typescript
420
+ import express from 'express';
421
+ import {
422
+ PatchServer,
423
+ PatchStoreBackend,
424
+ Change,
425
+ VersionMetadata, //... other types
426
+ } from '@dabble/patches';
427
+
428
+ // --- Basic In-Memory Store (Replace with a real backend!) ---
429
+ class InMemoryStore implements PatchStoreBackend {
430
+ private docs: Map<string, { state: any; rev: number; changes: Change[]; versions: VersionMetadata[] }> = new Map();
431
+
432
+ async getLatestRevision(docId: string): Promise<number> {
433
+ return this.docs.get(docId)?.rev ?? 0;
434
+ }
435
+ async getLatestState(docId: string): Promise<any | undefined> {
436
+ const doc = this.docs.get(docId);
437
+ // Return a deep copy to prevent accidental mutation
438
+ return doc ? JSON.parse(JSON.stringify(doc.state)) : undefined;
439
+ }
440
+ async getStateAtRevision(docId: string, rev: number): Promise<any | undefined> {
441
+ // IMPORTANT: In-memory store cannot easily reconstruct past states without snapshots.
442
+ // A real implementation would replay changes or load version snapshots.
443
+ // This basic version only returns the latest state if rev matches.
444
+ const doc = this.docs.get(docId);
445
+ if (doc && doc.rev === rev) {
446
+ return JSON.parse(JSON.stringify(doc.state)); // Return copy
447
+ }
448
+ // Try finding a version snapshot matching the revision
449
+ const version = doc?.versions.find(v => v.endDate === rev); // Approximation!
450
+ if (version) {
451
+ return JSON.parse(JSON.stringify(version.state));
452
+ }
453
+ // Fallback: Cannot reconstruct this revision
454
+ if (rev === 0 && !doc) return {}; // Initial empty state at rev 0
455
+ console.warn(
456
+ `In-Memory Store: Cannot get state at revision ${rev} for doc ${docId}. Returning latest or undefined.`
457
+ );
458
+ return doc ? JSON.parse(JSON.stringify(doc.state)) : undefined; // Or throw error
459
+ }
460
+ async saveChange(docId: string, change: Change): Promise<void> {
461
+ let doc = this.docs.get(docId);
462
+ if (!doc) {
463
+ doc = { state: {}, rev: 0, changes: [], versions: [] };
464
+ this.docs.set(docId, doc);
465
+ }
466
+ // Apply change to get new state (use library's apply function)
467
+ const { applyChanges } = await import('@dabble/patches'); // Assuming exported
468
+ doc.state = applyChanges(doc.state, [change]);
469
+ doc.rev = change.rev;
470
+ doc.changes.push(change); // Store history of changes
471
+ console.log(`[Store] Saved change rev ${change.rev} for doc ${docId}. New state:`, doc.state);
472
+ }
473
+ async listChanges(docId: string, options: any): Promise<Change[]> {
474
+ const doc = this.docs.get(docId);
475
+ if (!doc) return [];
476
+ let changes = doc.changes;
477
+ if (options.startAfterRev !== undefined) {
478
+ changes = changes.filter(c => c.rev > options.startAfterRev!);
479
+ }
480
+ // Add other filter/limit/sort logic here based on options
481
+ changes.sort((a, b) => a.rev - b.rev); // Ensure ascending order
482
+ if (options.limit) {
483
+ changes = changes.slice(0, options.limit);
484
+ }
485
+ return changes;
486
+ }
487
+ async saveVersion(docId: string, version: VersionMetadata): Promise<void> {
488
+ const doc = this.docs.get(docId);
489
+ if (!doc) {
490
+ // This case is less likely if saveChange created the doc, but handle defensively
491
+ console.warn(`[Store] Cannot save version for non-existent doc ${docId}`);
492
+ return;
493
+ }
494
+ // Simple: just add to list. A real store might index/optimize.
495
+ doc.versions.push(version);
496
+ console.log(`[Store] Saved version ${version.id} (${version.origin}) for doc ${docId}.`);
497
+ }
498
+ async listVersions(docId: string, options: any): Promise<VersionMetadata[]> {
499
+ const doc = this.docs.get(docId);
500
+ if (!doc) return [];
501
+ let versions = doc.versions;
502
+ // Apply filtering/sorting based on options (simplified)
503
+ if (options.origin) {
504
+ versions = versions.filter(v => v.origin === options.origin);
505
+ }
506
+ if (options.groupId) {
507
+ versions = versions.filter(v => v.groupId === options.groupId);
508
+ }
509
+ // ... other filters ...
510
+ versions.sort((a, b) => (options.reverse ? b.startDate - a.startDate : a.startDate - b.startDate));
511
+ if (options.limit) {
512
+ versions = versions.slice(0, options.limit);
513
+ }
514
+ return versions;
515
+ }
516
+ async loadVersionMetadata(docId: string, versionId: string): Promise<VersionMetadata | null> {
517
+ const doc = this.docs.get(docId);
518
+ return doc?.versions.find(v => v.id === versionId) ?? null;
519
+ }
520
+ async loadVersionState(docId: string, versionId: string): Promise<any | undefined> {
521
+ const meta = await this.loadVersionMetadata(docId, versionId);
522
+ return meta ? JSON.parse(JSON.stringify(meta.state)) : undefined; // Return copy
523
+ }
524
+ async loadVersionChanges(docId: string, versionId: string): Promise<Change[]> {
525
+ const meta = await this.loadVersionMetadata(docId, versionId);
526
+ return meta ? meta.changes : [];
527
+ }
528
+ async getLatestVersionMetadata(docId: string): Promise<VersionMetadata | null> {
529
+ const doc = this.docs.get(docId);
530
+ if (!doc || doc.versions.length === 0) return null;
531
+ // Find version with the latest endDate
532
+ return doc.versions.reduce(
533
+ (latest, current) => (!latest || current.endDate > latest.endDate ? current : latest),
534
+ null as VersionMetadata | null
535
+ );
536
+ }
537
+ // Implement BranchingStoreBackend methods if needed...
538
+ }
539
+
540
+ // --- Server Setup ---
541
+ const store = new InMemoryStore();
542
+ const server = new PatchServer(store);
543
+ const app = express();
544
+ app.use(express.json());
545
+
546
+ // --- Mock Broadcast (Replace with WebSocket/SSE etc.) ---
547
+ const clients = new Map<string, Set<any>>(); // docId -> Set<client connections>
548
+ function broadcastChanges(docId: string, changes: Change[], senderClientId: string | null) {
549
+ console.log(`Broadcasting changes for ${docId} to other clients:`, changes);
550
+ // Implement actual broadcast logic here (e.g., WebSockets)
551
+ // clients.get(docId)?.forEach(client => {
552
+ // if (client.id !== senderClientId) { // Don't send back to sender
553
+ // client.send(JSON.stringify({ type: 'changes', docId, changes }));
554
+ // }
555
+ // });
556
+ }
557
+
558
+ // --- API Endpoint ---
559
+ app.post('/docs/:docId/changes', async (req, res) => {
560
+ const docId = req.params.docId;
561
+ const clientChanges: Change[] = req.body.changes;
562
+ const clientId = req.headers['x-client-id'] as string | null; // Example client ID header
563
+
564
+ // Basic validation
565
+ if (!Array.isArray(clientChanges)) {
566
+ return res.status(400).json({ error: 'Invalid request: expected changes array.' });
567
+ }
568
+
569
+ console.log(`Received ${clientChanges.length} changes for doc ${docId} from client ${clientId || 'unknown'}`);
570
+
571
+ try {
572
+ const committedChanges = await server.receiveChanges(docId, clientChanges);
573
+ console.log(`Committed ${committedChanges.length} changes for doc ${docId}, rev: ${committedChanges[0]?.rev}`);
574
+ res.json(committedChanges); // Send confirmation back to sender
575
+
576
+ // Broadcast to others if changes were made
577
+ if (committedChanges.length > 0) {
578
+ broadcastChanges(docId, committedChanges, clientId);
579
+ }
580
+ } catch (error: any) {
581
+ console.error(`Error processing changes for doc ${docId}:`, error);
582
+ // Use 409 Conflict for revision mismatches, 500 for others
583
+ const statusCode = error.message.includes('out of sync') ? 409 : 500;
584
+ res.status(statusCode).json({ error: error.message });
585
+ }
586
+ });
587
+
588
+ // Endpoint to get latest state (for new clients)
589
+ app.get('/docs/:docId', async (req, res) => {
590
+ const docId = req.params.docId;
591
+ try {
592
+ const { state, rev } = await server.getLatestDocumentStateAndRev(docId);
593
+ res.json({ state: state ?? {}, rev }); // Provide empty object if state is undefined
594
+ } catch (error: any) {
595
+ console.error(`Error fetching state for doc ${docId}:`, error);
596
+ res.status(500).json({ error: 'Failed to fetch document state.' });
597
+ }
598
+ });
599
+
600
+ const PORT = 3000;
601
+ app.listen(PORT, () => {
602
+ console.log(`Server listening on port ${PORT}`);
603
+ });
604
+ ```
605
+
606
+ ## Advanced Topics
607
+
608
+ ### Offline Support & Versioning
609
+
610
+ See [`PatchServer Versioning`](./docs/PatchServer.md#versioning) and [`HistoryManager`](./docs/HistoryManager.md).
611
+
612
+ ### Branching and Merging
613
+
614
+ See [`BranchManager`](./docs/BranchManager.md).
615
+
616
+ ### Custom OT Types
617
+
618
+ See [`Operational Transformation > Operation Handlers`](./docs/operational-transformation.md#operation-handlers).
619
+
620
+ ## JSON Patch (Legacy)
621
+
622
+ See [`docs/json-patch.md`](./docs/json-patch.md) for documentation on the JSON Patch features, including [`JSONPatch`](./docs/json-patch.md#jsonpatch-class) and [`createJSONPatch`](./docs/json-patch.md#createjsonpatch-helper).
623
+
624
+ ## Contributing
625
+
626
+ Contributions are welcome! Please feel free to open issues or submit pull requests.
627
+
628
+ _(TODO: Add contribution guidelines)_
629
+
630
+ ## License
631
+
632
+ [MIT](./LICENSE_MIT)