@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.
- package/dist/algorithms/client/makeChange.d.ts +2 -3
- package/dist/algorithms/client/makeChange.js +1 -1
- package/dist/algorithms/server/getSnapshotAtRevision.d.ts +1 -1
- package/dist/algorithms/server/getStateAtRevision.d.ts +1 -1
- package/dist/algorithms/server/handleOfflineSessionsAndBatches.d.ts +1 -1
- package/dist/client/InMemoryStore.js +1 -3
- package/dist/client/IndexedDBStore.js +345 -342
- package/dist/client/Patches.js +156 -156
- package/dist/client/PatchesDoc.d.ts +4 -5
- package/dist/client/PatchesDoc.js +16 -13
- package/dist/client/PatchesHistoryClient.js +12 -8
- package/dist/json-patch/JSONPatch.js +2 -0
- package/dist/json-patch/createJSONPatch.d.ts +15 -18
- package/dist/json-patch/createJSONPatch.js +18 -20
- package/dist/json-patch/pathProxy.d.ts +22 -0
- package/dist/json-patch/pathProxy.js +50 -0
- package/dist/json-patch/utils/getType.d.ts +1 -1
- package/dist/net/PatchesSync.js +307 -303
- package/dist/net/error.js +1 -0
- package/dist/net/protocol/JSONRPCClient.js +4 -3
- package/dist/net/protocol/JSONRPCServer.js +6 -8
- package/dist/net/webrtc/WebRTCAwareness.js +12 -7
- package/dist/net/webrtc/WebRTCTransport.d.ts +1 -1
- package/dist/net/webrtc/WebRTCTransport.js +27 -21
- package/dist/net/websocket/PatchesWebSocket.js +7 -2
- package/dist/net/websocket/RPCServer.js +5 -0
- package/dist/net/websocket/SignalingService.js +1 -3
- package/dist/net/websocket/WebSocketServer.js +2 -0
- package/dist/net/websocket/WebSocketTransport.js +21 -19
- package/dist/net/websocket/onlineState.js +2 -2
- package/dist/server/PatchesBranchManager.js +2 -0
- package/dist/server/PatchesHistoryManager.js +2 -0
- package/dist/server/PatchesServer.d.ts +2 -2
- package/dist/server/PatchesServer.js +7 -5
- package/dist/types.d.ts +15 -6
- package/dist/types.js +1 -1
- package/package.json +3 -2
- package/dist/json-patch/patchProxy.d.ts +0 -42
- 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
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
}
|