@docstack/pouchdb-adapter-googledrive 0.1.4 → 0.1.6
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/CHANGELOG.md +180 -0
- package/LICENSE +10 -0
- package/README.md +52 -1
- package/lib/adapter.d.ts +0 -0
- package/lib/adapter.js +374 -178
- package/lib/cache.d.ts +0 -0
- package/lib/cache.js +0 -0
- package/lib/client.d.ts +2 -0
- package/lib/client.js +6 -4
- package/lib/drive.d.ts +136 -2
- package/lib/drive.js +670 -89
- package/lib/index.d.ts +0 -0
- package/lib/index.js +0 -0
- package/lib/types.d.ts +34 -4
- package/lib/types.js +0 -0
- package/package.json +7 -3
- package/.env.example +0 -9
package/lib/adapter.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.GoogleDriveAdapter = GoogleDriveAdapter;
|
|
4
4
|
const drive_1 = require("./drive");
|
|
5
|
+
const pouchdb_adapter_utils_1 = require("pouchdb-adapter-utils");
|
|
6
|
+
const pouchdb_merge_1 = require("pouchdb-merge");
|
|
5
7
|
/**
|
|
6
8
|
* Schedule a function to run asynchronously.
|
|
7
9
|
*/
|
|
@@ -10,6 +12,50 @@ function nextTick(fn) {
|
|
|
10
12
|
queueMicrotask(fn);
|
|
11
13
|
}
|
|
12
14
|
}
|
|
15
|
+
/** Substituted by DriveHandler.updateIndex() once the batch's change-log file
|
|
16
|
+
* actually has an id - lets adapter.ts build a complete IndexEntry (including
|
|
17
|
+
* pointers at revisions being written in *this* batch) before that file exists. */
|
|
18
|
+
const SELF_FILE = '__SELF__';
|
|
19
|
+
/** True when a newEdits write's parent rev isn't actually present in the tree
|
|
20
|
+
* (the caller claimed a rev that doesn't exist) - same check, same name,
|
|
21
|
+
* `pouchdb-adapter-native` uses (traced from pouchdb-adapter-utils's processDocs.js). */
|
|
22
|
+
function rootIsMissing(docInfo) {
|
|
23
|
+
return docInfo.metadata.rev_tree[0].ids[1].status === 'missing';
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Builds a change's `changes` array.
|
|
27
|
+
*
|
|
28
|
+
* `style: 'all_docs'` - which is what `pouchdb-replication` asks for, and its default -
|
|
29
|
+
* means "list every leaf of the revision tree, not just the winner". Reporting only the
|
|
30
|
+
* winner hides conflict leaves from the changes feed, so they are never fetched, never
|
|
31
|
+
* pushed, and the two replicas quietly disagree about which revisions exist. The winner
|
|
32
|
+
* goes first, as CouchDB orders it.
|
|
33
|
+
*
|
|
34
|
+
* @param entry - The index entry for the document, carrying the serialized rev tree.
|
|
35
|
+
* @param style - The `style` the caller asked for; anything but `'all_docs'` yields the
|
|
36
|
+
* winning revision alone.
|
|
37
|
+
* @returns The `changes` array for the changes feed.
|
|
38
|
+
*/
|
|
39
|
+
function buildChangesList(entry, style) {
|
|
40
|
+
const winner = [{ rev: entry.rev }];
|
|
41
|
+
if (style !== 'all_docs')
|
|
42
|
+
return winner;
|
|
43
|
+
try {
|
|
44
|
+
const leaves = (0, pouchdb_merge_1.collectLeaves)(JSON.parse(entry.tree));
|
|
45
|
+
if (!leaves || !leaves.length)
|
|
46
|
+
return winner;
|
|
47
|
+
const others = leaves
|
|
48
|
+
.map(leaf => leaf.rev)
|
|
49
|
+
.filter(rev => rev !== entry.rev)
|
|
50
|
+
.map(rev => ({ rev }));
|
|
51
|
+
return [...winner, ...others];
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
// A tree that will not parse is a bigger problem than a missing conflict leaf;
|
|
55
|
+
// fall back to the winner rather than breaking the feed.
|
|
56
|
+
return winner;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
13
59
|
/**
|
|
14
60
|
* TODO: Implement dynamic method signature, since PouchDB CRUD methods can be called as promise, callback or (?)
|
|
15
61
|
*/
|
|
@@ -140,9 +186,27 @@ function GoogleDriveAdapter(PouchDB) {
|
|
|
140
186
|
if (!opts || typeof opts !== 'object') {
|
|
141
187
|
opts = {};
|
|
142
188
|
}
|
|
143
|
-
// PouchDB sometimes asks for metadata only (revs, revs_info)
|
|
189
|
+
// PouchDB sometimes asks for metadata only (revs, revs_info)
|
|
144
190
|
log('_get id:', id, 'opts:', JSON.stringify(opts));
|
|
145
|
-
|
|
191
|
+
// A specific non-winning rev was requested (e.g. to inspect a conflict
|
|
192
|
+
// via db.get(id, {rev, conflicts: true})'s follow-up fetch) - look it up
|
|
193
|
+
// among known conflict locations instead of always returning the current
|
|
194
|
+
// winner. The common case (no rev, or rev === the winner) is unaffected.
|
|
195
|
+
// api.get = api._get below means this replaces the whole public `get()`
|
|
196
|
+
// method (same "no _-prefixed hook, override the public method instead"
|
|
197
|
+
// situation @docstack/pouchdb-adapter-native's own revsDiff/bulkGet ran
|
|
198
|
+
// into) - AbstractPouchDB.prototype.get's own opts.conflicts handling
|
|
199
|
+
// (pouchdbMerge.collectConflicts(metadata)) is bypassed entirely, so it
|
|
200
|
+
// has to be done here instead.
|
|
201
|
+
const entry = db.getIndexEntry(id);
|
|
202
|
+
const fetchPromise = (async () => {
|
|
203
|
+
if (opts.rev && entry && entry.rev !== opts.rev) {
|
|
204
|
+
const loc = entry.conflictLocations && entry.conflictLocations[opts.rev];
|
|
205
|
+
return loc ? db.getRevisionBody(id, opts.rev, loc) : null;
|
|
206
|
+
}
|
|
207
|
+
return db.get(id);
|
|
208
|
+
})();
|
|
209
|
+
const promise = fetchPromise.then(doc => {
|
|
146
210
|
if (!doc) {
|
|
147
211
|
log('_get missing', id);
|
|
148
212
|
const err = {
|
|
@@ -166,6 +230,11 @@ function GoogleDriveAdapter(PouchDB) {
|
|
|
166
230
|
callback(null, result);
|
|
167
231
|
return result;
|
|
168
232
|
}
|
|
233
|
+
if (opts.conflicts && entry) {
|
|
234
|
+
const conflicts = (0, pouchdb_merge_1.collectConflicts)({ rev_tree: JSON.parse(entry.tree) });
|
|
235
|
+
if (conflicts.length)
|
|
236
|
+
doc._conflicts = conflicts;
|
|
237
|
+
}
|
|
169
238
|
log('_get returning standard doc for:', id);
|
|
170
239
|
// If only rev was requested? (Internal optimization)
|
|
171
240
|
// PouchDB core handles this if we return the full doc.
|
|
@@ -193,74 +262,72 @@ function GoogleDriveAdapter(PouchDB) {
|
|
|
193
262
|
opts = {};
|
|
194
263
|
}
|
|
195
264
|
const promise = (async () => {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (opts.include_docs) {
|
|
215
|
-
const docs = await db.getMulti(sliced);
|
|
216
|
-
const rows = sliced.map((id, i) => {
|
|
217
|
-
const doc = docs[i];
|
|
218
|
-
const entry = db.getIndexEntry(id);
|
|
219
|
-
if (!doc && (!entry || entry.deleted))
|
|
220
|
-
return { key: id, error: 'not_found' };
|
|
221
|
-
if (!doc && entry) {
|
|
222
|
-
// This implies fetch failed but exists in index? Or null result.
|
|
223
|
-
return { key: id, error: 'not_found' };
|
|
224
|
-
}
|
|
225
|
-
const row = {
|
|
226
|
-
id,
|
|
227
|
-
key: id,
|
|
228
|
-
value: { rev: entry?.rev || doc._rev }
|
|
229
|
-
};
|
|
230
|
-
row.doc = doc;
|
|
231
|
-
return row;
|
|
232
|
-
});
|
|
233
|
-
const result = {
|
|
234
|
-
total_rows: total,
|
|
235
|
-
offset: startIndex,
|
|
236
|
-
rows: rows.filter(r => !r.error || !opts.keys) // Filter errored unless specifically asked via keys?
|
|
237
|
-
// CouchDB usually returns error row if distinct keys requested.
|
|
238
|
-
};
|
|
239
|
-
if (opts.update_seq)
|
|
240
|
-
result.update_seq = db.seq;
|
|
241
|
-
return result;
|
|
265
|
+
// _local/* docs live in a separate namespace and never appear in allDocs,
|
|
266
|
+
// same as CouchDB - they aren't part of the B-tree of regular documents.
|
|
267
|
+
const indexKeys = (await db.getIndexKeys()).filter(k => !k.startsWith('_local/'));
|
|
268
|
+
// total_rows reflects live (non-deleted) documents, matching db.info().doc_count -
|
|
269
|
+
// deleted entries stay in the index forever but were never meant to count here.
|
|
270
|
+
let total = 0;
|
|
271
|
+
for (const k of indexKeys) {
|
|
272
|
+
const entry = db.getIndexEntry(k);
|
|
273
|
+
if (entry && !entry.deleted)
|
|
274
|
+
total++;
|
|
275
|
+
}
|
|
276
|
+
const startIndex = opts.skip || 0;
|
|
277
|
+
let candidateKeys;
|
|
278
|
+
if (opts.keys) {
|
|
279
|
+
// Explicit keys: return exactly these, in the given order. Deleted docs are
|
|
280
|
+
// included here (flagged via value.deleted), not treated as errors - only
|
|
281
|
+
// keys absent from the index entirely are 'not_found'.
|
|
282
|
+
candidateKeys = opts.keys;
|
|
242
283
|
}
|
|
243
284
|
else {
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
return {
|
|
250
|
-
id,
|
|
251
|
-
key: id,
|
|
252
|
-
value: { rev: entry.rev }
|
|
253
|
-
};
|
|
285
|
+
// Default listing: deleted docs never appear, same as a real CouchDB/PouchDB
|
|
286
|
+
// allDocs() with no keys specified.
|
|
287
|
+
candidateKeys = indexKeys.filter(k => {
|
|
288
|
+
const entry = db.getIndexEntry(k);
|
|
289
|
+
return entry && !entry.deleted;
|
|
254
290
|
});
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
291
|
+
if (opts.startkey)
|
|
292
|
+
candidateKeys = candidateKeys.filter(k => k >= opts.startkey);
|
|
293
|
+
if (opts.endkey)
|
|
294
|
+
candidateKeys = candidateKeys.filter(k => k <= opts.endkey);
|
|
295
|
+
if (opts.key)
|
|
296
|
+
candidateKeys = candidateKeys.filter(k => k === opts.key);
|
|
297
|
+
candidateKeys.sort();
|
|
298
|
+
if (opts.descending)
|
|
299
|
+
candidateKeys.reverse();
|
|
263
300
|
}
|
|
301
|
+
const limit = typeof opts.limit === 'number' ? opts.limit : candidateKeys.length;
|
|
302
|
+
const sliced = opts.keys ? candidateKeys : candidateKeys.slice(startIndex, startIndex + limit);
|
|
303
|
+
const docs = opts.include_docs ? await db.getMulti(sliced) : null;
|
|
304
|
+
const rows = sliced.map((id, i) => {
|
|
305
|
+
const entry = db.getIndexEntry(id);
|
|
306
|
+
if (!entry)
|
|
307
|
+
return { key: id, error: 'not_found' };
|
|
308
|
+
if (entry.deleted) {
|
|
309
|
+
const row = { id, key: id, value: { rev: entry.rev, deleted: true } };
|
|
310
|
+
if (opts.include_docs)
|
|
311
|
+
row.doc = null;
|
|
312
|
+
return row;
|
|
313
|
+
}
|
|
314
|
+
const row = { id, key: id, value: { rev: entry.rev } };
|
|
315
|
+
if (opts.include_docs) {
|
|
316
|
+
const doc = docs[i];
|
|
317
|
+
if (!doc)
|
|
318
|
+
return { key: id, error: 'not_found' };
|
|
319
|
+
row.doc = doc;
|
|
320
|
+
}
|
|
321
|
+
return row;
|
|
322
|
+
});
|
|
323
|
+
const result = {
|
|
324
|
+
total_rows: total,
|
|
325
|
+
offset: startIndex,
|
|
326
|
+
rows
|
|
327
|
+
};
|
|
328
|
+
if (opts.update_seq)
|
|
329
|
+
result.update_seq = db.seq;
|
|
330
|
+
return result;
|
|
264
331
|
})();
|
|
265
332
|
if (callback) {
|
|
266
333
|
promise.then(res => callback(null, res)).catch(err => callback(err));
|
|
@@ -274,32 +341,35 @@ function GoogleDriveAdapter(PouchDB) {
|
|
|
274
341
|
api._bulkGet = function (opts, callback) {
|
|
275
342
|
const docs = opts.docs;
|
|
276
343
|
const ids = docs.map((d) => d.id);
|
|
277
|
-
return db.getMulti(ids).then(results => {
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
id,
|
|
299
|
-
docs: [docResult]
|
|
344
|
+
return db.getMulti(ids).then(async (results) => {
|
|
345
|
+
const rows = await Promise.all(ids.map(async (id, i) => {
|
|
346
|
+
let doc = results[i];
|
|
347
|
+
const requestedRev = docs[i].rev;
|
|
348
|
+
const entry = db.getIndexEntry(id);
|
|
349
|
+
// Winning body didn't match the specific rev requested - it may
|
|
350
|
+
// still be a known conflict leaf, not necessarily missing.
|
|
351
|
+
if (requestedRev && (!doc || doc._rev !== requestedRev) && entry) {
|
|
352
|
+
const loc = entry.conflictLocations && entry.conflictLocations[requestedRev];
|
|
353
|
+
if (loc)
|
|
354
|
+
doc = await db.getRevisionBody(id, requestedRev, loc);
|
|
355
|
+
}
|
|
356
|
+
let docResult;
|
|
357
|
+
if (!doc || (requestedRev && doc._rev !== requestedRev)) {
|
|
358
|
+
docResult = {
|
|
359
|
+
error: {
|
|
360
|
+
status: 404,
|
|
361
|
+
error: true,
|
|
362
|
+
name: 'not_found',
|
|
363
|
+
message: 'missing'
|
|
364
|
+
}
|
|
300
365
|
};
|
|
301
|
-
}
|
|
302
|
-
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
docResult = { ok: doc };
|
|
369
|
+
}
|
|
370
|
+
return { id, docs: [docResult] };
|
|
371
|
+
}));
|
|
372
|
+
const response = { results: rows };
|
|
303
373
|
if (callback)
|
|
304
374
|
callback(null, response);
|
|
305
375
|
return response;
|
|
@@ -310,76 +380,145 @@ function GoogleDriveAdapter(PouchDB) {
|
|
|
310
380
|
});
|
|
311
381
|
};
|
|
312
382
|
api.bulkGet = api._bulkGet;
|
|
313
|
-
// Bulk document operations
|
|
383
|
+
// Bulk document operations. Merges every write into a real pouchdb-merge
|
|
384
|
+
// rev tree (mirroring @docstack/pouchdb-adapter-native's own, already-proven
|
|
385
|
+
// _bulkDocs, traced from pouchdb-adapter-utils's processDocs.js/updateDoc.js)
|
|
386
|
+
// instead of blindly overwriting the index - the previous version had no
|
|
387
|
+
// conflict detection at all on the new_edits:false (replication) path, so
|
|
388
|
+
// two peers syncing a concurrent edit through this adapter would each just
|
|
389
|
+
// keep whichever write landed last, silently disagreeing with each other.
|
|
314
390
|
api._bulkDocs = function (req, opts, callback) {
|
|
391
|
+
// `api.bulkDocs = api._bulkDocs` below means callers reach this directly and
|
|
392
|
+
// AbstractPouchDB.prototype.bulkDocs never runs, so its two normalizations
|
|
393
|
+
// have to happen here instead.
|
|
394
|
+
//
|
|
395
|
+
// First, the request shape: `db.bulkDocs([doc])` is the common call style and
|
|
396
|
+
// core turns the array into an envelope before any adapter sees it.
|
|
397
|
+
if (Array.isArray(req))
|
|
398
|
+
req = { docs: req };
|
|
399
|
+
// Second, `new_edits`, which arrives on either side. `pouchdb-replication`
|
|
400
|
+
// puts it on `req` (the CouchDB _bulk_docs body shape); `bulkDocs(docs,
|
|
401
|
+
// { new_edits: false })` and `put(doc, { new_edits: false })` - which core
|
|
402
|
+
// routes through `bulkDocs` - put it on `opts`. Core consults `opts` first and
|
|
403
|
+
// falls back to `req`; reading only one side silently mints fresh revisions
|
|
404
|
+
// for writes that were meant to land verbatim.
|
|
405
|
+
const newEdits = (opts && typeof opts === 'object' && 'new_edits' in opts)
|
|
406
|
+
? opts.new_edits !== false
|
|
407
|
+
: req.new_edits !== false;
|
|
315
408
|
const docs = req.docs;
|
|
316
|
-
const results =
|
|
317
|
-
const
|
|
409
|
+
const results = new Array(docs.length);
|
|
410
|
+
const revsLimit = adapterOpts.revs_limit || 1000;
|
|
318
411
|
const changes = [];
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
for (const doc of docs) {
|
|
412
|
+
for (let i = 0; i < docs.length; i++) {
|
|
413
|
+
const doc = docs[i];
|
|
322
414
|
const id = doc._id;
|
|
323
|
-
const seq = db.getNextSeq() + changes.length;
|
|
324
415
|
const entry = db.getIndexEntry(id);
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
416
|
+
const docInfo = (0, pouchdb_adapter_utils_1.parseDoc)(Object.assign({}, doc), newEdits);
|
|
417
|
+
if (!docInfo || !docInfo.metadata) {
|
|
418
|
+
results[i] = Object.assign({ id }, docInfo);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
let mergedTree;
|
|
422
|
+
let stemmedRevs;
|
|
423
|
+
let winning;
|
|
424
|
+
let winningDeleted;
|
|
425
|
+
let selfIsWinner;
|
|
426
|
+
let incomingRev;
|
|
427
|
+
let savedDocInfo = docInfo;
|
|
428
|
+
const conflictLocations = {};
|
|
429
|
+
if (!entry) {
|
|
430
|
+
// Brand new doc.
|
|
431
|
+
if (newEdits && rootIsMissing(docInfo)) {
|
|
432
|
+
results[i] = { ok: false, id, error: 'conflict', reason: 'Document update conflict' };
|
|
333
433
|
continue;
|
|
334
434
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
changes.push({
|
|
343
|
-
seq,
|
|
344
|
-
id,
|
|
345
|
-
rev: newRev,
|
|
346
|
-
deleted: true,
|
|
347
|
-
timestamp: Date.now()
|
|
348
|
-
});
|
|
349
|
-
results.push({ ok: true, id, rev: newRev });
|
|
435
|
+
const merged = (0, pouchdb_merge_1.merge)([], docInfo.metadata.rev_tree[0], revsLimit);
|
|
436
|
+
mergedTree = merged.tree;
|
|
437
|
+
stemmedRevs = merged.stemmedRevs;
|
|
438
|
+
winning = (0, pouchdb_merge_1.winningRev)({ rev_tree: mergedTree });
|
|
439
|
+
winningDeleted = (0, pouchdb_merge_1.isDeleted)({ rev_tree: mergedTree }, winning);
|
|
440
|
+
incomingRev = docInfo.metadata.rev;
|
|
441
|
+
selfIsWinner = true; // an empty starting tree can only ever produce one leaf
|
|
350
442
|
}
|
|
351
443
|
else {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
444
|
+
const existingTree = JSON.parse(entry.tree);
|
|
445
|
+
if ((0, pouchdb_merge_1.revExists)(existingTree, docInfo.metadata.rev) && !newEdits) {
|
|
446
|
+
// Replication redelivering a rev we already have - a no-op.
|
|
447
|
+
results[i] = { ok: true, id, rev: docInfo.metadata.rev };
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const previousWinningRev = entry.rev;
|
|
451
|
+
const previouslyDeleted = !!entry.deleted;
|
|
452
|
+
let deleted = docInfo.metadata.deleted !== undefined ? docInfo.metadata.deleted : false;
|
|
453
|
+
const isRoot = /^1-/.test(docInfo.metadata.rev);
|
|
454
|
+
// Undeleting via a fresh newEdits put re-parents onto the tombstone
|
|
455
|
+
// rev instead of conflicting (CouchDB "resurrection").
|
|
456
|
+
if (previouslyDeleted && !deleted && newEdits && isRoot) {
|
|
457
|
+
const resurrected = Object.assign({}, docInfo.data, { _id: id, _rev: previousWinningRev });
|
|
458
|
+
const reparsed = (0, pouchdb_adapter_utils_1.parseDoc)(resurrected, newEdits);
|
|
459
|
+
if (!reparsed || !reparsed.metadata) {
|
|
460
|
+
results[i] = Object.assign({ id }, reparsed);
|
|
461
|
+
continue;
|
|
368
462
|
}
|
|
463
|
+
savedDocInfo = reparsed;
|
|
464
|
+
deleted = savedDocInfo.metadata.deleted !== undefined ? savedDocInfo.metadata.deleted : false;
|
|
369
465
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
466
|
+
const merged = (0, pouchdb_merge_1.merge)(existingTree, savedDocInfo.metadata.rev_tree[0], revsLimit);
|
|
467
|
+
const inConflict = newEdits &&
|
|
468
|
+
((previouslyDeleted && deleted && merged.conflicts !== 'new_leaf') ||
|
|
469
|
+
(!previouslyDeleted && merged.conflicts !== 'new_leaf') ||
|
|
470
|
+
(previouslyDeleted && !deleted && merged.conflicts === 'new_branch'));
|
|
471
|
+
if (inConflict) {
|
|
472
|
+
results[i] = { ok: false, id, error: 'conflict', reason: 'Document update conflict' };
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
mergedTree = merged.tree;
|
|
476
|
+
stemmedRevs = merged.stemmedRevs;
|
|
477
|
+
winning = (0, pouchdb_merge_1.winningRev)({ rev_tree: mergedTree });
|
|
478
|
+
winningDeleted = (0, pouchdb_merge_1.isDeleted)({ rev_tree: mergedTree }, winning);
|
|
479
|
+
incomingRev = savedDocInfo.metadata.rev;
|
|
480
|
+
selfIsWinner = incomingRev === winning;
|
|
481
|
+
// Carry forward existing conflicts, minus anything just stemmed
|
|
482
|
+
// past revs_limit (pouchdb-adapter-native's own stemBodies
|
|
483
|
+
// handles the equivalent body cleanup for its own storage;
|
|
484
|
+
// compact() here does the same, see drive.ts).
|
|
485
|
+
if (entry.conflictLocations) {
|
|
486
|
+
for (const rev of Object.keys(entry.conflictLocations)) {
|
|
487
|
+
if (!stemmedRevs.includes(rev))
|
|
488
|
+
conflictLocations[rev] = entry.conflictLocations[rev];
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (selfIsWinner) {
|
|
492
|
+
// The old winner becomes a conflict leaf, unless it just got stemmed.
|
|
493
|
+
if (previousWinningRev !== winning && !stemmedRevs.includes(previousWinningRev)) {
|
|
494
|
+
conflictLocations[previousWinningRev] = entry.location;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
else if (!stemmedRevs.includes(incomingRev)) {
|
|
498
|
+
conflictLocations[incomingRev] = { fileId: SELF_FILE };
|
|
373
499
|
}
|
|
374
|
-
changes.push({
|
|
375
|
-
seq,
|
|
376
|
-
id,
|
|
377
|
-
rev: newRev,
|
|
378
|
-
doc: savedDoc,
|
|
379
|
-
timestamp: Date.now()
|
|
380
|
-
});
|
|
381
|
-
results.push({ ok: true, id, rev: newRev });
|
|
382
500
|
}
|
|
501
|
+
const seq = db.getNextSeq() + changes.length;
|
|
502
|
+
const savedDoc = Object.assign({}, savedDocInfo.data, { _id: id, _rev: incomingRev });
|
|
503
|
+
const nextIndexEntry = {
|
|
504
|
+
tree: JSON.stringify(mergedTree),
|
|
505
|
+
rev: winning,
|
|
506
|
+
deleted: winningDeleted,
|
|
507
|
+
location: selfIsWinner ? { fileId: SELF_FILE } : entry.location,
|
|
508
|
+
};
|
|
509
|
+
if (Object.keys(conflictLocations).length > 0) {
|
|
510
|
+
nextIndexEntry.conflictLocations = conflictLocations;
|
|
511
|
+
}
|
|
512
|
+
changes.push({
|
|
513
|
+
seq,
|
|
514
|
+
id,
|
|
515
|
+
rev: incomingRev,
|
|
516
|
+
deleted: savedDocInfo.metadata.deleted,
|
|
517
|
+
doc: savedDoc,
|
|
518
|
+
timestamp: Date.now(),
|
|
519
|
+
nextIndexEntry,
|
|
520
|
+
});
|
|
521
|
+
results[i] = { ok: true, id, rev: incomingRev };
|
|
383
522
|
}
|
|
384
523
|
log('_bulkDocs flushing', changes.length, 'changes');
|
|
385
524
|
// Append changes to log
|
|
@@ -421,11 +560,13 @@ function GoogleDriveAdapter(PouchDB) {
|
|
|
421
560
|
const change = {
|
|
422
561
|
id: id,
|
|
423
562
|
seq: entry.seq,
|
|
424
|
-
changes:
|
|
563
|
+
changes: buildChangesList(entry, opts.style)
|
|
425
564
|
};
|
|
426
565
|
if (opts.include_docs) {
|
|
427
|
-
|
|
428
|
-
|
|
566
|
+
// Same tombstone rule as the initial pass: a `null` body
|
|
567
|
+
// makes a filtered replication drop the deletion.
|
|
568
|
+
loadChangeBodies([{ id, entry }]).then(bodies => {
|
|
569
|
+
change.doc = bodies[id];
|
|
429
570
|
if (opts.onChange)
|
|
430
571
|
opts.onChange(change);
|
|
431
572
|
lastSeq = Math.max(lastSeq, change.seq);
|
|
@@ -441,37 +582,91 @@ function GoogleDriveAdapter(PouchDB) {
|
|
|
441
582
|
};
|
|
442
583
|
cancelLive = db.onChange(liveListener);
|
|
443
584
|
}
|
|
585
|
+
/**
|
|
586
|
+
* Fetches the document bodies for a whole batch in one pass.
|
|
587
|
+
*
|
|
588
|
+
* `getMulti` groups ids by the file that holds them, so a batch drawn from a
|
|
589
|
+
* single change-log file costs one download rather than one per document -
|
|
590
|
+
* which is what a serial `get()` per change was costing. This path is only
|
|
591
|
+
* ever taken because something asked for `include_docs`, and the one thing
|
|
592
|
+
* that always asks is a filtered replication: `pouchdb-replication` forces
|
|
593
|
+
* `include_docs: true` so it can run its filter over `change.doc`.
|
|
594
|
+
*
|
|
595
|
+
* Deleted documents have no body to fetch, and a `null` there is not
|
|
596
|
+
* harmless: `pouchdb-replication`'s `filterChange` substitutes `{}` for a
|
|
597
|
+
* missing `change.doc`, and a filter given `{}` has no id to judge, so it
|
|
598
|
+
* drops the change. A filtered replication would then never propagate a
|
|
599
|
+
* deletion. Emit the tombstone the rest of PouchDB expects instead.
|
|
600
|
+
*/
|
|
601
|
+
async function loadChangeBodies(batch) {
|
|
602
|
+
const bodies = {};
|
|
603
|
+
if (!batch.length)
|
|
604
|
+
return bodies;
|
|
605
|
+
const tombstone = (row) => ({
|
|
606
|
+
_id: row.id,
|
|
607
|
+
_rev: row.entry.rev,
|
|
608
|
+
_deleted: true
|
|
609
|
+
});
|
|
610
|
+
const live = batch.filter(row => !row.entry.deleted);
|
|
611
|
+
for (const row of batch) {
|
|
612
|
+
if (row.entry.deleted)
|
|
613
|
+
bodies[row.id] = tombstone(row);
|
|
614
|
+
}
|
|
615
|
+
if (!live.length)
|
|
616
|
+
return bodies;
|
|
617
|
+
try {
|
|
618
|
+
const docs = await db.getMulti(live.map(row => row.id));
|
|
619
|
+
live.forEach((row, index) => {
|
|
620
|
+
bodies[row.id] = docs[index] || tombstone(row);
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
catch (e) {
|
|
624
|
+
log('_changes include_docs error', e);
|
|
625
|
+
for (const row of live)
|
|
626
|
+
bodies[row.id] = tombstone(row);
|
|
627
|
+
}
|
|
628
|
+
return bodies;
|
|
629
|
+
}
|
|
444
630
|
// Process initial changes
|
|
445
631
|
async function processChangesAsync() {
|
|
446
632
|
log('_changes processing since', since, 'limit', limit, 'live', !!opts.live);
|
|
447
633
|
const keys = await db.getIndexKeys();
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
634
|
+
// The index is a plain object keyed by document id, so iterating it
|
|
635
|
+
// yields insertion order, not sequence order. Replication checkpoints on
|
|
636
|
+
// the highest seq in each batch and `limit` makes every batch a partial
|
|
637
|
+
// one, so an unordered batch can checkpoint past a change it never
|
|
638
|
+
// emitted - and that change is then never replicated again. Order by seq
|
|
639
|
+
// before the batch is cut.
|
|
640
|
+
const pending = keys
|
|
641
|
+
.filter((id) => !id.startsWith('_local/'))
|
|
642
|
+
.map((id) => ({ id, entry: db.getIndexEntry(id) }))
|
|
643
|
+
.filter((row) => Boolean(row.entry) && row.entry.seq > since)
|
|
644
|
+
.sort((a, b) => a.entry.seq - b.entry.seq);
|
|
645
|
+
// `descending` reverses the order the changes come back in. Its `since`
|
|
646
|
+
// semantics are CouchDB's (walk down from the given seq) and are not
|
|
647
|
+
// implemented here; replication never asks for it.
|
|
648
|
+
if (opts.descending)
|
|
649
|
+
pending.reverse();
|
|
650
|
+
// Cut the batch before fetching anything: `include_docs` costs a download
|
|
651
|
+
// per *file*, and there is no reason to pay it for changes beyond `limit`.
|
|
652
|
+
const batch = pending.slice(0, limit);
|
|
653
|
+
const bodies = opts.include_docs
|
|
654
|
+
? await loadChangeBodies(batch)
|
|
655
|
+
: null;
|
|
656
|
+
for (const { id, entry } of batch) {
|
|
657
|
+
if (complete)
|
|
451
658
|
break;
|
|
452
|
-
if (id.startsWith('_local/'))
|
|
453
|
-
continue;
|
|
454
|
-
const entry = db.getIndexEntry(id);
|
|
455
|
-
if (!entry || entry.seq <= since)
|
|
456
|
-
continue;
|
|
457
659
|
const change = {
|
|
458
660
|
id: id,
|
|
459
661
|
seq: entry.seq,
|
|
460
|
-
changes:
|
|
662
|
+
changes: buildChangesList(entry, opts.style)
|
|
461
663
|
};
|
|
462
|
-
if (
|
|
463
|
-
|
|
464
|
-
change.doc = await db.get(id);
|
|
465
|
-
}
|
|
466
|
-
catch (e) {
|
|
467
|
-
log('_changes include_docs error', e);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
664
|
+
if (bodies)
|
|
665
|
+
change.doc = bodies[id];
|
|
470
666
|
if (opts.onChange)
|
|
471
667
|
opts.onChange(change);
|
|
472
668
|
if (returnDocs)
|
|
473
669
|
results.push(change);
|
|
474
|
-
processed++;
|
|
475
670
|
lastSeq = Math.max(lastSeq, entry.seq);
|
|
476
671
|
}
|
|
477
672
|
// ✅ Call opts.complete() ONLY for non-live modes
|
|
@@ -498,7 +693,11 @@ function GoogleDriveAdapter(PouchDB) {
|
|
|
498
693
|
};
|
|
499
694
|
};
|
|
500
695
|
// Manual compaction trigger
|
|
501
|
-
api._compact = function (callback) {
|
|
696
|
+
api._compact = function (opts, callback) {
|
|
697
|
+
if (typeof opts === 'function') {
|
|
698
|
+
callback = opts;
|
|
699
|
+
opts = {};
|
|
700
|
+
}
|
|
502
701
|
const promise = db.compact().then(() => {
|
|
503
702
|
const result = { ok: true };
|
|
504
703
|
if (callback)
|
|
@@ -516,14 +715,11 @@ function GoogleDriveAdapter(PouchDB) {
|
|
|
516
715
|
if (!entry) {
|
|
517
716
|
return callback({ status: 404, error: true, name: 'not_found', message: 'missing' });
|
|
518
717
|
}
|
|
519
|
-
//
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
ids: [revHash, { status: 'available' }, []]
|
|
525
|
-
}];
|
|
526
|
-
callback(null, revTree);
|
|
718
|
+
// The real tree, as maintained by _bulkDocs's merge() calls - not a
|
|
719
|
+
// synthesized single-leaf stand-in. This is what makes PouchDB core's
|
|
720
|
+
// default revsDiff (which walks this) actually see conflicting revisions
|
|
721
|
+
// instead of only ever knowing about "whatever's currently winning".
|
|
722
|
+
callback(null, JSON.parse(entry.tree));
|
|
527
723
|
};
|
|
528
724
|
api._close = function (callback) {
|
|
529
725
|
db.stopPolling();
|