@dabble/patches 0.3.2 → 0.4.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 (39) hide show
  1. package/dist/algorithms/client/makeChange.d.ts +2 -3
  2. package/dist/algorithms/client/makeChange.js +1 -1
  3. package/dist/algorithms/server/getSnapshotAtRevision.d.ts +1 -1
  4. package/dist/algorithms/server/getStateAtRevision.d.ts +1 -1
  5. package/dist/algorithms/server/handleOfflineSessionsAndBatches.d.ts +1 -1
  6. package/dist/client/InMemoryStore.js +1 -3
  7. package/dist/client/IndexedDBStore.js +345 -342
  8. package/dist/client/Patches.js +156 -156
  9. package/dist/client/PatchesDoc.d.ts +4 -5
  10. package/dist/client/PatchesDoc.js +16 -13
  11. package/dist/client/PatchesHistoryClient.js +12 -8
  12. package/dist/json-patch/JSONPatch.js +2 -0
  13. package/dist/json-patch/createJSONPatch.d.ts +15 -18
  14. package/dist/json-patch/createJSONPatch.js +18 -20
  15. package/dist/json-patch/pathProxy.d.ts +22 -0
  16. package/dist/json-patch/pathProxy.js +50 -0
  17. package/dist/json-patch/utils/getType.d.ts +1 -1
  18. package/dist/net/PatchesSync.js +307 -303
  19. package/dist/net/error.js +1 -0
  20. package/dist/net/protocol/JSONRPCClient.js +4 -3
  21. package/dist/net/protocol/JSONRPCServer.js +6 -8
  22. package/dist/net/webrtc/WebRTCAwareness.js +12 -7
  23. package/dist/net/webrtc/WebRTCTransport.d.ts +1 -1
  24. package/dist/net/webrtc/WebRTCTransport.js +27 -21
  25. package/dist/net/websocket/PatchesWebSocket.js +7 -2
  26. package/dist/net/websocket/RPCServer.js +5 -0
  27. package/dist/net/websocket/SignalingService.js +1 -3
  28. package/dist/net/websocket/WebSocketServer.js +2 -0
  29. package/dist/net/websocket/WebSocketTransport.js +21 -19
  30. package/dist/net/websocket/onlineState.js +2 -2
  31. package/dist/server/PatchesBranchManager.js +2 -0
  32. package/dist/server/PatchesHistoryManager.js +2 -0
  33. package/dist/server/PatchesServer.d.ts +2 -2
  34. package/dist/server/PatchesServer.js +7 -5
  35. package/dist/types.d.ts +15 -6
  36. package/dist/types.js +1 -1
  37. package/package.json +3 -2
  38. package/dist/json-patch/patchProxy.d.ts +0 -42
  39. package/dist/json-patch/patchProxy.js +0 -126
@@ -51,7 +51,6 @@ const SNAPSHOT_INTERVAL = 200;
51
51
  * A snapshot will not be created if there are pending changes based on revisions older than the 200th committed change until those pending changes are committed.
52
52
  */
53
53
  let IndexedDBStore = (() => {
54
- var _a;
55
54
  let _instanceExtraInitializers = [];
56
55
  let _getDoc_decorators;
57
56
  let _deleteDoc_decorators;
@@ -62,369 +61,372 @@ let IndexedDBStore = (() => {
62
61
  let _replacePendingChanges_decorators;
63
62
  let _saveCommittedChanges_decorators;
64
63
  let _getLastRevs_decorators;
65
- return _a = class IndexedDBStore {
66
- constructor(dbName) {
67
- this.db = (__runInitializers(this, _instanceExtraInitializers), null);
68
- this.dbName = dbName;
69
- this.dbPromise = deferred();
70
- if (this.dbName) {
71
- this.initDB();
72
- }
73
- }
74
- async initDB() {
75
- if (!this.dbName)
76
- return;
77
- const request = indexedDB.open(this.dbName, DB_VERSION);
78
- request.onerror = () => this.dbPromise.reject(request.error);
79
- request.onsuccess = () => {
80
- this.db = request.result;
81
- this.dbPromise.resolve(this.db);
82
- };
83
- request.onupgradeneeded = event => {
84
- const db = event.target.result;
85
- // Create stores
86
- if (!db.objectStoreNames.contains('snapshots')) {
87
- db.createObjectStore('snapshots', { keyPath: 'docId' });
88
- }
89
- if (!db.objectStoreNames.contains('committedChanges')) {
90
- db.createObjectStore('committedChanges', { keyPath: ['docId', 'rev'] });
91
- }
92
- if (!db.objectStoreNames.contains('pendingChanges')) {
93
- db.createObjectStore('pendingChanges', { keyPath: ['docId', 'rev'] });
94
- }
95
- if (!db.objectStoreNames.contains('docs')) {
96
- db.createObjectStore('docs', { keyPath: 'docId' });
97
- }
98
- };
99
- }
100
- getDB() {
101
- return this.dbPromise.promise;
102
- }
103
- /**
104
- * Set the name of the database, loads a new database connection.
105
- * @param dbName - The new name of the database.
106
- */
107
- setName(dbName) {
108
- this.dbName = dbName;
109
- if (this.db) {
110
- this.db.close();
111
- this.db = null;
112
- this.dbPromise = deferred();
113
- }
64
+ return class IndexedDBStore {
65
+ static {
66
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
67
+ _getDoc_decorators = [blockable];
68
+ _deleteDoc_decorators = [blockable];
69
+ _confirmDeleteDoc_decorators = [blockable];
70
+ _saveDoc_decorators = [blockable];
71
+ _savePendingChanges_decorators = [blockable];
72
+ _getPendingChanges_decorators = [blockable];
73
+ _replacePendingChanges_decorators = [blockable];
74
+ _saveCommittedChanges_decorators = [blockable];
75
+ _getLastRevs_decorators = [blockable];
76
+ __esDecorate(this, null, _getDoc_decorators, { kind: "method", name: "getDoc", static: false, private: false, access: { has: obj => "getDoc" in obj, get: obj => obj.getDoc }, metadata: _metadata }, null, _instanceExtraInitializers);
77
+ __esDecorate(this, null, _deleteDoc_decorators, { kind: "method", name: "deleteDoc", static: false, private: false, access: { has: obj => "deleteDoc" in obj, get: obj => obj.deleteDoc }, metadata: _metadata }, null, _instanceExtraInitializers);
78
+ __esDecorate(this, null, _confirmDeleteDoc_decorators, { kind: "method", name: "confirmDeleteDoc", static: false, private: false, access: { has: obj => "confirmDeleteDoc" in obj, get: obj => obj.confirmDeleteDoc }, metadata: _metadata }, null, _instanceExtraInitializers);
79
+ __esDecorate(this, null, _saveDoc_decorators, { kind: "method", name: "saveDoc", static: false, private: false, access: { has: obj => "saveDoc" in obj, get: obj => obj.saveDoc }, metadata: _metadata }, null, _instanceExtraInitializers);
80
+ __esDecorate(this, null, _savePendingChanges_decorators, { kind: "method", name: "savePendingChanges", static: false, private: false, access: { has: obj => "savePendingChanges" in obj, get: obj => obj.savePendingChanges }, metadata: _metadata }, null, _instanceExtraInitializers);
81
+ __esDecorate(this, null, _getPendingChanges_decorators, { kind: "method", name: "getPendingChanges", static: false, private: false, access: { has: obj => "getPendingChanges" in obj, get: obj => obj.getPendingChanges }, metadata: _metadata }, null, _instanceExtraInitializers);
82
+ __esDecorate(this, null, _replacePendingChanges_decorators, { kind: "method", name: "replacePendingChanges", static: false, private: false, access: { has: obj => "replacePendingChanges" in obj, get: obj => obj.replacePendingChanges }, metadata: _metadata }, null, _instanceExtraInitializers);
83
+ __esDecorate(this, null, _saveCommittedChanges_decorators, { kind: "method", name: "saveCommittedChanges", static: false, private: false, access: { has: obj => "saveCommittedChanges" in obj, get: obj => obj.saveCommittedChanges }, metadata: _metadata }, null, _instanceExtraInitializers);
84
+ __esDecorate(this, null, _getLastRevs_decorators, { kind: "method", name: "getLastRevs", static: false, private: false, access: { has: obj => "getLastRevs" in obj, get: obj => obj.getLastRevs }, metadata: _metadata }, null, _instanceExtraInitializers);
85
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
86
+ }
87
+ db = (__runInitializers(this, _instanceExtraInitializers), null);
88
+ dbName;
89
+ dbPromise;
90
+ constructor(dbName) {
91
+ this.dbName = dbName;
92
+ this.dbPromise = deferred();
93
+ if (this.dbName) {
114
94
  this.initDB();
115
95
  }
116
- /**
117
- * Closes the database connection. After calling this method, the store
118
- * will no longer be usable. A new instance must be created to reopen
119
- * the database.
120
- */
121
- async close() {
122
- await this.dbPromise.promise;
123
- if (this.db) {
124
- this.db.close();
125
- this.db = null;
126
- this.dbPromise = deferred();
127
- this.dbPromise.resolve(null);
96
+ }
97
+ async initDB() {
98
+ if (!this.dbName)
99
+ return;
100
+ const request = indexedDB.open(this.dbName, DB_VERSION);
101
+ request.onerror = () => this.dbPromise.reject(request.error);
102
+ request.onsuccess = () => {
103
+ this.db = request.result;
104
+ this.dbPromise.resolve(this.db);
105
+ };
106
+ request.onupgradeneeded = event => {
107
+ const db = event.target.result;
108
+ // Create stores
109
+ if (!db.objectStoreNames.contains('snapshots')) {
110
+ db.createObjectStore('snapshots', { keyPath: 'docId' });
128
111
  }
129
- }
130
- async deleteDB() {
131
- if (!this.dbName)
132
- return;
133
- await this.close();
134
- await new Promise((resolve, reject) => {
135
- const request = indexedDB.deleteDatabase(this.dbName);
136
- request.onsuccess = () => resolve();
137
- request.onerror = () => reject(request.error);
138
- request.onblocked = () => reject(request.error);
139
- });
140
- }
141
- async transaction(storeNames, mode) {
142
- const db = await this.getDB();
143
- const tx = new IDBTransactionWrapper(db.transaction(storeNames, mode));
144
- const stores = storeNames.map(name => tx.getStore(name));
145
- return [tx, ...stores];
146
- }
147
- /**
148
- * Rebuilds a document snapshot + pending queue *without* loading
149
- * the full PatchesDoc into memory.
150
- *
151
- * 1. load the last snapshot (state + rev)
152
- * 2. load committedChanges[rev > snapshot.rev]
153
- * 3. load pendingChanges
154
- * 4. apply committed changes, rebase pending
155
- * 5. return { state, rev, changes: pending }
156
- */
157
- async getDoc(docId) {
158
- const [tx, docsStore, snapshots, committedChanges, pendingChanges] = await this.transaction(['docs', 'snapshots', 'committedChanges', 'pendingChanges'], 'readonly');
159
- const docMeta = await docsStore.get(docId);
160
- if (docMeta?.deleted) {
161
- await tx.complete();
162
- return undefined;
112
+ if (!db.objectStoreNames.contains('committedChanges')) {
113
+ db.createObjectStore('committedChanges', { keyPath: ['docId', 'rev'] });
163
114
  }
164
- const snapshot = await snapshots.get(docId);
165
- const committed = await committedChanges.getAll([docId, snapshot?.rev ?? 0], [docId, Infinity]);
166
- const pending = await pendingChanges.getAll([docId, 0], [docId, Infinity]);
167
- if (!snapshot && !committed.length && !pending.length)
168
- return undefined;
169
- // Apply any committed changes to the snapshot state
170
- const state = applyChanges(snapshot?.state, committed);
171
- // Rebase pending changes if there are any committed changes received since their baseRev
172
- const lastCommitted = committed[committed.length - 1];
173
- const baseRev = pending[0]?.baseRev;
174
- if (lastCommitted && baseRev && baseRev < lastCommitted.rev) {
175
- const patch = committed
176
- .filter(change => change.rev > baseRev)
177
- .map(change => change.ops)
178
- .flat();
179
- const offset = lastCommitted.rev - baseRev;
180
- pending.forEach(change => {
181
- change.rev += offset;
182
- change.ops = transformPatch(state, patch, change.ops);
183
- });
115
+ if (!db.objectStoreNames.contains('pendingChanges')) {
116
+ db.createObjectStore('pendingChanges', { keyPath: ['docId', 'rev'] });
184
117
  }
185
- await tx.complete();
186
- return {
187
- state,
188
- rev: committed[committed.length - 1]?.rev ?? snapshot?.rev ?? 0,
189
- changes: pending,
190
- };
118
+ if (!db.objectStoreNames.contains('docs')) {
119
+ db.createObjectStore('docs', { keyPath: 'docId' });
120
+ }
121
+ };
122
+ }
123
+ getDB() {
124
+ return this.dbPromise.promise;
125
+ }
126
+ /**
127
+ * Set the name of the database, loads a new database connection.
128
+ * @param dbName - The new name of the database.
129
+ */
130
+ setName(dbName) {
131
+ this.dbName = dbName;
132
+ if (this.db) {
133
+ this.db.close();
134
+ this.db = null;
135
+ this.dbPromise = deferred();
191
136
  }
192
- /**
193
- * Completely remove all data for this docId and mark it as deleted (tombstone).
194
- */
195
- async deleteDoc(docId) {
196
- const [tx, snapshots, committedChanges, pendingChanges, docsStore] = await this.transaction(['snapshots', 'committedChanges', 'pendingChanges', 'docs'], 'readwrite');
197
- const docMeta = (await docsStore.get(docId)) ?? { docId, committedRev: 0 };
198
- await docsStore.put({ ...docMeta, deleted: true });
199
- await Promise.all([
200
- snapshots.delete(docId),
201
- committedChanges.delete([docId, 0], [docId, Infinity]),
202
- pendingChanges.delete([docId, 0], [docId, Infinity]),
203
- ]);
204
- await tx.complete();
137
+ this.initDB();
138
+ }
139
+ /**
140
+ * Closes the database connection. After calling this method, the store
141
+ * will no longer be usable. A new instance must be created to reopen
142
+ * the database.
143
+ */
144
+ async close() {
145
+ await this.dbPromise.promise;
146
+ if (this.db) {
147
+ this.db.close();
148
+ this.db = null;
149
+ this.dbPromise = deferred();
150
+ this.dbPromise.resolve(null);
205
151
  }
206
- /**
207
- * Confirm the deletion of a document.
208
- * @param docId - The ID of the document to delete.
209
- */
210
- async confirmDeleteDoc(docId) {
211
- const [tx, docsStore] = await this.transaction(['docs'], 'readwrite');
212
- await docsStore.delete(docId);
152
+ }
153
+ async deleteDB() {
154
+ if (!this.dbName)
155
+ return;
156
+ await this.close();
157
+ await new Promise((resolve, reject) => {
158
+ const request = indexedDB.deleteDatabase(this.dbName);
159
+ request.onsuccess = () => resolve();
160
+ request.onerror = () => reject(request.error);
161
+ request.onblocked = () => reject(request.error);
162
+ });
163
+ }
164
+ async transaction(storeNames, mode) {
165
+ const db = await this.getDB();
166
+ const tx = new IDBTransactionWrapper(db.transaction(storeNames, mode));
167
+ const stores = storeNames.map(name => tx.getStore(name));
168
+ return [tx, ...stores];
169
+ }
170
+ /**
171
+ * Rebuilds a document snapshot + pending queue *without* loading
172
+ * the full PatchesDoc into memory.
173
+ *
174
+ * 1. load the last snapshot (state + rev)
175
+ * 2. load committedChanges[rev > snapshot.rev]
176
+ * 3. load pendingChanges
177
+ * 4. apply committed changes, rebase pending
178
+ * 5. return { state, rev, changes: pending }
179
+ */
180
+ async getDoc(docId) {
181
+ const [tx, docsStore, snapshots, committedChanges, pendingChanges] = await this.transaction(['docs', 'snapshots', 'committedChanges', 'pendingChanges'], 'readonly');
182
+ const docMeta = await docsStore.get(docId);
183
+ if (docMeta?.deleted) {
213
184
  await tx.complete();
185
+ return undefined;
214
186
  }
215
- /**
216
- * Save a document's state to the store.
217
- * @param docId - The ID of the document to save.
218
- * @param docState - The state of the document to save.
219
- */
220
- async saveDoc(docId, docState) {
221
- const [tx, snapshots, committedChanges, pendingChanges, docsStore] = await this.transaction(['snapshots', 'committedChanges', 'pendingChanges', 'docs'], 'readwrite');
222
- const { rev, state } = docState;
223
- await Promise.all([
224
- docsStore.put({ docId, committedRev: rev }),
225
- snapshots.put({ docId, state, rev }),
226
- committedChanges.delete([docId, 0], [docId, Infinity]),
227
- pendingChanges.delete([docId, 0], [docId, Infinity]),
228
- ]);
229
- await tx.complete();
187
+ const snapshot = await snapshots.get(docId);
188
+ const committed = await committedChanges.getAll([docId, snapshot?.rev ?? 0], [docId, Infinity]);
189
+ const pending = await pendingChanges.getAll([docId, 0], [docId, Infinity]);
190
+ if (!snapshot && !committed.length && !pending.length)
191
+ return undefined;
192
+ // Apply any committed changes to the snapshot state
193
+ const state = applyChanges(snapshot?.state, committed);
194
+ // Rebase pending changes if there are any committed changes received since their baseRev
195
+ const lastCommitted = committed[committed.length - 1];
196
+ const baseRev = pending[0]?.baseRev;
197
+ if (lastCommitted && baseRev && baseRev < lastCommitted.rev) {
198
+ const patch = committed
199
+ .filter(change => change.rev > baseRev)
200
+ .map(change => change.ops)
201
+ .flat();
202
+ const offset = lastCommitted.rev - baseRev;
203
+ pending.forEach(change => {
204
+ change.rev += offset;
205
+ change.ops = transformPatch(state, patch, change.ops);
206
+ });
230
207
  }
231
- /**
232
- * Append an array of local changes to the pending queue.
233
- * Called *before* you attempt to send them to the server.
234
- */
235
- async savePendingChanges(docId, changes) {
236
- const [tx, pendingChanges, docsStore] = await this.transaction(['pendingChanges', 'docs'], 'readwrite');
237
- let docMeta = await docsStore.get(docId);
238
- if (!docMeta) {
239
- docMeta = { docId, committedRev: 0 };
240
- await docsStore.put(docMeta);
241
- }
242
- else if (docMeta.deleted) {
243
- delete docMeta.deleted;
244
- await docsStore.put(docMeta);
245
- console.warn(`Revived document ${docId} by saving pending changes.`);
246
- }
247
- await Promise.all(changes.map(change => pendingChanges.put({ ...change, docId })));
248
- await tx.complete();
208
+ await tx.complete();
209
+ return {
210
+ state,
211
+ rev: committed[committed.length - 1]?.rev ?? snapshot?.rev ?? 0,
212
+ changes: pending,
213
+ };
214
+ }
215
+ /**
216
+ * Completely remove all data for this docId and mark it as deleted (tombstone).
217
+ */
218
+ async deleteDoc(docId) {
219
+ const [tx, snapshots, committedChanges, pendingChanges, docsStore] = await this.transaction(['snapshots', 'committedChanges', 'pendingChanges', 'docs'], 'readwrite');
220
+ const docMeta = (await docsStore.get(docId)) ?? { docId, committedRev: 0 };
221
+ await docsStore.put({ ...docMeta, deleted: true });
222
+ await Promise.all([
223
+ snapshots.delete(docId),
224
+ committedChanges.delete([docId, 0], [docId, Infinity]),
225
+ pendingChanges.delete([docId, 0], [docId, Infinity]),
226
+ ]);
227
+ await tx.complete();
228
+ }
229
+ /**
230
+ * Confirm the deletion of a document.
231
+ * @param docId - The ID of the document to delete.
232
+ */
233
+ async confirmDeleteDoc(docId) {
234
+ const [tx, docsStore] = await this.transaction(['docs'], 'readwrite');
235
+ await docsStore.delete(docId);
236
+ await tx.complete();
237
+ }
238
+ /**
239
+ * Save a document's state to the store.
240
+ * @param docId - The ID of the document to save.
241
+ * @param docState - The state of the document to save.
242
+ */
243
+ async saveDoc(docId, docState) {
244
+ const [tx, snapshots, committedChanges, pendingChanges, docsStore] = await this.transaction(['snapshots', 'committedChanges', 'pendingChanges', 'docs'], 'readwrite');
245
+ const { rev, state } = docState;
246
+ await Promise.all([
247
+ docsStore.put({ docId, committedRev: rev }),
248
+ snapshots.put({ docId, state, rev }),
249
+ committedChanges.delete([docId, 0], [docId, Infinity]),
250
+ pendingChanges.delete([docId, 0], [docId, Infinity]),
251
+ ]);
252
+ await tx.complete();
253
+ }
254
+ /**
255
+ * Append an array of local changes to the pending queue.
256
+ * Called *before* you attempt to send them to the server.
257
+ */
258
+ async savePendingChanges(docId, changes) {
259
+ const [tx, pendingChanges, docsStore] = await this.transaction(['pendingChanges', 'docs'], 'readwrite');
260
+ let docMeta = await docsStore.get(docId);
261
+ if (!docMeta) {
262
+ docMeta = { docId, committedRev: 0 };
263
+ await docsStore.put(docMeta);
249
264
  }
250
- /**
251
- * Read back all pending changes for this docId (in order).
252
- * @param docId - The ID of the document to get the pending changes for.
253
- * @returns The pending changes.
254
- */
255
- async getPendingChanges(docId) {
256
- const [tx, pendingChanges] = await this.transaction(['pendingChanges'], 'readonly');
257
- const result = await pendingChanges.getAll([docId, 0], [docId, Infinity]);
258
- await tx.complete();
259
- return result;
265
+ else if (docMeta.deleted) {
266
+ delete docMeta.deleted;
267
+ await docsStore.put(docMeta);
268
+ console.warn(`Revived document ${docId} by saving pending changes.`);
260
269
  }
261
- /**
262
- * Replace all pending changes for a document (used after rebasing).
263
- * @param docId - The ID of the document to replace the pending changes for.
264
- * @param changes - The changes to replace the pending changes with.
265
- */
266
- async replacePendingChanges(docId, changes) {
267
- const [tx, pendingChanges, docsStore] = await this.transaction(['pendingChanges', 'docs'], 'readwrite');
268
- // Ensure the document is tracked
269
- let docMeta = await docsStore.get(docId);
270
- if (!docMeta) {
271
- docMeta = { docId, committedRev: 0 };
272
- await docsStore.put(docMeta);
273
- }
274
- else if (docMeta.deleted) {
275
- delete docMeta.deleted;
276
- await docsStore.put(docMeta);
277
- console.warn(`Revived document ${docId} by replacing pending changes.`);
278
- }
279
- // Remove all existing pending changes and add the new ones
280
- await pendingChanges.delete([docId, 0], [docId, Infinity]);
281
- await Promise.all(changes.map(change => pendingChanges.put({ ...change, docId })));
282
- await tx.complete();
270
+ await Promise.all(changes.map(change => pendingChanges.put({ ...change, docId })));
271
+ await tx.complete();
272
+ }
273
+ /**
274
+ * Read back all pending changes for this docId (in order).
275
+ * @param docId - The ID of the document to get the pending changes for.
276
+ * @returns The pending changes.
277
+ */
278
+ async getPendingChanges(docId) {
279
+ const [tx, pendingChanges] = await this.transaction(['pendingChanges'], 'readonly');
280
+ const result = await pendingChanges.getAll([docId, 0], [docId, Infinity]);
281
+ await tx.complete();
282
+ return result;
283
+ }
284
+ /**
285
+ * Replace all pending changes for a document (used after rebasing).
286
+ * @param docId - The ID of the document to replace the pending changes for.
287
+ * @param changes - The changes to replace the pending changes with.
288
+ */
289
+ async replacePendingChanges(docId, changes) {
290
+ const [tx, pendingChanges, docsStore] = await this.transaction(['pendingChanges', 'docs'], 'readwrite');
291
+ // Ensure the document is tracked
292
+ let docMeta = await docsStore.get(docId);
293
+ if (!docMeta) {
294
+ docMeta = { docId, committedRev: 0 };
295
+ await docsStore.put(docMeta);
283
296
  }
284
- // ─── Committed Changes ─────────────────────────────────────────────────────
285
- /**
286
- * Store server‐confirmed changes. Will:
287
- * - persist them in the committedChanges store
288
- * - remove any pending changes whose rev falls within `sentPendingRange`
289
- * - optionally compact a new snapshot after N changes (hidden internally)
290
- * @param docId - The ID of the document to save the changes for
291
- * @param changes - The changes to save
292
- * @param sentPendingRange - The range of pending changes to remove, *must* be provided after receiving the changes
293
- * from the server in response to a patchesDoc request.
294
- */
295
- async saveCommittedChanges(docId, changes, sentPendingRange) {
296
- const [tx, committedChanges, pendingChanges, snapshots, docsStore] = await this.transaction(['committedChanges', 'pendingChanges', 'snapshots', 'docs'], 'readwrite');
297
- // Save committed changes
298
- await Promise.all(changes.map(change => committedChanges.put({ ...change, docId })));
299
- // Remove pending changes if range provided
300
- if (sentPendingRange) {
301
- await pendingChanges.delete([docId, sentPendingRange[0]], [docId, sentPendingRange[1]]);
302
- }
303
- // Check if we should create a snapshot
304
- const count = await committedChanges.count([docId, 0], [docId, Infinity]);
305
- if (count >= SNAPSHOT_INTERVAL) {
306
- // Update the snapshot. A snapshot will not be updated if there are pending changes based on revisions older than
307
- // the latest committed change until those pending changes are committed.
308
- const [snapshot, committed, firstPending] = await Promise.all([
309
- snapshots.get(docId),
310
- committedChanges.getAll([docId, 0], [docId, Infinity], SNAPSHOT_INTERVAL),
311
- pendingChanges.getFirstFromCursor([docId, 0], [docId, Infinity]),
297
+ else if (docMeta.deleted) {
298
+ delete docMeta.deleted;
299
+ await docsStore.put(docMeta);
300
+ console.warn(`Revived document ${docId} by replacing pending changes.`);
301
+ }
302
+ // Remove all existing pending changes and add the new ones
303
+ await pendingChanges.delete([docId, 0], [docId, Infinity]);
304
+ await Promise.all(changes.map(change => pendingChanges.put({ ...change, docId })));
305
+ await tx.complete();
306
+ }
307
+ // ─── Committed Changes ─────────────────────────────────────────────────────
308
+ /**
309
+ * Store server‐confirmed changes. Will:
310
+ * - persist them in the committedChanges store
311
+ * - remove any pending changes whose rev falls within `sentPendingRange`
312
+ * - optionally compact a new snapshot after N changes (hidden internally)
313
+ * @param docId - The ID of the document to save the changes for
314
+ * @param changes - The changes to save
315
+ * @param sentPendingRange - The range of pending changes to remove, *must* be provided after receiving the changes
316
+ * from the server in response to a patchesDoc request.
317
+ */
318
+ async saveCommittedChanges(docId, changes, sentPendingRange) {
319
+ const [tx, committedChanges, pendingChanges, snapshots, docsStore] = await this.transaction(['committedChanges', 'pendingChanges', 'snapshots', 'docs'], 'readwrite');
320
+ // Save committed changes
321
+ await Promise.all(changes.map(change => committedChanges.put({ ...change, docId })));
322
+ // Remove pending changes if range provided
323
+ if (sentPendingRange) {
324
+ await pendingChanges.delete([docId, sentPendingRange[0]], [docId, sentPendingRange[1]]);
325
+ }
326
+ // Check if we should create a snapshot
327
+ const count = await committedChanges.count([docId, 0], [docId, Infinity]);
328
+ if (count >= SNAPSHOT_INTERVAL) {
329
+ // Update the snapshot. A snapshot will not be updated if there are pending changes based on revisions older than
330
+ // the latest committed change until those pending changes are committed.
331
+ const [snapshot, committed, firstPending] = await Promise.all([
332
+ snapshots.get(docId),
333
+ committedChanges.getAll([docId, 0], [docId, Infinity], SNAPSHOT_INTERVAL),
334
+ pendingChanges.getFirstFromCursor([docId, 0], [docId, Infinity]),
335
+ ]);
336
+ // Update the snapshot
337
+ const lastRev = committed[committed.length - 1]?.rev;
338
+ if (!firstPending?.baseRev || firstPending?.baseRev >= lastRev) {
339
+ const state = applyChanges(snapshot?.state, committed);
340
+ await Promise.all([
341
+ snapshots.put({
342
+ docId,
343
+ rev: lastRev,
344
+ state,
345
+ }),
346
+ committedChanges.delete([docId, 0], [docId, lastRev]),
312
347
  ]);
313
- // Update the snapshot
314
- const lastRev = committed[committed.length - 1]?.rev;
315
- if (!firstPending?.baseRev || firstPending?.baseRev >= lastRev) {
316
- const state = applyChanges(snapshot?.state, committed);
317
- await Promise.all([
318
- snapshots.put({
319
- docId,
320
- rev: lastRev,
321
- state,
322
- }),
323
- committedChanges.delete([docId, 0], [docId, lastRev]),
324
- ]);
325
- }
326
348
  }
327
- // Update committedRev in the docs store if changes were saved
328
- const lastCommittedRev = changes.at(-1)?.rev;
329
- if (lastCommittedRev !== undefined) {
330
- const docMeta = (await docsStore.get(docId)) ?? { docId, committedRev: 0 };
331
- if (lastCommittedRev > docMeta.committedRev) {
332
- await docsStore.put({ ...docMeta, committedRev: lastCommittedRev, deleted: undefined });
333
- }
334
- }
335
- await tx.complete();
336
349
  }
337
- /**
338
- * List all documents in the store.
339
- * @param includeDeleted - Whether to include deleted documents.
340
- * @returns The list of documents.
341
- */
342
- async listDocs(includeDeleted = false) {
343
- const [tx, docsStore] = await this.transaction(['docs'], 'readonly');
344
- const allDocs = await docsStore.getAll();
345
- await tx.complete();
346
- return includeDeleted ? allDocs : allDocs.filter(doc => !doc.deleted);
350
+ // Update committedRev in the docs store if changes were saved
351
+ const lastCommittedRev = changes.at(-1)?.rev;
352
+ if (lastCommittedRev !== undefined) {
353
+ const docMeta = (await docsStore.get(docId)) ?? { docId, committedRev: 0 };
354
+ if (lastCommittedRev > docMeta.committedRev) {
355
+ await docsStore.put({ ...docMeta, committedRev: lastCommittedRev, deleted: undefined });
356
+ }
347
357
  }
348
- /**
349
- * Track a document.
350
- * @param docIds - The IDs of the documents to track.
351
- */
352
- async trackDocs(docIds) {
353
- const [tx, docsStore] = await this.transaction(['docs'], 'readwrite');
354
- await Promise.all(docIds.map(async (docId) => {
355
- const existing = await docsStore.get(docId);
356
- if (existing) {
357
- // If exists but deleted, undelete it
358
- if (existing.deleted) {
359
- await docsStore.put({ ...existing, deleted: undefined });
360
- }
361
- // Otherwise, it's already tracked and not deleted, do nothing
362
- }
363
- else {
364
- // If doesn't exist, add it
365
- await docsStore.put({ docId, committedRev: 0 });
358
+ await tx.complete();
359
+ }
360
+ /**
361
+ * List all documents in the store.
362
+ * @param includeDeleted - Whether to include deleted documents.
363
+ * @returns The list of documents.
364
+ */
365
+ async listDocs(includeDeleted = false) {
366
+ const [tx, docsStore] = await this.transaction(['docs'], 'readonly');
367
+ const allDocs = await docsStore.getAll();
368
+ await tx.complete();
369
+ return includeDeleted ? allDocs : allDocs.filter(doc => !doc.deleted);
370
+ }
371
+ /**
372
+ * Track a document.
373
+ * @param docIds - The IDs of the documents to track.
374
+ */
375
+ async trackDocs(docIds) {
376
+ const [tx, docsStore] = await this.transaction(['docs'], 'readwrite');
377
+ await Promise.all(docIds.map(async (docId) => {
378
+ const existing = await docsStore.get(docId);
379
+ if (existing) {
380
+ // If exists but deleted, undelete it
381
+ if (existing.deleted) {
382
+ await docsStore.put({ ...existing, deleted: undefined });
366
383
  }
367
- }));
368
- await tx.complete();
369
- }
370
- /**
371
- * Untrack a document.
372
- * @param docIds - The IDs of the documents to untrack.
373
- */
374
- async untrackDocs(docIds) {
375
- const [tx, docsStore, snapshots, committedChanges, pendingChanges] = await this.transaction(['docs', 'snapshots', 'committedChanges', 'pendingChanges'], 'readwrite');
376
- await Promise.all(docIds.map(docId => {
377
- return Promise.all([
378
- docsStore.delete(docId),
379
- snapshots.delete(docId),
380
- committedChanges.delete([docId, 0], [docId, Infinity]),
381
- pendingChanges.delete([docId, 0], [docId, Infinity]),
382
- ]);
383
- }));
384
- await tx.complete();
385
- }
386
- /**
387
- * Tell me the last committed revision you have *and* the highest
388
- * rev of any change. Use these to drive:
389
- * - fetch changes: api.getChangesSince(docId, committedRev)
390
- * - build new patch: newChange.rev = pendingRev; baseRev = committedRev
391
- */
392
- async getLastRevs(docId) {
393
- const [tx, committedChanges, pendingChanges] = await this.transaction(['committedChanges', 'pendingChanges'], 'readonly');
394
- const [lastCommitted, lastPending] = await Promise.all([
395
- committedChanges.getLastFromCursor([docId, 0], [docId, Infinity]),
396
- pendingChanges.getLastFromCursor([docId, 0], [docId, Infinity]),
384
+ // Otherwise, it's already tracked and not deleted, do nothing
385
+ }
386
+ else {
387
+ // If doesn't exist, add it
388
+ await docsStore.put({ docId, committedRev: 0 });
389
+ }
390
+ }));
391
+ await tx.complete();
392
+ }
393
+ /**
394
+ * Untrack a document.
395
+ * @param docIds - The IDs of the documents to untrack.
396
+ */
397
+ async untrackDocs(docIds) {
398
+ const [tx, docsStore, snapshots, committedChanges, pendingChanges] = await this.transaction(['docs', 'snapshots', 'committedChanges', 'pendingChanges'], 'readwrite');
399
+ await Promise.all(docIds.map(docId => {
400
+ return Promise.all([
401
+ docsStore.delete(docId),
402
+ snapshots.delete(docId),
403
+ committedChanges.delete([docId, 0], [docId, Infinity]),
404
+ pendingChanges.delete([docId, 0], [docId, Infinity]),
397
405
  ]);
398
- await tx.complete();
399
- return [lastCommitted?.rev ?? 0, lastPending?.rev ?? lastCommitted?.rev ?? 0];
400
- }
401
- },
402
- (() => {
403
- const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
404
- _getDoc_decorators = [blockable];
405
- _deleteDoc_decorators = [blockable];
406
- _confirmDeleteDoc_decorators = [blockable];
407
- _saveDoc_decorators = [blockable];
408
- _savePendingChanges_decorators = [blockable];
409
- _getPendingChanges_decorators = [blockable];
410
- _replacePendingChanges_decorators = [blockable];
411
- _saveCommittedChanges_decorators = [blockable];
412
- _getLastRevs_decorators = [blockable];
413
- __esDecorate(_a, null, _getDoc_decorators, { kind: "method", name: "getDoc", static: false, private: false, access: { has: obj => "getDoc" in obj, get: obj => obj.getDoc }, metadata: _metadata }, null, _instanceExtraInitializers);
414
- __esDecorate(_a, null, _deleteDoc_decorators, { kind: "method", name: "deleteDoc", static: false, private: false, access: { has: obj => "deleteDoc" in obj, get: obj => obj.deleteDoc }, metadata: _metadata }, null, _instanceExtraInitializers);
415
- __esDecorate(_a, null, _confirmDeleteDoc_decorators, { kind: "method", name: "confirmDeleteDoc", static: false, private: false, access: { has: obj => "confirmDeleteDoc" in obj, get: obj => obj.confirmDeleteDoc }, metadata: _metadata }, null, _instanceExtraInitializers);
416
- __esDecorate(_a, null, _saveDoc_decorators, { kind: "method", name: "saveDoc", static: false, private: false, access: { has: obj => "saveDoc" in obj, get: obj => obj.saveDoc }, metadata: _metadata }, null, _instanceExtraInitializers);
417
- __esDecorate(_a, null, _savePendingChanges_decorators, { kind: "method", name: "savePendingChanges", static: false, private: false, access: { has: obj => "savePendingChanges" in obj, get: obj => obj.savePendingChanges }, metadata: _metadata }, null, _instanceExtraInitializers);
418
- __esDecorate(_a, null, _getPendingChanges_decorators, { kind: "method", name: "getPendingChanges", static: false, private: false, access: { has: obj => "getPendingChanges" in obj, get: obj => obj.getPendingChanges }, metadata: _metadata }, null, _instanceExtraInitializers);
419
- __esDecorate(_a, null, _replacePendingChanges_decorators, { kind: "method", name: "replacePendingChanges", static: false, private: false, access: { has: obj => "replacePendingChanges" in obj, get: obj => obj.replacePendingChanges }, metadata: _metadata }, null, _instanceExtraInitializers);
420
- __esDecorate(_a, null, _saveCommittedChanges_decorators, { kind: "method", name: "saveCommittedChanges", static: false, private: false, access: { has: obj => "saveCommittedChanges" in obj, get: obj => obj.saveCommittedChanges }, metadata: _metadata }, null, _instanceExtraInitializers);
421
- __esDecorate(_a, null, _getLastRevs_decorators, { kind: "method", name: "getLastRevs", static: false, private: false, access: { has: obj => "getLastRevs" in obj, get: obj => obj.getLastRevs }, metadata: _metadata }, null, _instanceExtraInitializers);
422
- if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
423
- })(),
424
- _a;
406
+ }));
407
+ await tx.complete();
408
+ }
409
+ /**
410
+ * Tell me the last committed revision you have *and* the highest
411
+ * rev of any change. Use these to drive:
412
+ * - fetch changes: api.getChangesSince(docId, committedRev)
413
+ * - build new patch: newChange.rev = pendingRev; baseRev = committedRev
414
+ */
415
+ async getLastRevs(docId) {
416
+ const [tx, committedChanges, pendingChanges] = await this.transaction(['committedChanges', 'pendingChanges'], 'readonly');
417
+ const [lastCommitted, lastPending] = await Promise.all([
418
+ committedChanges.getLastFromCursor([docId, 0], [docId, Infinity]),
419
+ pendingChanges.getLastFromCursor([docId, 0], [docId, Infinity]),
420
+ ]);
421
+ await tx.complete();
422
+ return [lastCommitted?.rev ?? 0, lastPending?.rev ?? lastCommitted?.rev ?? 0];
423
+ }
424
+ };
425
425
  })();
426
426
  export { IndexedDBStore };
427
427
  class IDBTransactionWrapper {
428
+ tx;
429
+ promise;
428
430
  constructor(tx) {
429
431
  this.tx = tx;
430
432
  this.promise = new Promise((resolve, reject) => {
@@ -440,6 +442,7 @@ class IDBTransactionWrapper {
440
442
  }
441
443
  }
442
444
  class IDBStoreWrapper {
445
+ store;
443
446
  constructor(store) {
444
447
  this.store = store;
445
448
  }