@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.
- package/README.md +221 -208
- package/dist/BaseDoc-DkP3tUhT.d.ts +206 -0
- package/dist/algorithms/lww/consolidateOps.d.ts +40 -0
- package/dist/algorithms/lww/consolidateOps.js +103 -0
- package/dist/algorithms/lww/mergeServerWithLocal.d.ts +22 -0
- package/dist/algorithms/lww/mergeServerWithLocal.js +32 -0
- package/dist/algorithms/{client → ot/client}/applyCommittedChanges.d.ts +10 -3
- package/dist/algorithms/{client → ot/client}/applyCommittedChanges.js +7 -4
- package/dist/algorithms/{client → ot/client}/createStateFromSnapshot.d.ts +3 -3
- package/dist/algorithms/{client → ot/client}/createStateFromSnapshot.js +1 -1
- package/dist/algorithms/ot/server/commitChanges.d.ts +43 -0
- package/dist/algorithms/{server → ot/server}/commitChanges.js +22 -7
- package/dist/algorithms/{server → ot/server}/createVersion.d.ts +5 -5
- package/dist/algorithms/{server → ot/server}/createVersion.js +2 -2
- package/dist/algorithms/{server → ot/server}/getSnapshotAtRevision.d.ts +5 -5
- package/dist/algorithms/{server → ot/server}/getSnapshotAtRevision.js +1 -1
- package/dist/algorithms/{server → ot/server}/getStateAtRevision.d.ts +5 -5
- package/dist/algorithms/{server → ot/server}/getStateAtRevision.js +1 -1
- package/dist/algorithms/{server → ot/server}/handleOfflineSessionsAndBatches.d.ts +5 -5
- package/dist/algorithms/{server → ot/server}/handleOfflineSessionsAndBatches.js +3 -3
- package/dist/algorithms/{server → ot/server}/transformIncomingChanges.d.ts +3 -3
- package/dist/algorithms/{server → ot/server}/transformIncomingChanges.js +3 -3
- package/dist/algorithms/{shared → ot/shared}/applyChanges.d.ts +3 -3
- package/dist/algorithms/{shared → ot/shared}/applyChanges.js +2 -2
- package/dist/algorithms/{shared → ot/shared}/changeBatching.d.ts +3 -3
- package/dist/algorithms/{shared → ot/shared}/changeBatching.js +2 -2
- package/dist/algorithms/{shared → ot/shared}/rebaseChanges.d.ts +3 -3
- package/dist/algorithms/{shared → ot/shared}/rebaseChanges.js +2 -2
- package/dist/client/BaseDoc.d.ts +6 -0
- package/dist/client/BaseDoc.js +70 -0
- package/dist/client/ClientAlgorithm.d.ts +101 -0
- package/dist/client/ClientAlgorithm.js +0 -0
- package/dist/client/InMemoryStore.d.ts +5 -7
- package/dist/client/InMemoryStore.js +7 -36
- package/dist/client/IndexedDBStore.d.ts +39 -73
- package/dist/client/IndexedDBStore.js +17 -220
- package/dist/client/LWWAlgorithm.d.ts +43 -0
- package/dist/client/LWWAlgorithm.js +87 -0
- package/dist/client/LWWClientStore.d.ts +73 -0
- package/dist/client/LWWClientStore.js +0 -0
- package/dist/client/LWWDoc.d.ts +56 -0
- package/dist/client/LWWDoc.js +84 -0
- package/dist/client/LWWInMemoryStore.d.ts +88 -0
- package/dist/client/LWWInMemoryStore.js +208 -0
- package/dist/client/LWWIndexedDBStore.d.ts +91 -0
- package/dist/client/LWWIndexedDBStore.js +275 -0
- package/dist/client/OTAlgorithm.d.ts +42 -0
- package/dist/client/OTAlgorithm.js +113 -0
- package/dist/client/OTClientStore.d.ts +50 -0
- package/dist/client/OTClientStore.js +0 -0
- package/dist/client/OTDoc.d.ts +6 -0
- package/dist/client/OTDoc.js +97 -0
- package/dist/client/OTIndexedDBStore.d.ts +84 -0
- package/dist/client/OTIndexedDBStore.js +163 -0
- package/dist/client/Patches.d.ts +36 -16
- package/dist/client/Patches.js +60 -27
- package/dist/client/PatchesDoc.d.ts +4 -113
- package/dist/client/PatchesDoc.js +3 -153
- package/dist/client/PatchesHistoryClient.js +1 -1
- package/dist/client/PatchesStore.d.ts +8 -105
- package/dist/client/factories.d.ts +72 -0
- package/dist/client/factories.js +80 -0
- package/dist/client/index.d.ts +14 -5
- package/dist/client/index.js +9 -0
- package/dist/compression/index.d.ts +2 -2
- package/dist/compression/index.js +1 -1
- package/dist/{algorithms/shared → compression}/lz.js +1 -1
- package/dist/data/change.js +2 -0
- package/dist/fractionalIndex.d.ts +67 -0
- package/dist/fractionalIndex.js +241 -0
- package/dist/index.d.ts +13 -3
- package/dist/index.js +1 -0
- package/dist/json-patch/types.d.ts +2 -0
- package/dist/net/PatchesClient.js +15 -15
- package/dist/net/PatchesSync.d.ts +20 -8
- package/dist/net/PatchesSync.js +57 -65
- package/dist/net/index.d.ts +7 -11
- package/dist/net/index.js +6 -1
- package/dist/net/protocol/JSONRPCClient.d.ts +4 -4
- package/dist/net/protocol/JSONRPCClient.js +6 -4
- package/dist/net/protocol/JSONRPCServer.d.ts +45 -9
- package/dist/net/protocol/JSONRPCServer.js +63 -8
- package/dist/net/serverContext.d.ts +38 -0
- package/dist/net/serverContext.js +20 -0
- package/dist/net/webrtc/WebRTCTransport.js +1 -1
- package/dist/net/websocket/AuthorizationProvider.d.ts +3 -3
- package/dist/net/websocket/WebSocketServer.d.ts +29 -20
- package/dist/net/websocket/WebSocketServer.js +23 -12
- package/dist/server/BranchManager.d.ts +50 -0
- package/dist/server/BranchManager.js +0 -0
- package/dist/server/CompressedStoreBackend.d.ts +10 -8
- package/dist/server/CompressedStoreBackend.js +7 -13
- package/dist/server/LWWBranchManager.d.ts +82 -0
- package/dist/server/LWWBranchManager.js +99 -0
- package/dist/server/LWWMemoryStoreBackend.d.ts +78 -0
- package/dist/server/LWWMemoryStoreBackend.js +182 -0
- package/dist/server/LWWServer.d.ts +130 -0
- package/dist/server/LWWServer.js +214 -0
- package/dist/server/{PatchesBranchManager.d.ts → OTBranchManager.d.ts} +32 -12
- package/dist/server/{PatchesBranchManager.js → OTBranchManager.js} +27 -42
- package/dist/server/OTServer.d.ts +108 -0
- package/dist/server/OTServer.js +141 -0
- package/dist/server/PatchesHistoryManager.d.ts +21 -16
- package/dist/server/PatchesHistoryManager.js +23 -11
- package/dist/server/PatchesServer.d.ts +70 -81
- package/dist/server/PatchesServer.js +0 -175
- package/dist/server/branchUtils.d.ts +82 -0
- package/dist/server/branchUtils.js +66 -0
- package/dist/server/index.d.ts +18 -7
- package/dist/server/index.js +33 -4
- package/dist/server/tombstone.d.ts +29 -0
- package/dist/server/tombstone.js +32 -0
- package/dist/server/types.d.ts +109 -27
- package/dist/server/utils.d.ts +12 -0
- package/dist/server/utils.js +23 -0
- package/dist/solid/context.d.ts +4 -3
- package/dist/solid/doc-manager.d.ts +3 -3
- package/dist/solid/index.d.ts +4 -3
- package/dist/solid/primitives.d.ts +2 -3
- package/dist/types.d.ts +4 -2
- package/dist/vue/composables.d.ts +2 -3
- package/dist/vue/doc-manager.d.ts +3 -3
- package/dist/vue/index.d.ts +4 -3
- package/dist/vue/provider.d.ts +4 -3
- package/package.json +1 -1
- package/dist/algorithms/client/collapsePendingChanges.d.ts +0 -30
- package/dist/algorithms/client/collapsePendingChanges.js +0 -78
- package/dist/algorithms/client/makeChange.d.ts +0 -9
- package/dist/algorithms/client/makeChange.js +0 -29
- package/dist/algorithms/server/commitChanges.d.ts +0 -19
- package/dist/net/websocket/RPCServer.d.ts +0 -141
- package/dist/net/websocket/RPCServer.js +0 -204
- /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,
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
*
|
|
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
|
|
123
|
+
* @returns The last committed revision, or 0 if not found.
|
|
305
124
|
*/
|
|
306
|
-
async
|
|
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?.
|
|
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
|
+
};
|