@docstack/pouchdb-adapter-googledrive 0.0.6 → 0.0.8

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.
Files changed (160) hide show
  1. package/.env.example +3 -0
  2. package/.test-drive-integration/1xssnxhy3akj +1 -0
  3. package/.test-drive-integration/8p8eymf8afm +1 -0
  4. package/.test-drive-integration/d1a4udbbuh +1 -0
  5. package/.test-drive-integration/usx93j46xtl +1 -0
  6. package/.test-drive-integration/vc0pq3g8vp +1 -0
  7. package/.test-drive-integration/zenra4v6i +1 -0
  8. package/.test-drive-root/05wzn5mtlrzt +1 -0
  9. package/.test-drive-root/0axnnh1gmn2d +1 -0
  10. package/.test-drive-root/0l2pw73vk4i +1 -0
  11. package/.test-drive-root/0tzush8sunaj +1 -0
  12. package/.test-drive-root/143bh2d29rnl +1 -0
  13. package/.test-drive-root/1synnvu59il +1 -0
  14. package/.test-drive-root/239cd47eh23i +1 -0
  15. package/.test-drive-root/275ocoaj6nq +1 -0
  16. package/.test-drive-root/29d78af16s4 +1 -0
  17. package/.test-drive-root/2gzvpzclnwu +1 -0
  18. package/.test-drive-root/2pmk0leotvp +1 -0
  19. package/.test-drive-root/38ijn3s7p33 +1 -0
  20. package/.test-drive-root/3jm64la13rp +1 -0
  21. package/.test-drive-root/3l4zrzi5bm2 +1 -0
  22. package/.test-drive-root/3of67xqpsfh +1 -0
  23. package/.test-drive-root/3pi1rk70we5 +1 -0
  24. package/.test-drive-root/3w3n7tpdn4n +1 -0
  25. package/.test-drive-root/3xg31irodim +1 -0
  26. package/.test-drive-root/50asm455hsa +1 -0
  27. package/.test-drive-root/51ldbder4dm +1 -0
  28. package/.test-drive-root/5l7dt9r491r +1 -0
  29. package/.test-drive-root/65mfhb9yuq5 +1 -0
  30. package/.test-drive-root/6iocm2rs118 +1 -0
  31. package/.test-drive-root/6o6vlpa6r6j +1 -0
  32. package/.test-drive-root/6rrcl4sdhoi +1 -0
  33. package/.test-drive-root/6ucq10yu9bx +1 -0
  34. package/.test-drive-root/7de4gtsch5b +1 -0
  35. package/.test-drive-root/7j4j3eq54q +1 -0
  36. package/.test-drive-root/7n3ai8wgsvy +1 -0
  37. package/.test-drive-root/7v4b3eyitw3 +1 -0
  38. package/.test-drive-root/8e15eu4xksq +1 -0
  39. package/.test-drive-root/8pu2c63yh1j +1 -0
  40. package/.test-drive-root/8ryxg058q8g +1 -0
  41. package/.test-drive-root/99l00ber08g +1 -0
  42. package/.test-drive-root/9gxcumnvepb +1 -0
  43. package/.test-drive-root/a4xxhwpe2cp +1 -0
  44. package/.test-drive-root/a57knn0ufon +1 -0
  45. package/.test-drive-root/a7gszl8y5ir +1 -0
  46. package/.test-drive-root/b0nddw4fdbu +1 -0
  47. package/.test-drive-root/b3b1d1yuolh +1 -0
  48. package/.test-drive-root/b4syq7nig1f +1 -0
  49. package/.test-drive-root/bcc8mhlffkg +1 -0
  50. package/.test-drive-root/bmngbpsk34 +1 -0
  51. package/.test-drive-root/c9i4hvcll2s +1 -0
  52. package/.test-drive-root/cao688yanw +1 -0
  53. package/.test-drive-root/cdtduwukrek +1 -0
  54. package/.test-drive-root/ckzeli8w7ej +1 -0
  55. package/.test-drive-root/ctywezu2fi5 +1 -0
  56. package/.test-drive-root/cyxiv5vgtle +1 -0
  57. package/.test-drive-root/d83g0suoz08 +1 -0
  58. package/.test-drive-root/d9tefhps5m6 +1 -0
  59. package/.test-drive-root/dac156l068 +1 -0
  60. package/.test-drive-root/daqsvyj5cu9 +1 -0
  61. package/.test-drive-root/df0wbnhhz1 +1 -0
  62. package/.test-drive-root/difkbp1duv +1 -0
  63. package/.test-drive-root/djrry1wk98v +1 -0
  64. package/.test-drive-root/dx42jrz7ty9 +1 -0
  65. package/.test-drive-root/e0s76kp3ena +1 -0
  66. package/.test-drive-root/e9xvdv0mqig +1 -0
  67. package/.test-drive-root/f3xo9hy1956 +1 -0
  68. package/.test-drive-root/f4scgrpgejw +1 -0
  69. package/.test-drive-root/f9kbt63p7xp +1 -0
  70. package/.test-drive-root/fe07yvs6p14 +1 -0
  71. package/.test-drive-root/ff5suiec8xv +1 -0
  72. package/.test-drive-root/fwc1qm37as +1 -0
  73. package/.test-drive-root/gh39v4lzxjp +1 -0
  74. package/.test-drive-root/gxvkr1qq21b +1 -0
  75. package/.test-drive-root/h4p64z7djk5 +1 -0
  76. package/.test-drive-root/habe7525tnl +1 -0
  77. package/.test-drive-root/hhzddxj31ar +1 -0
  78. package/.test-drive-root/hyj9crn9sb +1 -0
  79. package/.test-drive-root/igey5y5f3mi +1 -0
  80. package/.test-drive-root/isvxwzkgym +1 -0
  81. package/.test-drive-root/javyqnt07ws +1 -0
  82. package/.test-drive-root/jevlhmyaczl +1 -0
  83. package/.test-drive-root/jve7ypin7bb +1 -0
  84. package/.test-drive-root/koycie6jmyr +1 -0
  85. package/.test-drive-root/kvlad8ps1l +1 -0
  86. package/.test-drive-root/l13wzs51qi +1 -0
  87. package/.test-drive-root/lbwwk07et6i +1 -0
  88. package/.test-drive-root/m3uoxcsft2g +1 -0
  89. package/.test-drive-root/m7l8z61tnei +1 -0
  90. package/.test-drive-root/mmwpyz6aa2e +1 -0
  91. package/.test-drive-root/mqbyniatwu +1 -0
  92. package/.test-drive-root/mz3jbwwr12 +1 -0
  93. package/.test-drive-root/mza04oxze7 +1 -0
  94. package/.test-drive-root/ndqlafk0c0c +1 -0
  95. package/.test-drive-root/noojz8gst2 +1 -0
  96. package/.test-drive-root/o8ujk9otq7 +1 -0
  97. package/.test-drive-root/oa54zbip13m +1 -0
  98. package/.test-drive-root/ore7c15j64a +1 -0
  99. package/.test-drive-root/oxj3tt9q16 +1 -0
  100. package/.test-drive-root/pac4f4x1r9 +1 -0
  101. package/.test-drive-root/pqawj7k06i +1 -0
  102. package/.test-drive-root/qujzt0hpj6i +1 -0
  103. package/.test-drive-root/r5sdb4w0d3 +1 -0
  104. package/.test-drive-root/r8pybcnme1r +1 -0
  105. package/.test-drive-root/rpht4r0nk9 +1 -0
  106. package/.test-drive-root/rru3wcrfved +1 -0
  107. package/.test-drive-root/rtq2tifpvgt +1 -0
  108. package/.test-drive-root/ru00ubalaar +1 -0
  109. package/.test-drive-root/rx65wqybqlm +1 -0
  110. package/.test-drive-root/s585nksqc5 +1 -0
  111. package/.test-drive-root/scvyemdb9gh +1 -0
  112. package/.test-drive-root/si2b3ac0lve +1 -0
  113. package/.test-drive-root/sjbanmc5v5k +1 -0
  114. package/.test-drive-root/sl2coirs8zo +1 -0
  115. package/.test-drive-root/so3w2nrxm7 +1 -0
  116. package/.test-drive-root/t3y2vj69hz9 +1 -0
  117. package/.test-drive-root/t95zgk2fl8f +1 -0
  118. package/.test-drive-root/t9f08z6ayj +1 -0
  119. package/.test-drive-root/ta501uh71xi +1 -0
  120. package/.test-drive-root/taqhm9suyu +1 -0
  121. package/.test-drive-root/tya2ijynx5k +1 -0
  122. package/.test-drive-root/u4llyu4o7en +1 -0
  123. package/.test-drive-root/u5wco26a6lb +1 -0
  124. package/.test-drive-root/u6sricrto +1 -0
  125. package/.test-drive-root/ukcmmzglxa +1 -0
  126. package/.test-drive-root/uvsiktw52p +1 -0
  127. package/.test-drive-root/uylv1bn9swb +1 -0
  128. package/.test-drive-root/uzkzgxo3k5b +1 -0
  129. package/.test-drive-root/vf68w1pfrv +1 -0
  130. package/.test-drive-root/vp4g881bu7 +1 -0
  131. package/.test-drive-root/vuv8aavujb +1 -0
  132. package/.test-drive-root/wg24h3o6df +1 -0
  133. package/.test-drive-root/wyi5oftc8nh +1 -0
  134. package/.test-drive-root/x5zl3x2eh +1 -0
  135. package/.test-drive-root/xb3bpq9xtk +1 -0
  136. package/.test-drive-root/xfn089pthf9 +1 -0
  137. package/.test-drive-root/xrs7jul0mdl +1 -0
  138. package/.test-drive-root/y5qtilrbuzq +1 -0
  139. package/.test-drive-root/ysxlnyjuav +1 -0
  140. package/.test-drive-root/z2loakinuyk +1 -0
  141. package/.test-drive-root/z6lxmw8opjo +1 -0
  142. package/.test-drive-root/z74cnyignf9 +1 -0
  143. package/.test-drive-root/zi9agjhrfw +1 -0
  144. package/.test-drive-root/zspmm1wmo5l +1 -0
  145. package/README.md +72 -72
  146. package/jest.config.js +8 -8
  147. package/package.json +47 -40
  148. package/DOCUMENTATION.md +0 -54
  149. package/lib/adapter.d.ts +0 -17
  150. package/lib/adapter.js +0 -440
  151. package/lib/cache.d.ts +0 -12
  152. package/lib/cache.js +0 -42
  153. package/lib/client.d.ts +0 -32
  154. package/lib/client.js +0 -166
  155. package/lib/drive.d.ts +0 -72
  156. package/lib/drive.js +0 -553
  157. package/lib/index.d.ts +0 -10
  158. package/lib/index.js +0 -55
  159. package/lib/types.d.ts +0 -86
  160. package/lib/types.js +0 -2
package/lib/drive.d.ts DELETED
@@ -1,72 +0,0 @@
1
- import { GoogleDriveAdapterOptions, ChangeEntry, IndexEntry } from './types';
2
- /**
3
- * DriveHandler - Lazy Loading Implementation
4
- *
5
- * Storage structure:
6
- * /db-folder/
7
- * ├── _meta.json
8
- * ├── snapshot-index.json # Map<DocId, IndexEntry>
9
- * ├── snapshot-data.json # Map<DocId, DocBody>
10
- * └── changes-*.ndjson # Append logs
11
- */
12
- export declare class DriveHandler {
13
- private client;
14
- private options;
15
- private folderId;
16
- private folderName;
17
- private parents;
18
- private compactionThreshold;
19
- private compactionSizeThreshold;
20
- private meta;
21
- private metaEtag;
22
- private metaModifiedTime;
23
- private index;
24
- private docCache;
25
- private pendingChanges;
26
- private currentLogSizeEstimate;
27
- private listeners;
28
- private pollingInterval;
29
- constructor(options: GoogleDriveAdapterOptions, dbName: string);
30
- get seq(): number;
31
- /** Load the database (Index Only) */
32
- load(): Promise<void>;
33
- private filesFromLegacySnapshot;
34
- /**
35
- * Get a document body.
36
- * Index -> Cache -> Fetch
37
- */
38
- get(id: string): Promise<any | null>;
39
- /** Get multiple docs (Atomic-ish) used for _allDocs */
40
- getMulti(ids: string[]): Promise<any[]>;
41
- /** Return all keys in Index */
42
- getIndexKeys(): string[];
43
- /** Get metadata for a specific ID from Index */
44
- getIndexEntry(id: string): IndexEntry | undefined;
45
- /** Single change wrapper */
46
- appendChange(change: ChangeEntry): Promise<void>;
47
- /** Append changes with OCC */
48
- appendChanges(changes: ChangeEntry[]): Promise<void>;
49
- private tryAppendChanges;
50
- /** Update Index with a new change */
51
- private updateIndex;
52
- private checkConflicts;
53
- /** Compact: Create SnapshotIndex + SnapshotData */
54
- compact(): Promise<void>;
55
- private atomicUpdateMeta;
56
- private findOrCreateFolder;
57
- private findFile;
58
- private downloadJson;
59
- private downloadFileAny;
60
- private downloadNdjson;
61
- private writeChangeFile;
62
- private saveMeta;
63
- private countTotalChanges;
64
- private cleanupOldFiles;
65
- private startPolling;
66
- private notifyListeners;
67
- onChange(cb: any): void;
68
- stopPolling(): void;
69
- private escapeQuery;
70
- deleteFolder(): Promise<void>;
71
- getNextSeq(): number;
72
- }
package/lib/drive.js DELETED
@@ -1,553 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DriveHandler = void 0;
4
- const cache_1 = require("./cache");
5
- const client_1 = require("./client");
6
- const DEFAULT_COMPACTION_THRESHOLD = 100; // entries
7
- const DEFAULT_SIZE_THRESHOLD = 1024 * 1024; // 1MB
8
- const DEFAULT_CACHE_SIZE = 1000; // Number of docs
9
- /**
10
- * DriveHandler - Lazy Loading Implementation
11
- *
12
- * Storage structure:
13
- * /db-folder/
14
- * ├── _meta.json
15
- * ├── snapshot-index.json # Map<DocId, IndexEntry>
16
- * ├── snapshot-data.json # Map<DocId, DocBody>
17
- * └── changes-*.ndjson # Append logs
18
- */
19
- class DriveHandler {
20
- constructor(options, dbName) {
21
- this.folderId = null;
22
- this.meta = {
23
- seq: 0,
24
- changeLogIds: [],
25
- snapshotIndexId: null,
26
- lastCompaction: null,
27
- dbName: ''
28
- };
29
- this.metaEtag = null;
30
- this.metaModifiedTime = null;
31
- // In-Memory Index: ID -> Metadata/Pointer
32
- this.index = {};
33
- this.pendingChanges = [];
34
- this.currentLogSizeEstimate = 0;
35
- this.listeners = [];
36
- this.pollingInterval = null;
37
- this.client = new client_1.GoogleDriveClient(options);
38
- this.options = options;
39
- this.folderId = options.folderId || null;
40
- this.folderName = options.folderName || dbName;
41
- this.parents = options.parents || [];
42
- this.compactionThreshold = options.compactionThreshold || DEFAULT_COMPACTION_THRESHOLD;
43
- this.compactionSizeThreshold = options.compactionSizeThreshold || DEFAULT_SIZE_THRESHOLD;
44
- this.meta.dbName = dbName;
45
- this.docCache = new cache_1.LRUCache(options.cacheSize || DEFAULT_CACHE_SIZE);
46
- // Polling will be started in load() after folderId is resolved
47
- }
48
- // Public getter for Sequence (used by adapter)
49
- get seq() {
50
- return this.meta.seq;
51
- }
52
- /** Load the database (Index Only) */
53
- async load() {
54
- if (!this.folderId) {
55
- this.folderId = await this.findOrCreateFolder();
56
- }
57
- const metaFile = await this.findFile('_meta.json');
58
- if (metaFile) {
59
- this.meta = await this.downloadJson(metaFile.id);
60
- this.metaEtag = metaFile.etag || null;
61
- this.metaModifiedTime = metaFile.modifiedTime || null;
62
- }
63
- else {
64
- await this.saveMeta(this.meta);
65
- }
66
- // Initialize Index
67
- this.index = {};
68
- // 1. Load Snapshot Index
69
- if (this.meta.snapshotIndexId) {
70
- try {
71
- // Try strictly as new format first
72
- const snapshotIdx = await this.downloadJson(this.meta.snapshotIndexId);
73
- // Check if it's actually a legacy snapshot (has 'docs' with bodies)
74
- if (snapshotIdx.docs) {
75
- // Migration Path: Handle legacy snapshot
76
- this.filesFromLegacySnapshot(snapshotIdx);
77
- }
78
- else {
79
- this.index = snapshotIdx.entries || {};
80
- // We assume seq is synced with meta usually, but use snapshot's seq as base
81
- }
82
- }
83
- catch (e) {
84
- console.warn('Failed to load snapshot index', e);
85
- this.index = {};
86
- }
87
- }
88
- else if (this.meta.snapshotId) {
89
- // Legacy support: field was renamed
90
- try {
91
- const legacySnapshot = await this.downloadJson(this.meta.snapshotId);
92
- this.filesFromLegacySnapshot(legacySnapshot);
93
- }
94
- catch (e) {
95
- console.warn('Failed to load legacy snapshot', e);
96
- }
97
- }
98
- // 2. Replay Change Logs (Metadata only updates)
99
- this.pendingChanges = [];
100
- this.currentLogSizeEstimate = 0;
101
- for (const logId of this.meta.changeLogIds) {
102
- const changes = await this.downloadNdjson(logId);
103
- this.currentLogSizeEstimate += 100 * changes.length;
104
- for (const change of changes) {
105
- this.updateIndex(change, logId);
106
- // We do NOT load body into cache automatically
107
- // But we must invalidate cache if we had old data
108
- if (this.docCache.get(change.id)) {
109
- this.docCache.remove(change.id);
110
- }
111
- }
112
- }
113
- // 3. Start Polling (if enabled)
114
- if (this.options.pollingIntervalMs) {
115
- this.startPolling(this.options.pollingIntervalMs);
116
- }
117
- }
118
- // Migration helper
119
- filesFromLegacySnapshot(snapshot) {
120
- // Convert Legacy Snapshot (Docs in memory) to Index
121
- // Since we don't have a separate file pointer for each doc in legacy snapshot,
122
- // we say they are in the snapshot file itself.
123
- // BUT, lazy loading requires being able to fetch them.
124
- // We will cache them ALL now (since we downloaded them) and index them.
125
- for (const [id, doc] of Object.entries(snapshot.docs)) {
126
- this.index[id] = {
127
- rev: doc._rev,
128
- seq: snapshot.seq, // Approximate
129
- location: { fileId: 'LEGACY_MEMORY' } // Special validity marker
130
- };
131
- this.docCache.put(id, doc);
132
- }
133
- }
134
- /**
135
- * Get a document body.
136
- * Index -> Cache -> Fetch
137
- */
138
- async get(id) {
139
- const entry = this.index[id];
140
- if (!entry)
141
- return null;
142
- if (entry.deleted)
143
- return null;
144
- // 1. Check Cache
145
- const cached = this.docCache.get(id);
146
- if (cached)
147
- return cached;
148
- // 2. Fetch from Drive
149
- // If it's a legacy entry currently in memory (should have been cached), returns null if evicted?
150
- if (entry.location.fileId === 'LEGACY_MEMORY') {
151
- // If evicted, we are in trouble unless we re-download the legacy snapshot.
152
- // For robustness, let's say we reload the legacy snapshot if needed.
153
- // OR simpler: we assume compaction will fix this soon.
154
- // Let's implement fetch for safety.
155
- if (this.meta.snapshotId) {
156
- const legacy = await this.downloadJson(this.meta.snapshotId);
157
- if (legacy.docs[id]) {
158
- this.docCache.put(id, legacy.docs[id]);
159
- return legacy.docs[id];
160
- }
161
- }
162
- return null; // Should not happen
163
- }
164
- const fileId = entry.location.fileId;
165
- // Is it a change file (NDJSON) or snapshot file (JSON)?
166
- // We can infer or we could have stored type.
167
- // Usually, we just download the file.
168
- // Optimization: If we have many docs in one file, we might want to cache that file's contents?
169
- // For now, naive fetch: download file, find doc.
170
- const content = await this.downloadFileAny(fileId);
171
- let doc = null;
172
- if (Array.isArray(content)) {
173
- // It's a change log (array of entries)
174
- // Find the *last* entry for this ID in this file
175
- const match = content.reverse().find((c) => c.id === id);
176
- doc = match ? match.doc : null;
177
- }
178
- else if (content.docs) {
179
- // It's a snapshot-data chunk
180
- doc = content.docs[id];
181
- }
182
- else {
183
- // Single doc file? (Not used yet)
184
- doc = content;
185
- }
186
- if (doc) {
187
- this.docCache.put(id, doc);
188
- doc._rev = entry.rev; // Ensure consistent rev
189
- }
190
- return doc;
191
- }
192
- /** Get multiple docs (Atomic-ish) used for _allDocs */
193
- async getMulti(ids) {
194
- // Naive parallel fetch
195
- // Optimization: Group by fileID to reduce requests
196
- const byFile = {};
197
- const results = {};
198
- for (const id of ids) {
199
- const entry = this.index[id];
200
- if (!entry || entry.deleted) {
201
- results[id] = null;
202
- continue;
203
- }
204
- // Check cache
205
- const cached = this.docCache.get(id);
206
- if (cached) {
207
- results[id] = cached;
208
- continue;
209
- }
210
- // Group by file
211
- if (entry.location.fileId === 'LEGACY_MEMORY') {
212
- // Handle legacy separately
213
- const doc = await this.get(id); // fallback
214
- results[id] = doc;
215
- }
216
- else {
217
- if (!byFile[entry.location.fileId])
218
- byFile[entry.location.fileId] = [];
219
- byFile[entry.location.fileId].push(id);
220
- }
221
- }
222
- // Fetch files
223
- for (const [fileId, docIds] of Object.entries(byFile)) {
224
- try {
225
- const content = await this.downloadFileAny(fileId);
226
- for (const docId of docIds) {
227
- let doc = null;
228
- if (Array.isArray(content)) {
229
- const match = content.reverse().find((c) => c.id === docId);
230
- doc = match ? match.doc : null;
231
- }
232
- else if (content.docs) {
233
- doc = content.docs[docId];
234
- }
235
- if (doc) {
236
- // Add entry.rev to doc just in case
237
- if (this.index[docId])
238
- doc._rev = this.index[docId].rev;
239
- this.docCache.put(docId, doc);
240
- results[docId] = doc;
241
- }
242
- else {
243
- results[docId] = null;
244
- }
245
- }
246
- }
247
- catch (e) {
248
- console.error(`Failed to fetch file ${fileId} for docs ${docIds}`, e);
249
- // Return nulls
250
- docIds.forEach(id => results[id] = null);
251
- }
252
- }
253
- return ids.map(id => results[id]);
254
- }
255
- /** Return all keys in Index */
256
- getIndexKeys() {
257
- return Object.keys(this.index);
258
- }
259
- /** Get metadata for a specific ID from Index */
260
- getIndexEntry(id) {
261
- return this.index[id];
262
- }
263
- /** Single change wrapper */
264
- async appendChange(change) {
265
- return this.appendChanges([change]);
266
- }
267
- /** Append changes with OCC */
268
- async appendChanges(changes) {
269
- const MAX_RETRIES = 5;
270
- let attempt = 0;
271
- while (attempt < MAX_RETRIES) {
272
- try {
273
- return await this.tryAppendChanges(changes);
274
- }
275
- catch (err) {
276
- if (err.status === 412 || err.status === 409) {
277
- // Reload and RETRY
278
- await this.load();
279
- // Check conflicts against Index (Metadata sufficient)
280
- this.checkConflicts(changes);
281
- // Reseq
282
- let currentSeq = this.meta.seq;
283
- for (const change of changes) {
284
- currentSeq++;
285
- change.seq = currentSeq;
286
- }
287
- attempt++;
288
- await new Promise(r => setTimeout(r, Math.random() * 500 + 100));
289
- continue;
290
- }
291
- throw err;
292
- }
293
- }
294
- throw new Error('Failed to append changes');
295
- }
296
- async tryAppendChanges(changes) {
297
- // 1. Write Log File (Upload Data)
298
- const fileId = await this.writeChangeFile(changes);
299
- // 2. Prepare speculative meta update
300
- const nextMeta = { ...this.meta };
301
- nextMeta.changeLogIds = [...nextMeta.changeLogIds, fileId];
302
- nextMeta.seq = changes[changes.length - 1].seq;
303
- // 3. Commit Lock
304
- await this.saveMeta(nextMeta, this.metaEtag);
305
- // 4. Update Local State
306
- this.meta = nextMeta;
307
- for (const change of changes) {
308
- this.updateIndex(change, fileId);
309
- if (change.doc) {
310
- this.docCache.put(change.id, change.doc);
311
- }
312
- else if (change.deleted) {
313
- this.docCache.remove(change.id);
314
- }
315
- }
316
- // 5. Compaction Check
317
- // Count changes since last compaction *pointer*, not just list length
318
- const totalChanges = await this.countTotalChanges();
319
- if (totalChanges >= this.compactionThreshold ||
320
- this.currentLogSizeEstimate >= this.compactionSizeThreshold) {
321
- this.compact().catch(e => console.error('Compaction failed', e));
322
- }
323
- }
324
- /** Update Index with a new change */
325
- updateIndex(change, fileId) {
326
- this.index[change.id] = {
327
- rev: change.rev,
328
- seq: change.seq,
329
- deleted: !!change.deleted,
330
- location: { fileId }
331
- };
332
- }
333
- checkConflicts(changes) {
334
- for (const change of changes) {
335
- const docId = change.id;
336
- const newRevNum = parseInt(change.rev.split('-')[0], 10);
337
- const existing = this.index[docId];
338
- if (existing) {
339
- const currentRevNum = parseInt(existing.rev.split('-')[0], 10);
340
- if (currentRevNum >= newRevNum) {
341
- const err = new Error('Document update conflict');
342
- err.status = 409;
343
- err.name = 'conflict'; // PouchDB expectation
344
- throw err;
345
- }
346
- }
347
- }
348
- }
349
- /** Compact: Create SnapshotIndex + SnapshotData */
350
- async compact() {
351
- const snapshotSeq = this.meta.seq;
352
- const oldLogIds = [...this.meta.changeLogIds];
353
- const oldIndexId = this.meta.snapshotIndexId;
354
- // 1. Fetch ALL active documents
355
- // We need them to build the new large snapshot-data file
356
- // This is the one time we download everything if not cached.
357
- // Optimization: We could reuse existing `snapshot-data` chunks and only append new data
358
- // to a new chunk, but for simplicity: Merge All.
359
- const allIds = Object.keys(this.index).filter(id => !this.index[id].deleted);
360
- const allDocs = await this.getMulti(allIds);
361
- const snapshotData = { docs: {} };
362
- allIds.forEach((id, i) => {
363
- if (allDocs[i])
364
- snapshotData.docs[id] = allDocs[i];
365
- });
366
- // 2. Upload Data File
367
- const dataContent = JSON.stringify(snapshotData);
368
- const dataRes = await this.client.createFile(`snapshot-data-${Date.now()}.json`, [this.folderId], 'application/json', dataContent);
369
- const dataFileId = dataRes.id;
370
- // 3. Create Index pointing to this Data File
371
- const newIndexEntries = {};
372
- for (const id of Object.keys(snapshotData.docs)) {
373
- newIndexEntries[id] = {
374
- rev: this.index[id].rev,
375
- seq: this.index[id].seq,
376
- location: { fileId: dataFileId }
377
- };
378
- }
379
- const snapshotIndex = {
380
- entries: newIndexEntries,
381
- seq: snapshotSeq,
382
- createdAt: Date.now()
383
- };
384
- const indexContent = JSON.stringify(snapshotIndex);
385
- const indexRes = await this.client.createFile(`snapshot-index-${Date.now()}.json`, [this.folderId], 'application/json', indexContent);
386
- const newIndexId = indexRes.id;
387
- // 4. Update Meta
388
- await this.atomicUpdateMeta((latest) => {
389
- const remainingLogs = latest.changeLogIds.filter(id => !oldLogIds.includes(id));
390
- return {
391
- ...latest,
392
- snapshotIndexId: newIndexId,
393
- changeLogIds: remainingLogs,
394
- lastCompaction: Date.now()
395
- };
396
- });
397
- // 5. Cleanup
398
- this.cleanupOldFiles(oldIndexId, oldLogIds); // And potentially old data files if we tracked them
399
- this.currentLogSizeEstimate = 0;
400
- }
401
- // ... Helpers (atomicUpdateMeta, saveMeta, writeChangeFile same as before) ...
402
- async atomicUpdateMeta(modifier) {
403
- const MAX_RETRIES = 5;
404
- let attempt = 0;
405
- while (attempt < MAX_RETRIES) {
406
- try {
407
- const metaFile = await this.findFile('_meta.json');
408
- if (!metaFile)
409
- throw new Error('Meta missing');
410
- const validMeta = await this.downloadJson(metaFile.id);
411
- const newMeta = modifier(validMeta);
412
- await this.saveMeta(newMeta, metaFile.etag);
413
- this.meta = newMeta;
414
- return;
415
- }
416
- catch (err) {
417
- if (err.status === 412 || err.status === 409) {
418
- attempt++;
419
- await new Promise(r => setTimeout(r, Math.random() * 500 + 100));
420
- continue;
421
- }
422
- throw err;
423
- }
424
- }
425
- }
426
- // Reused helpers
427
- async findOrCreateFolder() {
428
- const safeName = this.escapeQuery(this.folderName);
429
- const q = `name = '${safeName}' and mimeType = 'application/vnd.google-apps.folder' and trashed = false`;
430
- const files = await this.client.listFiles(q);
431
- if (files.length > 0)
432
- return files[0].id;
433
- const createRes = await this.client.createFile(this.folderName, this.parents.length ? this.parents : undefined, 'application/vnd.google-apps.folder', '');
434
- return createRes.id;
435
- }
436
- async findFile(name) {
437
- if (!this.folderId)
438
- return null;
439
- const safeName = this.escapeQuery(name);
440
- const q = `name = '${safeName}' and '${this.folderId}' in parents and trashed = false`;
441
- const files = await this.client.listFiles(q);
442
- if (files.length > 0)
443
- return {
444
- id: files[0].id,
445
- etag: files[0].etag || '',
446
- modifiedTime: files[0].modifiedTime || ''
447
- };
448
- return null;
449
- }
450
- async downloadJson(fileId) {
451
- return await this.client.getFile(fileId);
452
- }
453
- async downloadFileAny(fileId) {
454
- return await this.client.getFile(fileId);
455
- }
456
- async downloadNdjson(fileId) {
457
- const data = await this.client.getFile(fileId);
458
- // data will likely be a string if NDJSON is returned and getFile sees weird content-type
459
- // Or if getFile auto-parsed standard "application/json" but NDJSON is just text.
460
- // Google Drive might return application/json for everything if we aren't careful?
461
- // Actually .ndjson is separate.
462
- // Safest: Handle string or object.
463
- const content = typeof data === 'string' ? data : JSON.stringify(data);
464
- const lines = content.trim().split('\n').filter((l) => l);
465
- return lines.map((line) => JSON.parse(line));
466
- }
467
- async writeChangeFile(changes) {
468
- const lines = changes.map(c => JSON.stringify(c)).join('\n') + '\n';
469
- const startSeq = changes[0].seq;
470
- const name = `changes-${startSeq}-${Math.random().toString(36).substring(7)}.ndjson`;
471
- const res = await this.client.createFile(name, [this.folderId], 'application/x-ndjson', lines);
472
- this.currentLogSizeEstimate += new Blob([lines]).size;
473
- return res.id;
474
- }
475
- async saveMeta(meta, expectedEtag = null) {
476
- const content = JSON.stringify(meta);
477
- const metaFile = await this.findFile('_meta.json');
478
- if (metaFile) {
479
- const res = await this.client.updateFile(metaFile.id, content, expectedEtag || undefined);
480
- this.metaEtag = res.etag;
481
- this.metaModifiedTime = res.modifiedTime;
482
- }
483
- else {
484
- const res = await this.client.createFile('_meta.json', [this.folderId], 'application/json', content);
485
- this.metaEtag = res.etag;
486
- this.metaModifiedTime = res.modifiedTime;
487
- }
488
- }
489
- async countTotalChanges() {
490
- // Calculate diff between meta.seq and snapshot seq
491
- // But we don't store snapshot seq in meta directly?
492
- // We can approximate by pending changes count + known gaps?
493
- // Actually we used to check snapshot.seq.
494
- // We can assume snapshot is somewhat recent.
495
- return this.pendingChanges.length + 10; // dummy for now, rely on log size
496
- }
497
- async cleanupOldFiles(oldIndexId, oldLogIds) {
498
- if (oldIndexId)
499
- try {
500
- await this.client.deleteFile(oldIndexId);
501
- }
502
- catch { }
503
- for (const id of oldLogIds)
504
- try {
505
- await this.client.deleteFile(id);
506
- }
507
- catch { }
508
- }
509
- startPolling(intervalMs) {
510
- if (this.pollingInterval)
511
- clearInterval(this.pollingInterval);
512
- this.pollingInterval = setInterval(async () => {
513
- try {
514
- const metaFile = await this.findFile('_meta.json');
515
- if (!metaFile)
516
- return;
517
- // Use modifiedTime for polling as it's readable in projections
518
- if (metaFile.modifiedTime !== this.metaModifiedTime) {
519
- await this.load();
520
- this.notifyListeners();
521
- }
522
- }
523
- catch (err) {
524
- console.error('Polling error', err);
525
- }
526
- }, intervalMs);
527
- }
528
- notifyListeners() {
529
- // Observers expecting 'docs' object might be broken if they expect FULL body.
530
- // We can pass empty object or partials?
531
- // Real PouchDB changes feed calls `db.changes()`.
532
- // Our `adapter.js` uses `db.onChange` effectively.
533
- // We should pass a map of { ID: { _rev, ... } } (Index entries)
534
- // Adapter needs to handle this.
535
- const changes = {};
536
- for (const [id, entry] of Object.entries(this.index)) {
537
- changes[id] = { _id: id, _rev: entry.rev, _deleted: entry.deleted };
538
- }
539
- for (const l of this.listeners)
540
- l(changes);
541
- }
542
- // For tests/debug
543
- onChange(cb) { this.listeners.push(cb); }
544
- stopPolling() { if (this.pollingInterval)
545
- clearInterval(this.pollingInterval); }
546
- escapeQuery(value) {
547
- return value.replace(/'/g, "\\'");
548
- }
549
- async deleteFolder() { if (this.folderId)
550
- await this.client.deleteFile(this.folderId); }
551
- getNextSeq() { return this.meta.seq + 1; }
552
- }
553
- exports.DriveHandler = DriveHandler;
package/lib/index.d.ts DELETED
@@ -1,10 +0,0 @@
1
- import { GoogleDriveAdapterOptions } from './types';
2
- export * from './types';
3
- /**
4
- * Google Drive Adapter Plugin Factory
5
- *
6
- * Usage:
7
- * const plugin = GoogleDriveAdapter({ drive: myDriveClient, ... });
8
- * PouchDB.plugin(plugin);
9
- */
10
- export default function GoogleDriveAdapter(config: GoogleDriveAdapterOptions): (PouchDB: any) => void;
package/lib/index.js DELETED
@@ -1,55 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
- for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
- };
16
- Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.default = GoogleDriveAdapter;
18
- const adapter_1 = require("./adapter");
19
- // Export types
20
- __exportStar(require("./types"), exports);
21
- /**
22
- * Google Drive Adapter Plugin Factory
23
- *
24
- * Usage:
25
- * const plugin = GoogleDriveAdapter({ drive: myDriveClient, ... });
26
- * PouchDB.plugin(plugin);
27
- */
28
- function GoogleDriveAdapter(config) {
29
- return function (PouchDB) {
30
- // Get the base adapter constructor (scoped to this PouchDB instance)
31
- const BaseAdapter = (0, adapter_1.GoogleDriveAdapter)(PouchDB);
32
- // Create a wrapper constructor that injects the config
33
- function ConfiguredAdapter(opts, callback) {
34
- // Merge factory config with constructor options
35
- // Constructor options take precedence (overrides)
36
- const mergedOpts = Object.assign({}, config, opts);
37
- // Call the base adapter
38
- BaseAdapter.call(this, mergedOpts, callback);
39
- }
40
- // Copy static properties required by PouchDB
41
- // @ts-ignore
42
- ConfiguredAdapter.valid = BaseAdapter.valid;
43
- // @ts-ignore
44
- ConfiguredAdapter.use_prefix = BaseAdapter.use_prefix;
45
- // Register the adapter manually
46
- // We use PouchDB.adapters object directly to avoid using the .adapter() method
47
- if (PouchDB.adapters) {
48
- PouchDB.adapters['googledrive'] = ConfiguredAdapter;
49
- }
50
- else {
51
- // Fallback/Warning if adapters object is somehow missing (should not happen in core)
52
- console.warn('PouchDB.adapters not found, unable to register googledrive adapter');
53
- }
54
- };
55
- }