@docstack/pouchdb-adapter-googledrive 0.1.1 → 0.1.3
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/lib/adapter.js +23 -3
- package/lib/drive.d.ts +2 -0
- package/lib/drive.js +98 -62
- package/lib/types.d.ts +3 -0
- package/package.json +1 -1
package/lib/adapter.js
CHANGED
|
@@ -350,15 +350,27 @@ function GoogleDriveAdapter(PouchDB) {
|
|
|
350
350
|
}
|
|
351
351
|
else {
|
|
352
352
|
let newRev;
|
|
353
|
+
let savedDoc;
|
|
353
354
|
if (newEdits) {
|
|
354
355
|
const oldRev = entry?.rev || '0-0';
|
|
355
356
|
const revNum = parseInt(oldRev.split('-')[0], 10) + 1;
|
|
356
|
-
|
|
357
|
+
const revHash = generateRevId();
|
|
358
|
+
newRev = revNum + '-' + revHash;
|
|
359
|
+
savedDoc = Object.assign({}, doc, { _rev: newRev });
|
|
360
|
+
if (doc._revisions) {
|
|
361
|
+
savedDoc._revisions = {
|
|
362
|
+
start: revNum,
|
|
363
|
+
ids: [revHash, ...(doc._revisions.ids || [])]
|
|
364
|
+
};
|
|
365
|
+
if (savedDoc._revisions.ids.length > 500) {
|
|
366
|
+
savedDoc._revisions.ids = savedDoc._revisions.ids.slice(0, 500);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
357
369
|
}
|
|
358
370
|
else {
|
|
359
371
|
newRev = doc._rev;
|
|
372
|
+
savedDoc = Object.assign({}, doc, { _rev: newRev });
|
|
360
373
|
}
|
|
361
|
-
const savedDoc = Object.assign({}, doc, { _rev: newRev });
|
|
362
374
|
changes.push({
|
|
363
375
|
seq,
|
|
364
376
|
id,
|
|
@@ -558,8 +570,16 @@ function GoogleDriveAdapter(PouchDB) {
|
|
|
558
570
|
opts = {};
|
|
559
571
|
}
|
|
560
572
|
const id = doc._id;
|
|
561
|
-
const
|
|
573
|
+
const revNum = 1;
|
|
574
|
+
const revHash = generateRevId();
|
|
575
|
+
const rev = revNum + '-' + revHash;
|
|
562
576
|
const savedDoc = Object.assign({}, doc, { _rev: rev });
|
|
577
|
+
if (doc._revisions) {
|
|
578
|
+
savedDoc._revisions = {
|
|
579
|
+
start: revNum,
|
|
580
|
+
ids: [revHash, ...(doc._revisions.ids || [])]
|
|
581
|
+
};
|
|
582
|
+
}
|
|
563
583
|
const change = {
|
|
564
584
|
seq: db.getNextSeq(),
|
|
565
585
|
id,
|
package/lib/drive.d.ts
CHANGED
|
@@ -35,6 +35,8 @@ export declare class DriveHandler {
|
|
|
35
35
|
private currentSnapshotIndexId;
|
|
36
36
|
private debug;
|
|
37
37
|
private isCompacting;
|
|
38
|
+
private pendingDownloads;
|
|
39
|
+
private pendingFinds;
|
|
38
40
|
private log;
|
|
39
41
|
constructor(options: GoogleDriveAdapterOptions, dbName: string);
|
|
40
42
|
get seq(): number;
|
package/lib/drive.js
CHANGED
|
@@ -45,6 +45,8 @@ class DriveHandler {
|
|
|
45
45
|
this.currentSnapshotIndexId = null;
|
|
46
46
|
this.debug = false;
|
|
47
47
|
this.isCompacting = false;
|
|
48
|
+
this.pendingDownloads = new Map();
|
|
49
|
+
this.pendingFinds = new Map();
|
|
48
50
|
const clientOptions = { ...options };
|
|
49
51
|
if (options.testMode) {
|
|
50
52
|
const serverUrl = options.testServerUrl || 'http://localhost:3000';
|
|
@@ -97,8 +99,8 @@ class DriveHandler {
|
|
|
97
99
|
}
|
|
98
100
|
const metaFile = await this.findFile('_meta.json');
|
|
99
101
|
if (metaFile) {
|
|
100
|
-
this.log('Retrieved meta file', { fileId: metaFile.
|
|
101
|
-
this.meta = await this.downloadJson(metaFile.
|
|
102
|
+
this.log('Retrieved meta file', { fileId: metaFile.fileId });
|
|
103
|
+
this.meta = await this.downloadJson(metaFile.fileId, true); // No cache for meta
|
|
102
104
|
this.metaEtag = metaFile.etag || null;
|
|
103
105
|
this.metaMd5 = metaFile.md5Checksum || null;
|
|
104
106
|
this.metaModifiedTime = metaFile.modifiedTime || null;
|
|
@@ -158,22 +160,31 @@ class DriveHandler {
|
|
|
158
160
|
return { id, changes: null };
|
|
159
161
|
}
|
|
160
162
|
}));
|
|
163
|
+
const foundNew = {};
|
|
161
164
|
for (const { id, changes } of logResults) {
|
|
162
165
|
if (!changes) {
|
|
163
166
|
this.log(`Skipping failed log ${id}`);
|
|
164
167
|
continue;
|
|
165
168
|
}
|
|
166
169
|
let changesArray = Array.isArray(changes) ? changes : [changes];
|
|
167
|
-
this.
|
|
170
|
+
this.log('Processing log file', id, 'changes', changesArray.length);
|
|
168
171
|
for (const change of changesArray) {
|
|
169
|
-
this.log('Processing change, sequence', change.seq);
|
|
170
172
|
this.updateIndex(change, id);
|
|
171
173
|
if (this.docCache.get(change.id)) {
|
|
172
174
|
this.docCache.remove(change.id);
|
|
173
175
|
}
|
|
176
|
+
foundNew[change.id] = {
|
|
177
|
+
_id: change.id,
|
|
178
|
+
_rev: change.rev,
|
|
179
|
+
_deleted: !!change.deleted,
|
|
180
|
+
seq: change.seq
|
|
181
|
+
};
|
|
174
182
|
}
|
|
175
183
|
this.processedLogIds.add(id);
|
|
176
|
-
|
|
184
|
+
}
|
|
185
|
+
if (Object.keys(foundNew).length > 0) {
|
|
186
|
+
this.log('Load complete, notifying of changes', Object.keys(foundNew).length);
|
|
187
|
+
this.notifyListeners(foundNew);
|
|
177
188
|
}
|
|
178
189
|
}
|
|
179
190
|
// 2. Replay NEW Change Logs (Metadata only updates)
|
|
@@ -290,45 +301,61 @@ class DriveHandler {
|
|
|
290
301
|
return cached;
|
|
291
302
|
}
|
|
292
303
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
if (
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
304
|
+
// Always check pending downloads. A download in progress is as fresh as it
|
|
305
|
+
// can be right now, so we can reuse it even if skipCache is true.
|
|
306
|
+
const pending = this.pendingDownloads.get(fileId);
|
|
307
|
+
if (pending) {
|
|
308
|
+
this.log('fetchFile reuse pending download', fileId);
|
|
309
|
+
return await pending;
|
|
310
|
+
}
|
|
311
|
+
const downloadPromise = (async () => {
|
|
312
|
+
try {
|
|
313
|
+
this.log('fetchFile downloading', fileId);
|
|
314
|
+
const data = await this.client.getFile(fileId);
|
|
315
|
+
let parsed;
|
|
316
|
+
if (typeof data === 'string') {
|
|
317
|
+
const trimmed = data.trim();
|
|
318
|
+
if (trimmed.startsWith('{')) {
|
|
319
|
+
// Could be JSON or NDJSON
|
|
320
|
+
if (trimmed.includes('\n')) {
|
|
321
|
+
// Definitely NDJSON (multiple lines)
|
|
322
|
+
try {
|
|
323
|
+
const lines = trimmed.split('\n').filter(l => l);
|
|
324
|
+
parsed = lines.map(line => JSON.parse(line));
|
|
325
|
+
}
|
|
326
|
+
catch (e) {
|
|
327
|
+
parsed = data;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
// Single line. Try regular JSON first.
|
|
332
|
+
try {
|
|
333
|
+
parsed = JSON.parse(trimmed);
|
|
334
|
+
}
|
|
335
|
+
catch (e) {
|
|
336
|
+
parsed = data;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
305
339
|
}
|
|
306
|
-
|
|
340
|
+
else {
|
|
307
341
|
parsed = data;
|
|
308
342
|
}
|
|
309
343
|
}
|
|
310
344
|
else {
|
|
311
|
-
|
|
312
|
-
try {
|
|
313
|
-
parsed = JSON.parse(trimmed);
|
|
314
|
-
// Optional: if we know it was supposed to be NDJSON?
|
|
315
|
-
// Our change files are always NDJSON.
|
|
316
|
-
// But _meta.json and snapshot-*.json are regular JSON.
|
|
317
|
-
}
|
|
318
|
-
catch (e) {
|
|
319
|
-
parsed = data;
|
|
320
|
-
}
|
|
345
|
+
parsed = data;
|
|
321
346
|
}
|
|
347
|
+
if (!skipCache)
|
|
348
|
+
this.fileCache.put(fileId, parsed);
|
|
349
|
+
return parsed;
|
|
322
350
|
}
|
|
323
|
-
|
|
324
|
-
|
|
351
|
+
finally {
|
|
352
|
+
if (!skipCache)
|
|
353
|
+
this.pendingDownloads.delete(fileId);
|
|
325
354
|
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
this.fileCache.put(fileId, parsed);
|
|
331
|
-
return parsed;
|
|
355
|
+
})();
|
|
356
|
+
if (!skipCache)
|
|
357
|
+
this.pendingDownloads.set(fileId, downloadPromise);
|
|
358
|
+
return await downloadPromise;
|
|
332
359
|
}
|
|
333
360
|
/** Get multiple docs (Atomic-ish) used for _allDocs */
|
|
334
361
|
async getMulti(ids) {
|
|
@@ -490,13 +517,21 @@ class DriveHandler {
|
|
|
490
517
|
}
|
|
491
518
|
this.localDocsEtag = res.etag;
|
|
492
519
|
// Update Index
|
|
520
|
+
const changedDocs = {};
|
|
493
521
|
for (const change of changes) {
|
|
494
522
|
this.updateIndex(change, res.id);
|
|
495
523
|
if (change.doc)
|
|
496
524
|
this.docCache.put(change.id, change.doc);
|
|
497
525
|
else
|
|
498
526
|
this.docCache.remove(change.id);
|
|
527
|
+
changedDocs[change.id] = {
|
|
528
|
+
_id: change.id,
|
|
529
|
+
_rev: change.rev,
|
|
530
|
+
_deleted: !!change.deleted,
|
|
531
|
+
seq: change.seq
|
|
532
|
+
};
|
|
499
533
|
}
|
|
534
|
+
this.notifyListeners(changedDocs);
|
|
500
535
|
return;
|
|
501
536
|
}
|
|
502
537
|
catch (err) {
|
|
@@ -521,6 +556,7 @@ class DriveHandler {
|
|
|
521
556
|
await this.saveMeta(nextMeta, this.metaEtag);
|
|
522
557
|
// 4. Update Local State
|
|
523
558
|
this.meta = nextMeta;
|
|
559
|
+
const changedDocs = {};
|
|
524
560
|
for (const change of changes) {
|
|
525
561
|
this.updateIndex(change, fileId);
|
|
526
562
|
if (change.doc) {
|
|
@@ -529,9 +565,15 @@ class DriveHandler {
|
|
|
529
565
|
else if (change.deleted) {
|
|
530
566
|
this.docCache.remove(change.id);
|
|
531
567
|
}
|
|
568
|
+
changedDocs[change.id] = {
|
|
569
|
+
_id: change.id,
|
|
570
|
+
_rev: change.rev,
|
|
571
|
+
_deleted: !!change.deleted,
|
|
572
|
+
seq: change.seq
|
|
573
|
+
};
|
|
532
574
|
}
|
|
533
|
-
// Notify local changes feed listeners about
|
|
534
|
-
this.notifyListeners();
|
|
575
|
+
// Notify local changes feed listeners about only what we just wrote
|
|
576
|
+
this.notifyListeners(changedDocs);
|
|
535
577
|
// 5. Compaction Check
|
|
536
578
|
const totalChanges = await this.countTotalChanges();
|
|
537
579
|
if (totalChanges >= this.compactionThreshold ||
|
|
@@ -652,7 +694,7 @@ class DriveHandler {
|
|
|
652
694
|
const metaFile = await this.findFile('_meta.json');
|
|
653
695
|
if (!metaFile)
|
|
654
696
|
throw new Error('Meta missing');
|
|
655
|
-
const validMeta = await this.downloadJson(metaFile.
|
|
697
|
+
const validMeta = await this.downloadJson(metaFile.fileId, true); // No cache
|
|
656
698
|
const newMeta = modifier(validMeta);
|
|
657
699
|
await this.saveMeta(newMeta, metaFile.etag);
|
|
658
700
|
this.meta = newMeta;
|
|
@@ -679,15 +721,12 @@ class DriveHandler {
|
|
|
679
721
|
return createRes.id;
|
|
680
722
|
}
|
|
681
723
|
async findFile(name) {
|
|
682
|
-
if (!this.folderId)
|
|
683
|
-
return null;
|
|
684
724
|
const safeName = this.escapeQuery(name);
|
|
685
725
|
const q = `name = '${safeName}' and '${this.folderId}' in parents and trashed = false`;
|
|
686
726
|
const files = await this.client.listFiles(q);
|
|
687
727
|
if (files.length > 0) {
|
|
688
728
|
let file = files[0];
|
|
689
729
|
if (!file.etag) {
|
|
690
|
-
// Robustness: Fetch metadata for the file if etag is missing from list
|
|
691
730
|
try {
|
|
692
731
|
file = await this.client.getFileMetadata(file.id);
|
|
693
732
|
}
|
|
@@ -696,8 +735,9 @@ class DriveHandler {
|
|
|
696
735
|
}
|
|
697
736
|
}
|
|
698
737
|
return {
|
|
699
|
-
|
|
738
|
+
fileId: file.id,
|
|
700
739
|
etag: file.etag,
|
|
740
|
+
md5Checksum: file.md5Checksum,
|
|
701
741
|
modifiedTime: file.modifiedTime
|
|
702
742
|
};
|
|
703
743
|
}
|
|
@@ -724,11 +764,11 @@ class DriveHandler {
|
|
|
724
764
|
const content = JSON.stringify(meta);
|
|
725
765
|
const metaFile = await this.findFile('_meta.json');
|
|
726
766
|
if (metaFile) {
|
|
727
|
-
const res = await this.client.updateFile(metaFile.
|
|
767
|
+
const res = await this.client.updateFile(metaFile.fileId, content, expectedEtag || undefined);
|
|
728
768
|
this.metaEtag = res.etag;
|
|
729
769
|
this.metaMd5 = res.md5Checksum || null;
|
|
730
770
|
this.metaModifiedTime = res.modifiedTime;
|
|
731
|
-
this.fileCache.remove(metaFile.
|
|
771
|
+
this.fileCache.remove(metaFile.fileId); // Invalidate cache
|
|
732
772
|
}
|
|
733
773
|
else {
|
|
734
774
|
const res = await this.client.createFile('_meta.json', [this.folderId], 'application/json', content);
|
|
@@ -824,24 +864,20 @@ class DriveHandler {
|
|
|
824
864
|
}
|
|
825
865
|
}, intervalMs);
|
|
826
866
|
}
|
|
827
|
-
notifyListeners() {
|
|
828
|
-
|
|
829
|
-
//
|
|
830
|
-
//
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
seq: entry.seq // IMPORTANT: Missing previously, preventing filtered changes from working
|
|
841
|
-
};
|
|
867
|
+
notifyListeners(changedDocs) {
|
|
868
|
+
const changes = changedDocs || {};
|
|
869
|
+
// If no specifically changed docs provided, we don't want to firehose the whole index anymore
|
|
870
|
+
// as it causes reprocessing loops in PouchDB sync.
|
|
871
|
+
if (Object.keys(changes).length === 0)
|
|
872
|
+
return;
|
|
873
|
+
for (const cb of this.listeners) {
|
|
874
|
+
try {
|
|
875
|
+
cb(changes);
|
|
876
|
+
}
|
|
877
|
+
catch (e) {
|
|
878
|
+
this.log('Error in change listener callback', e);
|
|
879
|
+
}
|
|
842
880
|
}
|
|
843
|
-
for (const cb of this.listeners)
|
|
844
|
-
cb(changes);
|
|
845
881
|
}
|
|
846
882
|
// For tests/debug
|
|
847
883
|
onChange(cb) {
|
package/lib/types.d.ts
CHANGED
|
@@ -40,6 +40,9 @@ export interface ChangeEntry {
|
|
|
40
40
|
/** Location pointer for lazy loading */
|
|
41
41
|
export interface FilePointer {
|
|
42
42
|
fileId: string;
|
|
43
|
+
etag?: string;
|
|
44
|
+
md5Checksum?: string;
|
|
45
|
+
modifiedTime?: string;
|
|
43
46
|
/** Optional offset/length for future optimization (packed files) */
|
|
44
47
|
offset?: number;
|
|
45
48
|
length?: number;
|