UNPKG

twilio-video

Version:

Twilio Video JavaScript Library

445 lines 16.2 kB
'use strict'; var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __read = (this && this.__read) || function (o, n) { var m = typeof Symbol === "function" && o[Symbol.iterator]; if (!m) return o; var i = m.call(o), r, ar = [], e; try { while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); } catch (error) { e = { error: error }; } finally { try { if (r && !r.done && (m = i["return"])) m.call(i); } finally { if (e) throw e.error; } } return ar; }; var __spreadArray = (this && this.__spreadArray) || function (to, from) { for (var i = 0, il = from.length, j = to.length; i < il; i++, j++) to[j] = from[i]; return to; }; var EventEmitter = require('events').EventEmitter; var util = require('./util'); /** * {@link StateMachine} represents a state machine. The state machine supports a * reentrant locking mechanism to allow asynchronous state transitions to ensure * they have not been preempted. Calls to {@link StateMachine#takeLock} are * guaranteed to be resolved in FIFO order. * @extends EventEmitter * @property {boolean} isLocked - whether or not the {@link StateMachine} is * locked performing asynchronous state transition * @property {string} state - the current state * @emits {@link StateMachine#stateChanged} */ var StateMachine = /** @class */ (function (_super) { __extends(StateMachine, _super); /** * Construct a {@link StateMachine}. * @param {string} initialState - the intiial state * @param {object} states */ function StateMachine(initialState, states) { var _this = _super.call(this) || this; var lock = null; var state = initialState; states = transformStates(states); Object.defineProperties(_this, { _lock: { get: function () { return lock; }, set: function (_lock) { lock = _lock; } }, _reachableStates: { value: reachable(states) }, _state: { get: function () { return state; }, set: function (_state) { state = _state; } }, _states: { value: states }, _whenDeferreds: { value: new Set() }, isLocked: { enumerable: true, get: function () { return lock !== null; } }, state: { enumerable: true, get: function () { return state; } } }); _this.on('stateChanged', function (state) { _this._whenDeferreds.forEach(function (deferred) { deferred.when(state, deferred.resolve, deferred.reject); }); }); return _this; } /** * Returns a promise whose executor function is called on each state change. * @param {function(state: string, resolve: function, reject: function): void} when * @returns {Promise.<*>} * @private */ StateMachine.prototype._whenPromise = function (when) { var _this = this; if (typeof when !== 'function') { return Promise.reject(new Error('when() executor must be a function')); } var deferred = util.defer(); deferred.when = when; this._whenDeferreds.add(deferred); return deferred.promise.then(function (payload) { _this._whenDeferreds.delete(deferred); return payload; }, function (error) { _this._whenDeferreds.delete(deferred); throw error; }); }; /** * This method takes a lock and passes the {@link StateMachine#Key} to your * transition function. You may perform zero or more state transitions in your * transition function, but you should check for preemption in each tick. You * may also reenter the lock. Once the Promise returned by your transition * function resolves or rejects, this method releases the lock it acquired for * you. * @param {string} name - a name for the lock * @param {function(StateMachine#Key): Promise} transitionFunction * @returns {Promise} */ // NOTE(mroberts): This method is named after a Haskell function: // https://hackage.haskell.org/package/base-4.8.2.0/docs/Control-Exception.html#v:bracket StateMachine.prototype.bracket = function (name, transitionFunction) { var key; var self = this; function releaseLock(error) { if (self.hasLock(key)) { self.releaseLockCompletely(key); } if (error) { throw error; } } return this.takeLock(name).then(function gotKey(_key) { key = _key; return transitionFunction(key); }).then(function success(result) { releaseLock(); return result; }, releaseLock); }; /** * Check whether or not a {@link StateMachine#Key} matches the lock. * @param {StateMachine#Key} key * @returns {boolean} */ StateMachine.prototype.hasLock = function (key) { return this._lock === key; }; /** * Preempt any pending state transitions and immediately transition to the new * state. If a lock name is specified, take the lock and return the * {@link StateMachine#Key}. * @param {string} newState * @param {?string} [name=null] - a name for the lock * @param {Array<*>} [payload=[]] * @returns {?StateMachine#Key} */ StateMachine.prototype.preempt = function (newState, name, payload) { // 1. Check that the new state is valid. if (!isValidTransition(this._states, this.state, newState)) { throw new Error("Cannot transition from \"" + this.state + "\" to \"" + newState + "\""); } // 2. Release the old lock, if any. var oldLock; if (this.isLocked) { oldLock = this._lock; this._lock = null; } // 3. Take the lock, if requested. var key = null; if (name) { key = this.takeLockSync(name); } // 4. If a lock wasn't requested, take a "preemption" lock in order to // maintain FIFO order of those taking locks. var preemptionKey = key ? null : this.takeLockSync('preemption'); // 5. Transition. this.transition(newState, key || preemptionKey, payload); // 6. Preempt anyone blocked on the old lock. if (oldLock) { oldLock.resolve(); } // 7. Release the "preemption" lock, if we took it. if (preemptionKey) { this.releaseLock(preemptionKey); } return key; }; /** * Release a lock. This method succeeds only if the {@link StateMachine} is * still locked and has not been preempted. * @param {StateMachine#Key} key * @throws Error */ StateMachine.prototype.releaseLock = function (key) { if (!this.isLocked) { throw new Error("Could not release the lock for " + key.name + " because the StateMachine is not locked"); } else if (!this.hasLock(key)) { throw new Error("Could not release the lock for " + key.name + " because " + this._lock.name + " has the lock"); } if (key.depth === 0) { this._lock = null; key.resolve(); } else { key.depth--; } }; /** * Release a lock completely, even if it has been reentered. This method * succeeds only if the {@link StateMachine} is still locked and has not been * preempted. * @param {StateMachine#Key} key * @throws Error */ StateMachine.prototype.releaseLockCompletely = function (key) { if (!this.isLocked) { throw new Error("Could not release the lock for " + key.name + " because the StateMachine is not locked"); } else if (!this.hasLock(key)) { throw new Error("Could not release the lock for " + key.name + " because " + this._lock.name + " has the lock"); } key.depth = 0; this._lock = null; key.resolve(); }; /** * Take a lock, returning a Promise for the {@link StateMachine#Key}. You should * take a lock anytime you intend to perform asynchronous transitions. Calls to * this method are guaranteed to be resolved in FIFO order. You may reenter * a lock by passing its {@link StateMachine#Key}. * @param {string|StateMachine#Key} nameOrKey - a name for the lock or an * existing {@link StateMachine#Key} * @returns {Promise<object>} */ StateMachine.prototype.takeLock = function (nameOrKey) { var _this = this; // Reentrant lock if (typeof nameOrKey === 'object') { var key_1 = nameOrKey; return new Promise(function (resolve) { resolve(_this.takeLockSync(key_1)); }); } // New lock var name = nameOrKey; if (this.isLocked) { var takeLock = this.takeLock.bind(this, name); return this._lock.promise.then(takeLock); } return Promise.resolve(this.takeLockSync(name)); }; /** * Take a lock, returning the {@Link StateMachine#Key}. This method throws if * the {@link StateMachine} is locked or the wrong {@link StateMachine#Key} is * provided. You may reenter a lock by passing its {@link StateMachine#Key}. * @param {string|StateMachine#Key} nameOrKey - a name for the lock or an * existing {@link StateMachine#Key} * @returns {object} * @throws Error */ StateMachine.prototype.takeLockSync = function (nameOrKey) { var key = typeof nameOrKey === 'string' ? null : nameOrKey; var name = key ? key.name : nameOrKey; if (key && !this.hasLock(key) || !key && this.isLocked) { throw new Error("Could not take the lock for " + name + " because the lock for " + this._lock.name + " was not released"); } // Reentrant lock if (key) { key.depth++; return key; } // New lock var lock = makeLock(name); this._lock = lock; return lock; }; /** * Transition to a new state. If the {@link StateMachine} is locked, you must * provide the {@link StateMachine#Key}. An invalid state or the wrong * {@link StateMachine#Key} will throw an error. * @param {string} newState * @param {?StateMachine#Key} [key=null] * @param {Array<*>} [payload=[]] * @throws {Error} */ StateMachine.prototype.transition = function (newState, key, payload) { payload = payload || []; // 1. If we're locked, required the key. if (this.isLocked) { if (!key) { throw new Error('You must provide the key in order to ' + 'transition'); } else if (!this.hasLock(key)) { throw new Error("Could not transition using the key for " + key.name + " because " + this._lock.name + " has the lock"); } } else if (key) { throw new Error("Key provided for " + key.name + ", but the StateMachine was not locked (possibly due to preemption)"); } // 2. Check that the new state is valid. if (!isValidTransition(this._states, this.state, newState)) { throw new Error("Cannot transition from \"" + this.state + "\" to \"" + newState + "\""); } // 3. Update the state and emit an event. this._state = newState; this.emit.apply(this, __spreadArray([], __read(['stateChanged', newState].concat(payload)))); }; /** * Attempt to transition to a new state. Unlike {@link StateMachine#transition}, * this method does not throw. * @param {string} newState * @param {?StateMachine#Key} [key=null] * @param {Array<*>} [payload=[]] * @returns {boolean} */ StateMachine.prototype.tryTransition = function (newState, key, payload) { try { this.transition(newState, key, payload); } catch (error) { return false; } return true; }; /** * Return a Promise that resolves when the {@link StateMachine} transitions to * the specified state. If the {@link StateMachine} transitions such that the * requested state becomes unreachable, the Promise rejects. * @param {string} state * @returns {Promise<this>} */ StateMachine.prototype.when = function (state) { var _this = this; if (this.state === state) { return Promise.resolve(this); } else if (!isValidTransition(this._reachableStates, this.state, state)) { return Promise.reject(createUnreachableError(this.state, state)); } return this._whenPromise(function (newState, resolve, reject) { if (newState === state) { resolve(_this); } else if (!isValidTransition(_this._reachableStates, newState, state)) { reject(createUnreachableError(newState, state)); } }); }; return StateMachine; }(EventEmitter)); /** * @event StateMachine#stateChanged * @param {string} newState */ /** * Check if a transition is valid. * @private * @param {Map<*, Set<*>>} graph * @param {*} from * @param {*} to * @returns {boolean} */ function isValidTransition(graph, from, to) { return graph.get(from).has(to); } /** * @typedef {object} StateMachine#Key */ function makeLock(name) { var lock = util.defer(); lock.name = name; lock.depth = 0; return lock; } /** * Compute the transitive closure of a graph (i.e. what nodes are reachable from * where). * @private * @param {Map<*, Set<*>>} graph * @returns {Map<*, Set<*>>} */ function reachable(graph) { return Array.from(graph.keys()).reduce(function (newGraph, from) { return newGraph.set(from, reachableFrom(graph, from)); }, new Map()); } /** * Compute the Set of node reachable from a particular node in the graph. * @private * @param {Map<*, Set<*>>} graph * @param {*} from * @param {Set<*>} [to] * @returns {Set<*>} */ function reachableFrom(graph, from, to) { to = to || new Set(); graph.get(from).forEach(function (node) { if (!to.has(node)) { to.add(node); reachableFrom(graph, node, to).forEach(to.add, to); } }); return to; } function transformStates(states) { var newStates = new Map(); for (var key in states) { newStates.set(key, new Set(states[key])); } return newStates; } /** * Create an "unreachable state" Error. * @param {string} here * @param {string} there * @returns {Error} */ function createUnreachableError(here, there) { return new Error("\"" + there + "\" cannot be reached from \"" + here + "\""); } module.exports = StateMachine; //# sourceMappingURL=statemachine.js.map