@dabble/patches 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +90 -169
  2. package/dist/client/Patches.d.ts +64 -0
  3. package/dist/client/Patches.js +166 -0
  4. package/dist/client/{PatchDoc.d.ts → PatchesDoc.d.ts} +33 -19
  5. package/dist/client/{PatchDoc.js → PatchesDoc.js} +57 -67
  6. package/dist/client/PatchesHistoryClient.d.ts +40 -0
  7. package/dist/client/PatchesHistoryClient.js +129 -0
  8. package/dist/client/index.d.ts +3 -2
  9. package/dist/client/index.js +3 -1
  10. package/dist/event-signal.js +13 -5
  11. package/dist/index.d.ts +3 -2
  12. package/dist/index.js +2 -1
  13. package/dist/net/AbstractTransport.js +2 -0
  14. package/dist/net/PatchesSync.d.ts +47 -0
  15. package/dist/net/PatchesSync.js +289 -0
  16. package/dist/net/index.d.ts +7 -7
  17. package/dist/net/index.js +5 -6
  18. package/dist/net/protocol/types.d.ts +3 -3
  19. package/dist/net/types.d.ts +6 -0
  20. package/dist/net/types.js +1 -0
  21. package/dist/net/websocket/PatchesWebSocket.d.ts +3 -3
  22. package/dist/net/websocket/WebSocketTransport.d.ts +13 -2
  23. package/dist/net/websocket/WebSocketTransport.js +105 -53
  24. package/dist/net/websocket/onlineState.d.ts +9 -0
  25. package/dist/net/websocket/onlineState.js +18 -0
  26. package/dist/persist/InMemoryStore.d.ts +23 -0
  27. package/dist/persist/InMemoryStore.js +103 -0
  28. package/dist/persist/IndexedDBStore.d.ts +13 -9
  29. package/dist/persist/IndexedDBStore.js +96 -14
  30. package/dist/persist/PatchesStore.d.ts +38 -0
  31. package/dist/persist/PatchesStore.js +1 -0
  32. package/dist/persist/index.d.ts +3 -2
  33. package/dist/persist/index.js +3 -1
  34. package/dist/server/{BranchManager.d.ts → PatchesBranchManager.d.ts} +4 -4
  35. package/dist/server/{BranchManager.js → PatchesBranchManager.js} +5 -5
  36. package/dist/server/{HistoryManager.d.ts → PatchesHistoryManager.d.ts} +4 -24
  37. package/dist/server/{HistoryManager.js → PatchesHistoryManager.js} +1 -34
  38. package/dist/server/{PatchServer.d.ts → PatchesServer.d.ts} +9 -9
  39. package/dist/server/{PatchServer.js → PatchesServer.js} +1 -1
  40. package/dist/server/index.d.ts +3 -4
  41. package/dist/server/index.js +3 -3
  42. package/dist/types.d.ts +11 -6
  43. package/dist/utils/batching.d.ts +5 -0
  44. package/dist/utils/batching.js +38 -0
  45. package/dist/utils.d.ts +2 -2
  46. package/dist/utils.js +8 -2
  47. package/package.json +1 -1
  48. package/dist/net/PatchesOfflineFirst.d.ts +0 -3
  49. package/dist/net/PatchesOfflineFirst.js +0 -3
  50. package/dist/net/PatchesRealtime.d.ts +0 -90
  51. 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
- - [PatchDoc](#patchdoc)
21
- - [PatchServer](#patchserver)
22
- - [HistoryManager](#historymanager)
23
- - [BranchManager](#branchmanager)
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 `PatchDoc`, make local changes, and handle sending/receiving updates.
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 { PatchDoc, Change } from '@dabble/patches';
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
- const patchDoc = new PatchDoc<MyDoc>();
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
- // React to Updates (e.g., update UI)
99
- patchDoc.onUpdate(newState => {
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 Local Changes
105
- patchDoc.change(draft => {
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 += 1;
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
- // Or Trigger Sync Periodically or On Events
116
- // setInterval(syncWithServer, 5000); // Example: sync every 5 seconds
117
- // syncWithServer(); // Initial sync
118
-
119
- // Syncing Logic (Simplified)
120
- async function syncWithServer() {
121
- // 1. Get local changes to send
122
- const changesToSend = patchDoc.getUpdatesForServer();
123
- if (changesToSend.length > 0) {
124
- try {
125
- // 2. Send changes via your network layer (e.g., fetch, WebSocket)
126
- // Replace 'sendChangesToServer' with your actual implementation
127
- const serverCommit = await sendChangesToServer(initialDocId, changesToSend);
128
- // 3. Apply server's confirmation
129
- patchDoc.applyServerConfirmation(serverCommit);
130
- } catch (error) {
131
- console.error('Error sending changes:', error);
132
- // Handle error (e.g., retry, show message)
133
- }
134
- }
135
-
136
- // 4. Receive external changes from server (e.g., via WebSocket listener)
137
- // Replace 'checkForServerUpdates' with your actual implementation
138
- const externalChanges = await checkForServerUpdates(initialDocId, patchDoc.rev);
139
- if (externalChanges && externalChanges.length > 0) {
140
- try {
141
- patchDoc.applyExternalServerUpdate(externalChanges);
142
- } catch (error) {
143
- console.error('Error applying external changes:', error);
144
- // Handle error (potentially requires full resync)
145
- }
146
- }
147
- }
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 `PatchServer` with an in-memory store.
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 { PatchServer, PatchStoreBackend, Change } from '@dabble/patches';
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 PatchServer(store);
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
- ### PatchDoc
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
- (`PatchDoc` Documentation: [`docs/PatchDoc.md`](./docs/PatchDoc.md))
201
+ See [`docs/PatchesDoc.md`](./docs/PatchesDoc.md) for detailed usage and examples.
214
202
 
215
- This is what you'll be working with on the client in your app. It is the client-side view of a collaborative document. You can check out [`docs/operational-transformation.md#patchdoc`](./docs/operational-transformation.md#patchdoc) to learn more about its role in the OT flow.
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/PatchDoc.md`](./docs/PatchDoc.md) for detailed usage and examples.
217
+ See [`docs/PatchesDoc.md`](./docs/PatchesDoc.md) for detailed usage and examples.
226
218
 
227
- ### PatchServer
219
+ ### PatchesServer
228
220
 
229
- (`PatchServer` Documentation: [`docs/PatchServer.md`](./docs/PatchServer.md))
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 `PatchStoreBackend` implementation to save/load document state, changes, and versions.
229
+ - **Persistence:** Uses a `PatchesStoreBackend` implementation to save/load document state, changes, and versions.
238
230
 
239
- See [`docs/PatchServer.md`](./docs/PatchServer.md) for detailed usage and examples.
231
+ See [`docs/PatchesServer.md`](./docs/PatchesServer.md) for detailed usage and examples.
240
232
 
241
- ### HistoryManager
233
+ ### PatchesHistoryManager
242
234
 
243
- (`HistoryManager` Documentation: [`docs/HistoryManager.md`](./docs/HistoryManager.md))
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/HistoryManager.md`](./docs/HistoryManager.md) for detailed usage and examples.
243
+ See [`docs/PatchesHistoryManager.md`](./docs/PatchesHistoryManager.md) for detailed usage and examples.
252
244
 
253
- ### BranchManager
245
+ ### PatchesBranchManager
254
246
 
255
- (`BranchManager` Documentation: [`docs/BranchManager.md`](./docs/BranchManager.md))
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/BranchManager.md`](./docs/BranchManager.md) for detailed usage and examples.
256
+ See [`docs/PatchesBranchManager.md`](./docs/PatchesBranchManager.md) for detailed usage and examples.
265
257
 
266
258
  ### Backend Store
267
259
 
268
- ([`PatchStoreBackend` / `BranchingStoreBackend`](./docs/operational-transformation.md#backend-store-interface) Documentation: [`docs/operational-transformation.md#backend-store-interface`](./docs/operational-transformation.md#backend-store-interface))
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_ (`PatchStoreBackend` and `BranchingStoreBackend`) that you need to implement. This interface defines how the `PatchServer`, `HistoryManager`, and `BranchManager` interact with your chosen persistence layer (e.g., a database, file system, in-memory store).
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 (`PatchDoc`)
292
+ ### Client-Side (`Patches` and `PatchesDoc`)
301
293
 
302
- 1. **Initialize `PatchDoc`:** Create an instance. See [`docs/PatchDoc.md#initialization`](./docs/PatchDoc.md#initialization).
303
- 2. **Subscribe to Updates:** Use [`doc.onUpdate`](./docs/PatchDoc.md#onupdate).
304
- 3. **Make Local Changes:** Use [`doc.change()`](./docs/PatchDoc.md#update).
305
- 4. **Send Changes:** Use [`doc.getUpdatesForServer()`](./docs/PatchDoc.md#getupdatesforserver) and [`doc.applyServerConfirmation()`](./docs/PatchDoc.md#applyserverconfirmation).
306
- 5. **Receive Server Changes:** Use [`doc.applyExternalServerUpdate()`](./docs/PatchDoc.md#applyexternalserverupdate).
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 (`PatchServer`)
300
+ ### Server-Side (`PatchesServer`)
309
301
 
310
- 1. **Initialize `PatchServer`:** Create an instance. See [`docs/PatchServer.md#initialization`](./docs/PatchServer.md#initialization).
311
- 2. **Receive Client Changes:** Use [`server.receiveChanges()`](./docs/PatchServer.md#core-method-receivechanges).
312
- 3. **Handle History/Branching:** Use [`HistoryManager`](./docs/HistoryManager.md) and [`BranchManager`](./docs/BranchManager.md).
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 { PatchDoc, Change } from '@dabble/patches';
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
- // Assume these are fetched initially
329
- const initialDocId = 'doc123';
330
- const initialServerState: MyDoc = { text: 'Hello', count: 0 };
331
- const initialServerRev = 5; // Revision corresponding to initialServerState
332
-
333
- // Your function to send changes and receive the server's commit
334
- async function sendChangesToServer(docId: string, changes: Change[]): Promise<Change[]> {
335
- const response = await fetch(`/docs/${docId}/changes`, {
336
- method: 'POST',
337
- headers: { 'Content-Type': 'application/json' },
338
- body: JSON.stringify({ changes }),
339
- });
340
- if (!response.ok) {
341
- const errorData = await response.json();
342
- throw new Error(errorData.error || `Server error: ${response.status}`);
343
- }
344
- return await response.json();
345
- }
346
-
347
- // --- Initialize PatchDoc ---
348
- const patchDoc = new PatchDoc<MyDoc>(initialServerState, initialServerRev);
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
- // --- UI Update Logic ---
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
- // --- Making a Local Change ---
357
- function handleTextInput(newText: string) {
358
- patchDoc.change(draft => {
359
- draft.text = newText;
360
- });
361
- // Trigger sending changes (e.g., debounced)
362
- sendLocalChanges();
363
- }
364
-
365
- function handleIncrement() {
366
- patchDoc.change(draft => {
367
- draft.count = (draft.count || 0) + 1;
368
- });
369
- sendLocalChanges();
370
- }
371
-
372
- // --- Sending Changes ---
373
- let isSending = false;
374
- async function sendLocalChanges() {
375
- if (isSending || !patchDoc.hasPending) return;
376
-
377
- isSending = true;
378
- try {
379
- const changesToSend = patchDoc.getUpdatesForServer();
380
- if (changesToSend.length > 0) {
381
- console.log('Sending changes:', changesToSend);
382
- const serverCommit = await sendChangesToServer(initialDocId, changesToSend);
383
- console.log('Received confirmation:', serverCommit);
384
- patchDoc.applyServerConfirmation(serverCommit);
385
- }
386
- } catch (error) {
387
- console.error('Failed to send changes:', error);
388
- // Handle error - maybe retry, revert local changes, or force resync
389
- // For simplicity, just log here. PatchDoc state might be inconsistent.
390
- } finally {
391
- isSending = false;
392
- // Check again in case new changes came in while sending
393
- if (patchDoc.hasPending) {
394
- setTimeout(sendLocalChanges, 100); // Basic retry/check again
395
- }
396
- }
397
- }
398
-
399
- // --- Receiving External Changes (e.g., via WebSocket) ---
400
- function handleServerBroadcast(externalChanges: Change[]) {
401
- if (!externalChanges || externalChanges.length === 0) return;
402
- console.log('Received external changes:', externalChanges);
403
- try {
404
- patchDoc.applyExternalServerUpdate(externalChanges);
405
- } catch (error) {
406
- console.error('Error applying external server changes:', error);
407
- // Critical error - likely need to resync the document state
408
- }
409
- }
410
-
411
- // --- Example Usage ---
412
- // handleTextInput("Hello World!");
413
- // handleIncrement();
414
- // Assume setup for receiving broadcasts via `handleServerBroadcast`
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
- PatchServer,
423
- PatchStoreBackend,
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 PatchStoreBackend {
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 PatchServer(store);
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 [`PatchServer Versioning`](./docs/PatchServer.md#versioning) and [`HistoryManager`](./docs/HistoryManager.md).
531
+ See [`PatchesServer Versioning`](./docs/PatchesServer.md#versioning) and [`PatchesHistoryManager`](./docs/PatchesHistoryManager.md).
611
532
 
612
533
  ### Branching and Merging
613
534
 
614
- See [`BranchManager`](./docs/BranchManager.md).
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
+ }