Skip to content

Commit 90b3ba1

Browse files
committed
fix: Do not use cached metadata for doc_count and seq
1 parent 70d531e commit 90b3ba1

3 files changed

Lines changed: 77 additions & 36 deletions

File tree

packages/node_modules/pouchdb-adapter-indexeddb/src/bulkDocs.js

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,7 @@ import { DOC_STORE, META_LOCAL_STORE, idbError } from './util';
2424
import { rewrite, sanitise } from './rewrite';
2525
const sanitisedAttachmentKey = sanitise('_attachments');
2626

27-
export default function (api, req, opts, metadata, dbOpts, idbChanges, callback) {
28-
29-
let txn;
27+
export default function (api, req, opts, connectionInfo, dbOpts, idbChanges, callback) {
3028

3129
// TODO: I would prefer to get rid of these globals
3230
let error;
@@ -49,7 +47,7 @@ export default function (api, req, opts, metadata, dbOpts, idbChanges, callback)
4947

5048
// Reads the original doc from the store if available
5149
// As in allDocs with keys option using multiple get calls is the fastest way
52-
function fetchExistingDocs(txn, docs) {
50+
function fetchExistingDocs(txn, metadata, docs) {
5351
let fetched = 0;
5452
const oldDocs = {};
5553

@@ -58,7 +56,7 @@ export default function (api, req, opts, metadata, dbOpts, idbChanges, callback)
5856
oldDocs[e.target.result.id] = e.target.result;
5957
}
6058
if (++fetched === docs.length) {
61-
processDocs(txn, docs, oldDocs);
59+
processDocs(txn, metadata, docs, oldDocs);
6260
}
6361
}
6462

@@ -76,7 +74,7 @@ export default function (api, req, opts, metadata, dbOpts, idbChanges, callback)
7674
});
7775
}
7876

79-
function processDocs(txn, docs, oldDocs) {
77+
function processDocs(txn, metadata, docs, oldDocs) {
8078

8179
docs.forEach(function (doc, i) {
8280
let newDoc;
@@ -116,7 +114,7 @@ export default function (api, req, opts, metadata, dbOpts, idbChanges, callback)
116114
} else {
117115
oldDocs[newDoc.id] = newDoc;
118116
lastWriteIndex = i;
119-
write(txn, newDoc, i);
117+
write(txn, metadata, newDoc, i);
120118
}
121119
});
122120
}
@@ -182,7 +180,7 @@ export default function (api, req, opts, metadata, dbOpts, idbChanges, callback)
182180
return doc;
183181
}
184182

185-
function write(txn, doc, i) {
183+
function write(txn, metadata, doc, i) {
186184

187185
// We copy the data from the winning revision into the root
188186
// of the document so that it can be indexed
@@ -287,7 +285,7 @@ export default function (api, req, opts, metadata, dbOpts, idbChanges, callback)
287285
rev: '0-0'
288286
};
289287
};
290-
updateSeq(i);
288+
updateSeq(txn, metadata, i);
291289
return;
292290
}
293291

@@ -298,11 +296,11 @@ export default function (api, req, opts, metadata, dbOpts, idbChanges, callback)
298296
id: doc.id,
299297
rev: writtenRev
300298
};
301-
updateSeq(i);
299+
updateSeq(txn, metadata, i);
302300
};
303301
}
304302

305-
function updateSeq(i) {
303+
function updateSeq(txn, metadata, i) {
306304
if (i === lastWriteIndex) {
307305
txn.objectStore(META_LOCAL_STORE).put(metadata);
308306
}
@@ -320,12 +318,12 @@ export default function (api, req, opts, metadata, dbOpts, idbChanges, callback)
320318
} catch (e) {
321319
return Promise.reject(createError(BAD_ARG, 'Attachment is not a valid base64 string'));
322320
}
323-
if (metadata.idb_attachment_format === 'binary') {
321+
if (connectionInfo.idb_attachment_format === 'binary') {
324322
attachment.data = binStringToBlobOrBuffer(binData, attachment.content_type);
325323
}
326324
} else {
327325
binData = attachment.data;
328-
if (metadata.idb_attachment_format === 'base64') {
326+
if (connectionInfo.idb_attachment_format === 'base64') {
329327
// TODO could run these in parallel, if we cared
330328
return new Promise(resolve => {
331329
blufferToBase64(attachment.data, function (b64) {
@@ -395,13 +393,11 @@ export default function (api, req, opts, metadata, dbOpts, idbChanges, callback)
395393
// We _could_ check doc ids here, and skip opening DOC_STORE if all docs are local.
396394
// This may marginally slow things down for local docs. It seems pragmatic to keep
397395
// the code simple and optimise for calls to bulkDocs() which include non-local docs.
398-
api._openTransactionSafely([DOC_STORE, META_LOCAL_STORE], 'readwrite', function (err, _txn) {
396+
api._openTransactionSafely([DOC_STORE, META_LOCAL_STORE], 'readwrite', function (err, txn) {
399397
if (err) {
400398
return callback(err);
401399
}
402400

403-
txn = _txn;
404-
405401
txn.onabort = function () {
406402
callback(error || createError(UNKNOWN_ERROR, 'transaction was aborted'));
407403
};
@@ -412,8 +408,11 @@ export default function (api, req, opts, metadata, dbOpts, idbChanges, callback)
412408
callback(null, results);
413409
};
414410

415-
// We would like to use promises here, but idb sucks
416-
fetchExistingDocs(txn, docs);
411+
const metaStore = txn.objectStore(META_LOCAL_STORE);
412+
metaStore.get(META_LOCAL_STORE).onsuccess = function (e) {
413+
const metadata = e.target.result;
414+
fetchExistingDocs(txn, metadata, docs);
415+
};
417416
});
418417
}).catch(function (err) {
419418
callback(err);

packages/node_modules/pouchdb-adapter-indexeddb/src/index.js

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,29 @@ function IndexeddbPouch(dbOpts, callback) {
3434
}
3535

3636
const api = this;
37-
let metadata = {};
37+
38+
// When the database is opened, we cache the "static" metadata fields i.e.
39+
// those that do not change when docs are updated. These are needed for some
40+
// async operations (e.g. preprocessing attachments) that cannot be performed
41+
// in the middle of a transaction due to the nature of the IndexedDB API, so
42+
// they need to know these settings without having to read from the object
43+
// store. "Dynamic" values like `doc_count` and `seq` must always be read
44+
// from the object store so that they are not stale.
45+
const CACHED_INFO_KEYS = ['db_uuid', 'idb_attachment_format'];
46+
const connectionInfo = {};
47+
48+
function updateConnectionInfo(metadata) {
49+
for (const key of CACHED_INFO_KEYS) {
50+
connectionInfo[key] = metadata[key];
51+
}
52+
}
3853

3954
// Wrapper that gives you an active DB handle. You probably want $t.
4055
const $ = function (fun) {
4156
return function () {
4257
const args = Array.prototype.slice.call(arguments);
4358
setup(openDatabases, api, dbOpts).then(function (res) {
44-
metadata = res.metadata;
59+
updateConnectionInfo(res.metadata);
4560
args.unshift(res.idb);
4661
fun.apply(api, args);
4762
}).catch(function (err) {
@@ -60,9 +75,8 @@ function IndexeddbPouch(dbOpts, callback) {
6075
const args = Array.prototype.slice.call(arguments);
6176

6277
return setup(openDatabases, api, dbOpts).then(function (res) {
63-
metadata = res.metadata;
78+
updateConnectionInfo(res.metadata);
6479
args.unshift(res.idb);
65-
6680
return fun.apply(api, args);
6781
});
6882
};
@@ -78,7 +92,7 @@ function IndexeddbPouch(dbOpts, callback) {
7892
const args = Array.prototype.slice.call(arguments);
7993
const txn = {};
8094
setup(openDatabases, api, dbOpts).then(function (res) {
81-
metadata = res.metadata;
95+
updateConnectionInfo(res.metadata);
8296
txn.txn = res.idb.transaction(stores, mode);
8397
}).catch(function (err) {
8498
console.error('Failed to establish transaction safely');
@@ -97,29 +111,44 @@ function IndexeddbPouch(dbOpts, callback) {
97111
}, stores, mode)(callback);
98112
};
99113

114+
api._getMetadata = $t(function (txn, cb) {
115+
const metaStore = txn.txn.objectStore(META_LOCAL_STORE);
116+
metaStore.get(META_LOCAL_STORE).onsuccess = function (e) {
117+
cb(null, e.target.result);
118+
};
119+
}, [META_LOCAL_STORE]);
120+
100121
api._remote = false;
101122
api.type = function () { return ADAPTER_NAME; };
102123

103-
api._id = $(function (_, cb) {
104-
cb(null, metadata.db_uuid);
105-
});
124+
api._id = function (cb) {
125+
api._getMetadata(function (err, metadata) {
126+
cb(null, metadata.db_uuid);
127+
});
128+
};
106129

107-
api._info = $(function (_, cb) {
108-
return info(metadata, cb);
109-
});
130+
api._info = function (cb) {
131+
api._getMetadata(function (err, metadata) {
132+
return info(metadata, cb);
133+
});
134+
};
110135

111136
api._get = $t(get, [DOC_STORE]);
112137
api._getLocal = $t(function (txn, id, callback) {
113138
return getLocal(txn, id, api, callback);
114139
}, [META_LOCAL_STORE]);
115140

116141
api._bulkDocs = $(function (_, req, opts, callback) {
117-
bulkDocs(api, req, opts, metadata, dbOpts, idbChanges, callback);
142+
bulkDocs(api, req, opts, connectionInfo, dbOpts, idbChanges, callback);
118143
});
119144

120145
api._allDocs = $t(function (txn, opts, cb) {
121-
allDocs(txn, metadata, opts, cb);
122-
}, [DOC_STORE]);
146+
const metaStore = txn.txn.objectStore(META_LOCAL_STORE);
147+
metaStore.get(META_LOCAL_STORE).onsuccess = function (e) {
148+
const metadata = e.target.result;
149+
allDocs(txn, metadata, opts, cb);
150+
};
151+
}, [DOC_STORE, META_LOCAL_STORE]);
123152

124153
api._getAttachment = getAttachment;
125154

packages/node_modules/pouchdb-adapter-indexeddb/src/setup.js

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,9 @@ function upgradePouchDbSchema(dbName, db, tx, pouchdbVersion) {
115115
}
116116

117117
function openDatabase(openDatabases, api, opts, resolve, reject) {
118-
const openReq = indexedDB.open(opts.name, createIdbVersion());
118+
const openReq = opts.versionChangedWhileOpen ?
119+
indexedDB.open(opts.name) :
120+
indexedDB.open(opts.name, createIdbVersion());
119121

120122
openReq.onupgradeneeded = function (e) {
121123
if (e.oldVersion > 0 && e.oldVersion < versionMultiplier) {
@@ -183,15 +185,23 @@ function openDatabase(openDatabases, api, opts, resolve, reject) {
183185
idb.close();
184186
};
185187

188+
// In IndexedDB you can only change the version, and thus the schema, when you are opening the database.
189+
// versionChangedWhileOpen means that something else outside of our control has likely updated the version.
190+
// One way this could happen is if you open multiple tabs, as the version number changes each time the database is opened.
191+
// If we suspect this we close the db and tag it, so that next time it's accessed it reopens the DB with the current version
192+
// as opposed to upping the version again
193+
// This avoids infinite loops of version updates if you have multiple tabs open
186194
idb.onversionchange = function () {
187195
console.log('Database was made stale, closing handle');
188-
delete openDatabases[opts.name];
196+
openDatabases[opts.name].versionChangedWhileOpen = true;
189197
idb.close();
190198
};
191199

192200
idb.onclose = function () {
193201
console.log('Database was made stale, closing handle');
194-
delete openDatabases[opts.name];
202+
if (opts.name in openDatabases) {
203+
openDatabases[opts.name].versionChangedWhileOpen = true;
204+
}
195205
};
196206

197207
let metadata = {id: META_LOCAL_STORE};
@@ -247,7 +257,10 @@ function openDatabase(openDatabases, api, opts, resolve, reject) {
247257
}
248258

249259
export default function (openDatabases, api, opts) {
250-
if (!openDatabases[opts.name]) {
260+
if (!openDatabases[opts.name] || openDatabases[opts.name].versionChangedWhileOpen) {
261+
opts.versionChangedWhileOpen = openDatabases[opts.name] &&
262+
openDatabases[opts.name].versionChangedWhileOpen;
263+
251264
openDatabases[opts.name] = new Promise(function (resolve, reject) {
252265
openDatabase(openDatabases, api, opts, resolve, reject);
253266
});

0 commit comments

Comments
 (0)