UNPKG

indexeddbshim

Version:
350 lines (314 loc) 12.9 kB
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;