implement wrapper for single IndexedDB database
This commit is contained in:
parent
b2ab89a151
commit
80b7203805
146
data/db.js
Normal file
146
data/db.js
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
export default class WbDb extends EventTarget {
|
||||||
|
static get EVENT_CHANGE() { return "change"; }
|
||||||
|
|
||||||
|
static get DB_NAME() { return "watterblock"; }
|
||||||
|
static get DB_NAME_TEST() { return "test-watterblock"; }
|
||||||
|
static get DB_VERSION() { return 1; }
|
||||||
|
|
||||||
|
static get OS_SESSIONS() { return "sessions"; }
|
||||||
|
|
||||||
|
/** Whether the WbDb constructor may be called. */
|
||||||
|
static #mayConstruct = false;
|
||||||
|
/** The single instance of this class.
|
||||||
|
* @type {?WbDb} */
|
||||||
|
static #instance = null;
|
||||||
|
|
||||||
|
constructor(testing, version) {
|
||||||
|
if (!WbDb.#mayConstruct) {
|
||||||
|
throw new TypeError("WbDb may not be constructed externally");
|
||||||
|
}
|
||||||
|
WbDb.#mayConstruct = false;
|
||||||
|
|
||||||
|
super();
|
||||||
|
|
||||||
|
let req = indexedDB.open(
|
||||||
|
testing ? WbDb.DB_NAME_TEST : WbDb.DB_NAME,
|
||||||
|
testing ? version : WbDb.DB_VERSION);
|
||||||
|
req.addEventListener("blocked", this.#handleBlocked.bind(this));
|
||||||
|
req.addEventListener("upgradeneeded", this.#handleUpgrade.bind(this));
|
||||||
|
req.addEventListener("error", this.#handleError.bind(this));
|
||||||
|
req.addEventListener("success", this.#handleSuccess.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get an instance of `WbDb`.
|
||||||
|
*
|
||||||
|
* If `testing` is `true`, a oneof instance is created, that connects to a
|
||||||
|
* testing database. In that case `version` can be used to set the database
|
||||||
|
* up for a specific version, so it can be tested.
|
||||||
|
*
|
||||||
|
* Otherwise all calls return the same instance.
|
||||||
|
*
|
||||||
|
* @param {boolean=} testing Whether to open a regular or testing connection.
|
||||||
|
* @param {number=} version The version to open a testing connection for.
|
||||||
|
*/
|
||||||
|
static get(testing, version) {
|
||||||
|
if (testing) {
|
||||||
|
WbDb.#mayConstruct = true;
|
||||||
|
return new WbDb(true, version ?? WbDb.DB_VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WbDb.#instance === null) {
|
||||||
|
WbDb.#mayConstruct = true;
|
||||||
|
WbDb.#instance = new WbDb();
|
||||||
|
}
|
||||||
|
|
||||||
|
return WbDb.#instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The actual IndexedDB.
|
||||||
|
* @type{?IDBDatabase}
|
||||||
|
*/
|
||||||
|
#db = null;
|
||||||
|
/** Whether the opening of the DB is currently blocked. */
|
||||||
|
#blocked = false;
|
||||||
|
/** Whether opening the DB has failed. */
|
||||||
|
#failed = false;
|
||||||
|
|
||||||
|
/** Get the actual database. */
|
||||||
|
get db() {
|
||||||
|
if (this.#db === null)
|
||||||
|
throw new Error("WbDb is not yet open");
|
||||||
|
|
||||||
|
return this.#db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shorthand for `WbDb.get().db()`; */
|
||||||
|
static getDb() {
|
||||||
|
return WbDb.get().db();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check whether the database opening has been blocked. */
|
||||||
|
get blocked() {
|
||||||
|
return this.#blocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check whether the database is opened yet. */
|
||||||
|
get open() {
|
||||||
|
return this.#db !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check whether opening the database has failed. */
|
||||||
|
get failed() {
|
||||||
|
return this.#failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Handle the `blocked` event for opening the DB.
|
||||||
|
* @param {IDBVersionChangeEvent} event The actual event.
|
||||||
|
*/
|
||||||
|
#handleBlocked(event) {
|
||||||
|
this.#blocked = true;
|
||||||
|
this.dispatchEvent(new CustomEvent(WbDb.EVENT_CHANGE));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Handle the `upgradeneeded` event from opening the DB.
|
||||||
|
* @param {IDBVersionChangeEvent} event The actual event.
|
||||||
|
*/
|
||||||
|
#handleUpgrade(event) {
|
||||||
|
let {
|
||||||
|
oldVersion: old,
|
||||||
|
newVersion: now,
|
||||||
|
target: { result: db, transaction: trans },
|
||||||
|
} = event;
|
||||||
|
|
||||||
|
if (old < 1 && now >= 1)
|
||||||
|
this.#version1(db, trans);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Handle the `error` event from opening the DB.
|
||||||
|
* @param {Event} event The actual event.
|
||||||
|
*/
|
||||||
|
#handleError(event) {
|
||||||
|
this.#failed = true;
|
||||||
|
this.dispatchEvent(new CustomEvent(WbDb.EVENT_CHANGE));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Handle the `success` event from opening the DB.
|
||||||
|
* @param {Event} event The actual event.
|
||||||
|
*/
|
||||||
|
#handleSuccess(event) {
|
||||||
|
this.#blocked = false;
|
||||||
|
this.#db = event.target.result;
|
||||||
|
this.dispatchEvent(new CustomEvent(WbDb.EVENT_CHANGE));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Do the migration for db version 1.
|
||||||
|
* @param {IDBDatabase} db The db to upgrade.
|
||||||
|
* @param {IDBTransaction} trans The db transaction.
|
||||||
|
*/
|
||||||
|
#version1(db, trans) {
|
||||||
|
db.createObjectStore(WbDb.OS_SESSIONS, {
|
||||||
|
keyPath: "id",
|
||||||
|
autoIncrement: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
142
data/db.test.js
Normal file
142
data/db.test.js
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
import { Round } from "../models/round.js";
|
||||||
|
import WbDb from "./db.js";
|
||||||
|
|
||||||
|
/** The instance used for the current test.
|
||||||
|
*
|
||||||
|
* Put test instances of `WbDb` into this variable. If it is used, the
|
||||||
|
* connection is automatically closed after the test is done. That in turn
|
||||||
|
* means that the database can then be deleted immediately, thus speeding up
|
||||||
|
* the next test.
|
||||||
|
*
|
||||||
|
* @type {WbDb}
|
||||||
|
*/
|
||||||
|
let inst = null;
|
||||||
|
|
||||||
|
/** Wait for the `WbDb.EVENT_CHANGE` to be fired on `instance`.
|
||||||
|
*
|
||||||
|
* Uses the `inst` global variable if no `instance` parameter is passed.
|
||||||
|
*
|
||||||
|
* @param {WbDb=} instance The instance to wait on.
|
||||||
|
*/
|
||||||
|
function waitForChange(instance) {
|
||||||
|
return new Promise(function(resolve) {
|
||||||
|
(instance ?? inst).addEventListener(WbDb.EVENT_CHANGE, function() {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function() {
|
||||||
|
QUnit.module("db", function(hooks) {
|
||||||
|
|
||||||
|
// delete test database before each test
|
||||||
|
hooks.beforeEach(function() {
|
||||||
|
return new Promise(function(resolve) {
|
||||||
|
let req = indexedDB.deleteDatabase(WbDb.DB_NAME_TEST);
|
||||||
|
req.onsuccess = function() {
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
hooks.afterEach(function() {
|
||||||
|
if (inst !== null)
|
||||||
|
if (inst.open)
|
||||||
|
inst.db.close();
|
||||||
|
inst = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
QUnit.test.if(
|
||||||
|
"cleanup works",
|
||||||
|
// not yet baseline widely available, should be in November 2026
|
||||||
|
// TODO: check in November 2026, make unconditional if possible then
|
||||||
|
"databases" in indexedDB,
|
||||||
|
async function(assert) {
|
||||||
|
let dbs = await indexedDB.databases();
|
||||||
|
assert.true(
|
||||||
|
dbs.every(({name}) => name !== WbDb.DB_NAME_TEST),
|
||||||
|
"no testing db");
|
||||||
|
});
|
||||||
|
|
||||||
|
QUnit.test("cannot call constructor", function(assert) {
|
||||||
|
assert.throws(function() {
|
||||||
|
new WbDb();
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.throws(function() {
|
||||||
|
new WbDb(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.throws(function() {
|
||||||
|
new WbDb(true, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
QUnit.test("open db", async function(assert) {
|
||||||
|
inst = WbDb.get(true);
|
||||||
|
await waitForChange();
|
||||||
|
|
||||||
|
assert.false(inst.blocked, "not blocked");
|
||||||
|
assert.false(inst.failed, "not failed");
|
||||||
|
assert.true(inst.open, "is open");
|
||||||
|
assert.notStrictEqual(inst.db, null, "getting db succeeds");
|
||||||
|
assert.strictEqual(inst.db.version, WbDb.DB_VERSION, "correct version");
|
||||||
|
});
|
||||||
|
|
||||||
|
QUnit.test("opening is blocked", async function(assert) {
|
||||||
|
let first = WbDb.get(true, 1);
|
||||||
|
await waitForChange(first);
|
||||||
|
assert.true(first.open, "first instance opened");
|
||||||
|
assert.strictEqual(first.db.version, 1, "first instance is version 1");
|
||||||
|
|
||||||
|
inst = WbDb.get(true, 2);
|
||||||
|
await waitForChange();
|
||||||
|
assert.true(inst.blocked, "second instance blocked");
|
||||||
|
assert.false(inst.open, "second instance not open");
|
||||||
|
assert.false(inst.failed, "second instance not failed");
|
||||||
|
|
||||||
|
first.db.close();
|
||||||
|
await waitForChange();
|
||||||
|
assert.false(inst.blocked, "second instance not blocked");
|
||||||
|
assert.true(inst.open, "second instance open");
|
||||||
|
assert.false(inst.failed, "second instance not failed");
|
||||||
|
assert.strictEqual(inst.db.version, 2, "second instance is version 2");
|
||||||
|
});
|
||||||
|
|
||||||
|
QUnit.test("opening fails", async function(assert) {
|
||||||
|
let first = WbDb.get(true, 2);
|
||||||
|
await waitForChange(first);
|
||||||
|
assert.true(first.open, "first instance opened");
|
||||||
|
assert.strictEqual(first.db.version, 2, "first instance is version 2");
|
||||||
|
first.db.close();
|
||||||
|
|
||||||
|
let second = WbDb.get(true, 1)
|
||||||
|
await waitForChange(second);
|
||||||
|
if (second.blocked)
|
||||||
|
await waitForChange(second);
|
||||||
|
|
||||||
|
assert.false(second.blocked, "second instance not blocked");
|
||||||
|
assert.false(second.open, "second instance not open");
|
||||||
|
assert.true(second.failed, "second instance failed");
|
||||||
|
});
|
||||||
|
|
||||||
|
QUnit.test("schema version 1", async function(assert) {
|
||||||
|
inst = WbDb.get(true, 1);
|
||||||
|
await waitForChange();
|
||||||
|
assert.true(inst.open, "db is opened");
|
||||||
|
assert.strictEqual(inst.db.version, 1, "db is version 1");
|
||||||
|
|
||||||
|
let osn = inst.db.objectStoreNames;
|
||||||
|
assert.strictEqual(osn.length, 1, "correct number of object stores");
|
||||||
|
assert.true(osn.contains(WbDb.OS_SESSIONS), "contains sessions");
|
||||||
|
|
||||||
|
let trans = inst.db.transaction(osn);
|
||||||
|
let sessions = trans.objectStore(WbDb.OS_SESSIONS);
|
||||||
|
assert.strictEqual(sessions.keyPath, "id", "sessions keyPath");
|
||||||
|
assert.true(sessions.autoIncrement, "sessions autoIncrement");
|
||||||
|
assert.strictEqual(sessions.indexNames.length, 0, "sessions no indexes");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
8
test.js
8
test.js
@ -1,11 +1,19 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
import round from "./models/round.test.js";
|
import round from "./models/round.test.js";
|
||||||
import roundResult from "./models/round_result.test.js";
|
import roundResult from "./models/round_result.test.js";
|
||||||
import game from "./models/game.test.js";
|
import game from "./models/game.test.js";
|
||||||
import session from "./models/session.test.js";
|
import session from "./models/session.test.js";
|
||||||
|
|
||||||
|
import db from "./data/db.test.js";
|
||||||
|
|
||||||
QUnit.module("models", function() {
|
QUnit.module("models", function() {
|
||||||
round();
|
round();
|
||||||
roundResult();
|
roundResult();
|
||||||
game();
|
game();
|
||||||
session();
|
session();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
QUnit.module("data", function() {
|
||||||
|
db();
|
||||||
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user