@docstack/pouchdb-adapter-googledrive 0.0.6 → 0.0.9

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.
@@ -0,0 +1,481 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "60e24526",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Debug: Replica Bidirezionale Google Drive PouchDB Adapter\n",
9
+ "\n",
10
+ "## Problema\n",
11
+ "La replica `localDB.replicate.to(googleDriveDB)` funziona ✅ \n",
12
+ "Ma `localDB.replicate.from(googleDriveDB)` non sincronizza i dati ❌\n",
13
+ "\n",
14
+ "Questo notebook diagnostica il flusso di replicazione per identificare dove il blocco si verifica."
15
+ ]
16
+ },
17
+ {
18
+ "cell_type": "markdown",
19
+ "id": "3796436e",
20
+ "metadata": {},
21
+ "source": [
22
+ "## 1️⃣ Section: Setup e Configurazione Replica\n",
23
+ "\n",
24
+ "### Aggiungi questo codice al tuo componente per diagnosticare il problema:"
25
+ ]
26
+ },
27
+ {
28
+ "cell_type": "code",
29
+ "execution_count": null,
30
+ "id": "5a4749c9",
31
+ "metadata": {
32
+ "vscode": {
33
+ "languageId": "javascript"
34
+ }
35
+ },
36
+ "outputs": [],
37
+ "source": [
38
+ "// Setup Google Drive Adapter con debug logging\n",
39
+ "const plugin = GoogleDriveAdapter({\n",
40
+ " accessToken: async () => driveConfig.accessToken,\n",
41
+ " folderName: 'my-db',\n",
42
+ " pollingIntervalMs: 5000,\n",
43
+ " debug: true // 🔍 Abilita logging dettagliato\n",
44
+ "});\n",
45
+ "\n",
46
+ "PouchDB.plugin(plugin);\n",
47
+ "\n",
48
+ "const googleDriveDB = new PouchDB('paper-drive-db', {\n",
49
+ " adapter: 'googledrive'\n",
50
+ "});\n",
51
+ "\n",
52
+ "const localDB = stack?.db;\n",
53
+ "\n",
54
+ "// 🔍 Aggiungi listener per monitorare gli eventi di replicazione\n",
55
+ "console.log('=== SETUP REPLICA BIDIREZIONALE ===');\n",
56
+ "\n",
57
+ "// Replica TO (localDB → googleDriveDB) - Funzionante ✅\n",
58
+ "const repTo = localDB.replicate.to(googleDriveDB, { live: true, retry: true });\n",
59
+ "repTo.on('change', (info) => {\n",
60
+ " console.log('📤 TO: change event', { \n",
61
+ " direction: 'local→gdrive',\n",
62
+ " docs: info.docs?.length,\n",
63
+ " ok: info.ok\n",
64
+ " });\n",
65
+ "});\n",
66
+ "repTo.on('complete', (info) => {\n",
67
+ " console.log('✅ TO: replication complete', info);\n",
68
+ "});\n",
69
+ "repTo.on('error', (err) => {\n",
70
+ " console.error('❌ TO: replication error', err);\n",
71
+ "});\n",
72
+ "\n",
73
+ "// Replica FROM (googleDriveDB → localDB) - Problema ❌\n",
74
+ "const repFrom = localDB.replicate.from(googleDriveDB, { live: true, retry: true });\n",
75
+ "repFrom.on('change', (info) => {\n",
76
+ " console.log('📥 FROM: change event', { \n",
77
+ " direction: 'gdrive→local',\n",
78
+ " docs: info.docs?.length,\n",
79
+ " ok: info.ok\n",
80
+ " });\n",
81
+ "});\n",
82
+ "repFrom.on('complete', (info) => {\n",
83
+ " console.log('✅ FROM: replication complete', info);\n",
84
+ "});\n",
85
+ "repFrom.on('error', (err) => {\n",
86
+ " console.error('❌ FROM: replication error', err);\n",
87
+ "});"
88
+ ]
89
+ },
90
+ {
91
+ "cell_type": "markdown",
92
+ "id": "56686c46",
93
+ "metadata": {},
94
+ "source": [
95
+ "## 2️⃣ Section: Diagnosi - Verifica Flusso di Replicazione\n",
96
+ "\n",
97
+ "### Controlla le sequenze e i metadati del database"
98
+ ]
99
+ },
100
+ {
101
+ "cell_type": "code",
102
+ "execution_count": null,
103
+ "id": "fb620ba8",
104
+ "metadata": {
105
+ "vscode": {
106
+ "languageId": "javascript"
107
+ }
108
+ },
109
+ "outputs": [],
110
+ "source": [
111
+ "// Confronta lo stato dei due database\n",
112
+ "async function compareDatabases() {\n",
113
+ " const localInfo = await localDB.info();\n",
114
+ " const googleDriveInfo = await googleDriveDB.info();\n",
115
+ " \n",
116
+ " console.log('📊 DATABASE INFO COMPARISON:');\n",
117
+ " console.log('Local DB:', {\n",
118
+ " doc_count: localInfo.doc_count,\n",
119
+ " update_seq: localInfo.update_seq,\n",
120
+ " db_name: localInfo.db_name\n",
121
+ " });\n",
122
+ " console.log('Google Drive DB:', {\n",
123
+ " doc_count: googleDriveInfo.doc_count,\n",
124
+ " update_seq: googleDriveInfo.update_seq,\n",
125
+ " db_name: googleDriveInfo.db_name\n",
126
+ " });\n",
127
+ " \n",
128
+ " return { localInfo, googleDriveInfo };\n",
129
+ "}\n",
130
+ "\n",
131
+ "// Esegui la diagnosi dopo 25 secondi (dopo che il polling ha avuto tempo di rilevare)\n",
132
+ "setTimeout(async () => {\n",
133
+ " console.log('\\n=== DIAGNOSI DOPO 25 SEC ===\\n');\n",
134
+ " \n",
135
+ " const { localInfo, googleDriveInfo } = await compareDatabases();\n",
136
+ " \n",
137
+ " // 🔍 Verifica se le sequenze sono sincronizzate\n",
138
+ " if (localInfo.update_seq === googleDriveInfo.update_seq) {\n",
139
+ " console.log('✅ Sequenze sincronizzate');\n",
140
+ " } else {\n",
141
+ " console.warn('⚠️ Sequenze NON sincronizzate:', {\n",
142
+ " local: localInfo.update_seq,\n",
143
+ " googleDrive: googleDriveInfo.update_seq,\n",
144
+ " diff: googleDriveInfo.update_seq - localInfo.update_seq\n",
145
+ " });\n",
146
+ " }\n",
147
+ " \n",
148
+ " // 🔍 Verifica il documento specifico su Google Drive\n",
149
+ " try {\n",
150
+ " const docOnDrive = await googleDriveDB.get('Notebook-0');\n",
151
+ " console.log('✅ Notebook-0 trovato su Google Drive:', {\n",
152
+ " _id: docOnDrive._id,\n",
153
+ " _rev: docOnDrive._rev\n",
154
+ " });\n",
155
+ " } catch (err) {\n",
156
+ " console.error('❌ Notebook-0 NON trovato su Google Drive:', err.message);\n",
157
+ " }\n",
158
+ " \n",
159
+ " // 🔍 Verifica il documento su Local DB\n",
160
+ " try {\n",
161
+ " const docLocal = await localDB.get('Notebook-0');\n",
162
+ " console.log('✅ Notebook-0 trovato su Local DB:', {\n",
163
+ " _id: docLocal._id,\n",
164
+ " _rev: docLocal._rev\n",
165
+ " });\n",
166
+ " } catch (err) {\n",
167
+ " console.error('❌ Notebook-0 NON trovato su Local DB:', err.message);\n",
168
+ " }\n",
169
+ " \n",
170
+ "}, 25000);"
171
+ ]
172
+ },
173
+ {
174
+ "cell_type": "markdown",
175
+ "id": "5015299b",
176
+ "metadata": {},
177
+ "source": [
178
+ "## 3️⃣ Section: Debug - Polling e Change Detection\n",
179
+ "\n",
180
+ "### Il problema probabilmente è nel meccanismo di Polling\n",
181
+ "\n",
182
+ "Verifica se il polling sta effettivamente rilevando i cambiamenti da Google Drive:"
183
+ ]
184
+ },
185
+ {
186
+ "cell_type": "code",
187
+ "execution_count": null,
188
+ "id": "5d57b557",
189
+ "metadata": {
190
+ "vscode": {
191
+ "languageId": "javascript"
192
+ }
193
+ },
194
+ "outputs": [],
195
+ "source": [
196
+ "// 🔍 Monitora i cambiamenti rilevati tramite _changes API\n",
197
+ "async function debugChangesAPI() {\n",
198
+ " console.log('\\n=== TESTING _CHANGES API ===\\n');\n",
199
+ " \n",
200
+ " // Ottieni gli ultimi 10 cambiamenti dal Google Drive DB\n",
201
+ " try {\n",
202
+ " const changes = await googleDriveDB.changes({\n",
203
+ " include_docs: true,\n",
204
+ " descending: true,\n",
205
+ " limit: 10\n",
206
+ " });\n",
207
+ " \n",
208
+ " console.log('Recent changes from Google Drive DB:', {\n",
209
+ " results: changes.results.length,\n",
210
+ " last_seq: changes.last_seq,\n",
211
+ " results: changes.results.map(r => ({\n",
212
+ " id: r.id,\n",
213
+ " seq: r.seq,\n",
214
+ " changes: r.changes,\n",
215
+ " deleted: r.deleted,\n",
216
+ " hasDoc: !!r.doc\n",
217
+ " }))\n",
218
+ " });\n",
219
+ " \n",
220
+ " return changes;\n",
221
+ " } catch (err) {\n",
222
+ " console.error('❌ Error fetching changes from Google Drive DB:', err);\n",
223
+ " }\n",
224
+ "}\n",
225
+ "\n",
226
+ "// Testa la _changes API subito\n",
227
+ "await debugChangesAPI();\n",
228
+ "\n",
229
+ "// Testa di nuovo dopo 10 secondi per vedere se il polling ha rilevato nuovi cambiamenti\n",
230
+ "setTimeout(async () => {\n",
231
+ " console.log('\\n=== TESTING _CHANGES API AFTER POLLING ===\\n');\n",
232
+ " await debugChangesAPI();\n",
233
+ "}, 10000);"
234
+ ]
235
+ },
236
+ {
237
+ "cell_type": "markdown",
238
+ "id": "39d29701",
239
+ "metadata": {},
240
+ "source": [
241
+ "## 🚨 POSSIBILI CULPRIT IDENTIFICATI\n",
242
+ "\n",
243
+ "### Problema 1: _changes feed con `include_docs` potrebbe essere lento\n",
244
+ "\n",
245
+ "Nel tuo `adapter.ts`, quando `opts.include_docs` è vero, il metodo `_changes` deve:\n",
246
+ "- Scaricare il CORPO di ogni documento (lento)\n",
247
+ "- Questo blocca la replica\n",
248
+ "\n",
249
+ "**Diagnosi:** \n",
250
+ "Controlla la console del browser. Se vedi molti `fetchFile` calls per ogni change, questo è il collo di bottiglia.\n",
251
+ "\n",
252
+ "### Problema 2: Change detection non rispetta il `since` parameter\n",
253
+ "\n",
254
+ "La replica PouchDB invia `since: lastSeq` per ricevere solo i cambiamenti NUOVI. \n",
255
+ "Se `_changes` non filtra correttamente, la replica non rileva i nuovi documenti.\n",
256
+ "\n",
257
+ "### Problema 3: Live listener potrebbe non essere chiamato\n",
258
+ "\n",
259
+ "Nel `DriveHandler`, il polling rileva modifiche a `_meta.json`, ma:\n",
260
+ "- Il polling ha un delay di 5 secondi\n",
261
+ "- La replicazione potrebbe timeout prima di ricevere notifiche"
262
+ ]
263
+ },
264
+ {
265
+ "cell_type": "markdown",
266
+ "id": "de439ea3",
267
+ "metadata": {},
268
+ "source": [
269
+ "## 4️⃣ Section: Troubleshooting - Conflitti di Revisione\n",
270
+ "\n",
271
+ "### Verifica validazione revisioni"
272
+ ]
273
+ },
274
+ {
275
+ "cell_type": "code",
276
+ "execution_count": null,
277
+ "id": "08da678c",
278
+ "metadata": {
279
+ "vscode": {
280
+ "languageId": "javascript"
281
+ }
282
+ },
283
+ "outputs": [],
284
+ "source": [
285
+ "// Verifica i conflitti di revisione durante la replica\n",
286
+ "async function checkReplicationConflicts() {\n",
287
+ " console.log('\\n=== CHECKING REPLICATION CONFLICTS ===\\n');\n",
288
+ " \n",
289
+ " // Leggi lo stesso documento da entrambi i database\n",
290
+ " const docId = 'Notebook-0';\n",
291
+ " \n",
292
+ " try {\n",
293
+ " const localDoc = await localDB.get(docId);\n",
294
+ " console.log('Local Doc:', { _id: localDoc._id, _rev: localDoc._rev });\n",
295
+ " } catch (e) {\n",
296
+ " console.warn('Local doc not found');\n",
297
+ " }\n",
298
+ " \n",
299
+ " try {\n",
300
+ " const remoteDoc = await googleDriveDB.get(docId);\n",
301
+ " console.log('Remote Doc:', { _id: remoteDoc._id, _rev: remoteDoc._rev });\n",
302
+ " } catch (e) {\n",
303
+ " console.warn('Remote doc not found');\n",
304
+ " }\n",
305
+ " \n",
306
+ " // 🔍 Verifica _conflicts (documenti con conflitti di merge)\n",
307
+ " try {\n",
308
+ " const allDocs = await localDB.allDocs({ conflicts: true });\n",
309
+ " const conflictedDocs = allDocs.rows.filter(r => r.value.conflicts && r.value.conflicts.length > 0);\n",
310
+ " if (conflictedDocs.length > 0) {\n",
311
+ " console.warn('⚠️ Found documents with conflicts:', conflictedDocs);\n",
312
+ " } else {\n",
313
+ " console.log('✅ No conflicts found in local DB');\n",
314
+ " }\n",
315
+ " } catch (e) {\n",
316
+ " console.error('Error checking conflicts:', e);\n",
317
+ " }\n",
318
+ "}\n",
319
+ "\n",
320
+ "// Esegui il check dopo 30 secondi\n",
321
+ "setTimeout(async () => {\n",
322
+ " await checkReplicationConflicts();\n",
323
+ "}, 30000);"
324
+ ]
325
+ },
326
+ {
327
+ "cell_type": "markdown",
328
+ "id": "52a770f6",
329
+ "metadata": {},
330
+ "source": [
331
+ "## 5️⃣ Section: Validazione - Test della Replica Bidirezionale\n",
332
+ "\n",
333
+ "### Test Manuale: Crea un documento su Google Drive e verifica la sincronizzazione"
334
+ ]
335
+ },
336
+ {
337
+ "cell_type": "code",
338
+ "execution_count": null,
339
+ "id": "0f47da2f",
340
+ "metadata": {
341
+ "vscode": {
342
+ "languageId": "javascript"
343
+ }
344
+ },
345
+ "outputs": [],
346
+ "source": [
347
+ "// 🧪 Test Completo della Replica Bidirezionale\n",
348
+ "\n",
349
+ "async function runFullReplicationTest() {\n",
350
+ " console.log('\\n=== FULL REPLICATION TEST ===\\n');\n",
351
+ " \n",
352
+ " // Step 1: Leggi lo stato iniziale\n",
353
+ " const localInfoBefore = await localDB.info();\n",
354
+ " const googleDriveInfoBefore = await googleDriveDB.info();\n",
355
+ " \n",
356
+ " console.log('Initial State:');\n",
357
+ " console.log(' Local:', { doc_count: localInfoBefore.doc_count, seq: localInfoBefore.update_seq });\n",
358
+ " console.log(' Google Drive:', { doc_count: googleDriveInfoBefore.doc_count, seq: googleDriveInfoBefore.update_seq });\n",
359
+ " \n",
360
+ " // Step 2: Crea un nuovo documento SU GOOGLE DRIVE\n",
361
+ " const testDocId = `test-sync-${Date.now()}`;\n",
362
+ " const testDoc = {\n",
363
+ " _id: testDocId,\n",
364
+ " title: 'Test Document for Replication',\n",
365
+ " createdAt: new Date().toISOString(),\n",
366
+ " source: 'google-drive-db'\n",
367
+ " };\n",
368
+ " \n",
369
+ " try {\n",
370
+ " const saveResult = await googleDriveDB.put(testDoc);\n",
371
+ " console.log('\\n✅ Created document on Google Drive:', { id: saveResult.id, rev: saveResult.rev });\n",
372
+ " } catch (err) {\n",
373
+ " console.error('❌ Failed to create document on Google Drive:', err);\n",
374
+ " return;\n",
375
+ " }\n",
376
+ " \n",
377
+ " // Step 3: Attendi il polling + replicazione (20 secondi dovrebbe essere sufficiente)\n",
378
+ " console.log('\\n⏳ Waiting for polling and replication (20 seconds)...');\n",
379
+ " \n",
380
+ " await new Promise(resolve => setTimeout(resolve, 20000));\n",
381
+ " \n",
382
+ " // Step 4: Verifica se il documento è arrivato al Local DB\n",
383
+ " try {\n",
384
+ " const docOnLocal = await localDB.get(testDocId);\n",
385
+ " console.log('\\n✅✅✅ SUCCESS! Document replicated to local DB:', {\n",
386
+ " id: docOnLocal._id,\n",
387
+ " rev: docOnLocal._rev,\n",
388
+ " title: docOnLocal.title\n",
389
+ " });\n",
390
+ " } catch (err) {\n",
391
+ " console.error('\\n❌ FAILED! Document NOT found on local DB:', err.message);\n",
392
+ " \n",
393
+ " // Diagnosi aggiuntiva\n",
394
+ " console.log('\\n🔍 DIAGNOSTIC INFO:');\n",
395
+ " const localInfoAfter = await localDB.info();\n",
396
+ " const googleDriveInfoAfter = await googleDriveDB.info();\n",
397
+ " \n",
398
+ " console.log('After Test:');\n",
399
+ " console.log(' Local:', { doc_count: localInfoAfter.doc_count, seq: localInfoAfter.update_seq });\n",
400
+ " console.log(' Google Drive:', { doc_count: googleDriveInfoAfter.doc_count, seq: googleDriveInfoAfter.update_seq });\n",
401
+ " \n",
402
+ " // Verifica che il doc sia effettivamente su Google Drive\n",
403
+ " try {\n",
404
+ " const verifyDoc = await googleDriveDB.get(testDocId);\n",
405
+ " console.log('✅ Document IS on Google Drive:', {\n",
406
+ " id: verifyDoc._id,\n",
407
+ " rev: verifyDoc._rev\n",
408
+ " });\n",
409
+ " console.log('❌ BUT NOT replicated to local. Issue: REPLICATION FROM REMOTE NOT WORKING');\n",
410
+ " } catch (e) {\n",
411
+ " console.error('❌ Document not even on Google Drive. Save failed?');\n",
412
+ " }\n",
413
+ " }\n",
414
+ "}\n",
415
+ "\n",
416
+ "// Esegui il test\n",
417
+ "await runFullReplicationTest();"
418
+ ]
419
+ },
420
+ {
421
+ "cell_type": "markdown",
422
+ "id": "051a4d4b",
423
+ "metadata": {},
424
+ "source": [
425
+ "---\n",
426
+ "\n",
427
+ "## 🔧 SOLUZIONI CONSIGLIATE\n",
428
+ "\n",
429
+ "Basandoti sui risultati del test sopra, ecco i passaggi per risolvere il problema:\n",
430
+ "\n",
431
+ "### Se il test fallisce (documento non replica from remote):\n",
432
+ "\n",
433
+ "**Soluzione 1: Aumenta il pollingIntervalMs**\n",
434
+ "```javascript\n",
435
+ "pollingIntervalMs: 2000 // Invece di 5000 - controlla più frequentemente\n",
436
+ "```\n",
437
+ "\n",
438
+ "**Soluzione 2: Riduci il timeout di replicazione**\n",
439
+ "```javascript\n",
440
+ "const repFrom = localDB.replicate.from(googleDriveDB, { \n",
441
+ " live: true, \n",
442
+ " retry: true,\n",
443
+ " timeout: 15000 // Aumenta timeout da default 30s\n",
444
+ "});\n",
445
+ "```\n",
446
+ "\n",
447
+ "**Soluzione 3: Aggiungi log nel adapter._changes in adapter.ts**\n",
448
+ "\n",
449
+ "Modifica `src/adapter.ts` linea ~428 per aggiungere debug:\n",
450
+ "```typescript\n",
451
+ "api._changes = function (opts: any): { cancel: () => void } {\n",
452
+ " console.log('[_changes] Called with opts:', {\n",
453
+ " since: opts.since,\n",
454
+ " limit: opts.limit,\n",
455
+ " live: opts.live,\n",
456
+ " include_docs: !!opts.include_docs\n",
457
+ " });\n",
458
+ " // ...\n",
459
+ "```\n",
460
+ "\n",
461
+ "**Soluzione 4: Verifica che il live listener sia correttamente configurato**\n",
462
+ "\n",
463
+ "Nel `DriveHandler.load()`, assicurati che il polling chiami `notifyListeners()` ogni volta che rileva cambiamenti:\n",
464
+ "```typescript\n",
465
+ "if (metaFile.modifiedTime !== this.metaModifiedTime) {\n",
466
+ " this.log('Polling detected change!', metaFile.modifiedTime);\n",
467
+ " await this.load();\n",
468
+ " this.notifyListeners(); // <-- DEVE ESSERE CHIAMATO\n",
469
+ "}\n",
470
+ "```"
471
+ ]
472
+ }
473
+ ],
474
+ "metadata": {
475
+ "language_info": {
476
+ "name": "python"
477
+ }
478
+ },
479
+ "nbformat": 4,
480
+ "nbformat_minor": 5
481
+ }
@@ -52,3 +52,64 @@ Compaction is a critical maintenance task that merges the `snapshot-data` with r
52
52
  ### Conflict Handling
53
53
  - **PouchDB Level**: Standard CouchDB revision conflicts (409) are preserved. A "winner" is chosen deterministically, but conflicting revisions are kept in the tree (requires `snapshot-index` to store the full revision tree, not just the winner).
54
54
  - **Adapter Level**: Drive API 409s handling (retry logic) ensures the transport layer is reliable.
55
+
56
+ ## 3. Testing with Local Express Server
57
+
58
+ The adapter includes a built-in test mode that emulates the Google Drive API using a local Express server. This allows for full integration testing without needing real Google Cloud credentials or network calls.
59
+
60
+ ### Configuration
61
+ To enable the test emulator:
62
+
63
+ ```typescript
64
+ const db = new PouchDB('testdb', {
65
+ adapter: 'googledrive',
66
+ testMode: true,
67
+ // testServerUrl: 'http://localhost:3000' // Optional, defaults to localhost:3000
68
+ });
69
+ ```
70
+
71
+ ### Test Server
72
+ The `TestServer` class (exported in `test-src/server.ts`) provides:
73
+ - In-memory metadata storage
74
+ - Local file system storage for content (defaults to `.test-drive-root` directory)
75
+ - Emulation of `files.list`, `files.get`, `files.create` (multipart), `files.update`, and `files.delete`.
76
+
77
+ ### Example Usage
78
+ ```typescript
79
+ import { TestServer } from './tests-src/server';
80
+
81
+ const server = new TestServer(3000);
82
+ await server.start();
83
+
84
+ // Run PouchDB operations...
85
+
86
+
87
+ ## 4. Production Testing (Real Google Drive API)
88
+
89
+ To verify the adapter against the actual Google Drive service, you can run the production test suite. This requires a valid Google OAuth2 Access Token.
90
+
91
+ ### A. Environment Setup
92
+ Create a `.env` file in the project root (see `.env.example`):
93
+ ```env
94
+ GOOGLE_ACCESS_TOKEN=your_temporary_access_token_here
95
+ ```
96
+
97
+ ### B. Obtaining a Token via OAuth1/2 Playground
98
+ The easiest way to get a temporary token for manual testing:
99
+ 1. Go to the [Google OAuth2 Playground](https://developers.google.com/oauthplayground/).
100
+ 2. **Step 1 (Select & authorize APIs)**:
101
+ * Find "Drive API v3" in the list.
102
+ * Select the scope: `https://www.googleapis.com/auth/drive.file` (this is the recommended scope as it only allows the app to see files it creates).
103
+ * Click **Authorize APIs** and sign in with your Google account.
104
+ 3. **Step 2 (Exchange authorization code for tokens)**:
105
+ * Click **Exchange authorization code for tokens**.
106
+ 4. **Step 3 (Configure request to API)**:
107
+ * Copy the **Access Token** string.
108
+ * Paste it into your `.env` file as `GOOGLE_ACCESS_TOKEN`.
109
+
110
+ ### C. Running the Tests
111
+ Execute the following command to run the tests in production mode:
112
+ ```bash
113
+ npm run test:prod
114
+ ```
115
+ This command sets `TEST_ENV=production`, which tells the test runner to skip the local Express emulator and use the real Google Drive endpoints with your provided token.
@@ -0,0 +1,75 @@
1
+ # Promise Support Documentation
2
+
3
+ The adapter now fully supports all three PouchDB call patterns:
4
+
5
+ ## 1. Callback Pattern
6
+ ```javascript
7
+ db.get('mydoc', function(err, doc) {
8
+ if (err) { return console.log(err); }
9
+ // handle doc
10
+ });
11
+ ```
12
+
13
+ ## 2. Promise Pattern
14
+ ```javascript
15
+ db.get('mydoc').then(function (doc) {
16
+ // handle doc
17
+ }).catch(function (err) {
18
+ console.log(err);
19
+ });
20
+ ```
21
+
22
+ ## 3. Async/Await Pattern
23
+ ```javascript
24
+ try {
25
+ const doc = await db.get('mydoc');
26
+ // handle doc
27
+ } catch (err) {
28
+ console.log(err);
29
+ }
30
+ ```
31
+
32
+ ## Methods Updated
33
+
34
+ The following adapter methods now support all three patterns:
35
+
36
+ ### Query Methods
37
+ - `_get()` / `get()` - Get a single document
38
+ - `_allDocs()` / `allDocs()` - Get all documents
39
+ - `_getLocal()` - Get a local document
40
+ - `_getRevisionTree()` - Get revision tree (internal)
41
+
42
+ ### Mutation Methods
43
+ - `_bulkDocs()` / `bulkDocs()` - Bulk document operations
44
+ - `_bulkGet()` / `bulkGet()` - Bulk get operation
45
+ - `_putLocal()` - Put a local document
46
+ - `_removeLocal()` - Remove a local document
47
+
48
+ ### Maintenance Methods
49
+ - `_compact()` - Compact the database
50
+
51
+ ## Implementation Details
52
+
53
+ Each method now:
54
+ 1. **Always returns a Promise** (even when a callback is provided)
55
+ 2. **Calls the callback if provided** for backward compatibility
56
+ 3. **Properly propagates errors** through both callbacks and rejections
57
+ 4. **Handles callback overloading** (when `opts` is actually the callback)
58
+
59
+ This allows PouchDB and its replication engine to use the adapter with promises and async/await, fixing the error:
60
+ ```
61
+ TypeError: can't access property "then", db.get(...) is undefined
62
+ ```
63
+
64
+ ## Migration Path
65
+
66
+ Your existing callback-based code continues to work:
67
+ ```javascript
68
+ // Old callback pattern - still works!
69
+ db.get('doc1', (err, doc) => {
70
+ if (err) console.error(err);
71
+ else console.log(doc);
72
+ });
73
+ ```
74
+
75
+ You can gradually migrate to promises/async-await as needed.
@@ -0,0 +1,63 @@
1
+ # Root Cause Analysis: Replication FROM Google Drive Issues
2
+
3
+ After extensive debugging with a live emulation test suite, multiple critical issues were identified in the `_changes` and `notifyListeners` implementations.
4
+
5
+ ## 1. Missing `seq` Property in Notifications (CRITICAL)
6
+ **Location**: `src/drive.ts` -> `notifyListeners()`
7
+
8
+ **Problem**:
9
+ The `notifyListeners` method was emitting change objects that included `id`, `rev`, and `deleted`, but omitted the `seq` (sequence number).
10
+
11
+ **Consequence**:
12
+ PouchDB's replication engine uses the `seq` to track checkpoints. If a change arrives without a `seq`, PouchDB silently ignores it or fails to update its "since" pointer, meaning the replication appears to "detect" a change but never actually processes the document.
13
+
14
+ **Fix**:
15
+ Updated the notification object to include the correct sequence number from the index.
16
+
17
+ ---
18
+
19
+ ## 2. Infinite Reconnect Loop in Live Changes
20
+ **Location**: `src/adapter.ts` -> `_changes()`
21
+
22
+ **Problem**:
23
+ An initial "fix" (based on standard CouchDB behavior) suggested calling `opts.complete()` at the end of the initial changes batch, even for live feeds.
24
+
25
+ **Consequence**:
26
+ In PouchDB, calling `complete()` on a **live** stream signals that the stream has ended gracefully. Because the replication is set to `live: true`, PouchDB interprets this as a connection drop and immediately restarts the changes feed. This created an infinite loop of:
27
+ `Connect -> Fetch Batch -> Call Complete -> Disconnect -> Reconnect`
28
+ This loop caused high CPU usage and eventually a "JavaScript heap out of memory" crash.
29
+
30
+ **Fix**:
31
+ Restricted `opts.complete()` to only be called if `!opts.live`.
32
+
33
+ ---
34
+
35
+ ## 3. System Document Leakage (`_local/`)
36
+ **Location**: `src/adapter.ts` -> `_changes()`
37
+
38
+ **Problem**:
39
+ PouchDB uses local documents (IDs starting with `_local/`) to store replication checkpoints. Our adapter was incorrectly including these internal documents in the `_changes` feed results.
40
+
41
+ **Consequence**:
42
+ When the remote side (Google Drive) reported a change to a `_local/` document, the local PouchDB tried to fetch it to replicate it. However, local documents are non-replicable and often missing from the full document fetch logic, leading to `not_found: missing` errors that crashed the replication process.
43
+
44
+ **Fix**:
45
+ Added a filter to both the initial batch and the live listener to ignore any document ID starting with `_local/`.
46
+
47
+ ---
48
+
49
+ ## 4. Inefficient Polling & Redundant `load()` Calls
50
+ **Location**: `src/drive.ts` -> `startPolling()` / `load()`
51
+
52
+ **Problem**:
53
+ The polling mechanism was frequently triggering `load()`, which cleared the entire `processedLogIds` cache and re-downloaded all change logs from Google Drive. Additionally, `load()` was being called on every conflict retry without checking if the metadata had actually changed.
54
+
55
+ **Fix**:
56
+ 1. Implemented **ETag-based change detection** in polling.
57
+ 2. Added an **ETag check at the start of `load()`** to skip processing if the metadata on the server haven't changed since the last load.
58
+ 3. Updated `tryAppendChanges` to proactively call `notifyListeners()` for local writes, reducing reliance on the next polling tick for local UI updates.
59
+
60
+ ---
61
+
62
+ ## Summary of Results
63
+ With these fixes applied, bidirectional replication is now stable and efficient. The "Out of Memory" crashes have been resolved, and changes from Google Drive are correctly detected and propagated to the local database within the specified polling interval.