@dabble/patches 0.6.0 → 0.7.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 (133) hide show
  1. package/README.md +221 -208
  2. package/dist/BaseDoc-DkP3tUhT.d.ts +206 -0
  3. package/dist/algorithms/lww/consolidateOps.d.ts +40 -0
  4. package/dist/algorithms/lww/consolidateOps.js +103 -0
  5. package/dist/algorithms/lww/mergeServerWithLocal.d.ts +22 -0
  6. package/dist/algorithms/lww/mergeServerWithLocal.js +32 -0
  7. package/dist/algorithms/{client → ot/client}/applyCommittedChanges.d.ts +10 -3
  8. package/dist/algorithms/{client → ot/client}/applyCommittedChanges.js +7 -4
  9. package/dist/algorithms/{client → ot/client}/createStateFromSnapshot.d.ts +3 -3
  10. package/dist/algorithms/{client → ot/client}/createStateFromSnapshot.js +1 -1
  11. package/dist/algorithms/ot/server/commitChanges.d.ts +43 -0
  12. package/dist/algorithms/{server → ot/server}/commitChanges.js +22 -7
  13. package/dist/algorithms/{server → ot/server}/createVersion.d.ts +5 -5
  14. package/dist/algorithms/{server → ot/server}/createVersion.js +2 -2
  15. package/dist/algorithms/{server → ot/server}/getSnapshotAtRevision.d.ts +5 -5
  16. package/dist/algorithms/{server → ot/server}/getSnapshotAtRevision.js +1 -1
  17. package/dist/algorithms/{server → ot/server}/getStateAtRevision.d.ts +5 -5
  18. package/dist/algorithms/{server → ot/server}/getStateAtRevision.js +1 -1
  19. package/dist/algorithms/{server → ot/server}/handleOfflineSessionsAndBatches.d.ts +5 -5
  20. package/dist/algorithms/{server → ot/server}/handleOfflineSessionsAndBatches.js +3 -3
  21. package/dist/algorithms/{server → ot/server}/transformIncomingChanges.d.ts +3 -3
  22. package/dist/algorithms/{server → ot/server}/transformIncomingChanges.js +3 -3
  23. package/dist/algorithms/{shared → ot/shared}/applyChanges.d.ts +3 -3
  24. package/dist/algorithms/{shared → ot/shared}/applyChanges.js +2 -2
  25. package/dist/algorithms/{shared → ot/shared}/changeBatching.d.ts +3 -3
  26. package/dist/algorithms/{shared → ot/shared}/changeBatching.js +2 -2
  27. package/dist/algorithms/{shared → ot/shared}/rebaseChanges.d.ts +3 -3
  28. package/dist/algorithms/{shared → ot/shared}/rebaseChanges.js +2 -2
  29. package/dist/client/BaseDoc.d.ts +6 -0
  30. package/dist/client/BaseDoc.js +70 -0
  31. package/dist/client/ClientAlgorithm.d.ts +101 -0
  32. package/dist/client/ClientAlgorithm.js +0 -0
  33. package/dist/client/InMemoryStore.d.ts +5 -7
  34. package/dist/client/InMemoryStore.js +7 -36
  35. package/dist/client/IndexedDBStore.d.ts +39 -73
  36. package/dist/client/IndexedDBStore.js +17 -220
  37. package/dist/client/LWWAlgorithm.d.ts +43 -0
  38. package/dist/client/LWWAlgorithm.js +87 -0
  39. package/dist/client/LWWClientStore.d.ts +73 -0
  40. package/dist/client/LWWClientStore.js +0 -0
  41. package/dist/client/LWWDoc.d.ts +56 -0
  42. package/dist/client/LWWDoc.js +84 -0
  43. package/dist/client/LWWInMemoryStore.d.ts +88 -0
  44. package/dist/client/LWWInMemoryStore.js +208 -0
  45. package/dist/client/LWWIndexedDBStore.d.ts +91 -0
  46. package/dist/client/LWWIndexedDBStore.js +275 -0
  47. package/dist/client/OTAlgorithm.d.ts +42 -0
  48. package/dist/client/OTAlgorithm.js +113 -0
  49. package/dist/client/OTClientStore.d.ts +50 -0
  50. package/dist/client/OTClientStore.js +0 -0
  51. package/dist/client/OTDoc.d.ts +6 -0
  52. package/dist/client/OTDoc.js +97 -0
  53. package/dist/client/OTIndexedDBStore.d.ts +84 -0
  54. package/dist/client/OTIndexedDBStore.js +163 -0
  55. package/dist/client/Patches.d.ts +36 -16
  56. package/dist/client/Patches.js +60 -27
  57. package/dist/client/PatchesDoc.d.ts +4 -113
  58. package/dist/client/PatchesDoc.js +3 -153
  59. package/dist/client/PatchesHistoryClient.js +1 -1
  60. package/dist/client/PatchesStore.d.ts +8 -105
  61. package/dist/client/factories.d.ts +72 -0
  62. package/dist/client/factories.js +80 -0
  63. package/dist/client/index.d.ts +14 -5
  64. package/dist/client/index.js +9 -0
  65. package/dist/compression/index.d.ts +2 -2
  66. package/dist/compression/index.js +1 -1
  67. package/dist/{algorithms/shared → compression}/lz.js +1 -1
  68. package/dist/data/change.js +2 -0
  69. package/dist/fractionalIndex.d.ts +67 -0
  70. package/dist/fractionalIndex.js +241 -0
  71. package/dist/index.d.ts +13 -3
  72. package/dist/index.js +1 -0
  73. package/dist/json-patch/types.d.ts +2 -0
  74. package/dist/net/PatchesClient.js +15 -15
  75. package/dist/net/PatchesSync.d.ts +20 -8
  76. package/dist/net/PatchesSync.js +57 -65
  77. package/dist/net/index.d.ts +7 -11
  78. package/dist/net/index.js +6 -1
  79. package/dist/net/protocol/JSONRPCClient.d.ts +4 -4
  80. package/dist/net/protocol/JSONRPCClient.js +6 -4
  81. package/dist/net/protocol/JSONRPCServer.d.ts +45 -9
  82. package/dist/net/protocol/JSONRPCServer.js +63 -8
  83. package/dist/net/serverContext.d.ts +38 -0
  84. package/dist/net/serverContext.js +20 -0
  85. package/dist/net/webrtc/WebRTCTransport.js +1 -1
  86. package/dist/net/websocket/AuthorizationProvider.d.ts +3 -3
  87. package/dist/net/websocket/WebSocketServer.d.ts +29 -20
  88. package/dist/net/websocket/WebSocketServer.js +23 -12
  89. package/dist/server/BranchManager.d.ts +50 -0
  90. package/dist/server/BranchManager.js +0 -0
  91. package/dist/server/CompressedStoreBackend.d.ts +10 -8
  92. package/dist/server/CompressedStoreBackend.js +7 -13
  93. package/dist/server/LWWBranchManager.d.ts +82 -0
  94. package/dist/server/LWWBranchManager.js +99 -0
  95. package/dist/server/LWWMemoryStoreBackend.d.ts +78 -0
  96. package/dist/server/LWWMemoryStoreBackend.js +182 -0
  97. package/dist/server/LWWServer.d.ts +130 -0
  98. package/dist/server/LWWServer.js +214 -0
  99. package/dist/server/{PatchesBranchManager.d.ts → OTBranchManager.d.ts} +32 -12
  100. package/dist/server/{PatchesBranchManager.js → OTBranchManager.js} +27 -42
  101. package/dist/server/OTServer.d.ts +108 -0
  102. package/dist/server/OTServer.js +141 -0
  103. package/dist/server/PatchesHistoryManager.d.ts +21 -16
  104. package/dist/server/PatchesHistoryManager.js +23 -11
  105. package/dist/server/PatchesServer.d.ts +70 -81
  106. package/dist/server/PatchesServer.js +0 -175
  107. package/dist/server/branchUtils.d.ts +82 -0
  108. package/dist/server/branchUtils.js +66 -0
  109. package/dist/server/index.d.ts +18 -7
  110. package/dist/server/index.js +33 -4
  111. package/dist/server/tombstone.d.ts +29 -0
  112. package/dist/server/tombstone.js +32 -0
  113. package/dist/server/types.d.ts +109 -27
  114. package/dist/server/utils.d.ts +12 -0
  115. package/dist/server/utils.js +23 -0
  116. package/dist/solid/context.d.ts +4 -3
  117. package/dist/solid/doc-manager.d.ts +3 -3
  118. package/dist/solid/index.d.ts +4 -3
  119. package/dist/solid/primitives.d.ts +2 -3
  120. package/dist/types.d.ts +4 -2
  121. package/dist/vue/composables.d.ts +2 -3
  122. package/dist/vue/doc-manager.d.ts +3 -3
  123. package/dist/vue/index.d.ts +4 -3
  124. package/dist/vue/provider.d.ts +4 -3
  125. package/package.json +1 -1
  126. package/dist/algorithms/client/collapsePendingChanges.d.ts +0 -30
  127. package/dist/algorithms/client/collapsePendingChanges.js +0 -78
  128. package/dist/algorithms/client/makeChange.d.ts +0 -9
  129. package/dist/algorithms/client/makeChange.js +0 -29
  130. package/dist/algorithms/server/commitChanges.d.ts +0 -19
  131. package/dist/net/websocket/RPCServer.d.ts +0 -141
  132. package/dist/net/websocket/RPCServer.js +0 -204
  133. /package/dist/{algorithms/shared → compression}/lz.d.ts +0 -0
@@ -1,24 +1,10 @@
1
- import {
2
- __decorateElement,
3
- __decoratorMetadata,
4
- __decoratorStart,
5
- __publicField,
6
- __runInitializers
7
- } from "../chunk-IZ2YBCUP.js";
8
- var _getLastRevs_dec, _saveCommittedChanges_dec, _replacePendingChanges_dec, _getPendingChanges_dec, _savePendingChanges_dec, _saveDoc_dec, _confirmDeleteDoc_dec, _deleteDoc_dec, _getDoc_dec, _init;
9
- import { applyChanges } from "../algorithms/shared/applyChanges.js";
10
- import { transformPatch } from "../json-patch/transformPatch.js";
11
- import { blockable } from "../utils/concurrency.js";
1
+ import "../chunk-IZ2YBCUP.js";
12
2
  import { deferred } from "../utils/deferred.js";
13
- const DB_VERSION = 1;
14
- const SNAPSHOT_INTERVAL = 200;
15
- _getDoc_dec = [blockable], _deleteDoc_dec = [blockable], _confirmDeleteDoc_dec = [blockable], _saveDoc_dec = [blockable], _savePendingChanges_dec = [blockable], _getPendingChanges_dec = [blockable], _replacePendingChanges_dec = [blockable], _saveCommittedChanges_dec = [blockable], _getLastRevs_dec = [blockable];
16
3
  class IndexedDBStore {
4
+ db = null;
5
+ dbName;
6
+ dbPromise;
17
7
  constructor(dbName) {
18
- __runInitializers(_init, 5, this);
19
- __publicField(this, "db", null);
20
- __publicField(this, "dbName");
21
- __publicField(this, "dbPromise");
22
8
  this.dbName = dbName;
23
9
  this.dbPromise = deferred();
24
10
  if (this.dbName) {
@@ -27,7 +13,7 @@ class IndexedDBStore {
27
13
  }
28
14
  async initDB() {
29
15
  if (!this.dbName) return;
30
- const request = indexedDB.open(this.dbName, DB_VERSION);
16
+ const request = indexedDB.open(this.dbName, this.getDBVersion());
31
17
  request.onerror = () => this.dbPromise.reject(request.error);
32
18
  request.onsuccess = () => {
33
19
  this.db = request.result;
@@ -35,18 +21,14 @@ class IndexedDBStore {
35
21
  };
36
22
  request.onupgradeneeded = (event) => {
37
23
  const db = event.target.result;
24
+ const oldVersion = event.oldVersion;
38
25
  if (!db.objectStoreNames.contains("snapshots")) {
39
26
  db.createObjectStore("snapshots", { keyPath: "docId" });
40
27
  }
41
- if (!db.objectStoreNames.contains("committedChanges")) {
42
- db.createObjectStore("committedChanges", { keyPath: ["docId", "rev"] });
43
- }
44
- if (!db.objectStoreNames.contains("pendingChanges")) {
45
- db.createObjectStore("pendingChanges", { keyPath: ["docId", "rev"] });
46
- }
47
28
  if (!db.objectStoreNames.contains("docs")) {
48
29
  db.createObjectStore("docs", { keyPath: "docId" });
49
30
  }
31
+ this.onUpgrade(db, oldVersion);
50
32
  };
51
33
  }
52
34
  getDB() {
@@ -95,144 +77,15 @@ class IndexedDBStore {
95
77
  const stores = storeNames.map((name) => tx.getStore(name));
96
78
  return [tx, ...stores];
97
79
  }
98
- async getDoc(docId) {
99
- const [tx, docsStore, snapshots, committedChanges, pendingChanges] = await this.transaction(
100
- ["docs", "snapshots", "committedChanges", "pendingChanges"],
101
- "readonly"
102
- );
103
- const docMeta = await docsStore.get(docId);
104
- if (docMeta?.deleted) {
105
- await tx.complete();
106
- return void 0;
107
- }
108
- const snapshot = await snapshots.get(docId);
109
- const committed = await committedChanges.getAll([docId, snapshot?.rev ?? 0], [docId, Infinity]);
110
- const pending = await pendingChanges.getAll([docId, 0], [docId, Infinity]);
111
- if (!snapshot && !committed.length && !pending.length) return void 0;
112
- const state = applyChanges(snapshot?.state, committed);
113
- const lastCommitted = committed[committed.length - 1];
114
- const baseRev = pending[0]?.baseRev;
115
- if (lastCommitted && baseRev && baseRev < lastCommitted.rev) {
116
- const patch = committed.filter((change) => change.rev > baseRev).map((change) => change.ops).flat();
117
- const offset = lastCommitted.rev - baseRev;
118
- pending.forEach((change) => {
119
- change.rev += offset;
120
- change.ops = transformPatch(state, patch, change.ops);
121
- });
122
- }
123
- await tx.complete();
124
- return {
125
- state,
126
- rev: committed[committed.length - 1]?.rev ?? snapshot?.rev ?? 0,
127
- changes: pending
128
- };
129
- }
130
- async deleteDoc(docId) {
131
- const [tx, snapshots, committedChanges, pendingChanges, docsStore] = await this.transaction(
132
- ["snapshots", "committedChanges", "pendingChanges", "docs"],
133
- "readwrite"
134
- );
135
- const docMeta = await docsStore.get(docId) ?? { docId, committedRev: 0 };
136
- await docsStore.put({ ...docMeta, deleted: true });
137
- await Promise.all([
138
- snapshots.delete(docId),
139
- committedChanges.delete([docId, 0], [docId, Infinity]),
140
- pendingChanges.delete([docId, 0], [docId, Infinity])
141
- ]);
142
- await tx.complete();
143
- }
80
+ /**
81
+ * Confirm the deletion of a document.
82
+ * @param docId - The ID of the document to delete.
83
+ */
144
84
  async confirmDeleteDoc(docId) {
145
85
  const [tx, docsStore] = await this.transaction(["docs"], "readwrite");
146
86
  await docsStore.delete(docId);
147
87
  await tx.complete();
148
88
  }
149
- async saveDoc(docId, docState) {
150
- const [tx, snapshots, committedChanges, pendingChanges, docsStore] = await this.transaction(
151
- ["snapshots", "committedChanges", "pendingChanges", "docs"],
152
- "readwrite"
153
- );
154
- const { rev, state } = docState;
155
- await Promise.all([
156
- docsStore.put({ docId, committedRev: rev }),
157
- snapshots.put({ docId, state, rev }),
158
- committedChanges.delete([docId, 0], [docId, Infinity]),
159
- pendingChanges.delete([docId, 0], [docId, Infinity])
160
- ]);
161
- await tx.complete();
162
- }
163
- async savePendingChanges(docId, changes) {
164
- const [tx, pendingChanges, docsStore] = await this.transaction(["pendingChanges", "docs"], "readwrite");
165
- let docMeta = await docsStore.get(docId);
166
- if (!docMeta) {
167
- docMeta = { docId, committedRev: 0 };
168
- await docsStore.put(docMeta);
169
- } else if (docMeta.deleted) {
170
- delete docMeta.deleted;
171
- await docsStore.put(docMeta);
172
- console.warn(`Revived document ${docId} by saving pending changes.`);
173
- }
174
- await Promise.all(changes.map((change) => pendingChanges.put({ ...change, docId })));
175
- await tx.complete();
176
- }
177
- async getPendingChanges(docId) {
178
- const [tx, pendingChanges] = await this.transaction(["pendingChanges"], "readonly");
179
- const result = await pendingChanges.getAll([docId, 0], [docId, Infinity]);
180
- await tx.complete();
181
- return result;
182
- }
183
- async replacePendingChanges(docId, changes) {
184
- const [tx, pendingChanges, docsStore] = await this.transaction(["pendingChanges", "docs"], "readwrite");
185
- let docMeta = await docsStore.get(docId);
186
- if (!docMeta) {
187
- docMeta = { docId, committedRev: 0 };
188
- await docsStore.put(docMeta);
189
- } else if (docMeta.deleted) {
190
- delete docMeta.deleted;
191
- await docsStore.put(docMeta);
192
- console.warn(`Revived document ${docId} by replacing pending changes.`);
193
- }
194
- await pendingChanges.delete([docId, 0], [docId, Infinity]);
195
- await Promise.all(changes.map((change) => pendingChanges.put({ ...change, docId })));
196
- await tx.complete();
197
- }
198
- async saveCommittedChanges(docId, changes, sentPendingRange) {
199
- const [tx, committedChanges, pendingChanges, snapshots, docsStore] = await this.transaction(
200
- ["committedChanges", "pendingChanges", "snapshots", "docs"],
201
- "readwrite"
202
- );
203
- await Promise.all(changes.map((change) => committedChanges.put({ ...change, docId })));
204
- if (sentPendingRange) {
205
- await pendingChanges.delete([docId, sentPendingRange[0]], [docId, sentPendingRange[1]]);
206
- }
207
- const count = await committedChanges.count([docId, 0], [docId, Infinity]);
208
- if (count >= SNAPSHOT_INTERVAL) {
209
- const [snapshot, committed, firstPending] = await Promise.all([
210
- snapshots.get(docId),
211
- committedChanges.getAll([docId, 0], [docId, Infinity], SNAPSHOT_INTERVAL),
212
- pendingChanges.getFirstFromCursor([docId, 0], [docId, Infinity])
213
- ]);
214
- const lastRev = committed[committed.length - 1]?.rev;
215
- if (!firstPending?.baseRev || firstPending?.baseRev >= lastRev) {
216
- const state = applyChanges(snapshot?.state, committed);
217
- await Promise.all([
218
- snapshots.put({
219
- docId,
220
- rev: lastRev,
221
- state
222
- }),
223
- committedChanges.delete([docId, 0], [docId, lastRev])
224
- ]);
225
- }
226
- }
227
- const lastCommittedRev = changes.at(-1)?.rev;
228
- if (lastCommittedRev !== void 0) {
229
- const docMeta = await docsStore.get(docId) ?? { docId, committedRev: 0 };
230
- if (lastCommittedRev > docMeta.committedRev) {
231
- await docsStore.put({ ...docMeta, committedRev: lastCommittedRev, deleted: void 0 });
232
- }
233
- }
234
- await tx.complete();
235
- }
236
89
  /**
237
90
  * List all documents in the store.
238
91
  * @param includeDeleted - Whether to include deleted documents.
@@ -265,75 +118,17 @@ class IndexedDBStore {
265
118
  await tx.complete();
266
119
  }
267
120
  /**
268
- * Untrack a document.
269
- * @param docIds - The IDs of the documents to untrack.
270
- */
271
- async untrackDocs(docIds) {
272
- const [tx, docsStore, snapshots, committedChanges, pendingChanges] = await this.transaction(
273
- ["docs", "snapshots", "committedChanges", "pendingChanges"],
274
- "readwrite"
275
- );
276
- await Promise.all(
277
- docIds.map((docId) => {
278
- return Promise.all([
279
- docsStore.delete(docId),
280
- snapshots.delete(docId),
281
- committedChanges.delete([docId, 0], [docId, Infinity]),
282
- pendingChanges.delete([docId, 0], [docId, Infinity])
283
- ]);
284
- })
285
- );
286
- await tx.complete();
287
- }
288
- async getLastRevs(docId) {
289
- const [tx, committedChanges, pendingChanges] = await this.transaction(
290
- ["committedChanges", "pendingChanges"],
291
- "readonly"
292
- );
293
- const [lastCommitted, lastPending] = await Promise.all([
294
- committedChanges.getLastFromCursor([docId, 0], [docId, Infinity]),
295
- pendingChanges.getLastFromCursor([docId, 0], [docId, Infinity])
296
- ]);
297
- await tx.complete();
298
- return [lastCommitted?.rev ?? 0, lastPending?.rev ?? lastCommitted?.rev ?? 0];
299
- }
300
- // ─── Submission Bookmark ───────────────────────────────────────────────
301
- /**
302
- * Gets the last revision that was attempted to be submitted to the server.
121
+ * Returns the last committed revision for a document.
303
122
  * @param docId - The ID of the document.
304
- * @returns The last attempted submission revision, or undefined if none.
123
+ * @returns The last committed revision, or 0 if not found.
305
124
  */
306
- async getLastAttemptedSubmissionRev(docId) {
125
+ async getCommittedRev(docId) {
307
126
  const [tx, docsStore] = await this.transaction(["docs"], "readonly");
308
127
  const docMeta = await docsStore.get(docId);
309
128
  await tx.complete();
310
- return docMeta?.lastAttemptedSubmissionRev;
311
- }
312
- /**
313
- * Sets the last revision that was attempted to be submitted to the server.
314
- * @param docId - The ID of the document.
315
- * @param rev - The revision being submitted.
316
- */
317
- async setLastAttemptedSubmissionRev(docId, rev) {
318
- const [tx, docsStore] = await this.transaction(["docs"], "readwrite");
319
- const docMeta = await docsStore.get(docId);
320
- if (docMeta) {
321
- await docsStore.put({ ...docMeta, lastAttemptedSubmissionRev: rev });
322
- }
323
- await tx.complete();
129
+ return docMeta?.committedRev ?? 0;
324
130
  }
325
131
  }
326
- _init = __decoratorStart(null);
327
- __decorateElement(_init, 1, "getDoc", _getDoc_dec, IndexedDBStore);
328
- __decorateElement(_init, 1, "deleteDoc", _deleteDoc_dec, IndexedDBStore);
329
- __decorateElement(_init, 1, "confirmDeleteDoc", _confirmDeleteDoc_dec, IndexedDBStore);
330
- __decorateElement(_init, 1, "saveDoc", _saveDoc_dec, IndexedDBStore);
331
- __decorateElement(_init, 1, "savePendingChanges", _savePendingChanges_dec, IndexedDBStore);
332
- __decorateElement(_init, 1, "getPendingChanges", _getPendingChanges_dec, IndexedDBStore);
333
- __decorateElement(_init, 1, "replacePendingChanges", _replacePendingChanges_dec, IndexedDBStore);
334
- __decorateElement(_init, 1, "saveCommittedChanges", _saveCommittedChanges_dec, IndexedDBStore);
335
- __decorateElement(_init, 1, "getLastRevs", _getLastRevs_dec, IndexedDBStore);
336
- __decoratorMetadata(_init, IndexedDBStore);
337
132
  class IDBTransactionWrapper {
338
133
  tx;
339
134
  promise;
@@ -412,5 +207,7 @@ class IDBStoreWrapper {
412
207
  }
413
208
  }
414
209
  export {
210
+ IDBStoreWrapper,
211
+ IDBTransactionWrapper,
415
212
  IndexedDBStore
416
213
  };
@@ -0,0 +1,43 @@
1
+ import { JSONPatchOp } from '../json-patch/types.js';
2
+ import { PatchesSnapshot, Change } from '../types.js';
3
+ import { ClientAlgorithm } from './ClientAlgorithm.js';
4
+ import { LWWClientStore } from './LWWClientStore.js';
5
+ import { a as PatchesDoc } from '../BaseDoc-DkP3tUhT.js';
6
+ import { TrackedDoc } from './PatchesStore.js';
7
+ import '../json-patch/JSONPatch.js';
8
+ import '@dabble/delta';
9
+ import '../event-signal.js';
10
+
11
+ /**
12
+ * LWW (Last-Write-Wins) algorithm implementation.
13
+ *
14
+ * LWW uses timestamps for field-level conflict resolution.
15
+ * This algorithm owns an LWW-compatible store and handles all LWW-specific
16
+ * logic including consolidation of ops.
17
+ *
18
+ * Key differences from OT:
19
+ * - Single pending change at a time (not a list)
20
+ * - Field-level storage with timestamps
21
+ * - No rebasing - timestamps determine winner
22
+ * - Doc is very thin (just state, no committed/pending tracking)
23
+ */
24
+ declare class LWWAlgorithm implements ClientAlgorithm {
25
+ readonly name = "lww";
26
+ readonly store: LWWClientStore;
27
+ constructor(store: LWWClientStore);
28
+ createDoc<T extends object>(docId: string, snapshot?: PatchesSnapshot<T>): PatchesDoc<T>;
29
+ loadDoc(docId: string): Promise<PatchesSnapshot | undefined>;
30
+ handleDocChange<T extends object>(docId: string, ops: JSONPatchOp[], doc: PatchesDoc<T> | undefined, metadata: Record<string, any>): Promise<Change[]>;
31
+ getPendingToSend(docId: string): Promise<Change[] | null>;
32
+ applyServerChanges<T extends object>(docId: string, serverChanges: Change[], doc: PatchesDoc<T> | undefined): Promise<Change[]>;
33
+ confirmSent(docId: string, _changes: Change[]): Promise<void>;
34
+ trackDocs(docIds: string[]): Promise<void>;
35
+ untrackDocs(docIds: string[]): Promise<void>;
36
+ listDocs(includeDeleted?: boolean): Promise<TrackedDoc[]>;
37
+ getCommittedRev(docId: string): Promise<number>;
38
+ deleteDoc(docId: string): Promise<void>;
39
+ confirmDeleteDoc(docId: string): Promise<void>;
40
+ close(): Promise<void>;
41
+ }
42
+
43
+ export { LWWAlgorithm };
@@ -0,0 +1,87 @@
1
+ import "../chunk-IZ2YBCUP.js";
2
+ import { consolidateOps } from "../algorithms/lww/consolidateOps.js";
3
+ import { mergeServerWithLocal } from "../algorithms/lww/mergeServerWithLocal.js";
4
+ import { createChange } from "../data/change.js";
5
+ import { LWWDoc } from "./LWWDoc.js";
6
+ class LWWAlgorithm {
7
+ name = "lww";
8
+ store;
9
+ constructor(store) {
10
+ this.store = store;
11
+ }
12
+ createDoc(docId, snapshot) {
13
+ return new LWWDoc(docId, snapshot);
14
+ }
15
+ async loadDoc(docId) {
16
+ return this.store.getDoc(docId);
17
+ }
18
+ async handleDocChange(docId, ops, doc, metadata) {
19
+ if (ops.length === 0) return [];
20
+ const timestamp = Date.now();
21
+ const timedOps = ops.map((op) => ({ ...op, ts: timestamp }));
22
+ const pathPrefixes = timedOps.map((op) => op.path);
23
+ const existingOps = await this.store.getPendingOps(docId, pathPrefixes);
24
+ const { opsToSave, pathsToDelete } = consolidateOps(existingOps, timedOps);
25
+ await this.store.savePendingOps(docId, opsToSave, pathsToDelete);
26
+ const committedRev = doc?.committedRev ?? await this.store.getCommittedRev(docId);
27
+ const changes = [createChange(committedRev, committedRev + 1, timedOps, metadata)];
28
+ if (doc) {
29
+ doc.applyChanges(changes);
30
+ }
31
+ return changes;
32
+ }
33
+ async getPendingToSend(docId) {
34
+ const sendingChange = await this.store.getSendingChange(docId);
35
+ if (sendingChange) {
36
+ return [sendingChange];
37
+ }
38
+ const pendingOps = await this.store.getPendingOps(docId);
39
+ if (pendingOps.length === 0) {
40
+ return null;
41
+ }
42
+ const committedRev = await this.store.getCommittedRev(docId);
43
+ const change = createChange(committedRev, committedRev + 1, pendingOps);
44
+ await this.store.saveSendingChange(docId, change);
45
+ return [change];
46
+ }
47
+ async applyServerChanges(docId, serverChanges, doc) {
48
+ if (serverChanges.length === 0) return [];
49
+ await this.store.applyServerChanges(docId, serverChanges);
50
+ const sendingChange = await this.store.getSendingChange(docId);
51
+ const pendingOps = await this.store.getPendingOps(docId);
52
+ const localOps = [...sendingChange?.ops ?? [], ...pendingOps];
53
+ const mergedChanges = mergeServerWithLocal(serverChanges, localOps);
54
+ if (doc) {
55
+ doc.applyChanges(mergedChanges);
56
+ }
57
+ return mergedChanges;
58
+ }
59
+ async confirmSent(docId, _changes) {
60
+ await this.store.confirmSendingChange(docId);
61
+ }
62
+ // --- Store forwarding methods ---
63
+ async trackDocs(docIds) {
64
+ return this.store.trackDocs(docIds);
65
+ }
66
+ async untrackDocs(docIds) {
67
+ return this.store.untrackDocs(docIds);
68
+ }
69
+ async listDocs(includeDeleted) {
70
+ return this.store.listDocs(includeDeleted);
71
+ }
72
+ async getCommittedRev(docId) {
73
+ return this.store.getCommittedRev(docId);
74
+ }
75
+ async deleteDoc(docId) {
76
+ return this.store.deleteDoc(docId);
77
+ }
78
+ async confirmDeleteDoc(docId) {
79
+ return this.store.confirmDeleteDoc(docId);
80
+ }
81
+ async close() {
82
+ return this.store.close();
83
+ }
84
+ }
85
+ export {
86
+ LWWAlgorithm
87
+ };
@@ -0,0 +1,73 @@
1
+ import { JSONPatchOp } from '../json-patch/types.js';
2
+ import { Change } from '../types.js';
3
+ import { PatchesStore } from './PatchesStore.js';
4
+ import '../json-patch/JSONPatch.js';
5
+ import '@dabble/delta';
6
+
7
+ /**
8
+ * LWW-specific client store interface.
9
+ * Extends PatchesStore with field-level operations and sending change lifecycle.
10
+ *
11
+ * State reconstruction in getDoc:
12
+ * snapshot -> apply committedFields -> apply sendingChange.ops -> apply pendingOps -> return {state, rev}
13
+ *
14
+ * Idempotency: sendingChange stays until acked. No cancel - just retry until success.
15
+ */
16
+ interface LWWClientStore extends PatchesStore {
17
+ /**
18
+ * Get pending ops, optionally filtered by path prefixes for efficiency.
19
+ *
20
+ * When pathPrefixes is provided, returns all ops where the path starts with
21
+ * any of the given prefixes. For example, getPendingOps(docId, ['/user/name'])
22
+ * returns ops at '/user/name' or '/user/name/first', etc.
23
+ *
24
+ * @param docId Document identifier
25
+ * @param pathPrefixes Optional array of path prefixes to filter by
26
+ * @returns Array of pending JSONPatchOps
27
+ */
28
+ getPendingOps(docId: string, pathPrefixes?: string[]): Promise<JSONPatchOp[]>;
29
+ /**
30
+ * Save pending ops, optionally deleting paths.
31
+ *
32
+ * Used for consolidation when a parent path overwrites children. For example,
33
+ * when saving replace('/user/name', {first: 'Bob'}) we may need to delete
34
+ * the existing '/user/name/first' op.
35
+ *
36
+ * @param docId Document identifier
37
+ * @param ops Array of JSONPatchOps to save
38
+ * @param pathsToDelete Optional array of paths to delete (for consolidation)
39
+ */
40
+ savePendingOps(docId: string, ops: JSONPatchOp[], pathsToDelete?: string[]): Promise<void>;
41
+ /**
42
+ * Get the in-flight change for retry/reconnect scenarios.
43
+ *
44
+ * @param docId Document identifier
45
+ * @returns The sending change, or null if none
46
+ */
47
+ getSendingChange(docId: string): Promise<Change | null>;
48
+ /**
49
+ * Atomically save sending change AND clear all pending ops.
50
+ *
51
+ * This creates the in-flight change from pending ops. The pending ops
52
+ * are cleared because they are now encapsulated in the sending change.
53
+ *
54
+ * @param docId Document identifier
55
+ * @param change The change to save as sending
56
+ */
57
+ saveSendingChange(docId: string, change: Change): Promise<void>;
58
+ /**
59
+ * Clear sendingChange after server ack, move ops to committed.
60
+ *
61
+ * @param docId Document identifier
62
+ */
63
+ confirmSendingChange(docId: string): Promise<void>;
64
+ /**
65
+ * Apply server changes using LWW timestamp resolution.
66
+ *
67
+ * @param docId Document identifier
68
+ * @param serverChanges Changes from the server
69
+ */
70
+ applyServerChanges(docId: string, serverChanges: Change[]): Promise<void>;
71
+ }
72
+
73
+ export type { LWWClientStore };
File without changes
@@ -0,0 +1,56 @@
1
+ import { PatchesSnapshot, Change } from '../types.js';
2
+ import { B as BaseDoc } from '../BaseDoc-DkP3tUhT.js';
3
+ import '../json-patch/JSONPatch.js';
4
+ import '@dabble/delta';
5
+ import '../json-patch/types.js';
6
+ import '../event-signal.js';
7
+
8
+ /**
9
+ * LWW (Last-Write-Wins) document implementation.
10
+ *
11
+ * The `change()` method (inherited from BaseDoc) captures ops and emits them
12
+ * via `onChange` - it does NOT apply locally. The LWWStrategy handles:
13
+ * - Packaging ops with timestamps
14
+ * - Merging with pending fields
15
+ * - Updating the doc's state via `applyChanges()`
16
+ *
17
+ * Unlike OTDoc, LWWDoc doesn't need to track committed vs pending state
18
+ * separately - the strategy handles all conflict resolution by timestamp.
19
+ *
20
+ * ## Wire Efficiency
21
+ * For Worker-Tab communication, `applyChanges()` sends only changes over the wire,
22
+ * not the full state.
23
+ */
24
+ declare class LWWDoc<T extends object = object> extends BaseDoc<T> {
25
+ protected _committedRev: number;
26
+ protected _hasPending: boolean;
27
+ /**
28
+ * Creates an instance of LWWDoc.
29
+ * @param id The unique identifier for this document.
30
+ * @param snapshot Optional snapshot to initialize from.
31
+ */
32
+ constructor(id: string, snapshot?: PatchesSnapshot<T>);
33
+ /** Last committed revision number from the server. */
34
+ get committedRev(): number;
35
+ /** Are there local changes that haven't been committed yet? */
36
+ get hasPending(): boolean;
37
+ /**
38
+ * Imports document state from a snapshot (e.g., for recovery when out of sync).
39
+ * Resets state completely from the snapshot.
40
+ */
41
+ import(snapshot: PatchesSnapshot<T>): void;
42
+ /**
43
+ * Applies changes to the document state.
44
+ * Used for Worker→Tab communication where only changes are sent over the wire.
45
+ *
46
+ * For LWW, all ops are applied in order. The method distinguishes between
47
+ * committed and pending changes using `committedAt`:
48
+ * - `committedAt > 0`: Server-committed change (updates committedRev)
49
+ * - `committedAt === 0`: Pending local change (marks hasPending = true)
50
+ *
51
+ * @param changes Array of changes to apply
52
+ */
53
+ applyChanges(changes: Change[]): void;
54
+ }
55
+
56
+ export { LWWDoc };
@@ -0,0 +1,84 @@
1
+ import "../chunk-IZ2YBCUP.js";
2
+ import { applyPatch } from "../json-patch/applyPatch.js";
3
+ import { BaseDoc } from "./BaseDoc.js";
4
+ class LWWDoc extends BaseDoc {
5
+ _committedRev;
6
+ _hasPending;
7
+ /**
8
+ * Creates an instance of LWWDoc.
9
+ * @param id The unique identifier for this document.
10
+ * @param snapshot Optional snapshot to initialize from.
11
+ */
12
+ constructor(id, snapshot) {
13
+ const initialState = snapshot?.state ?? {};
14
+ super(id, initialState);
15
+ this._committedRev = snapshot?.rev ?? 0;
16
+ this._hasPending = (snapshot?.changes?.length ?? 0) > 0;
17
+ if (snapshot?.changes && snapshot.changes.length > 0) {
18
+ for (const change of snapshot.changes) {
19
+ for (const op of change.ops) {
20
+ this._state = applyPatch(this._state, [op], { partial: true });
21
+ }
22
+ }
23
+ }
24
+ }
25
+ /** Last committed revision number from the server. */
26
+ get committedRev() {
27
+ return this._committedRev;
28
+ }
29
+ /** Are there local changes that haven't been committed yet? */
30
+ get hasPending() {
31
+ return this._hasPending;
32
+ }
33
+ /**
34
+ * Imports document state from a snapshot (e.g., for recovery when out of sync).
35
+ * Resets state completely from the snapshot.
36
+ */
37
+ import(snapshot) {
38
+ this._state = snapshot.state;
39
+ this._committedRev = snapshot.rev;
40
+ this._hasPending = (snapshot.changes?.length ?? 0) > 0;
41
+ if (snapshot.changes && snapshot.changes.length > 0) {
42
+ for (const change of snapshot.changes) {
43
+ for (const op of change.ops) {
44
+ this._state = applyPatch(this._state, [op], { partial: true });
45
+ }
46
+ }
47
+ }
48
+ this.onUpdate.emit(this._state);
49
+ }
50
+ /**
51
+ * Applies changes to the document state.
52
+ * Used for Worker→Tab communication where only changes are sent over the wire.
53
+ *
54
+ * For LWW, all ops are applied in order. The method distinguishes between
55
+ * committed and pending changes using `committedAt`:
56
+ * - `committedAt > 0`: Server-committed change (updates committedRev)
57
+ * - `committedAt === 0`: Pending local change (marks hasPending = true)
58
+ *
59
+ * @param changes Array of changes to apply
60
+ */
61
+ applyChanges(changes) {
62
+ if (changes.length === 0) return;
63
+ for (const change of changes) {
64
+ for (const op of change.ops) {
65
+ this._state = applyPatch(this._state, [op], { partial: true });
66
+ }
67
+ }
68
+ let lastCommittedRev = this._committedRev;
69
+ let hasPendingChanges = false;
70
+ for (const change of changes) {
71
+ if (change.committedAt > 0) {
72
+ lastCommittedRev = change.rev;
73
+ } else {
74
+ hasPendingChanges = true;
75
+ }
76
+ }
77
+ this._committedRev = lastCommittedRev;
78
+ this._hasPending = hasPendingChanges;
79
+ this.onUpdate.emit(this._state);
80
+ }
81
+ }
82
+ export {
83
+ LWWDoc
84
+ };