@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 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
- newRev = revNum + '-' + generateRevId();
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 rev = '0-1';
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.id });
101
- this.meta = await this.downloadJson(metaFile.id, true); // No cache for meta
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.currentLogSizeEstimate += 100 * changesArray.length;
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
- this.log('Processed log', id);
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
- this.log('fetchFile downloading', fileId);
294
- const data = await this.client.getFile(fileId);
295
- let parsed;
296
- if (typeof data === 'string') {
297
- const trimmed = data.trim();
298
- if (trimmed.startsWith('{')) {
299
- // Could be JSON or NDJSON
300
- if (trimmed.includes('\n')) {
301
- // Definitely NDJSON (multiple lines)
302
- try {
303
- const lines = trimmed.split('\n').filter(l => l);
304
- parsed = lines.map(line => JSON.parse(line));
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
- catch (e) {
340
+ else {
307
341
  parsed = data;
308
342
  }
309
343
  }
310
344
  else {
311
- // Single line. Try regular JSON first.
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
- else {
324
- parsed = data;
351
+ finally {
352
+ if (!skipCache)
353
+ this.pendingDownloads.delete(fileId);
325
354
  }
326
- }
327
- else {
328
- parsed = data;
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 our own write
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.id, true); // No cache
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
- id: file.id,
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.id, content, expectedEtag || undefined);
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.id); // Invalidate cache
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
- // Observers expecting 'docs' object might be broken if they expect FULL body.
829
- // We can pass empty object or partials?
830
- // Real PouchDB changes feed calls `db.changes()`.
831
- // Our `adapter.js` uses `db.onChange` effectively.
832
- // We should pass a map of { ID: { _rev, ... } } (Index entries)
833
- // Adapter needs to handle this.
834
- const changes = {};
835
- for (const [id, entry] of Object.entries(this.index)) {
836
- changes[id] = {
837
- _id: id,
838
- _rev: entry.rev,
839
- _deleted: !!entry.deleted,
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docstack/pouchdb-adapter-googledrive",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "PouchDB adapter for Google Drive",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",