@dabble/patches 0.2.32 → 0.3.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/applyCommittedChanges.d.ts +8 -0
- package/dist/algorithms/client/applyCommittedChanges.js +40 -0
- package/dist/{utils → algorithms/client}/batching.d.ts +1 -1
- package/dist/{utils → algorithms/client}/batching.js +2 -2
- package/dist/{utils → algorithms/client}/breakChange.d.ts +2 -3
- package/dist/algorithms/client/breakChange.js +258 -0
- package/dist/algorithms/client/createStateFromSnapshot.d.ts +7 -0
- package/dist/algorithms/client/createStateFromSnapshot.js +9 -0
- package/dist/algorithms/client/getJSONByteSize.js +12 -0
- package/dist/algorithms/client/makeChange.d.ts +3 -0
- package/dist/algorithms/client/makeChange.js +37 -0
- package/dist/algorithms/server/commitChanges.d.ts +12 -0
- package/dist/algorithms/server/commitChanges.js +80 -0
- package/dist/algorithms/server/createVersion.d.ts +12 -0
- package/dist/algorithms/server/createVersion.js +28 -0
- package/dist/algorithms/server/getSnapshotAtRevision.d.ts +10 -0
- package/dist/algorithms/server/getSnapshotAtRevision.js +29 -0
- package/dist/algorithms/server/getStateAtRevision.d.ts +9 -0
- package/dist/algorithms/server/getStateAtRevision.js +18 -0
- package/dist/algorithms/server/handleOfflineSessionsAndBatches.d.ts +12 -0
- package/dist/algorithms/server/handleOfflineSessionsAndBatches.js +80 -0
- package/dist/algorithms/server/transformIncomingChanges.d.ts +11 -0
- package/dist/algorithms/server/transformIncomingChanges.js +40 -0
- package/dist/algorithms/shared/applyChanges.d.ts +10 -0
- package/dist/algorithms/shared/applyChanges.js +17 -0
- package/dist/{utils.d.ts → algorithms/shared/rebaseChanges.d.ts} +1 -11
- package/dist/{utils.js → algorithms/shared/rebaseChanges.js} +3 -43
- package/dist/client/InMemoryStore.d.ts +2 -1
- package/dist/client/InMemoryStore.js +9 -3
- package/dist/client/IndexedDBStore.d.ts +34 -2
- package/dist/client/IndexedDBStore.js +399 -282
- package/dist/client/Patches.d.ts +11 -41
- package/dist/client/Patches.js +197 -208
- package/dist/client/PatchesDoc.d.ts +24 -41
- package/dist/client/PatchesDoc.js +57 -214
- package/dist/client/PatchesHistoryClient.js +1 -1
- package/dist/client/PatchesStore.d.ts +186 -9
- package/dist/data/change.d.ts +3 -0
- package/dist/data/change.js +20 -0
- package/dist/data/version.d.ts +12 -0
- package/dist/data/version.js +17 -0
- package/dist/json-patch/ops/add.js +1 -1
- package/dist/json-patch/ops/move.js +1 -1
- package/dist/json-patch/ops/remove.js +1 -1
- package/dist/json-patch/ops/replace.js +1 -1
- package/dist/json-patch/utils/get.js +0 -1
- package/dist/json-patch/utils/log.d.ts +4 -1
- package/dist/json-patch/utils/log.js +2 -5
- package/dist/json-patch/utils/ops.d.ts +1 -1
- package/dist/json-patch/utils/ops.js +4 -1
- package/dist/json-patch/utils/paths.js +2 -2
- package/dist/json-patch/utils/toArrayIndex.js +1 -1
- package/dist/net/PatchesSync.d.ts +55 -24
- package/dist/net/PatchesSync.js +336 -258
- package/dist/net/protocol/types.d.ts +1 -1
- package/dist/net/websocket/AuthorizationProvider.d.ts +9 -2
- package/dist/net/websocket/AuthorizationProvider.js +14 -2
- package/dist/net/websocket/PatchesWebSocket.d.ts +2 -2
- package/dist/net/websocket/PatchesWebSocket.js +3 -2
- package/dist/net/websocket/RPCServer.d.ts +2 -2
- package/dist/net/websocket/RPCServer.js +3 -3
- package/dist/net/websocket/SignalingService.js +1 -1
- package/dist/net/websocket/WebSocketServer.d.ts +1 -1
- package/dist/net/websocket/WebSocketServer.js +2 -2
- package/dist/net/websocket/WebSocketTransport.js +1 -1
- package/dist/net/websocket/onlineState.d.ts +2 -2
- package/dist/net/websocket/onlineState.js +9 -3
- package/dist/server/PatchesBranchManager.js +9 -16
- package/dist/server/PatchesHistoryManager.js +1 -1
- package/dist/server/PatchesServer.d.ts +11 -38
- package/dist/server/PatchesServer.js +32 -255
- package/dist/server/index.d.ts +4 -4
- package/dist/server/index.js +3 -3
- package/dist/server/types.d.ts +1 -1
- package/dist/types.d.ts +8 -6
- package/dist/utils/concurrency.d.ts +26 -0
- package/dist/utils/concurrency.js +60 -0
- package/dist/utils/deferred.d.ts +7 -0
- package/dist/utils/deferred.js +23 -0
- package/package.json +11 -5
- package/dist/utils/breakChange.js +0 -302
- package/dist/utils/getJSONByteSize.js +0 -12
- /package/dist/{utils → algorithms/client}/getJSONByteSize.d.ts +0 -0
|
@@ -1,5 +1,41 @@
|
|
|
1
|
+
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
|
|
2
|
+
var useValue = arguments.length > 2;
|
|
3
|
+
for (var i = 0; i < initializers.length; i++) {
|
|
4
|
+
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
5
|
+
}
|
|
6
|
+
return useValue ? value : void 0;
|
|
7
|
+
};
|
|
8
|
+
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
9
|
+
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
10
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
11
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
12
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
13
|
+
var _, done = false;
|
|
14
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
15
|
+
var context = {};
|
|
16
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
17
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
18
|
+
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
19
|
+
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
20
|
+
if (kind === "accessor") {
|
|
21
|
+
if (result === void 0) continue;
|
|
22
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
23
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
24
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
25
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
26
|
+
}
|
|
27
|
+
else if (_ = accept(result)) {
|
|
28
|
+
if (kind === "field") initializers.unshift(_);
|
|
29
|
+
else descriptor[key] = _;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
33
|
+
done = true;
|
|
34
|
+
};
|
|
35
|
+
import { applyChanges } from '../algorithms/shared/applyChanges.js';
|
|
1
36
|
import { transformPatch } from '../json-patch/transformPatch.js';
|
|
2
|
-
import {
|
|
37
|
+
import { blockable } from '../utils/concurrency.js';
|
|
38
|
+
import { deferred } from '../utils/deferred.js';
|
|
3
39
|
const DB_VERSION = 1;
|
|
4
40
|
const SNAPSHOT_INTERVAL = 200;
|
|
5
41
|
/**
|
|
@@ -14,299 +50,380 @@ const SNAPSHOT_INTERVAL = 200;
|
|
|
14
50
|
* After every 200 committed changes, the class will save the current state to the snapshot store and delete the committed changes that went into it.
|
|
15
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.
|
|
16
52
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
// Create stores
|
|
38
|
-
if (!db.objectStoreNames.contains('snapshots')) {
|
|
39
|
-
db.createObjectStore('snapshots', { keyPath: 'docId' });
|
|
53
|
+
let IndexedDBStore = (() => {
|
|
54
|
+
var _a;
|
|
55
|
+
let _instanceExtraInitializers = [];
|
|
56
|
+
let _getDoc_decorators;
|
|
57
|
+
let _deleteDoc_decorators;
|
|
58
|
+
let _confirmDeleteDoc_decorators;
|
|
59
|
+
let _saveDoc_decorators;
|
|
60
|
+
let _savePendingChanges_decorators;
|
|
61
|
+
let _getPendingChanges_decorators;
|
|
62
|
+
let _replacePendingChanges_decorators;
|
|
63
|
+
let _saveCommittedChanges_decorators;
|
|
64
|
+
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
|
+
}
|
|
40
73
|
}
|
|
41
|
-
|
|
42
|
-
|
|
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
|
+
};
|
|
43
99
|
}
|
|
44
|
-
|
|
45
|
-
|
|
100
|
+
getDB() {
|
|
101
|
+
return this.dbPromise.promise;
|
|
46
102
|
}
|
|
47
|
-
|
|
48
|
-
|
|
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
|
+
}
|
|
114
|
+
this.initDB();
|
|
49
115
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
* 1. load the last snapshot (state + rev)
|
|
105
|
-
* 2. load committedChanges[rev > snapshot.rev]
|
|
106
|
-
* 3. load pendingChanges
|
|
107
|
-
* 4. apply committed changes, rebase pending
|
|
108
|
-
* 5. return { state, rev, changes: pending }
|
|
109
|
-
*/
|
|
110
|
-
async getDoc(docId) {
|
|
111
|
-
const [tx, docsStore, snapshots, committedChanges, pendingChanges] = await this.transaction(['docs', 'snapshots', 'committedChanges', 'pendingChanges'], 'readonly');
|
|
112
|
-
const docMeta = await docsStore.get(docId);
|
|
113
|
-
if (docMeta?.deleted) {
|
|
114
|
-
await tx.complete();
|
|
115
|
-
return undefined;
|
|
116
|
-
}
|
|
117
|
-
const snapshot = await snapshots.get(docId);
|
|
118
|
-
const committed = await committedChanges.getAll([docId, snapshot?.rev ?? 0], [docId, Infinity]);
|
|
119
|
-
const pending = await pendingChanges.getAll([docId, 0], [docId, Infinity]);
|
|
120
|
-
if (!snapshot && !committed.length && !pending.length)
|
|
121
|
-
return undefined;
|
|
122
|
-
// Apply any committed changes to the snapshot state
|
|
123
|
-
const state = applyChanges(snapshot?.state, committed);
|
|
124
|
-
// Rebase pending changes if there are any committed changes received since their baseRev
|
|
125
|
-
const lastCommitted = committed[committed.length - 1];
|
|
126
|
-
const baseRev = pending[0]?.baseRev;
|
|
127
|
-
if (lastCommitted && baseRev && baseRev < lastCommitted.rev) {
|
|
128
|
-
const patch = committed
|
|
129
|
-
.filter(change => change.rev > baseRev)
|
|
130
|
-
.map(change => change.ops)
|
|
131
|
-
.flat();
|
|
132
|
-
const offset = lastCommitted.rev - baseRev;
|
|
133
|
-
pending.forEach(change => {
|
|
134
|
-
change.rev += offset;
|
|
135
|
-
change.ops = transformPatch(state, patch, change.ops);
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
await tx.complete();
|
|
139
|
-
return {
|
|
140
|
-
state,
|
|
141
|
-
rev: committed[committed.length - 1]?.rev ?? snapshot?.rev ?? 0,
|
|
142
|
-
changes: pending,
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Completely remove all data for this docId and mark it as deleted (tombstone).
|
|
147
|
-
*/
|
|
148
|
-
async deleteDoc(docId) {
|
|
149
|
-
const [tx, snapshots, committedChanges, pendingChanges, docsStore] = await this.transaction(['snapshots', 'committedChanges', 'pendingChanges', 'docs'], 'readwrite');
|
|
150
|
-
const docMeta = (await docsStore.get(docId)) ?? { docId, committedRev: 0 };
|
|
151
|
-
await docsStore.put({ ...docMeta, deleted: true });
|
|
152
|
-
await Promise.all([
|
|
153
|
-
snapshots.delete(docId),
|
|
154
|
-
committedChanges.delete([docId, 0], [docId, Infinity]),
|
|
155
|
-
pendingChanges.delete([docId, 0], [docId, Infinity]),
|
|
156
|
-
]);
|
|
157
|
-
await tx.complete();
|
|
158
|
-
}
|
|
159
|
-
async confirmDeleteDoc(docId) {
|
|
160
|
-
const [tx, docsStore] = await this.transaction(['docs'], 'readwrite');
|
|
161
|
-
await docsStore.delete(docId);
|
|
162
|
-
await tx.complete();
|
|
163
|
-
}
|
|
164
|
-
// ─── Pending Changes ────────────────────────────────────────────────────────
|
|
165
|
-
async saveDoc(docId, docState) {
|
|
166
|
-
const [tx, snapshots, committedChanges, pendingChanges, docsStore] = await this.transaction(['snapshots', 'committedChanges', 'pendingChanges', 'docs'], 'readwrite');
|
|
167
|
-
const { rev, state } = docState;
|
|
168
|
-
await Promise.all([
|
|
169
|
-
docsStore.put({ docId, committedRev: rev }),
|
|
170
|
-
snapshots.put({ docId, state, rev }),
|
|
171
|
-
committedChanges.delete([docId, 0], [docId, Infinity]),
|
|
172
|
-
pendingChanges.delete([docId, 0], [docId, Infinity]),
|
|
173
|
-
]);
|
|
174
|
-
await tx.complete();
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* Append an array of local changes to the pending queue.
|
|
178
|
-
* Called *before* you attempt to send them to the server.
|
|
179
|
-
*/
|
|
180
|
-
async savePendingChange(docId, change) {
|
|
181
|
-
const [tx, pendingChanges, docsStore] = await this.transaction(['pendingChanges', 'docs'], 'readwrite');
|
|
182
|
-
let docMeta = await docsStore.get(docId);
|
|
183
|
-
if (!docMeta) {
|
|
184
|
-
docMeta = { docId, committedRev: 0 };
|
|
185
|
-
await docsStore.put(docMeta);
|
|
186
|
-
}
|
|
187
|
-
else if (docMeta.deleted) {
|
|
188
|
-
delete docMeta.deleted;
|
|
189
|
-
await docsStore.put(docMeta);
|
|
190
|
-
console.warn(`Revived document ${docId} by saving pending changes.`);
|
|
191
|
-
}
|
|
192
|
-
await pendingChanges.put({ ...change, docId });
|
|
193
|
-
await tx.complete();
|
|
194
|
-
}
|
|
195
|
-
/** Read back all pending changes for this docId (in order). */
|
|
196
|
-
async getPendingChanges(docId) {
|
|
197
|
-
const [tx, pendingChanges] = await this.transaction(['pendingChanges'], 'readonly');
|
|
198
|
-
const result = await pendingChanges.getAll([docId, 0], [docId, Infinity]);
|
|
199
|
-
await tx.complete();
|
|
200
|
-
return result;
|
|
201
|
-
}
|
|
202
|
-
// ─── Committed Changes ─────────────────────────────────────────────────────
|
|
203
|
-
/**
|
|
204
|
-
* Store server‐confirmed changes. Will:
|
|
205
|
-
* - persist them in the committedChanges store
|
|
206
|
-
* - remove any pending changes whose rev falls within `sentPendingRange`
|
|
207
|
-
* - optionally compact a new snapshot after N changes (hidden internally)
|
|
208
|
-
* @param docId - The ID of the document to save the changes for
|
|
209
|
-
* @param changes - The changes to save
|
|
210
|
-
* @param sentPendingRange - The range of pending changes to remove, *must* be provided after receiving the changes
|
|
211
|
-
* from the server in response to a patchesDoc request.
|
|
212
|
-
*/
|
|
213
|
-
async saveCommittedChanges(docId, changes, sentPendingRange) {
|
|
214
|
-
const [tx, committedChanges, pendingChanges, snapshots, docsStore] = await this.transaction(['committedChanges', 'pendingChanges', 'snapshots', 'docs'], 'readwrite');
|
|
215
|
-
// Save committed changes
|
|
216
|
-
await Promise.all(changes.map(change => committedChanges.put({ ...change, docId })));
|
|
217
|
-
// Remove pending changes if range provided
|
|
218
|
-
if (sentPendingRange) {
|
|
219
|
-
await pendingChanges.delete([docId, sentPendingRange[0]], [docId, sentPendingRange[1]]);
|
|
220
|
-
}
|
|
221
|
-
// Check if we should create a snapshot
|
|
222
|
-
const count = await committedChanges.count([docId, 0], [docId, Infinity]);
|
|
223
|
-
if (count >= SNAPSHOT_INTERVAL) {
|
|
224
|
-
// Update the snapshot. A snapshot will not be updated if there are pending changes based on revisions older than
|
|
225
|
-
// the latest committed change until those pending changes are committed.
|
|
226
|
-
const [snapshot, committed, firstPending] = await Promise.all([
|
|
227
|
-
snapshots.get(docId),
|
|
228
|
-
committedChanges.getAll([docId, 0], [docId, Infinity], SNAPSHOT_INTERVAL),
|
|
229
|
-
pendingChanges.getFirstFromCursor([docId, 0], [docId, Infinity]),
|
|
230
|
-
]);
|
|
231
|
-
// Update the snapshot
|
|
232
|
-
const lastRev = committed[committed.length - 1]?.rev;
|
|
233
|
-
if (!firstPending?.baseRev || firstPending?.baseRev >= lastRev) {
|
|
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);
|
|
128
|
+
}
|
|
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;
|
|
163
|
+
}
|
|
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
|
|
234
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
|
+
});
|
|
184
|
+
}
|
|
185
|
+
await tx.complete();
|
|
186
|
+
return {
|
|
187
|
+
state,
|
|
188
|
+
rev: committed[committed.length - 1]?.rev ?? snapshot?.rev ?? 0,
|
|
189
|
+
changes: pending,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
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 });
|
|
235
199
|
await Promise.all([
|
|
236
|
-
snapshots.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
state,
|
|
240
|
-
}),
|
|
241
|
-
committedChanges.delete([docId, 0], [docId, lastRev]),
|
|
200
|
+
snapshots.delete(docId),
|
|
201
|
+
committedChanges.delete([docId, 0], [docId, Infinity]),
|
|
202
|
+
pendingChanges.delete([docId, 0], [docId, Infinity]),
|
|
242
203
|
]);
|
|
204
|
+
await tx.complete();
|
|
243
205
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
await docsStore.
|
|
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);
|
|
213
|
+
await tx.complete();
|
|
251
214
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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();
|
|
230
|
+
}
|
|
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);
|
|
271
241
|
}
|
|
272
|
-
|
|
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();
|
|
273
249
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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;
|
|
277
260
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
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();
|
|
283
|
+
}
|
|
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]),
|
|
312
|
+
]);
|
|
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
|
+
}
|
|
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
|
+
}
|
|
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);
|
|
347
|
+
}
|
|
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 });
|
|
366
|
+
}
|
|
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]),
|
|
397
|
+
]);
|
|
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;
|
|
425
|
+
})();
|
|
426
|
+
export { IndexedDBStore };
|
|
310
427
|
class IDBTransactionWrapper {
|
|
311
428
|
constructor(tx) {
|
|
312
429
|
this.tx = tx;
|