Skip to content

Commit 230b08d

Browse files
feat: Add database existence check before purge and enhance close/purge logic
- Added `_verifyDatabaseExists` method to check if the database exists using `indexedDB.databases()` API. - Modified `purge` to check if the database exists before attempting deletion to prevent errors. - Updated `purge` logic to handle already closed files more gracefully by skipping unnecessary operations. - Improved sequential task handling in `close` and `purge` methods by integrating queue wait and ensuring all tasks are completed before closing. - Ensured the database is properly deleted and metadata cleared after purging. - Fixed potential lockup issues in `purge` by optimizing queue handling and ensuring the database is only deleted if it exists.
1 parent 35cc513 commit 230b08d

File tree

4 files changed

+124
-31
lines changed

4 files changed

+124
-31
lines changed

index.js

+75-29
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ class RandomAccessIdb extends EventEmitter {
5252
}
5353

5454
async ensureDBReady() {
55+
if (this.closing || this.purging) {
56+
return; // Don't reinitialize if closing or purging
57+
}
58+
5559
if (!this.opened || this.closed) {
5660
await this._initializeDB(); // Initialize if the database is not open
5761
}
@@ -64,6 +68,7 @@ class RandomAccessIdb extends EventEmitter {
6468
}
6569

6670
get fileName() {
71+
if (!this.meta) return null;
6772
return this.meta.fileName;
6873
}
6974

@@ -219,23 +224,30 @@ class RandomAccessIdb extends EventEmitter {
219224
}
220225

221226
close(cb = () => {}) {
222-
this.queue.addTask(async () => {
227+
// Check if the file is already closed
228+
if (this.closed) {
229+
console.warn('File is already closed.');
230+
cb(null);
231+
return;
232+
}
233+
234+
// Add close task to the queue, waiting for all pending tasks
235+
this.queue.waitUntilQueueEmpty().then(async () => {
223236
try {
224-
// Ensure any pending operations are completed
225-
await this.ensureDBReady();
237+
// Clear the database connection and other resources
226238
this.db = null;
227-
this.meta = null;
228-
this.queue = null; // Assuming the queue should also be cleared
229-
// console.log('File closed successfully');
230-
this.emit('close');
239+
this.queue = {}; // Clearing queue to avoid further operations
240+
this.closed = true; // Mark file as closed
241+
console.log('File closed successfully');
231242
cb(null);
232243
} catch (error) {
233-
// console.error('Error during close:', error.message);
244+
console.error('Error during close:', error.message);
234245
cb(error);
235246
}
236247
}).catch(cb);
237248
}
238249

250+
239251
truncate(offset, cb = () => {}) {
240252
this.queue.addTask(async () => {
241253
await this.ensureDBReady();
@@ -305,36 +317,70 @@ class RandomAccessIdb extends EventEmitter {
305317
}, 0);
306318
}
307319

320+
// Updated purge method
321+
async _verifyDatabaseExists() {
322+
if (!('databases' in indexedDB)) {
323+
console.warn('IndexedDB.databases() is not supported in this browser');
324+
return true; // Assume it exists for browsers without support
325+
}
326+
327+
const databases = await indexedDB.databases();
328+
return databases.some(db => db.name === this.fileName);
329+
}
330+
308331
purge(cb = () => {}) {
309-
this.queue.resumeQueue();
310-
this.queue.addTask(async () => {
311-
await this.ensureDBReady().catch(e => false);
332+
if (this.closed) {
333+
console.log('File already closed, purging directly');
334+
this._performPurge(cb);
335+
} else {
336+
this.queue.waitUntilQueueEmpty()
337+
.then(() => this._performPurge(cb))
338+
.catch(cb);
339+
}
340+
}
312341

313-
try {
314-
// console.log(`Purging file ${this.fileName}`);
342+
async _performPurge(cb = () => {}) {
343+
try {
344+
// Check if the database exists
345+
const dbExists = await this._verifyDatabaseExists();
346+
if (!dbExists) {
347+
console.warn(`Database ${this.fileName} does not exist, skipping purge.`);
348+
return cb(null);
349+
}
315350

316-
// Delete the file from IndexedDB
317-
await IDB.deleteDB(this.fileName);
351+
console.log(`Purging file ${this.fileName}`);
318352

319-
// Delete the metadata associated with this file
320-
await this.metaManager.del(this.fileName);
353+
// Delete the file from IndexedDB using idb library
354+
await IDB.deleteDB(this.fileName);
321355

322-
// Mark the database as purged by setting db to null
323-
this.db = null;
356+
// Verify the deletion
357+
const isDeleted = !(await this._verifyDatabaseExists());
358+
if (!isDeleted) {
359+
throw new Error(`Failed to delete database: ${this.fileName}`);
360+
}
324361

325-
// Reset the metadata specific to this file
326-
this.meta = {fileName: this.fileName, chunkSize: this.chunkSize, length: 0};
362+
// Delete the metadata associated with this file
363+
await this.metaManager.del(this.fileName);
327364

328-
// Remove the file from the allLoadedFiles map
329-
allLoadedFiles.delete(this.fileName);
330-
// console.log(`Removed ${this.fileName} from allLoadedFiles`);
365+
// Mark the database as purged by setting db to null
366+
this.db = null;
331367

332-
// console.log(`Purge complete for file ${this.fileName}`);
333-
} catch (e) {
334-
cb(null);
335-
}
368+
// Reset the metadata specific to this file
369+
this.meta = { fileName: this.fileName, chunkSize: this.chunkSize, length: 0 };
370+
371+
// Remove the file from the allLoadedFiles map
372+
allLoadedFiles.delete(this.fileName);
373+
374+
console.log(`Purge complete for file ${this.fileName}`);
336375
cb(null);
337-
}).catch(cb); // Handle any errors
376+
} catch (e) {
377+
console.error(`Error during purge: ${e.message}`);
378+
cb(e);
379+
}
380+
}
381+
382+
unlink(cb = () => {}) {
383+
this.purge(cb);
338384
}
339385

340386
_blocks(start, end) {

lib/QueueManager.js

+5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ export class QueueManager {
88
this._initQueue();
99
}
1010

11+
waitUntilQueueEmpty() {
12+
if (this.queue.idle()) return Promise.resolve();
13+
return this.queue.drained()
14+
}
15+
1116
_initQueue() {
1217
if (!this.queue) {
1318
this.queue = Q(async (task) => {

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zacharygriffee/random-access-idb",
33
"type": "module",
4-
"version": "4.0.2",
4+
"version": "4.0.3",
55
"main": "index.js",
66
"browser": "dist/index.min.js",
77
"description": "A rework of random-access-idb",

test/index.test.js

+43-1
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ test('not sync', async function (t) {
288288
test('random access write and read', async function (t) {
289289
t.plan(8);
290290
const { file, close } = newFile();
291-
t.teardown(close);
291+
// t.teardown(close);
292292

293293
file.write(10, b4a.from('hi'), function (err) {
294294
t.absent(err, 'no error');
@@ -359,6 +359,48 @@ test('purge deletes the file and resets metadata', async t => {
359359
});
360360

361361

362+
test('open, close, then purge', async t => {
363+
t.plan(3);
364+
365+
const fileName = 'testFile.txt';
366+
const ras = createFile(fileName);
367+
368+
// Write some data to the file
369+
await promisify(ras, 'write', 0, b4a.from('test data'));
370+
371+
// Close the file
372+
await promisify(ras, 'close');
373+
t.pass('File closed');
374+
375+
// Purge the file
376+
await promisify(ras, 'purge');
377+
t.pass('File purged');
378+
379+
// Verify that the file is removed from the allLoadedFiles map
380+
t.is(allLoadedFiles.has(fileName), false, 'File should be removed from allLoadedFiles');
381+
});
382+
383+
test('open, then purge', async t => {
384+
t.plan(3);
385+
386+
const fileName = 'testFile.txt';
387+
const ras = createFile(fileName);
388+
389+
// Write some data to the file
390+
await promisify(ras, 'write', 0, b4a.from('test data'));
391+
392+
// Purge the file directly (which should close it first)
393+
await promisify(ras, 'purge');
394+
t.pass('File purged');
395+
396+
// Verify that the file is removed from the allLoadedFiles map
397+
t.is(allLoadedFiles.has(fileName), false, 'File should be removed from allLoadedFiles');
398+
399+
// Ensure that trying to stat the file throws an error
400+
await t.exception(() => promisify(ras, 'stat'), 'File should not exist after purge');
401+
});
402+
403+
362404
test('createFile creates a new file', async t => {
363405
t.plan(2);
364406

0 commit comments

Comments
 (0)