Skip to content

Commit aaf95e2

Browse files
committed
Support calling close() on cached.Database()
Introduce a refcount for cached Database entries so that calling close() on the Database returned from cached.Database() does not invalidate existing or future uses of the same cached Database. This is a backwards-incompatible change because it changes the number of times close() must be called before the underlying Database is closed.
1 parent 7572125 commit aaf95e2

File tree

3 files changed

+123
-20
lines changed

3 files changed

+123
-20
lines changed

lib/sqlite3.js

+16-4
Original file line numberDiff line numberDiff line change
@@ -34,20 +34,32 @@ sqlite3.cached = {
3434

3535
var db;
3636
file = path.resolve(file);
37-
function cb() { callback.call(db, null); }
3837

39-
if (!sqlite3.cached.objects[file]) {
40-
db = sqlite3.cached.objects[file] = new Database(file, a, b);
38+
let cacheEntry = sqlite3.cached.objects[file];
39+
if (!cacheEntry) {
40+
cacheEntry = sqlite3.cached.objects[file] = {refCount: 0};
41+
db = cacheEntry.db = new (class extends Database {
42+
close(cb) {
43+
if (--cacheEntry.refCount <= 0) {
44+
delete sqlite3.cached.objects[file];
45+
super.close(cb);
46+
} else if (typeof cb === 'function') {
47+
process.nextTick(() => { cb.call(this, null); });
48+
}
49+
}
50+
})(file, a, b);
4151
}
4252
else {
4353
// Make sure the callback is called.
44-
db = sqlite3.cached.objects[file];
54+
db = sqlite3.cached.objects[file].db;
4555
var callback = (typeof a === 'number') ? b : a;
4656
if (typeof callback === 'function') {
57+
const cb = () => { callback.call(db, null); };
4758
if (db.open) process.nextTick(cb);
4859
else db.once('open', cb);
4960
}
5061
}
62+
cacheEntry.refCount++;
5163

5264
return db;
5365
},

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "sqlite3",
33
"description": "Asynchronous, non-blocking SQLite3 bindings",
4-
"version": "5.0.2",
4+
"version": "6.0.0",
55
"homepage": "https://github.com/mapbox/node-sqlite3",
66
"author": {
77
"name": "MapBox",

test/cache.test.js

+106-15
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,133 @@
11
var sqlite3 = require('..');
22
var assert = require('assert');
33
var helper = require('./support/helper');
4+
var util = require('util');
45

56
describe('cache', function() {
6-
before(function() {
7+
const filename = 'test/tmp/test_cache.db';
8+
const dbs = [];
9+
10+
const open = async (filename) => await new Promise((resolve, reject) => {
11+
new sqlite3.cached.Database(filename, function (err) {
12+
if (err != null) return reject(err);
13+
resolve(this);
14+
});
15+
});
16+
const close = async (db) => await util.promisify(db.close.bind(db))();
17+
18+
beforeEach(async function () {
19+
dbs.length = 0;
720
helper.ensureExists('test/tmp');
21+
helper.deleteFile(filename);
822
});
923

10-
it('should cache Database objects while opening', function(done) {
11-
var filename = 'test/tmp/test_cache.db';
24+
afterEach(async function () {
25+
await Promise.all(dbs.map(async (db) => await close(db)));
26+
dbs.length = 0;
1227
helper.deleteFile(filename);
28+
});
29+
30+
it('should cache Database objects while opening', function(done) {
1331
var opened1 = false, opened2 = false;
14-
var db1 = new sqlite3.cached.Database(filename, function(err) {
32+
dbs.push(new sqlite3.cached.Database(filename, function(err) {
1533
if (err) throw err;
1634
opened1 = true;
1735
if (opened1 && opened2) done();
18-
});
19-
var db2 = new sqlite3.cached.Database(filename, function(err) {
36+
}));
37+
dbs.push(new sqlite3.cached.Database(filename, function(err) {
2038
if (err) throw err;
2139
opened2 = true;
2240
if (opened1 && opened2) done();
23-
});
24-
assert.equal(db1, db2);
41+
}));
42+
assert.equal(dbs[0], dbs[1]);
2543
});
2644

2745
it('should cache Database objects after they are open', function(done) {
28-
var filename = 'test/tmp/test_cache2.db';
29-
helper.deleteFile(filename);
30-
var db1, db2;
31-
db1 = new sqlite3.cached.Database(filename, function(err) {
46+
dbs.push(new sqlite3.cached.Database(filename, function(err) {
3247
if (err) throw err;
3348
process.nextTick(function() {
34-
db2 = new sqlite3.cached.Database(filename, function(err) {
49+
dbs.push(new sqlite3.cached.Database(filename, function(err) {
50+
if (err) throw err;
3551
done();
52+
}));
53+
assert.equal(dbs[0], dbs[1]);
54+
});
55+
}));
56+
});
3657

58+
it('cached.Database() callback is called asynchronously', async function () {
59+
await Promise.all([0, 1].map(() => new Promise((resolve, reject) => {
60+
let callbackCalled = false;
61+
dbs.push(new sqlite3.cached.Database(filename, (err) => {
62+
callbackCalled = true;
63+
if (err != null) return reject(err);
64+
resolve();
65+
}));
66+
assert(!callbackCalled);
67+
})));
68+
});
69+
70+
it('cached.Database() callback is called with db as context', async function () {
71+
await Promise.all([0, 1].map((i) => new Promise((resolve, reject) => {
72+
dbs.push(new sqlite3.cached.Database(filename, function (err) {
73+
if (err != null) return reject(err);
74+
if (this !== dbs[i]) return reject(new Error('this !== dbs[i]'));
75+
resolve();
76+
}));
77+
})));
78+
});
79+
80+
it('db.close() callback is called asynchronously', async function () {
81+
dbs.push(await open(filename));
82+
dbs.push(await open(filename));
83+
while (dbs.length > 0) {
84+
await new Promise((resolve, reject) => {
85+
let callbackCalled = false;
86+
dbs.pop().close((err) => {
87+
callbackCalled = true;
88+
if (err != null) return reject(err);
89+
resolve();
3790
});
38-
assert.equal(db1, db2);
91+
assert(!callbackCalled);
3992
});
40-
});
93+
}
94+
});
95+
96+
it('db.close() callback is called with db as context', async function () {
97+
dbs.push(await open(filename));
98+
dbs.push(await open(filename));
99+
while (dbs.length > 0) {
100+
await new Promise((resolve, reject) => {
101+
const db = dbs.pop();
102+
db.close(function (err) {
103+
if (err) return reject(err);
104+
if (this !== db) return reject(new Error('this !== db'));
105+
resolve();
106+
});
107+
});
108+
}
109+
});
110+
111+
it('db.close() does not close other copies', async function () {
112+
dbs.push(await open(filename));
113+
dbs.push(await open(filename));
114+
await close(dbs.pop());
115+
assert(dbs[0].open);
116+
});
117+
118+
it('db.close() closes the underlying Database after closing the last copy', async function () {
119+
dbs.push(await open(filename));
120+
dbs.push(await open(filename));
121+
const db = dbs[0];
122+
await close(dbs.pop());
123+
await close(dbs.pop());
124+
assert(!db.open);
125+
});
126+
127+
it('cached.Database() returns an open Database after closing', async function () {
128+
dbs.push(await open(filename));
129+
await close(dbs.pop());
130+
dbs.push(await open(filename));
131+
assert(dbs[0].open);
41132
});
42133
});

0 commit comments

Comments
 (0)