@docstack/pouchdb-adapter-googledrive 0.0.8 → 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/compaction_test.log +725 -0
- package/docs/DEBUG_BIDIRECTIONAL_REPLICATION.ipynb +481 -0
- package/docs/DOCUMENTATION.md +115 -0
- package/docs/PROMISE_SUPPORT.md +75 -0
- package/docs/REPLICATION_ISSUE.md +63 -0
- package/emulation-test.log +163 -0
- package/failing_test.log +31 -0
- package/final_test.log +2620 -0
- package/lib/adapter.d.ts +20 -0
- package/lib/adapter.js +650 -0
- package/lib/cache.d.ts +12 -0
- package/lib/cache.js +42 -0
- package/lib/client.d.ts +37 -0
- package/lib/client.js +189 -0
- package/lib/drive.d.ts +82 -0
- package/lib/drive.js +737 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.js +56 -0
- package/lib/types.d.ts +92 -0
- package/lib/types.js +2 -0
- package/live_test.log +1209 -0
- package/package.json +1 -1
- package/test_output.log +563 -0
- package/test_output_2.log +1408 -0
- package/test_output_3.log +2445 -0
- package/.test-drive-integration/1xssnxhy3akj +0 -1
- package/.test-drive-integration/8p8eymf8afm +0 -1
- package/.test-drive-integration/d1a4udbbuh +0 -1
- package/.test-drive-integration/usx93j46xtl +0 -1
- package/.test-drive-integration/vc0pq3g8vp +0 -1
- package/.test-drive-integration/zenra4v6i +0 -1
- package/.test-drive-root/05wzn5mtlrzt +0 -1
- package/.test-drive-root/0axnnh1gmn2d +0 -1
- package/.test-drive-root/0l2pw73vk4i +0 -1
- package/.test-drive-root/0tzush8sunaj +0 -1
- package/.test-drive-root/143bh2d29rnl +0 -1
- package/.test-drive-root/1synnvu59il +0 -1
- package/.test-drive-root/239cd47eh23i +0 -1
- package/.test-drive-root/275ocoaj6nq +0 -1
- package/.test-drive-root/29d78af16s4 +0 -1
- package/.test-drive-root/2gzvpzclnwu +0 -1
- package/.test-drive-root/2pmk0leotvp +0 -1
- package/.test-drive-root/38ijn3s7p33 +0 -1
- package/.test-drive-root/3jm64la13rp +0 -1
- package/.test-drive-root/3l4zrzi5bm2 +0 -1
- package/.test-drive-root/3of67xqpsfh +0 -1
- package/.test-drive-root/3pi1rk70we5 +0 -1
- package/.test-drive-root/3w3n7tpdn4n +0 -1
- package/.test-drive-root/3xg31irodim +0 -1
- package/.test-drive-root/50asm455hsa +0 -1
- package/.test-drive-root/51ldbder4dm +0 -1
- package/.test-drive-root/5l7dt9r491r +0 -1
- package/.test-drive-root/65mfhb9yuq5 +0 -1
- package/.test-drive-root/6iocm2rs118 +0 -1
- package/.test-drive-root/6o6vlpa6r6j +0 -1
- package/.test-drive-root/6rrcl4sdhoi +0 -1
- package/.test-drive-root/6ucq10yu9bx +0 -1
- package/.test-drive-root/7de4gtsch5b +0 -1
- package/.test-drive-root/7j4j3eq54q +0 -1
- package/.test-drive-root/7n3ai8wgsvy +0 -1
- package/.test-drive-root/7v4b3eyitw3 +0 -1
- package/.test-drive-root/8e15eu4xksq +0 -1
- package/.test-drive-root/8pu2c63yh1j +0 -1
- package/.test-drive-root/8ryxg058q8g +0 -1
- package/.test-drive-root/99l00ber08g +0 -1
- package/.test-drive-root/9gxcumnvepb +0 -1
- package/.test-drive-root/a4xxhwpe2cp +0 -1
- package/.test-drive-root/a57knn0ufon +0 -1
- package/.test-drive-root/a7gszl8y5ir +0 -1
- package/.test-drive-root/b0nddw4fdbu +0 -1
- package/.test-drive-root/b3b1d1yuolh +0 -1
- package/.test-drive-root/b4syq7nig1f +0 -1
- package/.test-drive-root/bcc8mhlffkg +0 -1
- package/.test-drive-root/bmngbpsk34 +0 -1
- package/.test-drive-root/c9i4hvcll2s +0 -1
- package/.test-drive-root/cao688yanw +0 -1
- package/.test-drive-root/cdtduwukrek +0 -1
- package/.test-drive-root/ckzeli8w7ej +0 -1
- package/.test-drive-root/ctywezu2fi5 +0 -1
- package/.test-drive-root/cyxiv5vgtle +0 -1
- package/.test-drive-root/d83g0suoz08 +0 -1
- package/.test-drive-root/d9tefhps5m6 +0 -1
- package/.test-drive-root/dac156l068 +0 -1
- package/.test-drive-root/daqsvyj5cu9 +0 -1
- package/.test-drive-root/df0wbnhhz1 +0 -1
- package/.test-drive-root/difkbp1duv +0 -1
- package/.test-drive-root/djrry1wk98v +0 -1
- package/.test-drive-root/dx42jrz7ty9 +0 -1
- package/.test-drive-root/e0s76kp3ena +0 -1
- package/.test-drive-root/e9xvdv0mqig +0 -1
- package/.test-drive-root/f3xo9hy1956 +0 -1
- package/.test-drive-root/f4scgrpgejw +0 -1
- package/.test-drive-root/f9kbt63p7xp +0 -1
- package/.test-drive-root/fe07yvs6p14 +0 -1
- package/.test-drive-root/ff5suiec8xv +0 -1
- package/.test-drive-root/fwc1qm37as +0 -1
- package/.test-drive-root/gh39v4lzxjp +0 -1
- package/.test-drive-root/gxvkr1qq21b +0 -1
- package/.test-drive-root/h4p64z7djk5 +0 -1
- package/.test-drive-root/habe7525tnl +0 -1
- package/.test-drive-root/hhzddxj31ar +0 -1
- package/.test-drive-root/hyj9crn9sb +0 -1
- package/.test-drive-root/igey5y5f3mi +0 -1
- package/.test-drive-root/isvxwzkgym +0 -1
- package/.test-drive-root/javyqnt07ws +0 -1
- package/.test-drive-root/jevlhmyaczl +0 -1
- package/.test-drive-root/jve7ypin7bb +0 -1
- package/.test-drive-root/koycie6jmyr +0 -1
- package/.test-drive-root/kvlad8ps1l +0 -1
- package/.test-drive-root/l13wzs51qi +0 -1
- package/.test-drive-root/lbwwk07et6i +0 -1
- package/.test-drive-root/m3uoxcsft2g +0 -1
- package/.test-drive-root/m7l8z61tnei +0 -1
- package/.test-drive-root/mmwpyz6aa2e +0 -1
- package/.test-drive-root/mqbyniatwu +0 -1
- package/.test-drive-root/mz3jbwwr12 +0 -1
- package/.test-drive-root/mza04oxze7 +0 -1
- package/.test-drive-root/ndqlafk0c0c +0 -1
- package/.test-drive-root/noojz8gst2 +0 -1
- package/.test-drive-root/o8ujk9otq7 +0 -1
- package/.test-drive-root/oa54zbip13m +0 -1
- package/.test-drive-root/ore7c15j64a +0 -1
- package/.test-drive-root/oxj3tt9q16 +0 -1
- package/.test-drive-root/pac4f4x1r9 +0 -1
- package/.test-drive-root/pqawj7k06i +0 -1
- package/.test-drive-root/qujzt0hpj6i +0 -1
- package/.test-drive-root/r5sdb4w0d3 +0 -1
- package/.test-drive-root/r8pybcnme1r +0 -1
- package/.test-drive-root/rpht4r0nk9 +0 -1
- package/.test-drive-root/rru3wcrfved +0 -1
- package/.test-drive-root/rtq2tifpvgt +0 -1
- package/.test-drive-root/ru00ubalaar +0 -1
- package/.test-drive-root/rx65wqybqlm +0 -1
- package/.test-drive-root/s585nksqc5 +0 -1
- package/.test-drive-root/scvyemdb9gh +0 -1
- package/.test-drive-root/si2b3ac0lve +0 -1
- package/.test-drive-root/sjbanmc5v5k +0 -1
- package/.test-drive-root/sl2coirs8zo +0 -1
- package/.test-drive-root/so3w2nrxm7 +0 -1
- package/.test-drive-root/t3y2vj69hz9 +0 -1
- package/.test-drive-root/t95zgk2fl8f +0 -1
- package/.test-drive-root/t9f08z6ayj +0 -1
- package/.test-drive-root/ta501uh71xi +0 -1
- package/.test-drive-root/taqhm9suyu +0 -1
- package/.test-drive-root/tya2ijynx5k +0 -1
- package/.test-drive-root/u4llyu4o7en +0 -1
- package/.test-drive-root/u5wco26a6lb +0 -1
- package/.test-drive-root/u6sricrto +0 -1
- package/.test-drive-root/ukcmmzglxa +0 -1
- package/.test-drive-root/uvsiktw52p +0 -1
- package/.test-drive-root/uylv1bn9swb +0 -1
- package/.test-drive-root/uzkzgxo3k5b +0 -1
- package/.test-drive-root/vf68w1pfrv +0 -1
- package/.test-drive-root/vp4g881bu7 +0 -1
- package/.test-drive-root/vuv8aavujb +0 -1
- package/.test-drive-root/wg24h3o6df +0 -1
- package/.test-drive-root/wyi5oftc8nh +0 -1
- package/.test-drive-root/x5zl3x2eh +0 -1
- package/.test-drive-root/xb3bpq9xtk +0 -1
- package/.test-drive-root/xfn089pthf9 +0 -1
- package/.test-drive-root/xrs7jul0mdl +0 -1
- package/.test-drive-root/y5qtilrbuzq +0 -1
- package/.test-drive-root/ysxlnyjuav +0 -1
- package/.test-drive-root/z2loakinuyk +0 -1
- package/.test-drive-root/z6lxmw8opjo +0 -1
- package/.test-drive-root/z74cnyignf9 +0 -1
- package/.test-drive-root/zi9agjhrfw +0 -1
- package/.test-drive-root/zspmm1wmo5l +0 -1
package/lib/adapter.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { GoogleDriveAdapterOptions } from './types';
|
|
2
|
+
/** Combined options type for PouchDB adapter */
|
|
3
|
+
interface AdapterOptions extends GoogleDriveAdapterOptions {
|
|
4
|
+
name: string;
|
|
5
|
+
}
|
|
6
|
+
/** Callback type for adapter initialization */
|
|
7
|
+
type AdapterCallback = (err: Error | null, api?: any) => void;
|
|
8
|
+
/**
|
|
9
|
+
* TODO: Implement dynamic method signature, since PouchDB CRUD methods can be called as promise, callback or (?)
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* GoogleDriveAdapter - PouchDB adapter for Google Drive storage.
|
|
13
|
+
* Updated for Lazy Loading (Async Access).
|
|
14
|
+
*/
|
|
15
|
+
export declare function GoogleDriveAdapter(PouchDB: any): {
|
|
16
|
+
(this: any, opts: AdapterOptions, callback: AdapterCallback): void;
|
|
17
|
+
valid(): boolean;
|
|
18
|
+
use_prefix: boolean;
|
|
19
|
+
};
|
|
20
|
+
export default GoogleDriveAdapter;
|
package/lib/adapter.js
ADDED
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GoogleDriveAdapter = GoogleDriveAdapter;
|
|
4
|
+
const drive_1 = require("./drive");
|
|
5
|
+
/**
|
|
6
|
+
* Schedule a function to run asynchronously.
|
|
7
|
+
*/
|
|
8
|
+
function nextTick(fn) {
|
|
9
|
+
if (typeof fn === 'function') {
|
|
10
|
+
queueMicrotask(fn);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* TODO: Implement dynamic method signature, since PouchDB CRUD methods can be called as promise, callback or (?)
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* GoogleDriveAdapter - PouchDB adapter for Google Drive storage.
|
|
18
|
+
* Updated for Lazy Loading (Async Access).
|
|
19
|
+
*/
|
|
20
|
+
function GoogleDriveAdapter(PouchDB) {
|
|
21
|
+
function GoogleDrivePouch(opts, callback) {
|
|
22
|
+
const api = this;
|
|
23
|
+
const name = opts.name;
|
|
24
|
+
// Clone options to avoid mutation
|
|
25
|
+
const adapterOpts = Object.assign({}, opts);
|
|
26
|
+
// Internal state
|
|
27
|
+
let instanceId;
|
|
28
|
+
let db;
|
|
29
|
+
// Initialize DriveHandler
|
|
30
|
+
console.log('Initializing DriveHandler with options:', adapterOpts);
|
|
31
|
+
db = new drive_1.DriveHandler(adapterOpts, name);
|
|
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();
|
|
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
|
+
});
|
|
83
|
+
// Load data from Drive and initialize
|
|
84
|
+
db.load().then(() => {
|
|
85
|
+
log('Database loaded');
|
|
86
|
+
afterDBCreated();
|
|
87
|
+
}).catch((err) => {
|
|
88
|
+
log('Load error', err);
|
|
89
|
+
onceCallback(err);
|
|
90
|
+
});
|
|
91
|
+
// After database is initialized
|
|
92
|
+
function afterDBCreated() {
|
|
93
|
+
nextTick(function () {
|
|
94
|
+
onceCallback(null, api);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
// ============ PouchDB Adapter API Methods ============
|
|
98
|
+
api._remote = false;
|
|
99
|
+
api.type = function () {
|
|
100
|
+
return 'googledrive';
|
|
101
|
+
};
|
|
102
|
+
api._id = function (callback) {
|
|
103
|
+
callback(null, instanceId);
|
|
104
|
+
};
|
|
105
|
+
// Info now must be async-ish (calculated from Index)
|
|
106
|
+
api._info = function (callback) {
|
|
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
|
+
};
|
|
124
|
+
});
|
|
125
|
+
if (callback) {
|
|
126
|
+
promise.then(res => callback(null, res)).catch(err => callback(err));
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
return promise;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
api.info = api._info;
|
|
133
|
+
// Get a single document by ID (Async fetch)
|
|
134
|
+
api._get = function (id, opts, callback) {
|
|
135
|
+
if (typeof opts === 'function') {
|
|
136
|
+
callback = opts;
|
|
137
|
+
opts = {};
|
|
138
|
+
}
|
|
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 => {
|
|
146
|
+
if (!doc) {
|
|
147
|
+
log('_get missing', id);
|
|
148
|
+
const err = {
|
|
149
|
+
status: 404,
|
|
150
|
+
error: true,
|
|
151
|
+
name: 'not_found',
|
|
152
|
+
message: 'missing'
|
|
153
|
+
};
|
|
154
|
+
if (callback)
|
|
155
|
+
callback(err);
|
|
156
|
+
throw err;
|
|
157
|
+
}
|
|
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;
|
|
181
|
+
});
|
|
182
|
+
return promise;
|
|
183
|
+
};
|
|
184
|
+
api.get = api._get;
|
|
185
|
+
// Get all documents (Lazy stream or fetch)
|
|
186
|
+
api._allDocs = function (opts, callback) {
|
|
187
|
+
if (typeof opts === 'function') {
|
|
188
|
+
callback = opts;
|
|
189
|
+
opts = {};
|
|
190
|
+
}
|
|
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);
|
|
216
|
+
const rows = sliced.map((id, i) => {
|
|
217
|
+
const doc = docs[i];
|
|
218
|
+
const entry = db.getIndexEntry(id);
|
|
219
|
+
if (!doc && (!entry || entry.deleted))
|
|
220
|
+
return { key: id, error: 'not_found' };
|
|
221
|
+
if (!doc && entry) {
|
|
222
|
+
// This implies fetch failed but exists in index? Or null result.
|
|
223
|
+
return { key: id, error: 'not_found' };
|
|
224
|
+
}
|
|
225
|
+
const row = {
|
|
226
|
+
id,
|
|
227
|
+
key: id,
|
|
228
|
+
value: { rev: entry?.rev || doc._rev }
|
|
229
|
+
};
|
|
230
|
+
row.doc = doc;
|
|
231
|
+
return row;
|
|
232
|
+
});
|
|
233
|
+
const result = {
|
|
234
|
+
total_rows: total,
|
|
235
|
+
offset: startIndex,
|
|
236
|
+
rows: rows.filter(r => !r.error || !opts.keys) // Filter errored unless specifically asked via keys?
|
|
237
|
+
// CouchDB usually returns error row if distinct keys requested.
|
|
238
|
+
};
|
|
239
|
+
if (opts.update_seq)
|
|
240
|
+
result.update_seq = db.seq;
|
|
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));
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
return promise;
|
|
270
|
+
}
|
|
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;
|
|
313
|
+
// Bulk document operations
|
|
314
|
+
api._bulkDocs = function (req, opts, callback) {
|
|
315
|
+
const docs = req.docs;
|
|
316
|
+
const results = [];
|
|
317
|
+
const newEdits = opts.new_edits !== false;
|
|
318
|
+
const changes = [];
|
|
319
|
+
// We need to validate revisions against Index
|
|
320
|
+
// This does NOT require fetching bodies usually
|
|
321
|
+
for (const doc of docs) {
|
|
322
|
+
const id = doc._id;
|
|
323
|
+
const seq = db.getNextSeq() + changes.length;
|
|
324
|
+
const entry = db.getIndexEntry(id);
|
|
325
|
+
if (doc._deleted) {
|
|
326
|
+
if (!entry || entry.deleted) {
|
|
327
|
+
results.push({
|
|
328
|
+
ok: false,
|
|
329
|
+
id,
|
|
330
|
+
error: 'not_found',
|
|
331
|
+
reason: 'missing'
|
|
332
|
+
});
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
// Check rev
|
|
336
|
+
const oldRev = entry.rev || '0-0'; // Index has latest
|
|
337
|
+
// If mismatch? PouchDB handles conflict logic before calling us sometimes?
|
|
338
|
+
// But we should verify.
|
|
339
|
+
// If doc._rev matches entry.rev, we are good.
|
|
340
|
+
const revNum = parseInt(oldRev.split('-')[0], 10) + 1;
|
|
341
|
+
const newRev = revNum + '-' + generateRevId();
|
|
342
|
+
changes.push({
|
|
343
|
+
seq,
|
|
344
|
+
id,
|
|
345
|
+
rev: newRev,
|
|
346
|
+
deleted: true,
|
|
347
|
+
timestamp: Date.now()
|
|
348
|
+
});
|
|
349
|
+
results.push({ ok: true, id, rev: newRev });
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
let newRev;
|
|
353
|
+
if (newEdits) {
|
|
354
|
+
const oldRev = entry?.rev || '0-0';
|
|
355
|
+
const revNum = parseInt(oldRev.split('-')[0], 10) + 1;
|
|
356
|
+
newRev = revNum + '-' + generateRevId();
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
newRev = doc._rev;
|
|
360
|
+
}
|
|
361
|
+
const savedDoc = Object.assign({}, doc, { _rev: newRev });
|
|
362
|
+
changes.push({
|
|
363
|
+
seq,
|
|
364
|
+
id,
|
|
365
|
+
rev: newRev,
|
|
366
|
+
doc: savedDoc,
|
|
367
|
+
timestamp: Date.now()
|
|
368
|
+
});
|
|
369
|
+
results.push({ ok: true, id, rev: newRev });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
log('_bulkDocs flushing', changes.length, 'changes');
|
|
373
|
+
// Append changes to log
|
|
374
|
+
const promise = db.appendChanges(changes).then(() => {
|
|
375
|
+
if (callback)
|
|
376
|
+
nextTick(() => callback(null, results));
|
|
377
|
+
return results;
|
|
378
|
+
}).catch((err) => {
|
|
379
|
+
log('_bulkDocs error', err);
|
|
380
|
+
if (callback)
|
|
381
|
+
callback(err);
|
|
382
|
+
throw err;
|
|
383
|
+
});
|
|
384
|
+
return promise;
|
|
385
|
+
};
|
|
386
|
+
api.bulkDocs = api._bulkDocs;
|
|
387
|
+
// Changes feed
|
|
388
|
+
api._changes = function (opts) {
|
|
389
|
+
opts = Object.assign({}, opts);
|
|
390
|
+
const since = opts.since || 0;
|
|
391
|
+
const limit = typeof opts.limit === 'number' ? opts.limit : Infinity;
|
|
392
|
+
const returnDocs = opts.return_docs !== false;
|
|
393
|
+
const results = [];
|
|
394
|
+
let lastSeq = since;
|
|
395
|
+
let complete = false;
|
|
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
|
+
}
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
cancelLive = db.onChange(liveListener);
|
|
431
|
+
}
|
|
432
|
+
// Process initial changes
|
|
433
|
+
async function processChangesAsync() {
|
|
434
|
+
log('_changes processing since', since, 'limit', limit, 'live', !!opts.live);
|
|
435
|
+
const keys = await db.getIndexKeys();
|
|
436
|
+
let processed = 0;
|
|
437
|
+
for (const id of keys) {
|
|
438
|
+
if (complete || processed >= limit)
|
|
439
|
+
break;
|
|
440
|
+
if (id.startsWith('_local/'))
|
|
441
|
+
continue;
|
|
442
|
+
const entry = db.getIndexEntry(id);
|
|
443
|
+
if (!entry || entry.seq <= since)
|
|
444
|
+
continue;
|
|
445
|
+
const change = {
|
|
446
|
+
id: id,
|
|
447
|
+
seq: entry.seq,
|
|
448
|
+
changes: [{ rev: entry.rev }]
|
|
449
|
+
};
|
|
450
|
+
if (opts.include_docs) {
|
|
451
|
+
try {
|
|
452
|
+
change.doc = await db.get(id);
|
|
453
|
+
}
|
|
454
|
+
catch (e) {
|
|
455
|
+
log('_changes include_docs error', e);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (opts.onChange)
|
|
459
|
+
opts.onChange(change);
|
|
460
|
+
if (returnDocs)
|
|
461
|
+
results.push(change);
|
|
462
|
+
processed++;
|
|
463
|
+
lastSeq = Math.max(lastSeq, entry.seq);
|
|
464
|
+
}
|
|
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 });
|
|
469
|
+
opts.complete(null, { results, last_seq: lastSeq });
|
|
470
|
+
}
|
|
471
|
+
}
|
|
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);
|
|
478
|
+
});
|
|
479
|
+
return {
|
|
480
|
+
cancel() {
|
|
481
|
+
log('_changes cancel');
|
|
482
|
+
complete = true;
|
|
483
|
+
if (cancelLive)
|
|
484
|
+
cancelLive();
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
};
|
|
488
|
+
// Manual compaction trigger
|
|
489
|
+
api._compact = function (callback) {
|
|
490
|
+
const promise = db.compact().then(() => {
|
|
491
|
+
const result = { ok: true };
|
|
492
|
+
if (callback)
|
|
493
|
+
callback(null, result);
|
|
494
|
+
return result;
|
|
495
|
+
}).catch((err) => {
|
|
496
|
+
if (callback)
|
|
497
|
+
callback(err);
|
|
498
|
+
throw err;
|
|
499
|
+
});
|
|
500
|
+
return promise;
|
|
501
|
+
};
|
|
502
|
+
api._getRevisionTree = function (docId, 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);
|
|
515
|
+
};
|
|
516
|
+
api._close = function (callback) {
|
|
517
|
+
db.stopPolling();
|
|
518
|
+
if (typeof callback === 'function') {
|
|
519
|
+
nextTick(callback);
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
return Promise.resolve();
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
api.close = api._close;
|
|
526
|
+
api._destroy = function (opts, callback) {
|
|
527
|
+
if (typeof opts === 'function') {
|
|
528
|
+
callback = opts;
|
|
529
|
+
opts = {};
|
|
530
|
+
}
|
|
531
|
+
db.stopPolling();
|
|
532
|
+
if (opts.deleteFolder) {
|
|
533
|
+
return db.deleteFolder().then(() => {
|
|
534
|
+
if (typeof callback === 'function') {
|
|
535
|
+
callback(null, { ok: true });
|
|
536
|
+
}
|
|
537
|
+
return { ok: true };
|
|
538
|
+
}).catch((err) => {
|
|
539
|
+
if (typeof callback === 'function') {
|
|
540
|
+
callback(err);
|
|
541
|
+
}
|
|
542
|
+
throw err;
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
if (typeof callback === 'function') {
|
|
547
|
+
nextTick(() => callback(null, { ok: true }));
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
return Promise.resolve({ ok: true });
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
api.destroy = api._destroy;
|
|
555
|
+
api._putLocal = function (doc, opts, callback) {
|
|
556
|
+
if (typeof opts === 'function') {
|
|
557
|
+
callback = opts;
|
|
558
|
+
opts = {};
|
|
559
|
+
}
|
|
560
|
+
const id = doc._id;
|
|
561
|
+
const rev = '0-1';
|
|
562
|
+
const savedDoc = Object.assign({}, doc, { _rev: rev });
|
|
563
|
+
const change = {
|
|
564
|
+
seq: db.getNextSeq(),
|
|
565
|
+
id,
|
|
566
|
+
rev,
|
|
567
|
+
doc: savedDoc,
|
|
568
|
+
timestamp: Date.now()
|
|
569
|
+
};
|
|
570
|
+
const promise = db.appendChanges([change]).then(() => {
|
|
571
|
+
const result = { ok: true, id, rev };
|
|
572
|
+
if (callback)
|
|
573
|
+
callback(null, result);
|
|
574
|
+
return result;
|
|
575
|
+
}).catch((err) => {
|
|
576
|
+
if (callback)
|
|
577
|
+
callback(err);
|
|
578
|
+
throw err;
|
|
579
|
+
});
|
|
580
|
+
return promise;
|
|
581
|
+
};
|
|
582
|
+
api._getLocal = function (id, 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;
|
|
599
|
+
};
|
|
600
|
+
api._removeLocal = function (doc, opts, callback) {
|
|
601
|
+
// ... Similar async update ...
|
|
602
|
+
if (typeof opts === 'function') {
|
|
603
|
+
callback = opts;
|
|
604
|
+
opts = {};
|
|
605
|
+
}
|
|
606
|
+
const id = doc._id;
|
|
607
|
+
// Check existence async if we want to be strict, but index check is ok
|
|
608
|
+
if (!db.getIndexEntry(id)) {
|
|
609
|
+
const err = { status: 404, error: true, name: 'not_found' };
|
|
610
|
+
if (callback)
|
|
611
|
+
callback(err);
|
|
612
|
+
return Promise.reject(err);
|
|
613
|
+
}
|
|
614
|
+
// ...
|
|
615
|
+
// Simplified removeLocal
|
|
616
|
+
const change = {
|
|
617
|
+
seq: db.getNextSeq(),
|
|
618
|
+
id,
|
|
619
|
+
rev: '0-0',
|
|
620
|
+
deleted: true,
|
|
621
|
+
timestamp: Date.now()
|
|
622
|
+
};
|
|
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;
|
|
628
|
+
}).catch((err) => {
|
|
629
|
+
if (callback)
|
|
630
|
+
callback(err);
|
|
631
|
+
throw err;
|
|
632
|
+
});
|
|
633
|
+
return promise;
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
// Static properties
|
|
637
|
+
GoogleDrivePouch.valid = function () {
|
|
638
|
+
return true;
|
|
639
|
+
};
|
|
640
|
+
GoogleDrivePouch.use_prefix = false;
|
|
641
|
+
return GoogleDrivePouch;
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Generate a random revision ID
|
|
645
|
+
*/
|
|
646
|
+
function generateRevId() {
|
|
647
|
+
return Math.random().toString(36).substring(2, 11) +
|
|
648
|
+
Math.random().toString(36).substring(2, 11);
|
|
649
|
+
}
|
|
650
|
+
exports.default = GoogleDriveAdapter;
|
package/lib/cache.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple Least Recently Used (LRU) Cache
|
|
3
|
+
*/
|
|
4
|
+
export declare class LRUCache<K, V> {
|
|
5
|
+
private capacity;
|
|
6
|
+
private map;
|
|
7
|
+
constructor(capacity: number);
|
|
8
|
+
get(key: K): V | undefined;
|
|
9
|
+
put(key: K, value: V): void;
|
|
10
|
+
remove(key: K): void;
|
|
11
|
+
clear(): void;
|
|
12
|
+
}
|