@docstack/pouchdb-adapter-googledrive 0.1.1 → 0.1.2
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 +88 -57
- 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;
|
|
@@ -290,45 +292,61 @@ class DriveHandler {
|
|
|
290
292
|
return cached;
|
|
291
293
|
}
|
|
292
294
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
if (
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
295
|
+
// Always check pending downloads. A download in progress is as fresh as it
|
|
296
|
+
// can be right now, so we can reuse it even if skipCache is true.
|
|
297
|
+
const pending = this.pendingDownloads.get(fileId);
|
|
298
|
+
if (pending) {
|
|
299
|
+
this.log('fetchFile reuse pending download', fileId);
|
|
300
|
+
return await pending;
|
|
301
|
+
}
|
|
302
|
+
const downloadPromise = (async () => {
|
|
303
|
+
try {
|
|
304
|
+
this.log('fetchFile downloading', fileId);
|
|
305
|
+
const data = await this.client.getFile(fileId);
|
|
306
|
+
let parsed;
|
|
307
|
+
if (typeof data === 'string') {
|
|
308
|
+
const trimmed = data.trim();
|
|
309
|
+
if (trimmed.startsWith('{')) {
|
|
310
|
+
// Could be JSON or NDJSON
|
|
311
|
+
if (trimmed.includes('\n')) {
|
|
312
|
+
// Definitely NDJSON (multiple lines)
|
|
313
|
+
try {
|
|
314
|
+
const lines = trimmed.split('\n').filter(l => l);
|
|
315
|
+
parsed = lines.map(line => JSON.parse(line));
|
|
316
|
+
}
|
|
317
|
+
catch (e) {
|
|
318
|
+
parsed = data;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
// Single line. Try regular JSON first.
|
|
323
|
+
try {
|
|
324
|
+
parsed = JSON.parse(trimmed);
|
|
325
|
+
}
|
|
326
|
+
catch (e) {
|
|
327
|
+
parsed = data;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
305
330
|
}
|
|
306
|
-
|
|
331
|
+
else {
|
|
307
332
|
parsed = data;
|
|
308
333
|
}
|
|
309
334
|
}
|
|
310
335
|
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
|
-
}
|
|
336
|
+
parsed = data;
|
|
321
337
|
}
|
|
338
|
+
if (!skipCache)
|
|
339
|
+
this.fileCache.put(fileId, parsed);
|
|
340
|
+
return parsed;
|
|
322
341
|
}
|
|
323
|
-
|
|
324
|
-
|
|
342
|
+
finally {
|
|
343
|
+
if (!skipCache)
|
|
344
|
+
this.pendingDownloads.delete(fileId);
|
|
325
345
|
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
this.fileCache.put(fileId, parsed);
|
|
331
|
-
return parsed;
|
|
346
|
+
})();
|
|
347
|
+
if (!skipCache)
|
|
348
|
+
this.pendingDownloads.set(fileId, downloadPromise);
|
|
349
|
+
return await downloadPromise;
|
|
332
350
|
}
|
|
333
351
|
/** Get multiple docs (Atomic-ish) used for _allDocs */
|
|
334
352
|
async getMulti(ids) {
|
|
@@ -652,7 +670,7 @@ class DriveHandler {
|
|
|
652
670
|
const metaFile = await this.findFile('_meta.json');
|
|
653
671
|
if (!metaFile)
|
|
654
672
|
throw new Error('Meta missing');
|
|
655
|
-
const validMeta = await this.downloadJson(metaFile.
|
|
673
|
+
const validMeta = await this.downloadJson(metaFile.fileId, true); // No cache
|
|
656
674
|
const newMeta = modifier(validMeta);
|
|
657
675
|
await this.saveMeta(newMeta, metaFile.etag);
|
|
658
676
|
this.meta = newMeta;
|
|
@@ -679,29 +697,42 @@ class DriveHandler {
|
|
|
679
697
|
return createRes.id;
|
|
680
698
|
}
|
|
681
699
|
async findFile(name) {
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
700
|
+
const pending = this.pendingFinds.get(name);
|
|
701
|
+
if (pending) {
|
|
702
|
+
this.log('findFile reuse pending search', name);
|
|
703
|
+
return await pending;
|
|
704
|
+
}
|
|
705
|
+
const findPromise = (async () => {
|
|
706
|
+
const safeName = this.escapeQuery(name);
|
|
707
|
+
const q = `name = '${safeName}' and '${this.folderId}' in parents and trashed = false`;
|
|
708
|
+
try {
|
|
709
|
+
const files = await this.client.listFiles(q);
|
|
710
|
+
if (files.length > 0) {
|
|
711
|
+
let file = files[0];
|
|
712
|
+
if (!file.etag) {
|
|
713
|
+
// Robustness: Fetch metadata for the file if etag is missing from list
|
|
714
|
+
try {
|
|
715
|
+
file = await this.client.getFileMetadata(file.id);
|
|
716
|
+
}
|
|
717
|
+
catch (e) {
|
|
718
|
+
this.log('Failed to fetch file metadata for etag', file.id, e);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return {
|
|
722
|
+
fileId: file.id,
|
|
723
|
+
etag: file.etag,
|
|
724
|
+
md5Checksum: file.md5Checksum,
|
|
725
|
+
modifiedTime: file.modifiedTime
|
|
726
|
+
};
|
|
696
727
|
}
|
|
728
|
+
return null;
|
|
697
729
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
return null;
|
|
730
|
+
finally {
|
|
731
|
+
this.pendingFinds.delete(name);
|
|
732
|
+
}
|
|
733
|
+
})();
|
|
734
|
+
this.pendingFinds.set(name, findPromise);
|
|
735
|
+
return await findPromise;
|
|
705
736
|
}
|
|
706
737
|
async downloadJson(fileId, skipCache = false) {
|
|
707
738
|
return await this.fetchFile(fileId, skipCache);
|
|
@@ -724,11 +755,11 @@ class DriveHandler {
|
|
|
724
755
|
const content = JSON.stringify(meta);
|
|
725
756
|
const metaFile = await this.findFile('_meta.json');
|
|
726
757
|
if (metaFile) {
|
|
727
|
-
const res = await this.client.updateFile(metaFile.
|
|
758
|
+
const res = await this.client.updateFile(metaFile.fileId, content, expectedEtag || undefined);
|
|
728
759
|
this.metaEtag = res.etag;
|
|
729
760
|
this.metaMd5 = res.md5Checksum || null;
|
|
730
761
|
this.metaModifiedTime = res.modifiedTime;
|
|
731
|
-
this.fileCache.remove(metaFile.
|
|
762
|
+
this.fileCache.remove(metaFile.fileId); // Invalidate cache
|
|
732
763
|
}
|
|
733
764
|
else {
|
|
734
765
|
const res = await this.client.createFile('_meta.json', [this.folderId], 'application/json', content);
|
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;
|