mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
fix: fail fast when the plugin storage database is blocked #840
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import 'fake-indexeddb/auto';
|
||||
import { fileStorage } from '../plugin-storage';
|
||||
|
||||
const DB_NAME = 'bulwark-plugins';
|
||||
// One below the version plugin-storage.ts opens, simulating an older tab.
|
||||
const PREVIOUS_DB_VERSION = 2;
|
||||
|
||||
function openAt(version: number): Promise<IDBDatabase> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = indexedDB.open(DB_NAME, version);
|
||||
req.onsuccess = () => resolve(req.result);
|
||||
req.onerror = () => reject(req.error);
|
||||
});
|
||||
}
|
||||
|
||||
describe('plugin storage when the upgrade is blocked (#840)', () => {
|
||||
it('rejects saveFile instead of hanging while an older tab keeps the DB open', async () => {
|
||||
// An "older tab" that never reacts to versionchange, so the upgrade to the
|
||||
// current DB_VERSION cannot proceed.
|
||||
const olderTab = await openAt(PREVIOUS_DB_VERSION);
|
||||
expect(olderTab.version).toBe(PREVIOUS_DB_VERSION);
|
||||
|
||||
const file = new File(['hello'], 'hello.txt', { type: 'text/plain' });
|
||||
const outcome = await Promise.race([
|
||||
fileStorage.saveFile('blocked-file', file).then(
|
||||
() => 'resolved',
|
||||
(err: unknown) => (err instanceof Error ? err.message : String(err)),
|
||||
),
|
||||
new Promise<string>((resolve) => setTimeout(() => resolve('timeout'), 2000)),
|
||||
]);
|
||||
|
||||
expect(outcome).not.toBe('timeout');
|
||||
expect(outcome).not.toBe('resolved');
|
||||
expect(outcome).toMatch(/blocked/i);
|
||||
|
||||
olderTab.close();
|
||||
});
|
||||
|
||||
it('closes its own connection so a later upgrade is not blocked by it', async () => {
|
||||
const file = new File(['hello'], 'hello.txt', { type: 'text/plain' });
|
||||
await fileStorage.saveFile('closed-file', file);
|
||||
expect(await fileStorage.getFile('closed-file')).not.toBeNull();
|
||||
|
||||
// If plugin-storage left its connection open, opening at a higher version
|
||||
// would fire `blocked` (no onversionchange handler on a foreign handle);
|
||||
// with the fix the module closes after every transaction.
|
||||
const blocked = await new Promise<boolean>((resolve, reject) => {
|
||||
const req = indexedDB.open(DB_NAME, 99);
|
||||
req.onblocked = () => resolve(true);
|
||||
req.onsuccess = () => {
|
||||
req.result.close();
|
||||
resolve(false);
|
||||
};
|
||||
req.onerror = () => reject(req.error);
|
||||
});
|
||||
expect(blocked).toBe(false);
|
||||
});
|
||||
});
|
||||
+34
-5
@@ -32,8 +32,23 @@ function openDB(): Promise<IDBDatabase> {
|
||||
}
|
||||
};
|
||||
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onsuccess = () => {
|
||||
const db = request.result;
|
||||
// Another tab bumping DB_VERSION needs every older connection gone
|
||||
// before its upgrade can run; close ours instead of blocking it.
|
||||
db.onversionchange = () => db.close();
|
||||
resolve(db);
|
||||
};
|
||||
request.onerror = () => reject(request.error);
|
||||
// Without this the open request never settles while an older tab keeps
|
||||
// the previous version open, and every caller (attachment upload, plugin
|
||||
// loading) hangs silently. (#840)
|
||||
request.onblocked = () =>
|
||||
reject(
|
||||
new Error(
|
||||
'Plugin storage database is blocked by another open tab. Close other Bulwark tabs and try again.',
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -42,8 +57,14 @@ async function putItem(storeName: string, key: string, value: string | Blob | Fi
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(storeName, 'readwrite');
|
||||
tx.objectStore(storeName).put(value, key);
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(tx.error);
|
||||
tx.oncomplete = () => {
|
||||
db.close();
|
||||
resolve();
|
||||
};
|
||||
tx.onerror = () => {
|
||||
db.close();
|
||||
reject(tx.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -54,6 +75,8 @@ async function getItem<T = string>(storeName: string, key: string): Promise<T |
|
||||
const request = tx.objectStore(storeName).get(key);
|
||||
request.onsuccess = () => resolve(request.result ?? null);
|
||||
request.onerror = () => reject(request.error);
|
||||
tx.oncomplete = () => db.close();
|
||||
tx.onerror = () => db.close();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -62,8 +85,14 @@ async function deleteItem(storeName: string, key: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = db.transaction(storeName, 'readwrite');
|
||||
tx.objectStore(storeName).delete(key);
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(tx.error);
|
||||
tx.oncomplete = () => {
|
||||
db.close();
|
||||
resolve();
|
||||
};
|
||||
tx.onerror = () => {
|
||||
db.close();
|
||||
reject(tx.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user