@dabble/patches 0.2.13 → 0.2.15
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 +121 -293
- package/dist/server/PatchesBranchManager.d.ts +2 -1
- package/dist/server/PatchesHistoryManager.d.ts +11 -7
- package/dist/server/PatchesHistoryManager.js +15 -12
- package/dist/server/PatchesServer.d.ts +2 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/types.d.ts +48 -0
- package/dist/server/types.js +1 -0
- package/dist/types.d.ts +0 -47
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
# Patches
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
7
|
+
## What Is This Thing?
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
**
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:**
|
|
55
|
-
- **Rebasing:** Client changes
|
|
56
|
-
- **Linear History:** The server
|
|
57
|
-
- **Client-Server
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
92
|
-
import { PatchesSync } from '@dabble/patches/net
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
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(); //
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
(
|
|
194
|
+
([`docs/Patches.md`](./docs/Patches.md))
|
|
193
195
|
|
|
194
|
-
This is
|
|
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:**
|
|
197
|
-
- **Persistence:**
|
|
198
|
-
- **Sync Integration:**
|
|
199
|
-
- **Event Emitters:**
|
|
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
|
-
(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
(
|
|
219
|
+
([`docs/PatchesServer.md`](./docs/PatchesServer.md))
|
|
222
220
|
|
|
223
|
-
The heart of
|
|
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
|
|
227
|
-
- **Applies Changes:** Applies
|
|
228
|
-
- **Versioning:** Creates version snapshots based on
|
|
229
|
-
- **Persistence:** Uses
|
|
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
|
-
(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
(
|
|
241
|
+
([`docs/PatchesBranchManager.md`](./docs/PatchesBranchManager.md))
|
|
248
242
|
|
|
249
|
-
Manages branching
|
|
243
|
+
Manages branching and merging workflows.
|
|
250
244
|
|
|
251
|
-
- **Create Branch:**
|
|
252
|
-
- **List Branches:**
|
|
253
|
-
- **Merge Branch:** Merges
|
|
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
|
-
([`
|
|
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
|
-
|
|
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
|
-
|
|
256
|
+
You're responsible for making it work with your backend!
|
|
267
257
|
|
|
268
|
-
|
|
258
|
+
### Transport & Networking
|
|
269
259
|
|
|
270
|
-
Patches
|
|
260
|
+
Patches gives you flexible networking options:
|
|
271
261
|
|
|
272
|
-
- **WebSocket Transport:** For most
|
|
273
|
-
- **WebRTC Transport:** For peer-to-peer
|
|
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
|
-
-
|
|
280
|
-
-
|
|
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
|
-
|
|
270
|
+
### Awareness (Presence, Cursors, etc.)
|
|
285
271
|
|
|
286
|
-
"Awareness" lets you show who
|
|
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
|
-
|
|
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
|
|
278
|
+
### Client-Side
|
|
293
279
|
|
|
294
|
-
1.
|
|
295
|
-
2.
|
|
296
|
-
3.
|
|
297
|
-
4.
|
|
298
|
-
5.
|
|
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
|
|
286
|
+
### Server-Side
|
|
301
287
|
|
|
302
|
-
1.
|
|
303
|
-
2.
|
|
304
|
-
3.
|
|
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
|
|
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
|
-
//
|
|
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 (
|
|
333
|
+
// --- Basic In-Memory Store (Use a real database!) ---
|
|
350
334
|
class InMemoryStore implements PatchesStoreBackend {
|
|
351
|
-
private docs
|
|
335
|
+
private docs = new Map<string, { state: any; rev: number; changes: Change[]; versions: VersionMetadata[] }>();
|
|
352
336
|
|
|
353
|
-
|
|
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
|
-
//
|
|
468
|
-
|
|
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
|
-
|
|
371
|
+
For legacy JSON Patch features, see [`docs/json-patch.md`](./docs/json-patch.md).
|
|
544
372
|
|
|
545
373
|
## Contributing
|
|
546
374
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { Branch,
|
|
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
|
|
@@ -1,40 +1,44 @@
|
|
|
1
|
-
import type { Change, ListVersionsOptions,
|
|
1
|
+
import type { Change, ListVersionsOptions, VersionMetadata } from '../types.js';
|
|
2
|
+
import type { PatchesStoreBackend } from './types.js';
|
|
2
3
|
/**
|
|
3
4
|
* Helps retrieve historical information (versions, changes) for a document
|
|
4
5
|
* using the new versioning model based on IDs and metadata.
|
|
5
6
|
*/
|
|
6
7
|
export declare class PatchesHistoryManager {
|
|
7
|
-
private readonly docId;
|
|
8
8
|
private readonly store;
|
|
9
|
-
constructor(
|
|
9
|
+
constructor(store: PatchesStoreBackend);
|
|
10
10
|
/**
|
|
11
11
|
* Lists version metadata for the document, supporting various filters.
|
|
12
|
+
* @param docId - The ID of the document.
|
|
12
13
|
* @param options Filtering and sorting options (e.g., limit, reverse, origin, groupId, date range).
|
|
13
14
|
* @returns A list of version metadata objects.
|
|
14
15
|
*/
|
|
15
|
-
listVersions(options?: ListVersionsOptions): Promise<VersionMetadata[]>;
|
|
16
|
+
listVersions(docId: string, options?: ListVersionsOptions): Promise<VersionMetadata[]>;
|
|
16
17
|
/**
|
|
17
18
|
* Loads the full document state snapshot for a specific version by its ID.
|
|
19
|
+
* @param docId - The ID of the document.
|
|
18
20
|
* @param versionId - The unique ID of the version.
|
|
19
21
|
* @returns The document state at that version.
|
|
20
22
|
* @throws Error if the version ID is not found or state loading fails.
|
|
21
23
|
*/
|
|
22
|
-
getStateAtVersion(versionId: string): Promise<any>;
|
|
24
|
+
getStateAtVersion(docId: string, versionId: string): Promise<any>;
|
|
23
25
|
/**
|
|
24
26
|
* Loads the list of original client changes that were included in a specific version.
|
|
25
27
|
* Useful for replaying/scrubbing through the operations within an offline or online session.
|
|
28
|
+
* @param docId - The ID of the document.
|
|
26
29
|
* @param versionId - The unique ID of the version.
|
|
27
30
|
* @returns An array of Change objects.
|
|
28
31
|
* @throws Error if the version ID is not found or change loading fails.
|
|
29
32
|
*/
|
|
30
|
-
getChangesForVersion(versionId: string): Promise<Change[]>;
|
|
33
|
+
getChangesForVersion(docId: string, versionId: string): Promise<Change[]>;
|
|
31
34
|
/**
|
|
32
35
|
* Lists committed server changes for the document, typically used for server-side processing
|
|
33
36
|
* or deep history analysis based on raw revisions.
|
|
37
|
+
* @param docId - The ID of the document.
|
|
34
38
|
* @param options - Options like start/end revision, limit.
|
|
35
39
|
* @returns The list of committed Change objects.
|
|
36
40
|
*/
|
|
37
|
-
listServerChanges(options?: {
|
|
41
|
+
listServerChanges(docId: string, options?: {
|
|
38
42
|
limit?: number;
|
|
39
43
|
startAfterRev?: number;
|
|
40
44
|
endBeforeRev?: number;
|
|
@@ -3,57 +3,60 @@
|
|
|
3
3
|
* using the new versioning model based on IDs and metadata.
|
|
4
4
|
*/
|
|
5
5
|
export class PatchesHistoryManager {
|
|
6
|
-
constructor(
|
|
7
|
-
this.docId = docId;
|
|
6
|
+
constructor(store) {
|
|
8
7
|
this.store = store;
|
|
9
8
|
}
|
|
10
9
|
/**
|
|
11
10
|
* Lists version metadata for the document, supporting various filters.
|
|
11
|
+
* @param docId - The ID of the document.
|
|
12
12
|
* @param options Filtering and sorting options (e.g., limit, reverse, origin, groupId, date range).
|
|
13
13
|
* @returns A list of version metadata objects.
|
|
14
14
|
*/
|
|
15
|
-
async listVersions(options = {}) {
|
|
16
|
-
return await this.store.listVersions(
|
|
15
|
+
async listVersions(docId, options = {}) {
|
|
16
|
+
return await this.store.listVersions(docId, options);
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
19
|
* Loads the full document state snapshot for a specific version by its ID.
|
|
20
|
+
* @param docId - The ID of the document.
|
|
20
21
|
* @param versionId - The unique ID of the version.
|
|
21
22
|
* @returns The document state at that version.
|
|
22
23
|
* @throws Error if the version ID is not found or state loading fails.
|
|
23
24
|
*/
|
|
24
|
-
async getStateAtVersion(versionId) {
|
|
25
|
+
async getStateAtVersion(docId, versionId) {
|
|
25
26
|
try {
|
|
26
|
-
return await this.store.loadVersionState(
|
|
27
|
+
return await this.store.loadVersionState(docId, versionId);
|
|
27
28
|
}
|
|
28
29
|
catch (error) {
|
|
29
|
-
console.error(`Failed to load state for version ${versionId} of doc ${
|
|
30
|
+
console.error(`Failed to load state for version ${versionId} of doc ${docId}.`, error);
|
|
30
31
|
throw new Error(`Could not load state for version ${versionId}.`);
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
/**
|
|
34
35
|
* Loads the list of original client changes that were included in a specific version.
|
|
35
36
|
* Useful for replaying/scrubbing through the operations within an offline or online session.
|
|
37
|
+
* @param docId - The ID of the document.
|
|
36
38
|
* @param versionId - The unique ID of the version.
|
|
37
39
|
* @returns An array of Change objects.
|
|
38
40
|
* @throws Error if the version ID is not found or change loading fails.
|
|
39
41
|
*/
|
|
40
|
-
async getChangesForVersion(versionId) {
|
|
42
|
+
async getChangesForVersion(docId, versionId) {
|
|
41
43
|
try {
|
|
42
|
-
return await this.store.loadVersionChanges(
|
|
44
|
+
return await this.store.loadVersionChanges(docId, versionId);
|
|
43
45
|
}
|
|
44
46
|
catch (error) {
|
|
45
|
-
console.error(`Failed to load changes for version ${versionId} of doc ${
|
|
47
|
+
console.error(`Failed to load changes for version ${versionId} of doc ${docId}.`, error);
|
|
46
48
|
throw new Error(`Could not load changes for version ${versionId}.`);
|
|
47
49
|
}
|
|
48
50
|
}
|
|
49
51
|
/**
|
|
50
52
|
* Lists committed server changes for the document, typically used for server-side processing
|
|
51
53
|
* or deep history analysis based on raw revisions.
|
|
54
|
+
* @param docId - The ID of the document.
|
|
52
55
|
* @param options - Options like start/end revision, limit.
|
|
53
56
|
* @returns The list of committed Change objects.
|
|
54
57
|
*/
|
|
55
|
-
async listServerChanges(options = {}) {
|
|
58
|
+
async listServerChanges(docId, options = {}) {
|
|
56
59
|
// Added return type
|
|
57
|
-
return await this.store.listChanges(
|
|
60
|
+
return await this.store.listChanges(docId, options);
|
|
58
61
|
}
|
|
59
62
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { Change, ListVersionsOptions, PatchesSnapshot, PatchesState,
|
|
1
|
+
import type { Change, ListVersionsOptions, PatchesSnapshot, PatchesState, VersionMetadata } from '../types.js';
|
|
2
|
+
import type { PatchesStoreBackend } from './types.js';
|
|
2
3
|
/**
|
|
3
4
|
* Configuration options for the PatchesServer.
|
|
4
5
|
*/
|
package/dist/server/index.d.ts
CHANGED
|
@@ -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
|
@@ -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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dabble/patches",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.15",
|
|
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": {
|