@dabble/patches 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -169
- package/dist/client/Patches.d.ts +64 -0
- package/dist/client/Patches.js +166 -0
- package/dist/client/{PatchDoc.d.ts → PatchesDoc.d.ts} +33 -19
- package/dist/client/{PatchDoc.js → PatchesDoc.js} +57 -67
- package/dist/client/PatchesHistoryClient.d.ts +40 -0
- package/dist/client/PatchesHistoryClient.js +129 -0
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +1 -1
- package/dist/event-signal.js +13 -5
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/net/AbstractTransport.js +2 -0
- package/dist/net/PatchesSync.d.ts +47 -0
- package/dist/net/PatchesSync.js +289 -0
- package/dist/net/index.d.ts +9 -7
- package/dist/net/index.js +8 -6
- package/dist/net/protocol/types.d.ts +3 -3
- package/dist/net/types.d.ts +6 -0
- package/dist/net/types.js +1 -0
- package/dist/net/websocket/PatchesWebSocket.d.ts +3 -3
- package/dist/net/websocket/WebSocketTransport.d.ts +13 -2
- package/dist/net/websocket/WebSocketTransport.js +105 -53
- package/dist/net/websocket/onlineState.d.ts +9 -0
- package/dist/net/websocket/onlineState.js +18 -0
- package/dist/persist/InMemoryStore.d.ts +23 -0
- package/dist/persist/InMemoryStore.js +103 -0
- package/dist/persist/IndexedDBStore.d.ts +13 -9
- package/dist/persist/IndexedDBStore.js +96 -14
- package/dist/persist/PatchesStore.d.ts +38 -0
- package/dist/persist/PatchesStore.js +1 -0
- package/dist/persist/index.d.ts +4 -2
- package/dist/persist/index.js +3 -1
- package/dist/server/{BranchManager.d.ts → PatchesBranchManager.d.ts} +4 -4
- package/dist/server/{BranchManager.js → PatchesBranchManager.js} +5 -5
- package/dist/server/{HistoryManager.d.ts → PatchesHistoryManager.d.ts} +4 -24
- package/dist/server/{HistoryManager.js → PatchesHistoryManager.js} +1 -34
- package/dist/server/{PatchServer.d.ts → PatchesServer.d.ts} +9 -9
- package/dist/server/{PatchServer.js → PatchesServer.js} +1 -1
- package/dist/server/index.d.ts +4 -4
- package/dist/server/index.js +3 -3
- package/dist/types.d.ts +11 -6
- package/dist/utils/batching.d.ts +5 -0
- package/dist/utils/batching.js +38 -0
- package/dist/utils.d.ts +2 -2
- package/dist/utils.js +8 -2
- package/package.json +1 -1
- package/dist/net/PatchesOfflineFirst.d.ts +0 -3
- package/dist/net/PatchesOfflineFirst.js +0 -3
- package/dist/net/PatchesRealtime.d.ts +0 -90
- package/dist/net/PatchesRealtime.js +0 -257
package/README.md
CHANGED
|
@@ -17,10 +17,11 @@ When working with a document in Patches, you are working with regular JavaScript
|
|
|
17
17
|
- [Client Example](#client-example)
|
|
18
18
|
- [Server Example](#server-example)
|
|
19
19
|
- [Core Components](#core-components)
|
|
20
|
-
- [
|
|
21
|
-
- [
|
|
22
|
-
- [
|
|
23
|
-
- [
|
|
20
|
+
- [Patches](#patches-main-client)
|
|
21
|
+
- [PatchesDoc](#patchesdoc-document-instance)
|
|
22
|
+
- [PatchesServer](#patchesserver)
|
|
23
|
+
- [PatchesHistoryManager](#patcheshistorymanager)
|
|
24
|
+
- [PatchesBranchManager](#patchesbranchmanager)
|
|
24
25
|
- [Backend Store](#backend-store)
|
|
25
26
|
- [Transport & Networking](#transport--networking)
|
|
26
27
|
- [Awareness (Presence, Cursors, etc.)](#awareness-presence-cursors-etc)
|
|
@@ -83,81 +84,59 @@ _(Note: These are simplified examples. Real-world implementations require proper
|
|
|
83
84
|
|
|
84
85
|
### Client Example
|
|
85
86
|
|
|
86
|
-
This shows how to initialize `
|
|
87
|
+
This shows how to initialize `Patches` (the main client interface) with an in-memory store and set up real-time sync with `PatchesSync`.
|
|
87
88
|
|
|
88
89
|
```typescript
|
|
89
|
-
import {
|
|
90
|
+
import { Patches } from '@dabble/patches';
|
|
91
|
+
import { InMemoryStore } from '@dabble/patches/persist/InMemoryStore';
|
|
92
|
+
import { PatchesSync } from '@dabble/patches/net/PatchesSync';
|
|
90
93
|
|
|
91
94
|
interface MyDoc {
|
|
92
95
|
text: string;
|
|
93
96
|
count: number;
|
|
94
97
|
}
|
|
95
98
|
|
|
96
|
-
|
|
99
|
+
// 1. Create a store (in-memory for demo; use IndexedDB or your own for production)
|
|
100
|
+
const store = new InMemoryStore();
|
|
101
|
+
|
|
102
|
+
// 2. Create the main Patches client instance
|
|
103
|
+
const patches = new Patches({ store });
|
|
104
|
+
|
|
105
|
+
// 3. Set up real-time sync with your server
|
|
106
|
+
const sync = new PatchesSync('wss://your-server-url', patches);
|
|
107
|
+
await sync.connect(); // Connect to the server (returns a promise)
|
|
97
108
|
|
|
98
|
-
//
|
|
99
|
-
|
|
109
|
+
// 4. Open or create a document by ID
|
|
110
|
+
const doc = await patches.openDoc<MyDoc>('my-doc-1');
|
|
111
|
+
|
|
112
|
+
// 5. React to updates (e.g., update UI)
|
|
113
|
+
doc.onUpdate(newState => {
|
|
100
114
|
console.log('Document updated:', newState);
|
|
101
115
|
// Update your UI here
|
|
102
116
|
});
|
|
103
117
|
|
|
104
|
-
// Make
|
|
105
|
-
|
|
118
|
+
// 6. Make local changes
|
|
119
|
+
// (Changes are applied optimistically and will be synced to the server)
|
|
120
|
+
doc.change(draft => {
|
|
106
121
|
draft.text = 'Hello World!';
|
|
107
|
-
draft.count
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// Triggered after local changes occur
|
|
111
|
-
patchDoc.onChange(change => {
|
|
112
|
-
syncWithServer();
|
|
122
|
+
draft.count = (draft.count || 0) + 1;
|
|
113
123
|
});
|
|
114
124
|
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
// syncWithServer(); // Initial sync
|
|
118
|
-
|
|
119
|
-
// Syncing Logic (Simplified)
|
|
120
|
-
async function syncWithServer() {
|
|
121
|
-
// 1. Get local changes to send
|
|
122
|
-
const changesToSend = patchDoc.getUpdatesForServer();
|
|
123
|
-
if (changesToSend.length > 0) {
|
|
124
|
-
try {
|
|
125
|
-
// 2. Send changes via your network layer (e.g., fetch, WebSocket)
|
|
126
|
-
// Replace 'sendChangesToServer' with your actual implementation
|
|
127
|
-
const serverCommit = await sendChangesToServer(initialDocId, changesToSend);
|
|
128
|
-
// 3. Apply server's confirmation
|
|
129
|
-
patchDoc.applyServerConfirmation(serverCommit);
|
|
130
|
-
} catch (error) {
|
|
131
|
-
console.error('Error sending changes:', error);
|
|
132
|
-
// Handle error (e.g., retry, show message)
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// 4. Receive external changes from server (e.g., via WebSocket listener)
|
|
137
|
-
// Replace 'checkForServerUpdates' with your actual implementation
|
|
138
|
-
const externalChanges = await checkForServerUpdates(initialDocId, patchDoc.rev);
|
|
139
|
-
if (externalChanges && externalChanges.length > 0) {
|
|
140
|
-
try {
|
|
141
|
-
patchDoc.applyExternalServerUpdate(externalChanges);
|
|
142
|
-
} catch (error) {
|
|
143
|
-
console.error('Error applying external changes:', error);
|
|
144
|
-
// Handle error (potentially requires full resync)
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
125
|
+
// 7. Changes are automatically synced using PatchesSync.
|
|
126
|
+
// If not using PatchesSync, you can manually flush changes to your backend as needed.
|
|
148
127
|
```
|
|
149
128
|
|
|
150
129
|
### Server Example
|
|
151
130
|
|
|
152
|
-
This outlines a basic Express server using `
|
|
131
|
+
This outlines a basic Express server using `PatchesServer` with an in-memory store.
|
|
153
132
|
|
|
154
133
|
```typescript
|
|
155
134
|
import express from 'express';
|
|
156
|
-
import {
|
|
135
|
+
import { PatchesServer, PatchesStoreBackend, Change } from '@dabble/patches';
|
|
157
136
|
|
|
158
137
|
// Server Setup
|
|
159
138
|
const store = new InMemoryStore(); // Fictional in-memory backend, use a database
|
|
160
|
-
const server = new
|
|
139
|
+
const server = new PatchesServer(store);
|
|
161
140
|
const app = express();
|
|
162
141
|
app.use(express.json());
|
|
163
142
|
|
|
@@ -208,11 +187,24 @@ Centralized OT has two different areas of focus, the server and the client. They
|
|
|
208
187
|
|
|
209
188
|
These are the main classes you'll interact with when building a collaborative application with Patches.
|
|
210
189
|
|
|
211
|
-
###
|
|
190
|
+
### Patches (Main Client)
|
|
191
|
+
|
|
192
|
+
(`Patches` Documentation: [`docs/Patches.md`](./docs/Patches.md))
|
|
193
|
+
|
|
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)`.
|
|
195
|
+
|
|
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.
|
|
212
200
|
|
|
213
|
-
|
|
201
|
+
See [`docs/PatchesDoc.md`](./docs/PatchesDoc.md) for detailed usage and examples.
|
|
214
202
|
|
|
215
|
-
|
|
203
|
+
### PatchesDoc (Document Instance)
|
|
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)`.
|
|
216
208
|
|
|
217
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).
|
|
218
210
|
- **Optimistic Updates:** Applies local changes immediately for a responsive UI.
|
|
@@ -222,11 +214,11 @@ This is what you'll be working with on the client in your app. It is the client-
|
|
|
222
214
|
- Applies external server updates from other clients (`applyExternalServerUpdate`), rebasing local changes as needed.
|
|
223
215
|
- **Event Emitters:** Provides hooks (`onUpdate`, `onChange`, etc.) to react to state changes.
|
|
224
216
|
|
|
225
|
-
See [`docs/
|
|
217
|
+
See [`docs/PatchesDoc.md`](./docs/PatchesDoc.md) for detailed usage and examples.
|
|
226
218
|
|
|
227
|
-
###
|
|
219
|
+
### PatchesServer
|
|
228
220
|
|
|
229
|
-
(`
|
|
221
|
+
(`PatchesServer` Documentation: [`docs/PatchesServer.md`](./docs/PatchesServer.md))
|
|
230
222
|
|
|
231
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.
|
|
232
224
|
|
|
@@ -234,13 +226,13 @@ The heart of the server-side logic. See [`docs/operational-transformation.md#pat
|
|
|
234
226
|
- **Transformation:** Transforms client changes against concurrent server changes using the OT algorithm.
|
|
235
227
|
- **Applies Changes:** Applies the final transformed changes to the authoritative document state.
|
|
236
228
|
- **Versioning:** Creates version snapshots based on time-based sessions or explicit triggers (useful for history and offline support).
|
|
237
|
-
- **Persistence:** Uses a `
|
|
229
|
+
- **Persistence:** Uses a `PatchesStoreBackend` implementation to save/load document state, changes, and versions.
|
|
238
230
|
|
|
239
|
-
See [`docs/
|
|
231
|
+
See [`docs/PatchesServer.md`](./docs/PatchesServer.md) for detailed usage and examples.
|
|
240
232
|
|
|
241
|
-
###
|
|
233
|
+
### PatchesHistoryManager
|
|
242
234
|
|
|
243
|
-
(`
|
|
235
|
+
(`PatchesHistoryManager` Documentation: [`docs/PatchesHistoryManager.md`](./docs/PatchesHistoryManager.md))
|
|
244
236
|
|
|
245
237
|
Provides an API for querying the history ([`VersionMetadata`](./docs/types.ts)) of a document.
|
|
246
238
|
|
|
@@ -248,11 +240,11 @@ Provides an API for querying the history ([`VersionMetadata`](./docs/types.ts))
|
|
|
248
240
|
- **Get Version State/Changes:** Load the full state or the specific changes associated with a past version.
|
|
249
241
|
- **List Server Changes:** Query the raw sequence of committed server changes based on revision numbers.
|
|
250
242
|
|
|
251
|
-
See [`docs/
|
|
243
|
+
See [`docs/PatchesHistoryManager.md`](./docs/PatchesHistoryManager.md) for detailed usage and examples.
|
|
252
244
|
|
|
253
|
-
###
|
|
245
|
+
### PatchesBranchManager
|
|
254
246
|
|
|
255
|
-
(`
|
|
247
|
+
(`PatchesBranchManager` Documentation: [`docs/PatchesBranchManager.md`](./docs/PatchesBranchManager.md))
|
|
256
248
|
|
|
257
249
|
Manages branching ([`Branch`](./docs/types.ts)) and merging workflows.
|
|
258
250
|
|
|
@@ -261,13 +253,13 @@ Manages branching ([`Branch`](./docs/types.ts)) and merging workflows.
|
|
|
261
253
|
- **Merge Branch:** Merges the changes made on a branch back into its source document (requires OT on the server to handle conflicts).
|
|
262
254
|
- **Close Branch:** Marks a branch as closed, merged, or abandoned.
|
|
263
255
|
|
|
264
|
-
See [`docs/
|
|
256
|
+
See [`docs/PatchesBranchManager.md`](./docs/PatchesBranchManager.md) for detailed usage and examples.
|
|
265
257
|
|
|
266
258
|
### Backend Store
|
|
267
259
|
|
|
268
|
-
([`
|
|
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))
|
|
269
261
|
|
|
270
|
-
This isn't a specific class provided by the library, but rather an _interface_ (`
|
|
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).
|
|
271
263
|
|
|
272
264
|
You are responsible for providing an implementation that fulfills the methods defined in the interface (e.g., `getLatestRevision`, `saveChange`, `listVersions`, `createBranch`).
|
|
273
265
|
|
|
@@ -297,19 +289,19 @@ See [Awareness documentation](./docs/awareness.md) for how to use awareness feat
|
|
|
297
289
|
|
|
298
290
|
## Basic Workflow
|
|
299
291
|
|
|
300
|
-
### Client-Side (`
|
|
292
|
+
### Client-Side (`Patches` and `PatchesDoc`)
|
|
301
293
|
|
|
302
|
-
1. **Initialize `
|
|
303
|
-
2. **
|
|
304
|
-
3. **
|
|
305
|
-
4. **
|
|
306
|
-
5. **
|
|
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.
|
|
307
299
|
|
|
308
|
-
### Server-Side (`
|
|
300
|
+
### Server-Side (`PatchesServer`)
|
|
309
301
|
|
|
310
|
-
1. **Initialize `
|
|
311
|
-
2. **Receive Client Changes:** Use [`server.receiveChanges()`](./docs/
|
|
312
|
-
3. **Handle History/Branching:** Use [`
|
|
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).
|
|
313
305
|
|
|
314
306
|
## Examples
|
|
315
307
|
|
|
@@ -318,100 +310,29 @@ _(Note: These are simplified examples. Real-world implementations require proper
|
|
|
318
310
|
### Simple Client Setup
|
|
319
311
|
|
|
320
312
|
```typescript
|
|
321
|
-
import {
|
|
313
|
+
import { Patches } from '@dabble/patches';
|
|
314
|
+
import { InMemoryStore } from '@dabble/patches/persist/InMemoryStore';
|
|
322
315
|
|
|
323
316
|
interface MyDoc {
|
|
324
317
|
text: string;
|
|
325
318
|
count: number;
|
|
326
319
|
}
|
|
327
320
|
|
|
328
|
-
|
|
329
|
-
const
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
// Your function to send changes and receive the server's commit
|
|
334
|
-
async function sendChangesToServer(docId: string, changes: Change[]): Promise<Change[]> {
|
|
335
|
-
const response = await fetch(`/docs/${docId}/changes`, {
|
|
336
|
-
method: 'POST',
|
|
337
|
-
headers: { 'Content-Type': 'application/json' },
|
|
338
|
-
body: JSON.stringify({ changes }),
|
|
339
|
-
});
|
|
340
|
-
if (!response.ok) {
|
|
341
|
-
const errorData = await response.json();
|
|
342
|
-
throw new Error(errorData.error || `Server error: ${response.status}`);
|
|
343
|
-
}
|
|
344
|
-
return await response.json();
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// --- Initialize PatchDoc ---
|
|
348
|
-
const patchDoc = new PatchDoc<MyDoc>(initialServerState, initialServerRev);
|
|
321
|
+
const store = new InMemoryStore();
|
|
322
|
+
const patches = new Patches({ store });
|
|
323
|
+
const docId = 'doc123';
|
|
324
|
+
await patches.trackDocs([docId]);
|
|
325
|
+
const doc = await patches.openDoc<MyDoc>(docId);
|
|
349
326
|
|
|
350
|
-
|
|
351
|
-
patchDoc.onUpdate(newState => {
|
|
327
|
+
doc.onUpdate(newState => {
|
|
352
328
|
console.log('Document updated:', newState);
|
|
353
|
-
// Update your UI element displaying newState.text, newState.count, etc.
|
|
354
329
|
});
|
|
355
330
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
// Trigger sending changes (e.g., debounced)
|
|
362
|
-
sendLocalChanges();
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
function handleIncrement() {
|
|
366
|
-
patchDoc.change(draft => {
|
|
367
|
-
draft.count = (draft.count || 0) + 1;
|
|
368
|
-
});
|
|
369
|
-
sendLocalChanges();
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// --- Sending Changes ---
|
|
373
|
-
let isSending = false;
|
|
374
|
-
async function sendLocalChanges() {
|
|
375
|
-
if (isSending || !patchDoc.hasPending) return;
|
|
376
|
-
|
|
377
|
-
isSending = true;
|
|
378
|
-
try {
|
|
379
|
-
const changesToSend = patchDoc.getUpdatesForServer();
|
|
380
|
-
if (changesToSend.length > 0) {
|
|
381
|
-
console.log('Sending changes:', changesToSend);
|
|
382
|
-
const serverCommit = await sendChangesToServer(initialDocId, changesToSend);
|
|
383
|
-
console.log('Received confirmation:', serverCommit);
|
|
384
|
-
patchDoc.applyServerConfirmation(serverCommit);
|
|
385
|
-
}
|
|
386
|
-
} catch (error) {
|
|
387
|
-
console.error('Failed to send changes:', error);
|
|
388
|
-
// Handle error - maybe retry, revert local changes, or force resync
|
|
389
|
-
// For simplicity, just log here. PatchDoc state might be inconsistent.
|
|
390
|
-
} finally {
|
|
391
|
-
isSending = false;
|
|
392
|
-
// Check again in case new changes came in while sending
|
|
393
|
-
if (patchDoc.hasPending) {
|
|
394
|
-
setTimeout(sendLocalChanges, 100); // Basic retry/check again
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// --- Receiving External Changes (e.g., via WebSocket) ---
|
|
400
|
-
function handleServerBroadcast(externalChanges: Change[]) {
|
|
401
|
-
if (!externalChanges || externalChanges.length === 0) return;
|
|
402
|
-
console.log('Received external changes:', externalChanges);
|
|
403
|
-
try {
|
|
404
|
-
patchDoc.applyExternalServerUpdate(externalChanges);
|
|
405
|
-
} catch (error) {
|
|
406
|
-
console.error('Error applying external server changes:', error);
|
|
407
|
-
// Critical error - likely need to resync the document state
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// --- Example Usage ---
|
|
412
|
-
// handleTextInput("Hello World!");
|
|
413
|
-
// handleIncrement();
|
|
414
|
-
// Assume setup for receiving broadcasts via `handleServerBroadcast`
|
|
331
|
+
doc.change(draft => {
|
|
332
|
+
draft.text = 'Hello';
|
|
333
|
+
draft.count = 0;
|
|
334
|
+
});
|
|
335
|
+
// If using PatchesSync, changes are synced automatically.
|
|
415
336
|
```
|
|
416
337
|
|
|
417
338
|
### Simple Server Setup
|
|
@@ -419,14 +340,14 @@ function handleServerBroadcast(externalChanges: Change[]) {
|
|
|
419
340
|
```typescript
|
|
420
341
|
import express from 'express';
|
|
421
342
|
import {
|
|
422
|
-
|
|
423
|
-
|
|
343
|
+
PatchesServer,
|
|
344
|
+
PatchesStoreBackend,
|
|
424
345
|
Change,
|
|
425
346
|
VersionMetadata, //... other types
|
|
426
347
|
} from '@dabble/patches';
|
|
427
348
|
|
|
428
349
|
// --- Basic In-Memory Store (Replace with a real backend!) ---
|
|
429
|
-
class InMemoryStore implements
|
|
350
|
+
class InMemoryStore implements PatchesStoreBackend {
|
|
430
351
|
private docs: Map<string, { state: any; rev: number; changes: Change[]; versions: VersionMetadata[] }> = new Map();
|
|
431
352
|
|
|
432
353
|
async getLatestRevision(docId: string): Promise<number> {
|
|
@@ -539,7 +460,7 @@ class InMemoryStore implements PatchStoreBackend {
|
|
|
539
460
|
|
|
540
461
|
// --- Server Setup ---
|
|
541
462
|
const store = new InMemoryStore();
|
|
542
|
-
const server = new
|
|
463
|
+
const server = new PatchesServer(store);
|
|
543
464
|
const app = express();
|
|
544
465
|
app.use(express.json());
|
|
545
466
|
|
|
@@ -607,11 +528,11 @@ app.listen(PORT, () => {
|
|
|
607
528
|
|
|
608
529
|
### Offline Support & Versioning
|
|
609
530
|
|
|
610
|
-
See [`
|
|
531
|
+
See [`PatchesServer Versioning`](./docs/PatchesServer.md#versioning) and [`PatchesHistoryManager`](./docs/PatchesHistoryManager.md).
|
|
611
532
|
|
|
612
533
|
### Branching and Merging
|
|
613
534
|
|
|
614
|
-
See [`
|
|
535
|
+
See [`PatchesBranchManager`](./docs/PatchesBranchManager.md).
|
|
615
536
|
|
|
616
537
|
### Custom OT Types
|
|
617
538
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type Unsubscriber } from '../event-signal.js';
|
|
2
|
+
import type { PatchesStore } from '../persist/PatchesStore.js';
|
|
3
|
+
import type { Change } from '../types.js';
|
|
4
|
+
import { PatchesDoc } from './PatchesDoc.js';
|
|
5
|
+
export interface PatchesOptions {
|
|
6
|
+
/** Persistence layer instance (e.g., new IndexedDBStore('my-db') or new InMemoryStore()). */
|
|
7
|
+
store: PatchesStore;
|
|
8
|
+
/** Initial metadata to attach to changes from this client (merged with per-doc metadata). */
|
|
9
|
+
metadata?: Record<string, any>;
|
|
10
|
+
}
|
|
11
|
+
interface ManagedDoc<T extends object> {
|
|
12
|
+
doc: PatchesDoc<T>;
|
|
13
|
+
onChangeUnsubscriber: Unsubscriber;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Main client-side entry point for the Patches library.
|
|
17
|
+
* Manages document instances (`PatchesDoc`) and persistence (`PatchesStore`).
|
|
18
|
+
* Can be used standalone or with PatchesSync for network synchronization.
|
|
19
|
+
*/
|
|
20
|
+
export declare class Patches {
|
|
21
|
+
protected options: PatchesOptions;
|
|
22
|
+
protected docs: Map<string, ManagedDoc<any>>;
|
|
23
|
+
readonly store: PatchesStore;
|
|
24
|
+
readonly trackedDocs: Set<string>;
|
|
25
|
+
readonly onError: import("../event-signal.js").Signal<(error: Error, context?: {
|
|
26
|
+
docId?: string;
|
|
27
|
+
}) => void>;
|
|
28
|
+
readonly onServerCommit: import("../event-signal.js").Signal<(docId: string, changes: Change[]) => void>;
|
|
29
|
+
readonly onTrackDocs: import("../event-signal.js").Signal<(docIds: string[]) => void>;
|
|
30
|
+
readonly onUntrackDocs: import("../event-signal.js").Signal<(docIds: string[]) => void>;
|
|
31
|
+
readonly onDeleteDoc: import("../event-signal.js").Signal<(docId: string) => void>;
|
|
32
|
+
constructor(opts: PatchesOptions);
|
|
33
|
+
trackDocs(docIds: string[]): Promise<void>;
|
|
34
|
+
untrackDocs(docIds: string[]): Promise<void>;
|
|
35
|
+
openDoc<T extends object>(docId: string, opts?: {
|
|
36
|
+
metadata?: Record<string, any>;
|
|
37
|
+
}): Promise<PatchesDoc<T>>;
|
|
38
|
+
closeDoc(docId: string): Promise<void>;
|
|
39
|
+
deleteDoc(docId: string): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Gets all tracked document IDs that are currently open.
|
|
42
|
+
* Used by PatchesSync to check which docs need syncing.
|
|
43
|
+
*/
|
|
44
|
+
getOpenDocIds(): string[];
|
|
45
|
+
/**
|
|
46
|
+
* Retrieves changes for a document that should be sent to the server.
|
|
47
|
+
* Used by PatchesSync during synchronization.
|
|
48
|
+
*/
|
|
49
|
+
getDocChanges(docId: string): Change[];
|
|
50
|
+
/**
|
|
51
|
+
* Handles failure to send changes to the server.
|
|
52
|
+
* Used by PatchesSync to requeue changes after failures.
|
|
53
|
+
*/
|
|
54
|
+
handleSendFailure(docId: string): void;
|
|
55
|
+
/**
|
|
56
|
+
* Apply server changes to a document.
|
|
57
|
+
* Used by PatchesSync to update documents with server changes.
|
|
58
|
+
*/
|
|
59
|
+
applyServerChanges(docId: string, changes: Change[]): void;
|
|
60
|
+
close(): void;
|
|
61
|
+
protected _setupLocalDocListener(docId: string, doc: PatchesDoc<any>): Unsubscriber;
|
|
62
|
+
private _handleServerCommit;
|
|
63
|
+
}
|
|
64
|
+
export {};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { signal } from '../event-signal.js';
|
|
2
|
+
import { PatchesDoc } from './PatchesDoc.js';
|
|
3
|
+
/**
|
|
4
|
+
* Main client-side entry point for the Patches library.
|
|
5
|
+
* Manages document instances (`PatchesDoc`) and persistence (`PatchesStore`).
|
|
6
|
+
* Can be used standalone or with PatchesSync for network synchronization.
|
|
7
|
+
*/
|
|
8
|
+
export class Patches {
|
|
9
|
+
constructor(opts) {
|
|
10
|
+
this.docs = new Map();
|
|
11
|
+
this.trackedDocs = new Set();
|
|
12
|
+
// Public signals
|
|
13
|
+
this.onError = signal();
|
|
14
|
+
this.onServerCommit = signal();
|
|
15
|
+
this.onTrackDocs = signal();
|
|
16
|
+
this.onUntrackDocs = signal();
|
|
17
|
+
this.onDeleteDoc = signal();
|
|
18
|
+
this._handleServerCommit = (docId, changes) => {
|
|
19
|
+
const managedDoc = this.docs.get(docId);
|
|
20
|
+
if (managedDoc) {
|
|
21
|
+
try {
|
|
22
|
+
// Apply confirmed/transformed changes from the server
|
|
23
|
+
managedDoc.doc.applyExternalServerUpdate(changes);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
console.error(`Error applying server commit for doc ${docId}:`, err);
|
|
27
|
+
this.onError.emit(err, { docId });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// If doc isn't open locally, changes were already saved to store by PatchesSync
|
|
31
|
+
};
|
|
32
|
+
this.options = opts;
|
|
33
|
+
this.store = opts.store;
|
|
34
|
+
this.store.listDocs().then(docs => {
|
|
35
|
+
this.trackDocs(docs.map(({ docId }) => docId));
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
// --- Public API Methods ---
|
|
39
|
+
async trackDocs(docIds) {
|
|
40
|
+
docIds = docIds.filter(id => !this.trackedDocs.has(id));
|
|
41
|
+
if (!docIds.length)
|
|
42
|
+
return;
|
|
43
|
+
docIds.forEach(this.trackedDocs.add, this.trackedDocs);
|
|
44
|
+
this.onTrackDocs.emit(docIds);
|
|
45
|
+
await this.store.trackDocs(docIds);
|
|
46
|
+
}
|
|
47
|
+
async untrackDocs(docIds) {
|
|
48
|
+
docIds = docIds.filter(id => this.trackedDocs.has(id));
|
|
49
|
+
if (!docIds.length)
|
|
50
|
+
return;
|
|
51
|
+
docIds.forEach(this.trackedDocs.delete, this.trackedDocs);
|
|
52
|
+
this.onUntrackDocs.emit(docIds);
|
|
53
|
+
// Close any open PatchesDoc instances first
|
|
54
|
+
const closedPromises = docIds.filter(id => this.docs.has(id)).map(id => this.closeDoc(id)); // closeDoc removes from this.docs map
|
|
55
|
+
await Promise.all(closedPromises);
|
|
56
|
+
// Remove from store
|
|
57
|
+
await this.store.untrackDocs(docIds);
|
|
58
|
+
}
|
|
59
|
+
async openDoc(docId, opts = {}) {
|
|
60
|
+
const existing = this.docs.get(docId);
|
|
61
|
+
if (existing)
|
|
62
|
+
return existing.doc;
|
|
63
|
+
// Ensure the doc is tracked before proceeding
|
|
64
|
+
await this.trackDocs([docId]);
|
|
65
|
+
// Load initial state from store
|
|
66
|
+
const snapshot = await this.store.getDoc(docId);
|
|
67
|
+
const initialState = (snapshot?.state ?? {});
|
|
68
|
+
const mergedMetadata = { ...this.options.metadata, ...opts.metadata };
|
|
69
|
+
const doc = new PatchesDoc(initialState, mergedMetadata);
|
|
70
|
+
doc.setId(docId);
|
|
71
|
+
if (snapshot) {
|
|
72
|
+
doc.import(snapshot);
|
|
73
|
+
}
|
|
74
|
+
// Set up local listener -> store
|
|
75
|
+
const unsub = this._setupLocalDocListener(docId, doc);
|
|
76
|
+
this.docs.set(docId, { doc, onChangeUnsubscriber: unsub });
|
|
77
|
+
return doc;
|
|
78
|
+
}
|
|
79
|
+
async closeDoc(docId) {
|
|
80
|
+
const managed = this.docs.get(docId);
|
|
81
|
+
if (managed) {
|
|
82
|
+
managed.onChangeUnsubscriber();
|
|
83
|
+
this.docs.delete(docId);
|
|
84
|
+
// Note: We do NOT call untrackDocs here automatically.
|
|
85
|
+
// Closing a doc just removes it from memory; it remains tracked
|
|
86
|
+
// for background sync unless explicitly untracked.
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async deleteDoc(docId) {
|
|
90
|
+
// Close if open locally
|
|
91
|
+
if (this.docs.has(docId)) {
|
|
92
|
+
await this.closeDoc(docId);
|
|
93
|
+
}
|
|
94
|
+
// Unsubscribe from server if tracked
|
|
95
|
+
if (this.trackedDocs.has(docId)) {
|
|
96
|
+
await this.untrackDocs([docId]);
|
|
97
|
+
}
|
|
98
|
+
// Mark document as deleted in store
|
|
99
|
+
await this.store.deleteDoc(docId);
|
|
100
|
+
this.onDeleteDoc.emit(docId);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Gets all tracked document IDs that are currently open.
|
|
104
|
+
* Used by PatchesSync to check which docs need syncing.
|
|
105
|
+
*/
|
|
106
|
+
getOpenDocIds() {
|
|
107
|
+
return Array.from(this.docs.keys());
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Retrieves changes for a document that should be sent to the server.
|
|
111
|
+
* Used by PatchesSync during synchronization.
|
|
112
|
+
*/
|
|
113
|
+
getDocChanges(docId) {
|
|
114
|
+
const doc = this.docs.get(docId)?.doc;
|
|
115
|
+
if (!doc)
|
|
116
|
+
return [];
|
|
117
|
+
try {
|
|
118
|
+
return doc.getUpdatesForServer();
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
console.error(`Error getting updates for doc ${docId}:`, err);
|
|
122
|
+
this.onError.emit(err, { docId });
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Handles failure to send changes to the server.
|
|
128
|
+
* Used by PatchesSync to requeue changes after failures.
|
|
129
|
+
*/
|
|
130
|
+
handleSendFailure(docId) {
|
|
131
|
+
const doc = this.docs.get(docId)?.doc;
|
|
132
|
+
if (doc) {
|
|
133
|
+
doc.handleSendFailure();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Apply server changes to a document.
|
|
138
|
+
* Used by PatchesSync to update documents with server changes.
|
|
139
|
+
*/
|
|
140
|
+
applyServerChanges(docId, changes) {
|
|
141
|
+
this._handleServerCommit(docId, changes);
|
|
142
|
+
}
|
|
143
|
+
close() {
|
|
144
|
+
// Clean up local PatchesDoc listeners
|
|
145
|
+
this.docs.forEach(managed => managed.onChangeUnsubscriber());
|
|
146
|
+
this.docs.clear();
|
|
147
|
+
// Close store connection
|
|
148
|
+
void this.store.close();
|
|
149
|
+
}
|
|
150
|
+
// --- Internal Handlers ---
|
|
151
|
+
_setupLocalDocListener(docId, doc) {
|
|
152
|
+
return doc.onChange(async () => {
|
|
153
|
+
const changes = doc.getUpdatesForServer();
|
|
154
|
+
if (!changes.length)
|
|
155
|
+
return;
|
|
156
|
+
try {
|
|
157
|
+
await this.store.savePendingChanges(docId, changes);
|
|
158
|
+
// Note: When used with PatchesSync, it will handle flushing the changes
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
console.error(`Error saving pending changes for doc ${docId}:`, err);
|
|
162
|
+
this.onError.emit(err, { docId });
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|