indexeddbshim
Version:
A polyfill for IndexedDB using WebSql
770 lines (719 loc) • 27.6 kB
JavaScript
import {EventTargetFactory} from 'eventtargeter';
import SyncPromise from 'sync-promise-expanded';
import {createEvent} from './Event.js';
import {logError, findError, webSQLErrback, createDOMException} from './DOMException.js';
import {IDBRequest} from './IDBRequest.js';
import * as util from './util.js';
import IDBObjectStore from './IDBObjectStore.js';
import CFG from './CFG.js';
let uniqueID = 0;
const listeners = ['onabort', 'oncomplete', 'onerror'];
const readonlyProperties = ['objectStoreNames', 'mode', 'db', 'error'];
/**
* @typedef {number} Integer
*/
/**
* @typedef {{
* op: SQLCallback,
* args: ObjectArray,
* req: import('./IDBRequest.js').IDBRequestFull|null
* }} RequestInfo
*/
/**
* @typedef {EventTarget & {
* mode: "readonly"|"readwrite"|"versionchange",
* db: import('./IDBDatabase.js').IDBDatabaseFull,
* on__abort: () => void,
* on__complete: () => void,
* on__beforecomplete: (ev: Event & {
* complete: () => void
* }) => void,
* on__preabort: () => void,
* __abortTransaction: (err: Error|DOMException|null) => void,
* __executeRequests: () => void,
* __tx: SQLTransaction,
* __id: Integer,
* __active: boolean,
* __running: boolean,
* __errored: boolean,
* __requests: RequestInfo[],
* __db: import('./IDBDatabase.js').IDBDatabaseFull,
* __mode: string,
* __error: null|DOMException|Error,
* __objectStoreNames: import('./DOMStringList.js').DOMStringListFull,
* __storeHandles: {
* [key: string]: import('./IDBObjectStore.js').IDBObjectStoreFull
* },
* __requestsFinished: boolean,
* __transFinishedCb: (err: boolean, cb: ((bool?: boolean) => void)) => void,
* __transactionEndCallback: () => void,
* __transactionFinished: boolean,
* __completed: boolean,
* __internal: boolean,
* __abortFinished: boolean,
* __createRequest: (
* source: import('./IDBDatabase.js').IDBDatabaseFull|
* import('./IDBObjectStore.js').IDBObjectStoreFull|
* import('./IDBIndex.js').IDBIndexFull|
* import('./IDBCursor.js').IDBCursorFull
* ) => import('./IDBRequest.js').IDBRequestFull,
* __pushToQueue: (
* request: import('./IDBRequest.js').IDBRequestFull|null,
* callback: SQLCallback,
* args?: ObjectArray
* ) => void,
* __assertActive: () => void,
* __addNonRequestToTransactionQueue: (
* callback: SQLCallback,
* args?: ObjectArray
* ) => void
* __addToTransactionQueue: (
* callback: SQLCallback,
* args: ObjectArray|undefined,
* source: import('./IDBDatabase.js').IDBDatabaseFull|
* import('./IDBObjectStore.js').IDBObjectStoreFull|
* import('./IDBIndex.js').IDBIndexFull|
* import('./IDBCursor.js').IDBCursorFull
* ) => import('./IDBRequest.js').IDBRequestFull
* __assertWritable: () => void,
* }} IDBTransactionFull
*/
/**
* The IndexedDB Transaction.
* @see http://dvcs.w3.org/hg/IndexedDB/raw-file/tip/Overview.html#idl-def-IDBTransaction
* @class
*/
function IDBTransaction () {
throw new TypeError('Illegal constructor');
}
const IDBTransactionAlias = IDBTransaction;
/**
* @param {import('./IDBDatabase.js').IDBDatabaseFull} db
* @param {import('./DOMStringList.js').DOMStringListFull} storeNames
* @param {string} mode
* @returns {IDBTransactionFull}
*/
IDBTransaction.__createInstance = function (db, storeNames, mode) {
/**
* @class
* @this {IDBTransactionFull}
*/
function IDBTransaction () {
const me = this;
// @ts-expect-error It's ok
me[Symbol.toStringTag] = 'IDBTransaction';
util.defineReadonlyProperties(me, readonlyProperties);
me.__id = ++uniqueID; // for debugging simultaneous transactions
me.__active = true;
me.__running = false;
me.__errored = false;
me.__requests = [];
me.__objectStoreNames = storeNames;
me.__mode = mode;
me.__db = db;
me.__error = null;
// @ts-expect-error Part of `ShimEventTarget`
me.__setOptions({
legacyOutputDidListenersThrowFlag: true // Event hook for IndexedB
});
readonlyProperties.forEach((readonlyProp) => {
Object.defineProperty(this, readonlyProp, {
configurable: true
});
});
util.defineListenerProperties(this, listeners);
me.__storeHandles = {};
// Kick off the transaction as soon as all synchronous code is done
setTimeout(() => { me.__executeRequests(); }, 0);
}
IDBTransaction.prototype = IDBTransactionAlias.prototype;
// @ts-expect-error It's ok
return new IDBTransaction();
};
// @ts-expect-error It's ok
IDBTransaction.prototype = EventTargetFactory.createInstance({
defaultSync: true,
// Ensure EventTarget preserves our properties
extraProperties: ['complete']
});
/**
*
* @param {boolean} err
* @param {(bool: boolean) => void} cb
* @returns {void}
*/
IDBTransaction.prototype.__transFinishedCb = function (err, cb) {
cb(Boolean(err));
};
/**
* @this {IDBTransactionFull}
* @returns {void}
*/
IDBTransaction.prototype.__executeRequests = function () {
const me = this;
if (me.__running) {
if (CFG.DEBUG) { console.log('Looks like the request set is already running', me.mode); }
return;
}
me.__running = true;
me.db.__db[me.mode === 'readonly' ? 'readTransaction' : 'transaction']( // `readTransaction` is optimized, at least in `node-websql`
function executeRequests (tx) {
me.__tx = tx;
/** @type {RequestInfo} */
let q,
i = -1;
/**
* @typedef {any} IDBRequestResult
*/
/**
* @param {IDBRequestResult} [result]
* @param {import('./IDBRequest.js').IDBRequestFull} [req]
* @returns {void}
*/
function success (result, req) {
if (me.__errored || me.__requestsFinished) {
// We've already called "onerror", "onabort", or thrown within the transaction, so don't do it again.
return;
}
if (req) {
q.req = req; // Need to do this in case of cursors
}
if (!q.req) { // TS guard
return;
}
if (q.req.__done) { // Avoid continuing with aborted requests
return;
}
q.req.__done = true;
q.req.__result = result;
q.req.__error = null;
me.__active = true;
const e = createEvent('success');
q.req.dispatchEvent(e);
// Do not set __active flag to false yet: https://github.com/w3c/IndexedDB/issues/87
if (e.__legacyOutputDidListenersThrowError) {
logError('Error', 'An error occurred in a success handler attached to request chain', e.__legacyOutputDidListenersThrowError); // We do nothing else with this error as per spec
// me.__active = false;
me.__abortTransaction(createDOMException('AbortError', 'A request was aborted (in user handler after success).'));
return;
}
executeNextRequest();
}
/**
* @param {[tx: SQLTransaction|DOMException|Error|SQLError, err?: SQLError]} args
* @returns {void}
*/
function error (...args /* tx, err */) {
if (me.__errored || me.__requestsFinished) {
// We've already called "onerror", "onabort", or thrown within
// the transaction, so don't do it again.
return;
}
if (q.req && q.req.__done) { // Avoid continuing with aborted requests
return;
}
const err = /** @type {Error|DOMException} */ (findError(args));
if (!q.req) {
me.__abortTransaction(err);
return;
}
// Fire an error event for the current IDBRequest
q.req.__done = true;
q.req.__error = err;
q.req.__result = undefined; // Must be undefined if an error per `result` getter
q.req.addLateEventListener('error', function (e) {
if (e.cancelable && e.defaultPrevented && !e.__legacyOutputDidListenersThrowError) {
executeNextRequest();
}
});
q.req.addDefaultEventListener('error', function () {
if (!q.req) { // TS guard
return;
}
me.__abortTransaction(q.req.__error);
});
me.__active = true;
const e = createEvent('error', err, {bubbles: true, cancelable: true});
q.req.dispatchEvent(e);
// Do not set __active flag to false yet: https://github.com/w3c/IndexedDB/issues/87
if (e.__legacyOutputDidListenersThrowError) {
logError('Error', 'An error occurred in an error handler attached to request chain', e.__legacyOutputDidListenersThrowError); // We do nothing else with this error as per spec
e.preventDefault(); // Prevent 'error' default as steps indicate we should abort with `AbortError` even without cancellation
me.__abortTransaction(createDOMException('AbortError', 'A request was aborted (in user handler after error).'));
}
}
/**
* @returns {void}
*/
function executeNextRequest () {
if (me.__errored || me.__requestsFinished) {
// We've already called "onerror", "onabort", or thrown within the transaction, so don't do it again.
return;
}
i++;
if (i >= me.__requests.length) {
// All requests in the transaction are done
me.__requests = [];
if (me.__active) {
requestsFinished();
}
} else {
try {
q = me.__requests[i];
if (!q.req) {
q.op(tx, q.args, executeNextRequest, error);
return;
}
if (q.req.__done) { // Avoid continuing with aborted requests
return;
}
q.op(tx, q.args, success, error, executeNextRequest);
} catch (e) {
error(/** @type {Error} */ (e));
}
}
}
executeNextRequest();
},
function webSQLError (webSQLErr) {
// @ts-expect-error It's ok
if (webSQLErr === true) { // Not a genuine SQL error
return;
}
const err = webSQLErrback(/** @type {SQLError} */ (webSQLErr));
me.__abortTransaction(err);
},
function () {
// For Node, we don't need to try running here as we can keep
// the transaction running long enough to rollback (in the
// next (non-standard) callback for this transaction call)
if (me.__transFinishedCb !== IDBTransaction.prototype.__transFinishedCb) { // Node
return;
}
if (!me.__transactionEndCallback && !me.__requestsFinished) {
me.__transactionFinished = true;
return;
}
if (me.__transactionEndCallback && !me.__completed) {
me.__transFinishedCb(me.__errored, me.__transactionEndCallback);
}
},
function (currentTask, err, done, rollback, commit) {
if (currentTask.readOnly || err) {
return true;
}
me.__transFinishedCb = function (err, cb) {
if (err) {
rollback(err, cb);
} else {
commit(cb);
}
};
if (me.__transactionEndCallback && !me.__completed) {
me.__transFinishedCb(me.__errored, me.__transactionEndCallback);
}
return false;
}
);
/**
* @returns {void}
*/
function requestsFinished () {
me.__active = false;
me.__requestsFinished = true;
/**
* @throws {Error}
* @returns {void}
*/
function complete () {
me.__completed = true;
if (CFG.DEBUG) { console.log('Transaction completed'); }
const evt = createEvent('complete');
try {
me.__internal = true;
me.dispatchEvent(evt);
me.__internal = false;
me.dispatchEvent(createEvent('__complete'));
} catch (e) {
me.__internal = false;
// An error occurred in the "oncomplete" handler.
// It's too late to call "onerror" or "onabort". Throw a global error instead.
// (this may seem odd/bad, but it's how all native IndexedDB implementations work)
me.__errored = true;
throw e;
} finally {
me.__storeHandles = {};
}
}
if (me.mode === 'readwrite') {
if (me.__transactionFinished) {
complete();
return;
}
me.__transactionEndCallback = complete;
return;
}
if (me.mode === 'readonly') {
complete();
return;
}
const ev = /** @type {Event & {complete: () => void}} */ (
createEvent('__beforecomplete')
);
ev.complete = complete;
me.dispatchEvent(ev);
}
};
/**
* Creates a new IDBRequest for the transaction.
* NOTE: The transaction is not queued until you call {@link IDBTransaction#__pushToQueue}.
* @param {import('./IDBDatabase.js').IDBDatabaseFull} source
* @this {IDBTransactionFull}
* @returns {IDBRequest}
*/
IDBTransaction.prototype.__createRequest = function (source) {
const me = this;
const request = IDBRequest.__createInstance();
request.__source = source !== undefined ? source : me.db;
request.__transaction = me;
return request;
};
/**
* @typedef {(
* tx: SQLTransaction,
* args: ObjectArray,
* success: (result?: any, req?: import('./IDBRequest.js').IDBRequestFull) => void,
* error: (tx: SQLTransaction|Error|DOMException|SQLError, err?: SQLError) => void,
* executeNextRequest?: () => void
* ) => void} SQLCallback
*/
/**
* Adds a callback function to the transaction queue.
* @param {SQLCallback} callback
* @param {ObjectArray} args
* @param {import('./IDBDatabase.js').IDBDatabaseFull|
* import('./IDBObjectStore.js').IDBObjectStoreFull|
* import('./IDBIndex.js').IDBIndexFull} source
* @this {IDBTransactionFull}
* @returns {import('./IDBRequest.js').IDBRequestFull}
*/
IDBTransaction.prototype.__addToTransactionQueue = function (callback, args, source) {
const request = this.__createRequest(source);
this.__pushToQueue(request, callback, args);
return request;
};
/**
* Adds a callback function to the transaction queue without generating a
* request.
* @param {SQLCallback} callback
* @param {ObjectArray} args
* @this {IDBTransactionFull}
* @returns {void}
*/
IDBTransaction.prototype.__addNonRequestToTransactionQueue = function (callback, args) {
this.__pushToQueue(null, callback, args);
};
/**
* Adds an IDBRequest to the transaction queue.
* @param {import('./IDBRequest.js').IDBRequestFull|null} request
* @param {SQLCallback} callback
* @param {ObjectArray} args
* @this {IDBTransactionFull}
* @returns {void}
*/
IDBTransaction.prototype.__pushToQueue = function (request, callback, args) {
this.__assertActive();
this.__requests.push({
op: callback,
args,
req: request
});
};
/**
* @throws {DOMException}
* @returns {void}
*/
IDBTransaction.prototype.__assertActive = function () {
if (!this.__active) {
throw createDOMException('TransactionInactiveError', 'A request was placed against a transaction which is currently not active, or which is finished');
}
};
/**
* @throws {DOMException}
* @this {IDBTransactionFull}
* @returns {void}
*/
IDBTransaction.prototype.__assertWritable = function () {
if (this.mode === 'readonly') {
throw createDOMException('ReadOnlyError', 'The transaction is read only');
}
};
/**
* @this {IDBTransactionFull}
* @returns {void}
*/
IDBTransaction.prototype.__assertVersionChange = function () {
IDBTransaction.__assertVersionChange(this);
};
/**
* Returns the specified object store.
* @param {string} objectStoreName
* @this {IDBTransactionFull}
* @returns {IDBObjectStore}
*/
IDBTransaction.prototype.objectStore = function (objectStoreName) {
const me = this;
if (!(me instanceof IDBTransaction)) {
throw new TypeError('Illegal invocation');
}
if (arguments.length === 0) {
throw new TypeError('No object store name was specified');
}
IDBTransaction.__assertNotFinished(me);
if (me.__objectStoreNames.indexOf(objectStoreName) === -1) { // eslint-disable-line unicorn/prefer-includes -- Not supported
throw createDOMException('NotFoundError', objectStoreName + ' is not participating in this transaction');
}
const store = me.db.__objectStores[objectStoreName];
if (!store) {
throw createDOMException('NotFoundError', objectStoreName + ' does not exist in ' + me.db.name);
}
if (!me.__storeHandles[objectStoreName] ||
// These latter conditions are to allow store
// recreation to create new clone object
me.__storeHandles[objectStoreName].__pendingDelete ||
me.__storeHandles[objectStoreName].__deleted) {
me.__storeHandles[objectStoreName] = IDBObjectStore.__clone(store, me);
}
return me.__storeHandles[objectStoreName];
};
/**
*
* @param {Error|DOMException|null} err
* @this {IDBTransactionFull}
* @returns {void}
*/
IDBTransaction.prototype.__abortTransaction = function (err) {
const me = this;
logError('Error', 'An error occurred in a transaction', err);
if (me.__errored) {
// We've already called "onerror", "onabort", or thrown, so don't do it again.
return;
}
me.__errored = true;
if (me.mode === 'versionchange') { // Steps for aborting an upgrade transaction
me.db.__version = me.db.__oldVersion;
me.db.__objectStoreNames = me.db.__oldObjectStoreNames;
me.__objectStoreNames = me.db.__oldObjectStoreNames;
Object.values(me.db.__objectStores).concat(
Object.values(me.__storeHandles)
).forEach(function (store) {
// Store was already created so we restore to name before the rename
if ('__pendingName' in store &&
me.db.__oldObjectStoreNames.indexOf(store.__pendingName) > -1 // eslint-disable-line unicorn/prefer-includes -- Not supported
) {
store.__name = store.__originalName;
}
store.__indexNames = store.__oldIndexNames;
delete store.__pendingDelete;
Object.values(store.__indexes).concat(
Object.values(store.__indexHandles)
).forEach(function (index) {
// Index was already created so we restore to name before the rename
if ('__pendingName' in index &&
store.__oldIndexNames.indexOf(index.__pendingName) > -1 // eslint-disable-line unicorn/prefer-includes -- Not supported
) {
index.__name = index.__originalName;
}
delete index.__pendingDelete;
});
});
}
me.__active = false; // Setting here and in requestsFinished for https://github.com/w3c/IndexedDB/issues/87
if (err !== null) {
me.__error = err;
}
if (me.__requestsFinished) {
// The transaction has already completed, so we can't call "onerror" or "onabort".
// So throw the error instead.
setTimeout(() => {
throw err;
}, 0);
}
/**
* @param {SQLTransaction|null} [tx]
* @param {SQLResultSet|SQLError|{code: 0}} [errOrResult]
* @returns {void}
*/
function abort (tx, errOrResult) {
if (!tx) {
if (CFG.DEBUG) { console.log('Rollback not possible due to missing transaction', me); }
} else if (errOrResult && 'code' in errOrResult && typeof errOrResult.code === 'number') {
if (CFG.DEBUG) { console.log('Rollback erred; feature is probably not supported as per WebSQL', me); }
} else if (CFG.DEBUG) { console.log('Rollback succeeded', me); }
me.dispatchEvent(createEvent('__preabort'));
me.__requests.filter(function (q, i, arr) { // eslint-disable-line promise/no-promise-in-callback -- Sync promise
return q.req && !q.req.__done && [i, -1].includes(
arr.map((q) => q.req).lastIndexOf(q.req)
);
}).reduce(function (promises, q) {
// We reduce to a chain of promises to be queued in order, so we cannot
// use `Promise.all`, and I'm unsure whether `setTimeout` currently
// behaves first-in-first-out with the same timeout so we could
// just use a `forEach`.
return promises.then(function () {
if (!q.req) { // TS guard
throw new Error('Missing request');
}
q.req.__done = true;
q.req.__result = undefined;
q.req.__error = createDOMException('AbortError', 'A request was aborted (an unfinished request).');
const reqEvt = createEvent('error', q.req.__error, {bubbles: true, cancelable: true});
return new SyncPromise(
/** @type {() => void} */
(resolve) => {
setTimeout(() => {
if (!q.req) { // TS guard
throw new Error('Missing request');
}
q.req.dispatchEvent(reqEvt); // No need to catch errors
resolve();
});
}
);
});
}, SyncPromise.resolve(undefined)).then(function () { // Also works when there are no pending requests
const evt = createEvent('abort', err, {bubbles: true, cancelable: false});
setTimeout(() => {
me.__abortFinished = true;
me.dispatchEvent(evt);
me.__storeHandles = {};
me.dispatchEvent(createEvent('__abort'));
});
return undefined;
}).catch((err) => {
console.log('Abort error');
throw err;
});
}
me.__transFinishedCb(true, function (rollback) {
if (rollback && me.__tx) { // Not supported in standard SQL (and WebSQL errors should
// rollback automatically), but for Node.js, etc., we give chance for
// manual aborts which would otherwise not work.
if (me.mode === 'readwrite') {
if (me.__transactionFinished) {
abort();
return;
}
me.__transactionEndCallback = abort;
return;
}
try {
me.__tx.executeSql(
'ROLLBACK',
[],
abort,
/** @type {SQLStatementErrorCallback} */ (abort)
); // Not working in some circumstances, even in Node
} catch (err) {
// Browser errs when transaction has ended and since it most likely already erred here,
// we call to abort
abort();
}
} else {
abort(null, {code: 0});
}
});
};
/**
* @this {IDBTransactionFull}
* @returns {void}
*/
IDBTransaction.prototype.abort = function () {
const me = this;
if (!(me instanceof IDBTransaction)) {
throw new TypeError('Illegal invocation');
}
if (CFG.DEBUG) { console.log('The transaction was aborted', me); }
IDBTransaction.__assertNotFinished(me);
me.__abortTransaction(null);
};
IDBTransaction.prototype[Symbol.toStringTag] = 'IDBTransactionPrototype';
/**
*
* @param {IDBTransactionFull|undefined} tx
* @returns {void}
*/
IDBTransaction.__assertVersionChange = function (tx) {
if (!tx || tx.mode !== 'versionchange') {
throw createDOMException('InvalidStateError', 'Not a version transaction');
}
};
/**
*
* @param {IDBTransactionFull} tx
* @throws {DOMException}
* @returns {void}
*/
IDBTransaction.__assertNotVersionChange = function (tx) {
if (tx && tx.mode === 'versionchange') {
throw createDOMException('InvalidStateError', 'Cannot be called during a version transaction');
}
};
/**
*
* @param {IDBTransactionFull|undefined} tx
* @throws {DOMException}
* @returns {void}
*/
IDBTransaction.__assertNotFinished = function (tx) {
if (!tx || tx.__completed || tx.__abortFinished || tx.__errored) {
throw createDOMException('InvalidStateError', 'Transaction finished by commit or abort');
}
};
// object store methods behave differently: see https://github.com/w3c/IndexedDB/issues/192
/**
*
* @param {IDBTransactionFull} tx
* @returns {void}
*/
IDBTransaction.__assertNotFinishedObjectStoreMethod = function (tx) {
try {
IDBTransaction.__assertNotFinished(tx);
} catch (err) {
if (tx && !tx.__completed && !tx.__abortFinished) {
throw createDOMException('TransactionInactiveError', 'A request was placed against a transaction which is currently not active, or which is finished');
}
throw err;
}
};
/**
*
* @param {IDBTransactionFull|undefined} tx
* @throws {DOMException}
* @returns {void}
*/
IDBTransaction.__assertActive = function (tx) {
if (!tx || !tx.__active) {
throw createDOMException('TransactionInactiveError', 'A request was placed against a transaction which is currently not active, or which is finished');
}
};
/**
* Used by our `EventTarget.prototype` library to implement bubbling/capturing.
* @this {IDBTransactionFull}
* @returns {import('./IDBDatabase.js').IDBDatabaseFull}
*/
IDBTransaction.prototype.__getParent = function () {
return this.db;
};
util.defineOuterInterface(IDBTransaction.prototype, listeners);
util.defineReadonlyOuterInterface(IDBTransaction.prototype, readonlyProperties);
Object.defineProperty(IDBTransaction.prototype, 'constructor', {
enumerable: false,
writable: true,
configurable: true,
value: IDBTransaction
});
Object.defineProperty(IDBTransaction, 'prototype', {
writable: false
});
export default IDBTransaction;