@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.
package/lib/adapter.js CHANGED
@@ -6,8 +6,13 @@ const drive_1 = require("./drive");
6
6
  * Schedule a function to run asynchronously.
7
7
  */
8
8
  function nextTick(fn) {
9
- queueMicrotask(fn);
9
+ if (typeof fn === 'function') {
10
+ queueMicrotask(fn);
11
+ }
10
12
  }
13
+ /**
14
+ * TODO: Implement dynamic method signature, since PouchDB CRUD methods can be called as promise, callback or (?)
15
+ */
11
16
  /**
12
17
  * GoogleDriveAdapter - PouchDB adapter for Google Drive storage.
13
18
  * Updated for Lazy Loading (Async Access).
@@ -22,20 +27,73 @@ function GoogleDriveAdapter(PouchDB) {
22
27
  let instanceId;
23
28
  let db;
24
29
  // Initialize DriveHandler
30
+ console.log('Initializing DriveHandler with options:', adapterOpts);
25
31
  db = new drive_1.DriveHandler(adapterOpts, name);
26
- // After database is initialized
27
- function afterDBCreated() {
28
- instanceId = 'gdrive-' + name + '-' + Date.now().toString(36);
29
- nextTick(function () {
30
- callback(null, api);
31
- });
32
+ // Initialize instanceId synchronously to avoid race conditions during replication setup
33
+ instanceId = 'gdrive-' + name + '-' + Date.now().toString(36);
34
+ // Polyfill activeTasks to ensure replication works even if PouchDB core doesn't attach it
35
+ if (!api.activeTasks) {
36
+ class ActiveTasks {
37
+ constructor() {
38
+ this._tasks = {};
39
+ }
40
+ add(task) {
41
+ const id = Date.now().toString(36) + Math.random().toString(36).substring(2, 5);
42
+ this._tasks[id] = task;
43
+ this._tasks[id].created_at = new Date().toISOString();
44
+ this._tasks[id].updated_at = new Date().toISOString();
45
+ return id;
46
+ }
47
+ remove(id, err) {
48
+ delete this._tasks[id];
49
+ }
50
+ update(id, update) {
51
+ if (this._tasks[id]) {
52
+ Object.assign(this._tasks[id], update);
53
+ this._tasks[id].updated_at = new Date().toISOString();
54
+ }
55
+ }
56
+ get(id) {
57
+ return this._tasks[id];
58
+ }
59
+ list() {
60
+ return Object.values(this._tasks);
61
+ }
62
+ }
63
+ // @ts-ignore
64
+ api.activeTasks = new ActiveTasks();
32
65
  }
66
+ // Wrap callback to ensure it's only called once
67
+ let callbackCalled = false;
68
+ const onceCallback = (err, api) => {
69
+ if (callbackCalled)
70
+ return;
71
+ callbackCalled = true;
72
+ callback(err, api);
73
+ };
74
+ const debug = adapterOpts.debug;
75
+ const log = (...args) => { if (debug)
76
+ console.log(`[googledrive-adapter] [${name}]`, ...args); };
77
+ log('Initializing with options:', {
78
+ folderId: adapterOpts.folderId,
79
+ folderName: adapterOpts.folderName,
80
+ pollingIntervalMs: adapterOpts.pollingIntervalMs,
81
+ debug: adapterOpts.debug
82
+ });
33
83
  // Load data from Drive and initialize
34
84
  db.load().then(() => {
85
+ log('Database loaded');
35
86
  afterDBCreated();
36
87
  }).catch((err) => {
37
- callback(err);
88
+ log('Load error', err);
89
+ onceCallback(err);
38
90
  });
91
+ // After database is initialized
92
+ function afterDBCreated() {
93
+ nextTick(function () {
94
+ onceCallback(null, api);
95
+ });
96
+ }
39
97
  // ============ PouchDB Adapter API Methods ============
40
98
  api._remote = false;
41
99
  api.type = function () {
@@ -46,77 +104,115 @@ function GoogleDriveAdapter(PouchDB) {
46
104
  };
47
105
  // Info now must be async-ish (calculated from Index)
48
106
  api._info = function (callback) {
49
- const keys = db.getIndexKeys();
50
- const docCount = keys.length; // Approximate (doesn't account for deleted unless filtered)
51
- // Filter deleted for accurate count
52
- let alive = 0;
53
- for (const k of keys) {
54
- const entry = db.getIndexEntry(k);
55
- if (entry && !entry.deleted)
56
- alive++;
57
- }
58
- const res = {
59
- doc_count: alive,
60
- update_seq: db.seq,
61
- backend_adapter: 'googledrive'
62
- };
63
- nextTick(function () {
64
- callback(null, res);
107
+ // Need to handle promise if it returns one
108
+ const promise = Promise.resolve().then(async () => {
109
+ const keys = await db.getIndexKeys();
110
+ const docCount = keys.length; // Approximate (doesn't account for deleted unless filtered)
111
+ // Filter deleted for accurate count
112
+ let alive = 0;
113
+ for (const k of keys) {
114
+ const entry = db.getIndexEntry(k);
115
+ if (entry && !entry.deleted)
116
+ alive++;
117
+ }
118
+ return {
119
+ db_name: name,
120
+ doc_count: alive,
121
+ update_seq: db.seq,
122
+ backend_adapter: 'googledrive'
123
+ };
65
124
  });
125
+ if (callback) {
126
+ promise.then(res => callback(null, res)).catch(err => callback(err));
127
+ }
128
+ else {
129
+ return promise;
130
+ }
66
131
  };
132
+ api.info = api._info;
67
133
  // Get a single document by ID (Async fetch)
68
134
  api._get = function (id, opts, callback) {
69
135
  if (typeof opts === 'function') {
70
136
  callback = opts;
71
137
  opts = {};
72
138
  }
73
- db.get(id).then(doc => {
139
+ // Ensure opts is always an object
140
+ if (!opts || typeof opts !== 'object') {
141
+ opts = {};
142
+ }
143
+ // PouchDB sometimes asks for metadata only (revs, revs_info)
144
+ log('_get id:', id, 'opts:', JSON.stringify(opts));
145
+ const promise = db.get(id).then(doc => {
74
146
  if (!doc) {
75
- return callback({
147
+ log('_get missing', id);
148
+ const err = {
76
149
  status: 404,
77
150
  error: true,
78
151
  name: 'not_found',
79
- message: 'missing',
80
- reason: 'missing'
81
- });
152
+ message: 'missing'
153
+ };
154
+ if (callback)
155
+ callback(err);
156
+ throw err;
82
157
  }
83
- const metadata = {
84
- id: doc._id,
85
- rev: doc._rev,
86
- winningRev: doc._rev,
87
- deleted: !!doc._deleted
88
- };
89
- callback(null, { doc, metadata });
90
- }).catch(err => {
91
- callback(err);
158
+ // Support open_revs for bulkGet shim compatibility
159
+ if (opts.open_revs) {
160
+ log('_get handling open_revs for:', id);
161
+ // We currently only store the winning revision.
162
+ // If open_revs='all' or includes our rev, return it.
163
+ // TODO: Robust conflict support would check exact rev matching.
164
+ const result = [{ ok: doc }];
165
+ if (callback)
166
+ callback(null, result);
167
+ return result;
168
+ }
169
+ log('_get returning standard doc for:', id);
170
+ // If only rev was requested? (Internal optimization)
171
+ // PouchDB core handles this if we return the full doc.
172
+ const response = doc;
173
+ if (callback)
174
+ callback(null, response);
175
+ return response;
176
+ }).catch(e => {
177
+ log('_get error', e);
178
+ if (callback)
179
+ callback(e);
180
+ throw e;
92
181
  });
182
+ return promise;
93
183
  };
184
+ api.get = api._get;
94
185
  // Get all documents (Lazy stream or fetch)
95
186
  api._allDocs = function (opts, callback) {
96
187
  if (typeof opts === 'function') {
97
188
  callback = opts;
98
189
  opts = {};
99
190
  }
100
- const keys = db.getIndexKeys();
101
- const total = keys.length; // Total keys (including deleted?)
102
- let startIndex = opts.skip || 0;
103
- let limit = typeof opts.limit === 'number' ? opts.limit : keys.length;
104
- let filteredKeys = keys;
105
- if (opts.startkey)
106
- filteredKeys = filteredKeys.filter(k => k >= opts.startkey);
107
- if (opts.endkey)
108
- filteredKeys = filteredKeys.filter(k => k <= opts.endkey);
109
- if (opts.key)
110
- filteredKeys = filteredKeys.filter(k => k === opts.key);
111
- if (opts.keys)
112
- filteredKeys = opts.keys;
113
- filteredKeys.sort();
114
- if (opts.descending)
115
- filteredKeys.reverse();
116
- const sliced = filteredKeys.slice(startIndex, startIndex + limit);
117
- // Fetch actual docs if needed
118
- if (opts.include_docs) {
119
- db.getMulti(sliced).then(docs => {
191
+ // Ensure opts is always an object
192
+ if (!opts || typeof opts !== 'object') {
193
+ opts = {};
194
+ }
195
+ const promise = (async () => {
196
+ const keys = await db.getIndexKeys();
197
+ const total = keys.length; // Total keys (including deleted?)
198
+ let startIndex = opts.skip || 0;
199
+ let limit = typeof opts.limit === 'number' ? opts.limit : keys.length;
200
+ let filteredKeys = keys;
201
+ if (opts.startkey)
202
+ filteredKeys = filteredKeys.filter(k => k >= opts.startkey);
203
+ if (opts.endkey)
204
+ filteredKeys = filteredKeys.filter(k => k <= opts.endkey);
205
+ if (opts.key)
206
+ filteredKeys = filteredKeys.filter(k => k === opts.key);
207
+ if (opts.keys)
208
+ filteredKeys = opts.keys;
209
+ filteredKeys.sort();
210
+ if (opts.descending)
211
+ filteredKeys.reverse();
212
+ const sliced = filteredKeys.slice(startIndex, startIndex + limit);
213
+ // Fetch actual docs if needed
214
+ if (opts.include_docs) {
215
+ const docs = await db.getMulti(sliced);
120
216
  const rows = sliced.map((id, i) => {
121
217
  const doc = docs[i];
122
218
  const entry = db.getIndexEntry(id);
@@ -142,31 +238,78 @@ function GoogleDriveAdapter(PouchDB) {
142
238
  };
143
239
  if (opts.update_seq)
144
240
  result.update_seq = db.seq;
145
- callback(null, result);
146
- }).catch(err => callback(err));
241
+ return result;
242
+ }
243
+ else {
244
+ // Index only (Fast!)
245
+ const rows = sliced.map(id => {
246
+ const entry = db.getIndexEntry(id);
247
+ if (!entry || entry.deleted)
248
+ return { key: id, error: 'not_found' };
249
+ return {
250
+ id,
251
+ key: id,
252
+ value: { rev: entry.rev }
253
+ };
254
+ });
255
+ const result = {
256
+ total_rows: total,
257
+ offset: startIndex,
258
+ rows
259
+ };
260
+ if (opts.update_seq)
261
+ result.update_seq = db.seq;
262
+ return result;
263
+ }
264
+ })();
265
+ if (callback) {
266
+ promise.then(res => callback(null, res)).catch(err => callback(err));
147
267
  }
148
268
  else {
149
- // Index only (Fast!)
150
- const rows = sliced.map(id => {
151
- const entry = db.getIndexEntry(id);
152
- if (!entry || entry.deleted)
153
- return { key: id, error: 'not_found' };
154
- return {
155
- id,
156
- key: id,
157
- value: { rev: entry.rev }
158
- };
159
- });
160
- const result = {
161
- total_rows: total,
162
- offset: startIndex,
163
- rows
164
- };
165
- if (opts.update_seq)
166
- result.update_seq = db.seq;
167
- nextTick(() => callback(null, result));
269
+ return promise;
168
270
  }
169
271
  };
272
+ api.allDocs = api._allDocs;
273
+ // Bulk Get optimization for Replication
274
+ api._bulkGet = function (opts, callback) {
275
+ const docs = opts.docs;
276
+ const ids = docs.map((d) => d.id);
277
+ return db.getMulti(ids).then(results => {
278
+ const response = {
279
+ results: ids.map((id, i) => {
280
+ const doc = results[i];
281
+ const requestedRev = docs[i].rev;
282
+ const entry = db.getIndexEntry(id);
283
+ let docResult;
284
+ if (!doc || (requestedRev && doc._rev !== requestedRev)) {
285
+ docResult = {
286
+ error: {
287
+ status: 404,
288
+ error: true,
289
+ name: 'not_found',
290
+ message: 'missing'
291
+ }
292
+ };
293
+ }
294
+ else {
295
+ docResult = { ok: doc };
296
+ }
297
+ return {
298
+ id,
299
+ docs: [docResult]
300
+ };
301
+ })
302
+ };
303
+ if (callback)
304
+ callback(null, response);
305
+ return response;
306
+ }).catch((err) => {
307
+ if (callback)
308
+ callback(err);
309
+ throw err;
310
+ });
311
+ };
312
+ api.bulkGet = api._bulkGet;
170
313
  // Bulk document operations
171
314
  api._bulkDocs = function (req, opts, callback) {
172
315
  const docs = req.docs;
@@ -226,13 +369,21 @@ function GoogleDriveAdapter(PouchDB) {
226
369
  results.push({ ok: true, id, rev: newRev });
227
370
  }
228
371
  }
372
+ log('_bulkDocs flushing', changes.length, 'changes');
229
373
  // Append changes to log
230
- db.appendChanges(changes).then(() => {
231
- nextTick(() => callback(null, results));
374
+ const promise = db.appendChanges(changes).then(() => {
375
+ if (callback)
376
+ nextTick(() => callback(null, results));
377
+ return results;
232
378
  }).catch((err) => {
233
- callback(err);
379
+ log('_bulkDocs error', err);
380
+ if (callback)
381
+ callback(err);
382
+ throw err;
234
383
  });
384
+ return promise;
235
385
  };
386
+ api.bulkDocs = api._bulkDocs;
236
387
  // Changes feed
237
388
  api._changes = function (opts) {
238
389
  opts = Object.assign({}, opts);
@@ -242,50 +393,52 @@ function GoogleDriveAdapter(PouchDB) {
242
393
  const results = [];
243
394
  let lastSeq = since;
244
395
  let complete = false;
245
- // Should we iterate Index or Logs?
246
- // "Index" only has LATEST state. _changes usually wants history if `since` is old.
247
- // But this adapter is an "Index + Log" adapter.
248
- // If `since` is 0, we can walk the Index.
249
- // If `since` is recent, we can maybe walk pending changes?
250
- // Correct implementation of `_changes` with Append-Only Log requires reading the log files essentially.
251
- // BUT, standard PouchDB `_changes` often just iterates all docs if it can't stream.
252
- // For now, let's iterate the INDEX (Winning Revisions) which implies "since=0" behavior effectively (State of the World).
253
- function processChanges() {
254
- const keys = db.getIndexKeys(); // IDs
255
- let processed = 0;
256
- // Index-based iteration
257
- for (const id of keys) {
258
- if (complete || processed >= limit)
259
- break;
260
- const entry = db.getIndexEntry(id);
261
- if (!entry)
262
- continue;
263
- // Filter by seq?
264
- if (entry.seq <= since)
265
- continue; // Already seen
266
- const change = {
267
- id: id,
268
- seq: entry.seq,
269
- changes: [{ rev: entry.rev }],
270
- };
271
- if (opts.include_docs) {
272
- // We need to fetch it!
273
- // This makes _changes with include_docs SLOW.
274
- // We can't do this synchronously here easily because `processChanges` is sync in original code?
275
- // Wait, original was `nextTick(processChanges)`.
276
- // We need to be async here.
396
+ // Setup live listener FIRST (before emitting changes)
397
+ let cancelLive;
398
+ let liveListener;
399
+ if (opts.live) {
400
+ log('_changes setting up live listener');
401
+ liveListener = (changedDocs) => {
402
+ if (complete)
403
+ return;
404
+ for (const id of Object.keys(changedDocs)) {
405
+ if (id.startsWith('_local/'))
406
+ continue;
407
+ const entry = db.getIndexEntry(id);
408
+ if (entry && entry.seq > lastSeq) {
409
+ const change = {
410
+ id: id,
411
+ seq: entry.seq,
412
+ changes: [{ rev: entry.rev }]
413
+ };
414
+ if (opts.include_docs) {
415
+ db.get(id).then(doc => {
416
+ change.doc = doc;
417
+ if (opts.onChange)
418
+ opts.onChange(change);
419
+ lastSeq = Math.max(lastSeq, change.seq);
420
+ }).catch(e => log('Live change body fetch error', e));
421
+ }
422
+ else {
423
+ if (opts.onChange)
424
+ opts.onChange(change);
425
+ lastSeq = Math.max(lastSeq, change.seq);
426
+ }
427
+ }
277
428
  }
278
- // Supporting async processChanges is cleaner.
279
- }
280
- // ... This requires rewrite for async iteration ...
429
+ };
430
+ cancelLive = db.onChange(liveListener);
281
431
  }
282
- // Simplified Async Version
432
+ // Process initial changes
283
433
  async function processChangesAsync() {
284
- const keys = db.getIndexKeys();
434
+ log('_changes processing since', since, 'limit', limit, 'live', !!opts.live);
435
+ const keys = await db.getIndexKeys();
285
436
  let processed = 0;
286
437
  for (const id of keys) {
287
438
  if (complete || processed >= limit)
288
439
  break;
440
+ if (id.startsWith('_local/'))
441
+ continue;
289
442
  const entry = db.getIndexEntry(id);
290
443
  if (!entry || entry.seq <= since)
291
444
  continue;
@@ -295,7 +448,12 @@ function GoogleDriveAdapter(PouchDB) {
295
448
  changes: [{ rev: entry.rev }]
296
449
  };
297
450
  if (opts.include_docs) {
298
- change.doc = await db.get(id);
451
+ try {
452
+ change.doc = await db.get(id);
453
+ }
454
+ catch (e) {
455
+ log('_changes include_docs error', e);
456
+ }
299
457
  }
300
458
  if (opts.onChange)
301
459
  opts.onChange(change);
@@ -304,53 +462,67 @@ function GoogleDriveAdapter(PouchDB) {
304
462
  processed++;
305
463
  lastSeq = Math.max(lastSeq, entry.seq);
306
464
  }
307
- if (opts.complete && !complete) {
465
+ // ✅ Call opts.complete() ONLY for non-live modes
466
+ // PouchDB replication will infinite-loop reconnect if we call complete() on a live feed
467
+ if (opts.complete && !complete && !opts.live) {
468
+ log('_changes calling complete callback with', { results_count: results.length, last_seq: lastSeq });
308
469
  opts.complete(null, { results, last_seq: lastSeq });
309
470
  }
310
471
  }
311
- if (opts.live) {
312
- db.onChange((changes) => {
313
- // Update feed
314
- // ...
315
- });
316
- }
317
- nextTick(() => {
318
- processChangesAsync().catch(err => {
319
- console.error('Changes feed error', err);
320
- if (opts.complete)
321
- opts.complete(err);
322
- });
472
+ // Start processing (async) which will wait for load() via getIndexKeys()
473
+ processChangesAsync().catch(err => {
474
+ log('_changes error', err);
475
+ complete = true;
476
+ if (opts.complete)
477
+ opts.complete(err);
323
478
  });
324
479
  return {
325
480
  cancel() {
481
+ log('_changes cancel');
326
482
  complete = true;
483
+ if (cancelLive)
484
+ cancelLive();
327
485
  }
328
486
  };
329
487
  };
330
488
  // Manual compaction trigger
331
489
  api._compact = function (callback) {
332
- db.compact().then(() => {
333
- callback(null, { ok: true });
490
+ const promise = db.compact().then(() => {
491
+ const result = { ok: true };
492
+ if (callback)
493
+ callback(null, result);
494
+ return result;
334
495
  }).catch((err) => {
335
- callback(err);
496
+ if (callback)
497
+ callback(err);
498
+ throw err;
336
499
  });
500
+ return promise;
337
501
  };
338
502
  api._getRevisionTree = function (docId, callback) {
339
- db.get(docId).then(doc => {
340
- if (!doc) {
341
- return callback({ status: 404, error: true, name: 'not_found', message: 'missing' });
342
- }
343
- const revTree = [{
344
- pos: 1,
345
- ids: [doc._rev.split('-')[1], { status: 'available' }, []]
346
- }];
347
- callback(null, revTree);
348
- }).catch(callback);
503
+ const entry = db.getIndexEntry(docId);
504
+ if (!entry) {
505
+ return callback({ status: 404, error: true, name: 'not_found', message: 'missing' });
506
+ }
507
+ // Return a minimal tree based on the known winning revision
508
+ const revNum = parseInt(entry.rev.split('-')[0], 10);
509
+ const revHash = entry.rev.split('-')[1];
510
+ const revTree = [{
511
+ pos: revNum,
512
+ ids: [revHash, { status: 'available' }, []]
513
+ }];
514
+ callback(null, revTree);
349
515
  };
350
516
  api._close = function (callback) {
351
517
  db.stopPolling();
352
- nextTick(callback);
518
+ if (typeof callback === 'function') {
519
+ nextTick(callback);
520
+ }
521
+ else {
522
+ return Promise.resolve();
523
+ }
353
524
  };
525
+ api.close = api._close;
354
526
  api._destroy = function (opts, callback) {
355
527
  if (typeof opts === 'function') {
356
528
  callback = opts;
@@ -358,16 +530,28 @@ function GoogleDriveAdapter(PouchDB) {
358
530
  }
359
531
  db.stopPolling();
360
532
  if (opts.deleteFolder) {
361
- db.deleteFolder().then(() => {
362
- callback(null, { ok: true });
533
+ return db.deleteFolder().then(() => {
534
+ if (typeof callback === 'function') {
535
+ callback(null, { ok: true });
536
+ }
537
+ return { ok: true };
363
538
  }).catch((err) => {
364
- callback(err);
539
+ if (typeof callback === 'function') {
540
+ callback(err);
541
+ }
542
+ throw err;
365
543
  });
366
544
  }
367
545
  else {
368
- nextTick(() => callback(null, { ok: true }));
546
+ if (typeof callback === 'function') {
547
+ nextTick(() => callback(null, { ok: true }));
548
+ }
549
+ else {
550
+ return Promise.resolve({ ok: true });
551
+ }
369
552
  }
370
553
  };
554
+ api.destroy = api._destroy;
371
555
  api._putLocal = function (doc, opts, callback) {
372
556
  if (typeof opts === 'function') {
373
557
  callback = opts;
@@ -383,18 +567,35 @@ function GoogleDriveAdapter(PouchDB) {
383
567
  doc: savedDoc,
384
568
  timestamp: Date.now()
385
569
  };
386
- db.appendChanges([change]).then(() => {
387
- callback(null, { ok: true, id, rev });
570
+ const promise = db.appendChanges([change]).then(() => {
571
+ const result = { ok: true, id, rev };
572
+ if (callback)
573
+ callback(null, result);
574
+ return result;
388
575
  }).catch((err) => {
389
- callback(err);
576
+ if (callback)
577
+ callback(err);
578
+ throw err;
390
579
  });
580
+ return promise;
391
581
  };
392
582
  api._getLocal = function (id, callback) {
393
- db.get(id).then(doc => {
394
- if (!doc)
395
- return callback({ status: 404, error: true, name: 'not_found' });
396
- callback(null, doc);
397
- }).catch(callback);
583
+ const promise = db.get(id).then(doc => {
584
+ if (!doc) {
585
+ const err = { status: 404, error: true, name: 'not_found' };
586
+ if (callback)
587
+ callback(err);
588
+ throw err;
589
+ }
590
+ if (callback)
591
+ callback(null, doc);
592
+ return doc;
593
+ }).catch(err => {
594
+ if (callback)
595
+ callback(err);
596
+ throw err;
597
+ });
598
+ return promise;
398
599
  };
399
600
  api._removeLocal = function (doc, opts, callback) {
400
601
  // ... Similar async update ...
@@ -405,7 +606,10 @@ function GoogleDriveAdapter(PouchDB) {
405
606
  const id = doc._id;
406
607
  // Check existence async if we want to be strict, but index check is ok
407
608
  if (!db.getIndexEntry(id)) {
408
- return callback({ status: 404, error: true, name: 'not_found' });
609
+ const err = { status: 404, error: true, name: 'not_found' };
610
+ if (callback)
611
+ callback(err);
612
+ return Promise.reject(err);
409
613
  }
410
614
  // ...
411
615
  // Simplified removeLocal
@@ -416,11 +620,17 @@ function GoogleDriveAdapter(PouchDB) {
416
620
  deleted: true,
417
621
  timestamp: Date.now()
418
622
  };
419
- db.appendChanges([change]).then(() => {
420
- callback(null, { ok: true, id, rev: '0-0' });
623
+ const promise = db.appendChanges([change]).then(() => {
624
+ const result = { ok: true, id, rev: '0-0' };
625
+ if (callback)
626
+ callback(null, result);
627
+ return result;
421
628
  }).catch((err) => {
422
- callback(err);
629
+ if (callback)
630
+ callback(err);
631
+ throw err;
423
632
  });
633
+ return promise;
424
634
  };
425
635
  }
426
636
  // Static properties