@dabble/patches 0.2.12 → 0.2.14

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 CHANGED
@@ -1,12 +1,26 @@
1
1
  # Patches
2
2
 
3
- A friendly realtime library based on operational transformations.
3
+ **Hello, friend!** Meet your new favorite realtime library. It's based on operational transformations, but don't let that scare you!
4
4
 
5
5
  <img src="./patches.png" alt="Patches the Dog" style="width: 300px;">
6
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.
7
+ ## What Is This Thing?
8
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.
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!
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.
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!
14
+
15
+ ## Why You'll Love It
16
+
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:
18
+
19
+ ```js
20
+ doc.change(state => (state.prop = 'new value'));
21
+ ```
22
+
23
+ And bam! You get a fresh new state with your changes applied.
10
24
 
11
25
  ## Table of Contents
12
26
 
@@ -17,56 +31,49 @@ When working with a document in Patches, you are working with regular JavaScript
17
31
  - [Client Example](#client-example)
18
32
  - [Server Example](#server-example)
19
33
  - [Core Components](#core-components)
20
- - [Patches](#patches-main-client)
21
- - [PatchesDoc](#patchesdoc-document-instance)
22
- - [PatchesServer](#patchesserver)
23
- - [PatchesHistoryManager](#patcheshistorymanager)
24
- - [PatchesBranchManager](#patchesbranchmanager)
25
- - [Backend Store](#backend-store)
26
- - [Transport & Networking](#transport--networking)
27
- - [Awareness (Presence, Cursors, etc.)](#awareness-presence-cursors-etc)
28
34
  - [Basic Workflow](#basic-workflow)
29
- - [Client-Side](#client-side)
30
- - [Server-Side](#server-side)
31
35
  - [Examples](#examples)
32
- - [Simple Client Setup](#simple-client-setup)
33
- - [Simple Server Setup](#simple-server-setup)
34
36
  - [Advanced Topics](#advanced-topics)
35
- - [Offline Support & Versioning](#offline-support--versioning)
36
- - [Branching and Merging](#branching-and-merging)
37
- - [Custom OT Types](#custom-ot-types)
38
37
  - [JSON Patch (Legacy)](#json-patch-legacy)
39
38
  - [Contributing](#contributing)
40
39
  - [License](#license)
41
40
 
42
41
  ## Why Operational Transformations?
43
42
 
44
- **OT vs CRDT**
45
- [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.
43
+ **"Wait, shouldn't I be using CRDTs instead?"**
44
+
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.
46
46
 
47
- **What about Y.js?**
48
- 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.
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.
49
48
 
50
- 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 largeor more importantly long-lived (many changes over time)—documents, you may find OT a better choice.
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.
51
50
 
52
51
  ## Key Concepts
53
52
 
54
- - **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)).
55
- - **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.
56
- - **Linear History:** The server maintains a single, linear history of document revisions.
57
- - **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.
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.
58
57
 
59
58
  **Why Centralized?**
60
- 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.
61
59
 
62
- **Snapshots**
63
- 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.
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.
64
61
 
65
- **Versioning as Snapshots**
66
- 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._
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!
63
+
64
+ **Snapshots = Performance Magic**
65
+
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!
67
73
 
68
74
  **Immutable State**
69
- 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).
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).
70
77
 
71
78
  ## Installation
72
79
 
@@ -78,64 +85,61 @@ yarn add @dabble/patches
78
85
 
79
86
  ## Getting Started
80
87
 
81
- 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).
82
-
83
- _(Note: These are simplified examples. Real-world implementations require proper error handling, network communication, authentication, and a persistent backend store.)_
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.)
84
89
 
85
90
  ### Client Example
86
91
 
87
- This shows how to initialize `Patches` (the main client interface) with an in-memory store and set up real-time sync with `PatchesSync`.
92
+ Here's how to get rolling with Patches on the client:
88
93
 
89
94
  ```typescript
90
95
  import { Patches } from '@dabble/patches';
91
- import { InMemoryStore } from '@dabble/patches/persist/InMemoryStore';
92
- import { PatchesSync } from '@dabble/patches/net/PatchesSync';
96
+ import { InMemoryStore } from '@dabble/patches/persist';
97
+ import { PatchesSync } from '@dabble/patches/net';
93
98
 
94
99
  interface MyDoc {
95
100
  text: string;
96
101
  count: number;
97
102
  }
98
103
 
99
- // 1. Create a store (in-memory for demo; use IndexedDB or your own for production)
104
+ // 1. Create a store (just using in-memory for this demo)
100
105
  const store = new InMemoryStore();
101
106
 
102
- // 2. Create the main Patches client instance
107
+ // 2. Create the main Patches client
103
108
  const patches = new Patches({ store });
104
109
 
105
110
  // 3. Set up real-time sync with your server
106
111
  const sync = new PatchesSync('wss://your-server-url', patches);
107
- await sync.connect(); // Connect to the server (returns a promise)
112
+ await sync.connect(); // Connect to the server!
108
113
 
109
114
  // 4. Open or create a document by ID
110
115
  const doc = await patches.openDoc<MyDoc>('my-doc-1');
111
116
 
112
- // 5. React to updates (e.g., update UI)
117
+ // 5. React to updates (update your UI here)
113
118
  doc.onUpdate(newState => {
114
119
  console.log('Document updated:', newState);
115
120
  // Update your UI here
116
121
  });
117
122
 
118
123
  // 6. Make local changes
119
- // (Changes are applied optimistically and will be synced to the server)
124
+ // (Changes apply immediately locally and sync to the server automatically)
120
125
  doc.change(draft => {
121
126
  draft.text = 'Hello World!';
122
127
  draft.count = (draft.count || 0) + 1;
123
128
  });
124
129
 
125
- // 7. Changes are automatically synced using PatchesSync.
126
- // If not using PatchesSync, you can manually flush changes to your backend as needed.
130
+ // 7. That's it! Changes sync automatically with PatchesSync
127
131
  ```
128
132
 
129
133
  ### Server Example
130
134
 
131
- This outlines a basic Express server using `PatchesServer` with an in-memory store.
135
+ Here's a basic Express server using `PatchesServer`:
132
136
 
133
137
  ```typescript
134
138
  import express from 'express';
135
- import { PatchesServer, PatchesStoreBackend, Change } from '@dabble/patches';
139
+ import { PatchesServer, PatchesStoreBackend, Change } from '@dabble/patches/server';
136
140
 
137
141
  // Server Setup
138
- const store = new InMemoryStore(); // Fictional in-memory backend, use a database
142
+ const store = new InMemoryStore(); // Use a real database in production!
139
143
  const server = new PatchesServer(store);
140
144
  const app = express();
141
145
  app.use(express.json());
@@ -143,7 +147,7 @@ app.use(express.json());
143
147
  // Endpoint to receive changes
144
148
  app.post('/docs/:docId/changes', async (req, res) => {
145
149
  const docId = req.params.docId;
146
- const clientChanges: Change[] = req.body.changes;
150
+ const clientChanges = req.body.changes;
147
151
 
148
152
  if (!Array.isArray(clientChanges)) {
149
153
  return res.status(400).json({ error: 'Invalid request' });
@@ -156,7 +160,7 @@ app.post('/docs/:docId/changes', async (req, res) => {
156
160
  res.json(committedChanges);
157
161
  // Broadcast committed changes to other connected clients (via WebSockets, etc.)
158
162
  // broadcastChanges(docId, committedChanges, req.headers['x-client-id']);
159
- } catch (error: any) {
163
+ } catch (error) {
160
164
  console.error(`Error processing changes for ${docId}:`, error);
161
165
  const statusCode = error.message.includes('out of sync') ? 409 : 500;
162
166
  res.status(statusCode).json({ error: error.message });
@@ -179,139 +183,119 @@ const PORT = 3000;
179
183
  app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
180
184
  ```
181
185
 
182
- For more detailed explanations and advanced features, dive into the [Core Components](#core-components) and [Examples](#examples) sections.
186
+ For more details and advanced features, check out the rest of the docs!
183
187
 
184
188
  ## Core Components
185
189
 
186
- Centralized OT has two different areas of focus, the server and the client. They both have very different jobs and interaction patterns.
187
-
188
- These are the main classes you'll interact with when building a collaborative application with Patches.
190
+ Centralized OT has two very different areas: server and client. They do completely different jobs!
189
191
 
190
192
  ### Patches (Main Client)
191
193
 
192
- (`Patches` Documentation: [`docs/Patches.md`](./docs/Patches.md))
194
+ ([`docs/Patches.md`](./docs/Patches.md))
193
195
 
194
- This is the main entry point you'll use on the client in your app. It manages document instances (`PatchesDoc`) and persistence (`PatchesStore`). You obtain a `PatchesDoc` by calling `patches.openDoc(docId)`.
196
+ This is your main entry point on the client. It manages document instances and persistence. You get a `PatchesDoc` by calling `patches.openDoc(docId)`.
195
197
 
196
- - **Document Management:** Handles opening, tracking, and closing collaborative documents.
197
- - **Persistence:** Integrates with a pluggable store (e.g., in-memory, IndexedDB, custom backend).
198
- - **Sync Integration:** Works with `PatchesSync` for real-time server sync.
199
- - **Event Emitters:** Provides hooks (`onError`, `onServerCommit`, etc.) to react to system-level events.
200
-
201
- See [`docs/PatchesDoc.md`](./docs/PatchesDoc.md) for detailed usage and examples.
198
+ - **Document Management:** Opens, tracks, and closes collaborative documents
199
+ - **Persistence:** Works with pluggable storage (in-memory, IndexedDB, custom)
200
+ - **Sync Integration:** Pairs with `PatchesSync` for real-time server communication
201
+ - **Event Emitters:** Hooks like `onError` and `onServerCommit` for reacting to events
202
202
 
203
203
  ### PatchesDoc (Document Instance)
204
204
 
205
- (`PatchesDoc` Documentation: [`docs/PatchesDoc.md`](./docs/PatchesDoc.md))
206
-
207
- A `PatchesDoc` represents a single collaborative document. You do not instantiate this directly in most apps; instead, use `patches.openDoc(docId)`.
205
+ ([`docs/PatchesDoc.md`](./docs/PatchesDoc.md))
208
206
 
209
- - **Local State Management:** Maintains the _committed_ state (last known server state), sending changes (awaiting server confirmation), and pending changes (local edits not yet sent).
210
- - **Optimistic Updates:** Applies local changes immediately for a responsive UI.
211
- - **Synchronization:** Implements the client-side OT logic:
212
- - Sends pending changes to the server (`getUpdatesForServer`).
213
- - Applies server confirmations (`applyServerConfirmation`).
214
- - Applies external server updates from other clients (`applyExternalServerUpdate`), rebasing local changes as needed.
215
- - **Event Emitters:** Provides hooks (`onUpdate`, `onChange`, etc.) to react to state changes.
207
+ This represents a single collaborative document. You don't create this directly; use `patches.openDoc(docId)` instead.
216
208
 
217
- See [`docs/PatchesDoc.md`](./docs/PatchesDoc.md) for detailed usage and examples.
209
+ - **Local State Management:** Tracks committed state, sending changes, and pending changes
210
+ - **Optimistic Updates:** Applies local changes immediately for snappy UIs
211
+ - **Synchronization:** Handles client-side OT magic:
212
+ - Sends pending changes to server
213
+ - Applies server confirmations
214
+ - Applies external updates, rebasing local changes as needed
215
+ - **Event Emitters:** Hooks like `onUpdate` and `onChange` to react to state changes
218
216
 
219
217
  ### PatchesServer
220
218
 
221
- (`PatchesServer` Documentation: [`docs/PatchesServer.md`](./docs/PatchesServer.md))
219
+ ([`docs/PatchesServer.md`](./docs/PatchesServer.md))
222
220
 
223
- 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.
221
+ The heart of server-side logic!
224
222
 
225
- - **Receives Changes:** Handles incoming `Change` objects from clients.
226
- - **Transformation:** Transforms client changes against concurrent server changes using the OT algorithm.
227
- - **Applies Changes:** Applies the final transformed changes to the authoritative document state.
228
- - **Versioning:** Creates version snapshots based on time-based sessions or explicit triggers (useful for history and offline support).
229
- - **Persistence:** Uses a `PatchesStoreBackend` implementation to save/load document state, changes, and versions.
230
-
231
- See [`docs/PatchesServer.md`](./docs/PatchesServer.md) for detailed usage and examples.
223
+ - **Receives Changes:** Handles incoming `Change` objects from clients
224
+ - **Transformation:** Transforms client changes against concurrent server changes
225
+ - **Applies Changes:** Applies transformed changes to the authoritative document state
226
+ - **Versioning:** Creates version snapshots based on user sessions
227
+ - **Persistence:** Uses `PatchesStoreBackend` to save/load document state and history
232
228
 
233
229
  ### PatchesHistoryManager
234
230
 
235
- (`PatchesHistoryManager` Documentation: [`docs/PatchesHistoryManager.md`](./docs/PatchesHistoryManager.md))
236
-
237
- Provides an API for querying the history ([`VersionMetadata`](./docs/types.ts)) of a document.
231
+ ([`docs/PatchesHistoryManager.md`](./docs/PatchesHistoryManager.md))
238
232
 
239
- - **List Versions:** Retrieve metadata about saved document versions (snapshots).
240
- - **Get Version State/Changes:** Load the full state or the specific changes associated with a past version.
241
- - **List Server Changes:** Query the raw sequence of committed server changes based on revision numbers.
233
+ Helps you query document history.
242
234
 
243
- See [`docs/PatchesHistoryManager.md`](./docs/PatchesHistoryManager.md) for detailed usage and examples.
235
+ - **List Versions:** Get metadata about saved document versions
236
+ - **Get Version State/Changes:** Load the full state or specific changes for a version
237
+ - **List Server Changes:** Query raw server changes by revision numbers
244
238
 
245
239
  ### PatchesBranchManager
246
240
 
247
- (`PatchesBranchManager` Documentation: [`docs/PatchesBranchManager.md`](./docs/PatchesBranchManager.md))
241
+ ([`docs/PatchesBranchManager.md`](./docs/PatchesBranchManager.md))
248
242
 
249
- Manages branching ([`Branch`](./docs/types.ts)) and merging workflows.
243
+ Manages branching and merging workflows.
250
244
 
251
- - **Create Branch:** Creates a new document branching off from a source document at a specific revision.
252
- - **List Branches:** Retrieves information about existing branches.
253
- - **Merge Branch:** Merges the changes made on a branch back into its source document (requires OT on the server to handle conflicts).
254
- - **Close Branch:** Marks a branch as closed, merged, or abandoned.
255
-
256
- See [`docs/PatchesBranchManager.md`](./docs/PatchesBranchManager.md) for detailed usage and examples.
245
+ - **Create Branch:** Makes a new document branching off from a source doc
246
+ - **List Branches:** Shows info about existing branches
247
+ - **Merge Branch:** Merges changes back into the source document
248
+ - **Close Branch:** Marks a branch as closed, merged, or abandoned
257
249
 
258
250
  ### Backend Store
259
251
 
260
- ([`PatchesStoreBackend` / `BranchingStoreBackend`](./docs/operational-transformation.md#backend-store-interface) Documentation: [`docs/operational-transformation.md#backend-store-interface`](./docs/operational-transformation.md#backend-store-interface))
261
-
262
- This isn't a specific class provided by the library, but rather an _interface_ (`PatchesStoreBackend` and `BranchingStoreBackend`) that you need to implement. This interface defines how the `PatchesServer`, `PatchesHistoryManager`, and `PatchesBranchManager` interact with your chosen persistence layer (e.g., a database, file system, in-memory store).
252
+ ([`docs/operational-transformation.md#backend-store-interface`](./docs/operational-transformation.md#backend-store-interface))
263
253
 
264
- You are responsible for providing an implementation that fulfills the methods defined in the interface (e.g., `getLatestRevision`, `saveChange`, `listVersions`, `createBranch`).
254
+ 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).
265
255
 
266
- See [`docs/operational-transformation.md#backend-store-interface`](./docs/operational-transformation.md#backend-store-interface) for the interface definition.
256
+ You're responsible for making it work with your backend!
267
257
 
268
- ## Transport & Networking
258
+ ### Transport & Networking
269
259
 
270
- Patches provides flexible networking options for real-time collaboration:
260
+ Patches gives you flexible networking options:
271
261
 
272
- - **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.
273
- - **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.
274
-
275
- See [WebSocket Transport](./docs/websocket.md) and [Awareness](./docs/awareness.md) for detailed usage and examples.
262
+ - **WebSocket Transport:** For most apps, use [`PatchesWebSocket`](./docs/websocket.md) to connect to a central server
263
+ - **WebRTC Transport:** For peer-to-peer, use [`WebRTCTransport`](./docs/operational-transformation.md#webrtc) and [`WebRTCAwareness`](./docs/awareness.md)
276
264
 
277
265
  **When to use which?**
278
266
 
279
- - Use WebSocket for most collaborative apps with a central server.
280
- - Use WebRTC for peer-to-peer or hybrid topologies, or to reduce server load for awareness/presence.
281
-
282
- ---
267
+ - WebSocket for most collaborative apps with a central server
268
+ - WebRTC for peer-to-peer or to reduce server load for awareness/presence
283
269
 
284
- ## Awareness (Presence, Cursors, etc.)
270
+ ### Awareness (Presence, Cursors, etc.)
285
271
 
286
- "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.
272
+ "Awareness" lets you show who's online, where their cursor is, and more. Patches supports awareness over both WebSocket and WebRTC.
287
273
 
288
- See [Awareness documentation](./docs/awareness.md) for how to use awareness features in your app.
274
+ Check the [Awareness documentation](./docs/awareness.md) for how to build collaborative cursors, user lists, and other cool features.
289
275
 
290
276
  ## Basic Workflow
291
277
 
292
- ### Client-Side (`Patches` and `PatchesDoc`)
278
+ ### Client-Side
293
279
 
294
- 1. **Initialize `Patches`:** Create an instance with a store (e.g., `InMemoryStore`).
295
- 2. **Track and Open a Document:** Use `patches.trackDocs([docId])` and `patches.openDoc(docId)` to get a `PatchesDoc` instance.
296
- 3. **Subscribe to Updates:** Use `doc.onUpdate`.
297
- 4. **Make Local Changes:** Use `doc.change()`.
298
- 5. **Sync Changes:** If using `PatchesSync`, changes are synced automatically. Otherwise, use the store or your own sync logic.
280
+ 1. **Initialize `Patches`** with a store
281
+ 2. **Track and Open a Document** with `patches.trackDocs([docId])` and `patches.openDoc(docId)`
282
+ 3. **Subscribe to Updates** with `doc.onUpdate`
283
+ 4. **Make Local Changes** with `doc.change()`
284
+ 5. **Sync Changes** automatically with `PatchesSync` or manually with your own logic
299
285
 
300
- ### Server-Side (`PatchesServer`)
286
+ ### Server-Side
301
287
 
302
- 1. **Initialize `PatchesServer`:** Create an instance. See [`docs/PatchesServer.md#initialization`](./docs/PatchesServer.md#initialization).
303
- 2. **Receive Client Changes:** Use [`server.receiveChanges()`](./docs/PatchesServer.md#core-method-receivechanges).
304
- 3. **Handle History/Branching:** Use [`PatchesHistoryManager`](./docs/PatchesHistoryManager.md) and [`PatchesBranchManager`](./docs/PatchesBranchManager.md).
288
+ 1. **Initialize `PatchesServer`** with your backend store
289
+ 2. **Receive Client Changes** with `server.receiveChanges()`
290
+ 3. **Handle History/Branching** with `PatchesHistoryManager` and `PatchesBranchManager`
305
291
 
306
292
  ## Examples
307
293
 
308
- _(Note: These are simplified examples. Real-world implementations require proper error handling, network communication, authentication, and backend setup.)_
309
-
310
294
  ### Simple Client Setup
311
295
 
312
296
  ```typescript
313
297
  import { Patches } from '@dabble/patches';
314
- import { InMemoryStore } from '@dabble/patches/persist/InMemoryStore';
298
+ import { InMemoryStore } from '@dabble/patches/persist';
315
299
 
316
300
  interface MyDoc {
317
301
  text: string;
@@ -332,7 +316,7 @@ doc.change(draft => {
332
316
  draft.text = 'Hello';
333
317
  draft.count = 0;
334
318
  });
335
- // If using PatchesSync, changes are synced automatically.
319
+ // With PatchesSync, changes sync automatically
336
320
  ```
337
321
 
338
322
  ### Simple Server Setup
@@ -344,118 +328,13 @@ import {
344
328
  PatchesStoreBackend,
345
329
  Change,
346
330
  VersionMetadata, //... other types
347
- } from '@dabble/patches';
331
+ } from '@dabble/patches/server';
348
332
 
349
- // --- Basic In-Memory Store (Replace with a real backend!) ---
333
+ // --- Basic In-Memory Store (Use a real database!) ---
350
334
  class InMemoryStore implements PatchesStoreBackend {
351
- private docs: Map<string, { state: any; rev: number; changes: Change[]; versions: VersionMetadata[] }> = new Map();
335
+ private docs = new Map<string, { state: any; rev: number; changes: Change[]; versions: VersionMetadata[] }>();
352
336
 
353
- async getLatestRevision(docId: string): Promise<number> {
354
- return this.docs.get(docId)?.rev ?? 0;
355
- }
356
- async getLatestState(docId: string): Promise<any | undefined> {
357
- const doc = this.docs.get(docId);
358
- // Return a deep copy to prevent accidental mutation
359
- return doc ? JSON.parse(JSON.stringify(doc.state)) : undefined;
360
- }
361
- async getStateAtRevision(docId: string, rev: number): Promise<any | undefined> {
362
- // IMPORTANT: In-memory store cannot easily reconstruct past states without snapshots.
363
- // A real implementation would replay changes or load version snapshots.
364
- // This basic version only returns the latest state if rev matches.
365
- const doc = this.docs.get(docId);
366
- if (doc && doc.rev === rev) {
367
- return JSON.parse(JSON.stringify(doc.state)); // Return copy
368
- }
369
- // Try finding a version snapshot matching the revision
370
- const version = doc?.versions.find(v => v.endDate === rev); // Approximation!
371
- if (version) {
372
- return JSON.parse(JSON.stringify(version.state));
373
- }
374
- // Fallback: Cannot reconstruct this revision
375
- if (rev === 0 && !doc) return {}; // Initial empty state at rev 0
376
- console.warn(
377
- `In-Memory Store: Cannot get state at revision ${rev} for doc ${docId}. Returning latest or undefined.`
378
- );
379
- return doc ? JSON.parse(JSON.stringify(doc.state)) : undefined; // Or throw error
380
- }
381
- async saveChange(docId: string, change: Change): Promise<void> {
382
- let doc = this.docs.get(docId);
383
- if (!doc) {
384
- doc = { state: {}, rev: 0, changes: [], versions: [] };
385
- this.docs.set(docId, doc);
386
- }
387
- // Apply change to get new state (use library's apply function)
388
- const { applyChanges } = await import('@dabble/patches'); // Assuming exported
389
- doc.state = applyChanges(doc.state, [change]);
390
- doc.rev = change.rev;
391
- doc.changes.push(change); // Store history of changes
392
- console.log(`[Store] Saved change rev ${change.rev} for doc ${docId}. New state:`, doc.state);
393
- }
394
- async listChanges(docId: string, options: any): Promise<Change[]> {
395
- const doc = this.docs.get(docId);
396
- if (!doc) return [];
397
- let changes = doc.changes;
398
- if (options.startAfterRev !== undefined) {
399
- changes = changes.filter(c => c.rev > options.startAfterRev!);
400
- }
401
- // Add other filter/limit/sort logic here based on options
402
- changes.sort((a, b) => a.rev - b.rev); // Ensure ascending order
403
- if (options.limit) {
404
- changes = changes.slice(0, options.limit);
405
- }
406
- return changes;
407
- }
408
- async saveVersion(docId: string, version: VersionMetadata): Promise<void> {
409
- const doc = this.docs.get(docId);
410
- if (!doc) {
411
- // This case is less likely if saveChange created the doc, but handle defensively
412
- console.warn(`[Store] Cannot save version for non-existent doc ${docId}`);
413
- return;
414
- }
415
- // Simple: just add to list. A real store might index/optimize.
416
- doc.versions.push(version);
417
- console.log(`[Store] Saved version ${version.id} (${version.origin}) for doc ${docId}.`);
418
- }
419
- async listVersions(docId: string, options: any): Promise<VersionMetadata[]> {
420
- const doc = this.docs.get(docId);
421
- if (!doc) return [];
422
- let versions = doc.versions;
423
- // Apply filtering/sorting based on options (simplified)
424
- if (options.origin) {
425
- versions = versions.filter(v => v.origin === options.origin);
426
- }
427
- if (options.groupId) {
428
- versions = versions.filter(v => v.groupId === options.groupId);
429
- }
430
- // ... other filters ...
431
- versions.sort((a, b) => (options.reverse ? b.startDate - a.startDate : a.startDate - b.startDate));
432
- if (options.limit) {
433
- versions = versions.slice(0, options.limit);
434
- }
435
- return versions;
436
- }
437
- async loadVersionMetadata(docId: string, versionId: string): Promise<VersionMetadata | null> {
438
- const doc = this.docs.get(docId);
439
- return doc?.versions.find(v => v.id === versionId) ?? null;
440
- }
441
- async loadVersionState(docId: string, versionId: string): Promise<any | undefined> {
442
- const meta = await this.loadVersionMetadata(docId, versionId);
443
- return meta ? JSON.parse(JSON.stringify(meta.state)) : undefined; // Return copy
444
- }
445
- async loadVersionChanges(docId: string, versionId: string): Promise<Change[]> {
446
- const meta = await this.loadVersionMetadata(docId, versionId);
447
- return meta ? meta.changes : [];
448
- }
449
- async getLatestVersionMetadata(docId: string): Promise<VersionMetadata | null> {
450
- const doc = this.docs.get(docId);
451
- if (!doc || doc.versions.length === 0) return null;
452
- // Find version with the latest endDate
453
- return doc.versions.reduce(
454
- (latest, current) => (!latest || current.endDate > latest.endDate ? current : latest),
455
- null as VersionMetadata | null
456
- );
457
- }
458
- // Implement BranchingStoreBackend methods if needed...
337
+ // Implementation details omitted for brevity...
459
338
  }
460
339
 
461
340
  // --- Server Setup ---
@@ -464,59 +343,8 @@ const server = new PatchesServer(store);
464
343
  const app = express();
465
344
  app.use(express.json());
466
345
 
467
- // --- Mock Broadcast (Replace with WebSocket/SSE etc.) ---
468
- const clients = new Map<string, Set<any>>(); // docId -> Set<client connections>
469
- function broadcastChanges(docId: string, changes: Change[], senderClientId: string | null) {
470
- console.log(`Broadcasting changes for ${docId} to other clients:`, changes);
471
- // Implement actual broadcast logic here (e.g., WebSockets)
472
- // clients.get(docId)?.forEach(client => {
473
- // if (client.id !== senderClientId) { // Don't send back to sender
474
- // client.send(JSON.stringify({ type: 'changes', docId, changes }));
475
- // }
476
- // });
477
- }
478
-
479
- // --- API Endpoint ---
480
- app.post('/docs/:docId/changes', async (req, res) => {
481
- const docId = req.params.docId;
482
- const clientChanges: Change[] = req.body.changes;
483
- const clientId = req.headers['x-client-id'] as string | null; // Example client ID header
484
-
485
- // Basic validation
486
- if (!Array.isArray(clientChanges)) {
487
- return res.status(400).json({ error: 'Invalid request: expected changes array.' });
488
- }
489
-
490
- console.log(`Received ${clientChanges.length} changes for doc ${docId} from client ${clientId || 'unknown'}`);
491
-
492
- try {
493
- const committedChanges = await server.receiveChanges(docId, clientChanges);
494
- console.log(`Committed ${committedChanges.length} changes for doc ${docId}, rev: ${committedChanges[0]?.rev}`);
495
- res.json(committedChanges); // Send confirmation back to sender
496
-
497
- // Broadcast to others if changes were made
498
- if (committedChanges.length > 0) {
499
- broadcastChanges(docId, committedChanges, clientId);
500
- }
501
- } catch (error: any) {
502
- console.error(`Error processing changes for doc ${docId}:`, error);
503
- // Use 409 Conflict for revision mismatches, 500 for others
504
- const statusCode = error.message.includes('out of sync') ? 409 : 500;
505
- res.status(statusCode).json({ error: error.message });
506
- }
507
- });
508
-
509
- // Endpoint to get latest state (for new clients)
510
- app.get('/docs/:docId', async (req, res) => {
511
- const docId = req.params.docId;
512
- try {
513
- const { state, rev } = await server.getLatestDocumentStateAndRev(docId);
514
- res.json({ state: state ?? {}, rev }); // Provide empty object if state is undefined
515
- } catch (error: any) {
516
- console.error(`Error fetching state for doc ${docId}:`, error);
517
- res.status(500).json({ error: 'Failed to fetch document state.' });
518
- }
519
- });
346
+ // API endpoints for changes and state...
347
+ // (see full example in code)
520
348
 
521
349
  const PORT = 3000;
522
350
  app.listen(PORT, () => {
@@ -540,7 +368,7 @@ See [`Operational Transformation > Operation Handlers`](./docs/operational-trans
540
368
 
541
369
  ## JSON Patch (Legacy)
542
370
 
543
- 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).
371
+ For legacy JSON Patch features, see [`docs/json-patch.md`](./docs/json-patch.md).
544
372
 
545
373
  ## Contributing
546
374
 
@@ -31,25 +31,6 @@ class LRUCache {
31
31
  this.cache.clear();
32
32
  }
33
33
  }
34
- /**
35
- * Simple event signal for update notifications (like in PatchesDoc)
36
- */
37
- class Signal {
38
- constructor() {
39
- this.listeners = new Set();
40
- }
41
- subscribe(cb) {
42
- this.listeners.add(cb);
43
- return () => this.listeners.delete(cb);
44
- }
45
- emit() {
46
- for (const cb of this.listeners)
47
- cb();
48
- }
49
- clear() {
50
- this.listeners.clear();
51
- }
52
- }
53
34
  /**
54
35
  * Client-side history/scrubbing interface for a document.
55
36
  * Read-only: allows listing versions, loading states/changes, and scrubbing.
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  export { Delta } from '@dabble/delta';
2
2
  export * from './client/Patches.js';
3
3
  export * from './client/PatchesDoc.js';
4
- export * from './event-signal';
4
+ export * from './event-signal.js';
5
5
  export * from './json-patch/JSONPatch.js';
6
6
  export type { ApplyJSONPatchOptions } from './json-patch/types.js';
7
7
  export type * from './persist/PatchesStore.js';
8
- export type * from './types';
8
+ export type * from './types.js';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { Delta } from '@dabble/delta';
2
2
  export * from './client/Patches.js';
3
3
  export * from './client/PatchesDoc.js';
4
- export * from './event-signal';
4
+ export * from './event-signal.js';
5
5
  export * from './json-patch/JSONPatch.js';
@@ -139,7 +139,7 @@ export class JSONPatch {
139
139
  addObjectsInPath(obj, path) {
140
140
  path = checkPath(path);
141
141
  const parts = path.split('/');
142
- for (var i = 1; i < parts.length - 1; i++) {
142
+ for (let i = 1; i < parts.length - 1; i++) {
143
143
  const prop = parts[i];
144
144
  if (!obj || !obj[prop]) {
145
145
  this.add(parts.slice(0, i + 1).join('/'), {});
@@ -5,5 +5,4 @@ export { applyBitmask, bitmask, combineBitmasks } from './ops/bitmask.js';
5
5
  export * as defaultOps from './ops/index.js';
6
6
  export { transformPatch } from './transformPatch.js';
7
7
  export * from './ops/index.js';
8
- export * from './ops/index.js';
9
8
  export type { ApplyJSONPatchOptions, JSONPatchOpHandlerMap as JSONPatchCustomTypes, JSONPatchOp } from './types.js';
@@ -4,5 +4,4 @@ export { invertPatch } from './invertPatch.js';
4
4
  export { applyBitmask, bitmask, combineBitmasks } from './ops/bitmask.js';
5
5
  export * as defaultOps from './ops/index.js';
6
6
  export { transformPatch } from './transformPatch.js';
7
- export * from './ops/index.js'; // Exports all ops: add, remove, etc.
8
7
  export * from './ops/index.js';
@@ -7,7 +7,6 @@ import { move } from './move.js';
7
7
  import { remove } from './remove.js';
8
8
  import { replace } from './replace.js';
9
9
  import { test } from './test.js';
10
- export * from './bitmask.js';
11
10
  export { add, bit, copy, increment, move, remove, replace, test };
12
11
  export declare function getTypes(custom?: JSONPatchOpHandlerMap): {
13
12
  test: import("../types.js").JSONPatchOpHandler;
@@ -7,7 +7,7 @@ import { remove } from './remove.js';
7
7
  import { replace } from './replace.js';
8
8
  import { test } from './test.js';
9
9
  import { text } from './text.js';
10
- export * from './bitmask.js';
10
+ // Export all patch operations
11
11
  export { add, bit, copy, increment, move, remove, replace, test };
12
12
  export function getTypes(custom) {
13
13
  return {
@@ -10,7 +10,7 @@ export const text = {
10
10
  if (!delta || !Array.isArray(delta.ops)) {
11
11
  return 'Invalid delta';
12
12
  }
13
- let existingData = get(state, path);
13
+ const existingData = get(state, path);
14
14
  let doc;
15
15
  if (Array.isArray(existingData)) {
16
16
  if (existingData.length && existingData[0].insert) {
@@ -1,3 +1,4 @@
1
+ import { JSONPatch } from './JSONPatch.js';
1
2
  // We use a function as the target so that `push` and other array methods can be called without error.
2
3
  const proxyFodder = {};
3
4
  export function createPatchProxy(target, patch) {
@@ -1,6 +1,6 @@
1
1
  import { getOpData } from './getOpData.js';
2
2
  export function get(state, path) {
3
3
  // eslint-disable-next-line no-unused-vars
4
- const [keys, lastKey, target] = getOpData(state, path);
4
+ const [, lastKey, target] = getOpData(state, path);
5
5
  return target ? target[lastKey] : undefined;
6
6
  }
@@ -1,3 +1,4 @@
1
+ import { Patches } from '../client/Patches.js';
1
2
  import { signal } from '../event-signal.js';
2
3
  import { breakIntoBatches } from '../utils/batching.js';
3
4
  import { PatchesWebSocket } from './websocket/PatchesWebSocket.js';
@@ -1,5 +1,6 @@
1
- import type { Branch, BranchingStoreBackend, BranchStatus, Change } from '../types.js';
1
+ import type { Branch, BranchStatus, Change } from '../types.js';
2
2
  import type { PatchesServer } from './PatchesServer.js';
3
+ import type { BranchingStoreBackend } from './types.js';
3
4
  /**
4
5
  * Helps manage branches for a document. A branch is a document that is branched from another document. Its first
5
6
  * version will be the point-in-time of the original document at the time of the branch. Branches allow for parallel
@@ -94,7 +94,7 @@ export class PatchesServer {
94
94
  await this._createVersion(docId, currentState, currentChanges);
95
95
  }
96
96
  // 3. Load committed changes *after* the client's baseRev for transformation and idempotency checks
97
- let committedChanges = await this.store.listChanges(docId, {
97
+ const committedChanges = await this.store.listChanges(docId, {
98
98
  startAfter: baseRev,
99
99
  withoutBatchId: batchId,
100
100
  });
@@ -1,3 +1,4 @@
1
1
  export { PatchesBranchManager } from './PatchesBranchManager';
2
2
  export { PatchesHistoryManager } from './PatchesHistoryManager';
3
3
  export { PatchesServer } from './PatchesServer';
4
+ export type { BranchingStoreBackend, PatchesStoreBackend } from './types';
@@ -0,0 +1,48 @@
1
+ import type { Branch, Change, ListChangesOptions, ListVersionsOptions, VersionMetadata } from '../types';
2
+ /**
3
+ * Interface for a backend storage system for patch synchronization.
4
+ * Defines methods needed by PatchesServer, PatchesHistoryManager, etc.
5
+ */
6
+ export interface PatchesStoreBackend {
7
+ /** Adds a subscription for a client to one or more documents. */
8
+ addSubscription(clientId: string, docIds: string[]): Promise<string[]>;
9
+ /** Removes a subscription for a client from one or more documents. */
10
+ removeSubscription(clientId: string, docIds: string[]): Promise<string[]>;
11
+ /** Saves a batch of committed server changes. */
12
+ saveChanges(docId: string, changes: Change[]): Promise<void>;
13
+ /** Lists committed server changes based on revision numbers. */
14
+ listChanges(docId: string, options: ListChangesOptions): Promise<Change[]>;
15
+ /**
16
+ * Saves version metadata, its state snapshot, and the original changes that constitute it.
17
+ * State and changes are stored separately from the core metadata.
18
+ */
19
+ createVersion(docId: string, metadata: VersionMetadata, state: any, changes: Change[]): Promise<void>;
20
+ /** Update a version's metadata. */
21
+ updateVersion(docId: string, versionId: string, metadata: Partial<VersionMetadata>): Promise<void>;
22
+ /** Lists version metadata based on filtering/sorting options. */
23
+ listVersions(docId: string, options: ListVersionsOptions): Promise<VersionMetadata[]>;
24
+ /** Loads the state snapshot for a specific version ID. */
25
+ loadVersionState(docId: string, versionId: string): Promise<any | undefined>;
26
+ /** Loads the original Change objects associated with a specific version ID. */
27
+ loadVersionChanges(docId: string, versionId: string): Promise<Change[]>;
28
+ /** Deletes a document. */
29
+ deleteDoc(docId: string): Promise<void>;
30
+ }
31
+ /**
32
+ * Extends PatchesStoreBackend with methods specifically for managing branches.
33
+ */
34
+ export interface BranchingStoreBackend extends PatchesStoreBackend {
35
+ /** Lists metadata records for branches originating from a document. */
36
+ listBranches(docId: string): Promise<Branch[]>;
37
+ /** Loads the metadata record for a specific branch ID. */
38
+ loadBranch(branchId: string): Promise<Branch | null>;
39
+ /** Creates or updates the metadata record for a branch. */
40
+ createBranch(branch: Branch): Promise<void>;
41
+ /** Updates specific fields (status, name, metadata) of an existing branch record. */
42
+ updateBranch(branchId: string, updates: Partial<Pick<Branch, 'status' | 'name' | 'metadata'>>): Promise<void>;
43
+ /**
44
+ * @deprecated Use updateBranch with status instead.
45
+ * Marks a branch as closed. Implementations might handle this via updateBranch.
46
+ */
47
+ closeBranch(branchId: string): Promise<void>;
48
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { JSONPatchOp } from './json-patch/types';
1
+ import type { JSONPatchOp } from './json-patch/types.js';
2
2
  export interface Change {
3
3
  /** Unique identifier for the change, generated client-side. */
4
4
  id: string;
@@ -111,53 +111,6 @@ export interface ListVersionsOptions {
111
111
  /** Filter by the group ID (branch ID or offline batch ID). */
112
112
  groupId?: string;
113
113
  }
114
- /**
115
- * Interface for a backend storage system for patch synchronization.
116
- * Defines methods needed by PatchesServer, PatchesHistoryManager, etc.
117
- */
118
- export interface PatchesStoreBackend {
119
- /** Adds a subscription for a client to one or more documents. */
120
- addSubscription(clientId: string, docIds: string[]): Promise<string[]>;
121
- /** Removes a subscription for a client from one or more documents. */
122
- removeSubscription(clientId: string, docIds: string[]): Promise<string[]>;
123
- /** Saves a batch of committed server changes. */
124
- saveChanges(docId: string, changes: Change[]): Promise<void>;
125
- /** Lists committed server changes based on revision numbers. */
126
- listChanges(docId: string, options: ListChangesOptions): Promise<Change[]>;
127
- /**
128
- * Saves version metadata, its state snapshot, and the original changes that constitute it.
129
- * State and changes are stored separately from the core metadata.
130
- */
131
- createVersion(docId: string, metadata: VersionMetadata, state: any, changes: Change[]): Promise<void>;
132
- /** Update a version's metadata. */
133
- updateVersion(docId: string, versionId: string, metadata: Partial<VersionMetadata>): Promise<void>;
134
- /** Lists version metadata based on filtering/sorting options. */
135
- listVersions(docId: string, options: ListVersionsOptions): Promise<VersionMetadata[]>;
136
- /** Loads the state snapshot for a specific version ID. */
137
- loadVersionState(docId: string, versionId: string): Promise<any | undefined>;
138
- /** Loads the original Change objects associated with a specific version ID. */
139
- loadVersionChanges(docId: string, versionId: string): Promise<Change[]>;
140
- /** Deletes a document. */
141
- deleteDoc(docId: string): Promise<void>;
142
- }
143
- /**
144
- * Extends PatchesStoreBackend with methods specifically for managing branches.
145
- */
146
- export interface BranchingStoreBackend extends PatchesStoreBackend {
147
- /** Lists metadata records for branches originating from a document. */
148
- listBranches(docId: string): Promise<Branch[]>;
149
- /** Loads the metadata record for a specific branch ID. */
150
- loadBranch(branchId: string): Promise<Branch | null>;
151
- /** Creates or updates the metadata record for a branch. */
152
- createBranch(branch: Branch): Promise<void>;
153
- /** Updates specific fields (status, name, metadata) of an existing branch record. */
154
- updateBranch(branchId: string, updates: Partial<Pick<Branch, 'status' | 'name' | 'metadata'>>): Promise<void>;
155
- /**
156
- * @deprecated Use updateBranch with status instead.
157
- * Marks a branch as closed. Implementations might handle this via updateBranch.
158
- */
159
- closeBranch(branchId: string): Promise<void>;
160
- }
161
114
  export interface Deferred<T = void> {
162
115
  promise: Promise<T>;
163
116
  resolve: (value: T) => void;
@@ -205,7 +205,6 @@ function breakLargeValueOp(origChange, op, maxBytes, startRev) {
205
205
  const targetChunkSize = Math.max(1, valueBudget);
206
206
  const numChunks = Math.ceil(text.length / targetChunkSize);
207
207
  const chunkSize = Math.ceil(text.length / numChunks);
208
- let currentPath = op.path;
209
208
  for (let i = 0; i < text.length; i += chunkSize) {
210
209
  const chunk = text.slice(i, i + chunkSize);
211
210
  const newOp = { op: 'add' }; // Default to add?
package/dist/utils.d.ts CHANGED
@@ -7,7 +7,7 @@ import type { Change, Deferred } from './types.js';
7
7
  * @param changes - Array of changes to split
8
8
  * @returns A tuple containing [changes before baseRev, changes with and after baseRev]
9
9
  */
10
- export declare function splitChanges<T>(changes: Change[]): [Change[], Change[]];
10
+ export declare function splitChanges(changes: Change[]): [Change[], Change[]];
11
11
  /**
12
12
  * Applies a sequence of changes to a state object.
13
13
  * Each change is applied in sequence using the applyPatch function.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dabble/patches",
3
- "version": "0.2.12",
3
+ "version": "0.2.14",
4
4
  "description": "Immutable JSON Patch implementation based on RFC 6902 supporting operational transformation and last-writer-wins",
5
5
  "author": "Jacob Wright <jacwright@gmail.com>",
6
6
  "bugs": {
@@ -66,13 +66,13 @@
66
66
  "@dabble/delta": "^1.2.4"
67
67
  },
68
68
  "devDependencies": {
69
- "@sveltejs/package": "^2.3.10",
69
+ "@sveltejs/package": "^2.3.11",
70
70
  "@types/simple-peer": "^9.11.8",
71
71
  "fake-indexeddb": "^6.0.0",
72
72
  "prettier": "^3.5.3",
73
73
  "typescript": "^5.8.3",
74
- "vite": "^6.2.6",
74
+ "vite": "^6.3.5",
75
75
  "vite-plugin-dts": "^4.5.3",
76
- "vitest": "^3.1.1"
76
+ "vitest": "^3.1.3"
77
77
  }
78
78
  }