indexeddbshim
Version:
A polyfill for IndexedDB using WebSql
350 lines (314 loc) • 12.9 kB
JavaScript
import {EventTargetFactory} from 'eventtargeter';
import {createDOMException} from './DOMException.js';
import {createEvent} from './Event.js';
import * as util from './util.js';
import DOMStringList from './DOMStringList.js';
import IDBObjectStore from './IDBObjectStore.js';
import IDBTransaction from './IDBTransaction.js';
const listeners = ['onabort', 'onclose', 'onerror', 'onversionchange'];
const readonlyProperties = ['name', 'version', 'objectStoreNames'];
/**
* @typedef {{
* name: string,
* keyPath: import('./Key.js').KeyPath,
* autoInc: boolean,
* indexList: {[key: string]: import('./IDBIndex.js').IDBIndexProperties},
* idbdb: IDBDatabaseFull,
* cursors?: (import('./IDBCursor.js').IDBCursorFull|
* import('./IDBCursor.js').IDBCursorWithValueFull)[],
* }} IDBObjectStoreProperties
*/
/**
* IDB Database Object.
* @see http://dvcs.w3.org/hg/IndexedDB/raw-file/tip/Overview.html#database-interface
* @class
*/
function IDBDatabase () {
this.__versionTransaction = null;
this.__objectStores = null;
/** @type {import('./IDBTransaction.js').IDBTransactionFull[]} */
this.__transactions = [];
throw new TypeError('Illegal constructor');
}
const IDBDatabaseAlias = IDBDatabase;
/**
* @typedef {number} Integer
*/
/**
* @typedef {IDBDatabase & EventTarget & {
* createObjectStore: (storeName: string) => IDBObjectStore,
* deleteObjectStore: (storeName: string) => void,
* close: () => void,
* transaction: (storeNames: string|string[], mode: string) => IDBTransaction,
* throwIfUpgradeTransactionNull: () => void,
* objectStoreNames: import('./DOMStringList.js').DOMStringListFull,
* name: string,
* __forceClose: (msg: string) => void,
* __db: import('websql-configurable/lib/websql/WebSQLDatabase.js').default,
* __oldVersion: Integer,
* __version: Integer,
* __name: string,
* __upgradeTransaction: null|import('./IDBTransaction.js').IDBTransactionFull,
* __versionTransaction: import('./IDBTransaction.js').IDBTransactionFull,
* __transactions: import('./IDBTransaction.js').IDBTransactionFull[],
* __objectStores: {[key: string]: IDBObjectStore},
* __objectStoreNames: import('./DOMStringList.js').DOMStringListFull,
* __oldObjectStoreNames: import('./DOMStringList.js').DOMStringListFull,
* __unblocking: {
* check: () => void
* }
* }} IDBDatabaseFull
*/
/**
* @param {import('websql-configurable').default} db
* @param {string} name
* @param {Integer} oldVersion
* @param {Integer} version
* @param {SQLResultSet} storeProperties
* @returns {IDBDatabaseFull}
*/
IDBDatabase.__createInstance = function (db, name, oldVersion, version, storeProperties) {
/**
* @class
* @this {IDBDatabaseFull}
*/
function IDBDatabase () {
// @ts-expect-error It's ok
this[Symbol.toStringTag] = 'IDBDatabase';
util.defineReadonlyProperties(this, readonlyProperties);
this.__db = db;
this.__closePending = false;
this.__oldVersion = oldVersion;
this.__version = version;
this.__name = name;
this.__upgradeTransaction = null;
util.defineListenerProperties(this, listeners);
// @ts-expect-error Part of `ShimEventTarget`
this.__setOptions({
legacyOutputDidListenersThrowFlag: true // Event hook for IndexedB
});
/** @type {import('./IDBTransaction.js').IDBTransactionFull[]} */
this.__transactions = [];
/** @type {{[key: string]: IDBObjectStore}} */
this.__objectStores = {};
this.__objectStoreNames = DOMStringList.__createInstance();
/**
* @type {IDBObjectStoreProperties}
*/
const itemCopy = {};
for (let i = 0; i < storeProperties.rows.length; i++) {
const item = storeProperties.rows.item(i);
// Safari implements `item` getter return object's properties
// as readonly, so we copy all its properties (except our
// custom `currNum` which we don't need) onto a new object
itemCopy.name = item.name;
itemCopy.keyPath = JSON.parse(item.keyPath);
// Though `autoInc` is coming from the database as a NUMERIC
// type (how SQLite stores BOOLEAN set in CREATE TABLE),
// and should thus be parsed into a number here (0 or 1),
// `IDBObjectStore.__createInstance` will convert to a boolean
// when setting the store's `autoIncrement`.
/** @type {const} */ (['autoInc', 'indexList']).forEach((prop) => {
itemCopy[prop] = JSON.parse(item[prop]);
});
itemCopy.idbdb = this;
const store = IDBObjectStore.__createInstance(itemCopy);
this.__objectStores[store.name] = store;
this.objectStoreNames.push(store.name);
}
this.__oldObjectStoreNames = this.objectStoreNames.clone();
}
IDBDatabase.prototype = IDBDatabaseAlias.prototype;
// @ts-expect-error It's ok
return new IDBDatabase();
};
// @ts-expect-error It's ok
IDBDatabase.prototype = EventTargetFactory.createInstance();
IDBDatabase.prototype[Symbol.toStringTag] = 'IDBDatabasePrototype';
/**
* Creates a new object store.
* @param {string} storeName
* @this {IDBDatabaseFull}
* @returns {IDBObjectStore}
*/
IDBDatabase.prototype.createObjectStore = function (storeName /* , createOptions */) {
// eslint-disable-next-line prefer-rest-params -- API
let createOptions = arguments[1];
storeName = String(storeName); // W3C test within IDBObjectStore.js seems to accept string conversion
if (!(this instanceof IDBDatabase)) {
throw new TypeError('Illegal invocation');
}
if (arguments.length === 0) {
throw new TypeError('No object store name was specified');
}
IDBTransaction.__assertVersionChange(this.__versionTransaction); // this.__versionTransaction may not exist if called mistakenly by user in onsuccess
this.throwIfUpgradeTransactionNull();
IDBTransaction.__assertActive(this.__versionTransaction);
createOptions = {...createOptions};
let {keyPath} = createOptions;
keyPath = keyPath === undefined ? null : util.convertToSequenceDOMString(keyPath);
if (keyPath !== null && !util.isValidKeyPath(keyPath)) {
throw createDOMException('SyntaxError', 'The keyPath argument contains an invalid key path.');
}
if (this.__objectStores[storeName] && !this.__objectStores[storeName].__pendingDelete) {
throw createDOMException('ConstraintError', 'Object store "' + storeName + '" already exists in ' + this.name);
}
const autoInc = createOptions.autoIncrement;
if (autoInc && (keyPath === '' || Array.isArray(keyPath))) {
throw createDOMException('InvalidAccessError', 'With autoIncrement set, the keyPath argument must not be an array or empty string.');
}
/** @type {IDBObjectStoreProperties} */
const storeProperties = {
name: storeName,
keyPath,
autoInc,
indexList: {},
idbdb: this
};
const store = IDBObjectStore.__createInstance(storeProperties, this.__versionTransaction);
return IDBObjectStore.__createObjectStore(this, store);
};
/**
* Deletes an object store.
* @param {string} storeName
* @throws {TypeError|DOMException}
* @this {IDBDatabaseFull}
* @returns {void}
*/
IDBDatabase.prototype.deleteObjectStore = function (storeName) {
if (!(this instanceof IDBDatabase)) {
throw new TypeError('Illegal invocation');
}
if (arguments.length === 0) {
throw new TypeError('No object store name was specified');
}
IDBTransaction.__assertVersionChange(this.__versionTransaction);
this.throwIfUpgradeTransactionNull();
IDBTransaction.__assertActive(this.__versionTransaction);
const store = this.__objectStores[storeName];
if (!store) {
throw createDOMException('NotFoundError', 'Object store "' + storeName + '" does not exist in ' + this.name);
}
IDBObjectStore.__deleteObjectStore(this, store);
};
/**
* @throws {TypeError}
* @this {IDBDatabaseFull}
* @returns {void}
*/
IDBDatabase.prototype.close = function () {
if (!(this instanceof IDBDatabase)) {
throw new TypeError('Illegal invocation');
}
this.__closePending = true;
if (this.__unblocking) {
this.__unblocking.check();
}
this.__transactions = [];
};
/**
* Starts a new transaction.
* @param {string|string[]} storeNames
* @this {IDBDatabaseFull}
* @returns {import('./IDBTransaction.js').IDBTransactionFull}
*/
IDBDatabase.prototype.transaction = function (storeNames /* , mode */) {
if (arguments.length === 0) {
throw new TypeError('You must supply a valid `storeNames` to `IDBDatabase.transaction`');
}
// eslint-disable-next-line prefer-rest-params -- API
let mode = arguments[1];
storeNames = util.isIterable(storeNames)
// Creating new array also ensures sequence is passed by value: https://heycam.github.io/webidl/#idl-sequence
? [...new Set( // to be unique
util.convertToSequenceDOMString(storeNames) // iterables have `ToString` applied (and we convert to array for convenience)
)].sort() // must be sorted
: [util.convertToDOMString(storeNames)];
/* (function () {
throw new TypeError('You must supply a valid `storeNames` to `IDBDatabase.transaction`');
}())); */
// Since SQLite (at least node-websql and definitely WebSQL) requires
// locking of the whole database, to allow simultaneous readwrite
// operations on transactions without overlapping stores, we'd probably
// need to save the stores in separate databases (we could also consider
// prioritizing readonly but not starving readwrite).
// Even for readonly transactions, due to [issue 17](https://github.com/nolanlawson/node-websql/issues/17),
// we're not currently actually running the SQL requests in parallel.
mode = mode || 'readonly';
IDBTransaction.__assertNotVersionChange(this.__versionTransaction);
if (this.__closePending) {
throw createDOMException('InvalidStateError', 'An attempt was made to start a new transaction on a database connection that is not open');
}
const objectStoreNames = DOMStringList.__createInstance();
storeNames.forEach((storeName) => {
if (!this.objectStoreNames.contains(storeName)) {
throw createDOMException('NotFoundError', 'The "' + storeName + '" object store does not exist');
}
objectStoreNames.push(storeName);
});
if (storeNames.length === 0) {
throw createDOMException('InvalidAccessError', 'No valid object store names were specified');
}
if (mode !== 'readonly' && mode !== 'readwrite') {
throw new TypeError('Invalid transaction mode: ' + mode);
}
// Do not set transaction state to "inactive" yet (will be set after
// timeout on creating transaction instance):
// https://github.com/w3c/IndexedDB/issues/87
const trans = IDBTransaction.__createInstance(this, objectStoreNames, mode);
this.__transactions.push(trans);
return trans;
};
/**
* @see https://github.com/w3c/IndexedDB/issues/192
* @throws {DOMException}
* @this {IDBDatabaseFull}
* @returns {void}
*/
IDBDatabase.prototype.throwIfUpgradeTransactionNull = function () {
if (this.__upgradeTransaction === null) {
throw createDOMException('InvalidStateError', 'No upgrade transaction associated with database.');
}
};
// Todo __forceClose: Add tests for `__forceClose`
/**
*
* @param {string} msg
* @this {IDBDatabaseFull}
* @returns {void}
*/
IDBDatabase.prototype.__forceClose = function (msg) {
const me = this;
me.close();
let ct = 0;
me.__transactions.forEach(function (trans) {
// eslint-disable-next-line camelcase -- Clear API
trans.on__abort = function () {
ct++;
if (ct === me.__transactions.length) {
// Todo __forceClose: unblock any pending `upgradeneeded` or `deleteDatabase` calls
const evt = createEvent('close');
setTimeout(() => {
me.dispatchEvent(evt);
});
}
};
trans.__abortTransaction(createDOMException(
'AbortError',
'The connection was force-closed: ' + (msg || '')
));
});
me.__transactions = [];
};
util.defineOuterInterface(IDBDatabase.prototype, listeners);
util.defineReadonlyOuterInterface(IDBDatabase.prototype, readonlyProperties);
Object.defineProperty(IDBDatabase.prototype, 'constructor', {
enumerable: false,
writable: true,
configurable: true,
value: IDBDatabase
});
Object.defineProperty(IDBDatabase, 'prototype', {
writable: false
});
export default IDBDatabase;