UNPKG

x2node-dbos

Version:
336 lines (288 loc) 9.26 kB
'use strict'; const common = require('x2node-common'); let gNextTxId = 1; /** * Transaction. * * <p>Note, that errors thrown by the transaction event listeners are logged, but * otherwise are ignored. Also note that the listeners are invoked asynchronously * in a <code>process.nextTick()</code>. All that makes the event fired by the * transaction good for notifications but not for implementing the main * transaction logic. * * @memberof module:x2node-dbos * @inner * @fires module:x2node-dbos~Transaction#begin * @fires module:x2node-dbos~Transaction#commit * @fires module:x2node-dbos~Transaction#rollback */ class Transaction { /** * Transaction start event. Fired upon successful transaction start. * * @event module:x2node-dbos~Transaction#begin * @type {string} */ /** * Transaction commit event. Fired upon successful transaction commit. * * @event module:x2node-dbos~Transaction#commit * @type {string} */ /** * Transaction rollback event. Fired upon transaction rollback, whether the * rollback was successful or not. If the rollback was unsuccessful, the * event listener receives the rollback error. * * @event module:x2node-dbos~Transaction#rollback * @type {string} */ /** * <strong>Note:</strong> The constructor is not available to the client * code. Transaction instances are provided by the * [TxFactory]{@link module:x2node-dbos~TxFactory}. * * @protected * @param {module:x2node-dbos.DBDriver} dbDriver DB driver. * @param {*} connection DB driver specific connection object. */ constructor(dbDriver, connection) { if (!connection) throw new common.X2UsageError( 'Database connection was not provided for the new transaction.'); this._id = String(gNextTxId++); this._startedOn = null; this._dbDriver = dbDriver; this._connection = connection; this._log = common.getDebugLogger('X2_DBO'); this._listeners = {}; this._active = false; this._finished = false; } /** * Transaction id unique for the process. * * @member {string} * @readonly */ get id() { return this._id; } /** * Timestamp when the transaction was started. * * @member {Date} * @readonly */ get startedOn() { return this._startedOn; } /** * Add listener for the specified transaction event. * * @param {string} eventName Event name. Can be "begin", "commit" or * "rollback". * @param {function} listener The listener function. The listener return * values are ignored. If any listener throws an error, the error is logged * but otherwise the process is not affected. The listeners are called * asynchronously in a <code>process.nextTick()</code>. * @returns {module:x2node-dbos~Transaction} This transaction for chaining. */ on(eventName, listener) { let listeners = this._listeners[eventName]; if (!listeners) listeners = this._listeners[eventName] = new Array(); if ((typeof listener) !== 'function') throw new common.X2UsageError('Listener is not a function.'); listeners.push(listener); return this; } /** * Call the event listeners. * * @protected * @param {string} eventName The event name. * @param {*} [arg] Optional argument to pass to the listeners. */ emit(eventName, arg) { const listeners = this._listeners[eventName]; if (listeners) process.nextTick(() => { for (let listener of listeners) { try { listener(arg); } catch (err) { common.error( `error in transaction #${this._id} ${eventName}` + ' event listener (ignoring it)', err); } } }); } /** * Start the transaction. * * @param {*} [passThrough] Arbitrary value to have the promise to return on * success. May not be a promise itself (use promise chaining for that). * @returns {Promise} Promise that is resolved with the * <code>passThrough</code> value if the transaction started successfully and * is rejected with an error object in the case of failure. * @throws {module:x2node-common.X2UsageError} If the transaction is already * in progress or if it has already finished. */ start(passThrough) { if (this._finished) throw new common.X2UsageError( 'The transaction is already complete.'); if (this._active) throw new common.X2UsageError( 'The transaction is already active.'); if (passThrough instanceof Promise) throw new common.X2UsageError( 'Passthrough may not be a Promise.'); this._active = true; const tx = this; return new Promise((resolve, reject) => { this._log(`(tx #${this._id}) starting transaction`); this._dbDriver.startTransaction(this._connection, { onSuccess() { tx._startedOn = new Date(); tx.emit('begin'); resolve(passThrough); }, onError(err) { common.error('error starting transaction', err); tx._finished = true; tx._active = false; reject(err); } }); }); } /** * Rollback the transaction. * * @param {*} [passThrough] Arbitrary value to have the promise to return * <em>on both success and failure.</em> May not be a promise itself (use * promise chaining for that). * @returns {Promise} Promise that is resolved with the * <code>passThrough</code> value if the transaction rolled back successfully * and is rejected <em>with the same <code>passThrough</code> value</em> * in the case of failure. The rollback failure error is only logged. * @throws {module:x2node-common.X2UsageError} If the transaction has not * been started or if it has already finished. */ rollback(passThrough) { return this.rollbackInternal(passThrough, false); } /** * Rollback the transaction and return promise that always rejects. * * @param {*} [passThrough] Arbitrary value to have the promise to return * <em>on both success and failure.</em> May not be a promise itself (use * promise chaining for that). * @returns {Promise} Promise that is rejected with the * <code>passThrough</code> value if the transaction rolled back successfully * and is rejected <em>with the same <code>passThrough</code> value</em> * in the case of failure. The rollback failure error is only logged. * @throws {module:x2node-common.X2UsageError} If the transaction has not * been started or if it has already finished. */ rollbackAndReject(passThrough) { return this.rollbackInternal(passThrough, true); } /** * Internal implementation of the rollback that can either resolve or reject * the resulting promise upon rollback success. * * @private */ rollbackInternal(passThrough, rejectOnly) { if (this._finished) throw new common.X2UsageError( 'The transaction is already complete.'); if (!this._active) throw new common.X2UsageError( 'The transaction has not been started.'); if (passThrough instanceof Promise) throw new common.X2UsageError( 'Passthrough may not be a Promise.'); this._finished = true; this._active = false; const tx = this; return new Promise((resolve, reject) => { this._log(`(tx #${this._id}) rolling back transaction`); this._dbDriver.rollbackTransaction(this._connection, { onSuccess() { tx.emit('rollback'); if (rejectOnly) reject(passThrough); else resolve(passThrough); }, onError(err) { common.error('error rolling transaction back', err); tx.emit('rollback', err); reject(passThrough); } }); }); } /** * Commit the transaction. * * @param {*} [passThrough] Arbitrary value to have the promise to return on * success. May not be a promise itself (use promise chaining for that). * @returns {Promise} Promise that is resolved with the * <code>passThrough</code> value if the transaction committed successfully * and is rejected with an error object in the case of failure. * @throws {module:x2node-common.X2UsageError} If the transaction has not * been started or if it has already finished. */ commit(passThrough) { if (this._finished) throw new common.X2UsageError( 'The transaction is already complete.'); if (!this._active) throw new common.X2UsageError( 'The transaction has not been started.'); if (passThrough instanceof Promise) throw new common.X2UsageError( 'Passthrough may not be a Promise.'); this._finished = true; this._active = false; const tx = this; return new Promise((resolve, reject) => { this._log(`(tx #${this._id}) committing transaction`); this._dbDriver.commitTransaction(this._connection, { onSuccess() { tx.emit('commit'); resolve(passThrough); }, onError(err) { common.error('error committing transaction', err); reject(err); } }); }); } /** * Tell if the transaction is started (and not finished). * * @returns {boolean} <code>true</code> if active transaction. */ isActive() { return this._active; } /** * The database driver. * * @member {module:x2node-dbos.DBDriver} * @readonly */ get dbDriver() { return this._dbDriver; } /** * Underlying DB driver specific connection object associated with the * transaction. * * @member {*} * @readonly */ get connection() { return this._connection; } } // export the class module.exports = Transaction;