import { isArray } from './util/isArray'; import { isObject } from './util/isObject'; import { isFunction } from './util/isFunction'; import { tryCatch } from './util/tryCatch'; import { errorObject } from './util/errorObject'; import { UnsubscriptionError } from './util/UnsubscriptionError'; /** * Represents a disposable resource, such as the execution of an Observable. A * Subscription has one important method, `unsubscribe`, that takes no argument * and just disposes the resource held by the subscription. * * Additionally, subscriptions may be grouped together through the `add()` * method, which will attach a child Subscription to the current Subscription. * When a Subscription is unsubscribed, all its children (and its grandchildren) * will be unsubscribed as well. * * @class Subscription */ export class Subscription { /** * @param {function(): void} [unsubscribe] A function describing how to * perform the disposal of resources when the `unsubscribe` method is called. */ constructor(unsubscribe) { /** * A flag to indicate whether this Subscription has already been unsubscribed. * @type {boolean} */ this.closed = false; this._parent = null; this._parents = null; this._subscriptions = null; if (unsubscribe) { this._unsubscribe = unsubscribe; } } /** * Disposes the resources held by the subscription. May, for instance, cancel * an ongoing Observable execution or cancel any other type of work that * started when the Subscription was created. * @return {void} */ unsubscribe() { let hasErrors = false; let errors; if (this.closed) { return; } let { _parent, _parents, _unsubscribe, _subscriptions } = this; this.closed = true; this._parent = null; this._parents = null; // null out _subscriptions first so any child subscriptions that attempt // to remove themselves from this subscription will noop this._subscriptions = null; let index = -1; let len = _parents ? _parents.length : 0; // if this._parent is null, then so is this._parents, and we // don't have to remove ourselves from any parent subscriptions. while (_parent) { _parent.remove(this); // if this._parents is null or index >= len, // then _parent is set to null, and the loop exits _parent = ++index < len && _parents[index] || null; } if (isFunction(_unsubscribe)) { let trial = tryCatch(_unsubscribe).call(this); if (trial === errorObject) { hasErrors = true; errors = errors || (errorObject.e instanceof UnsubscriptionError ? flattenUnsubscriptionErrors(errorObject.e.errors) : [errorObject.e]); } } if (isArray(_subscriptions)) { index = -1; len = _subscriptions.length; while (++index < len) { const sub = _subscriptions[index]; if (isObject(sub)) { let trial = tryCatch(sub.unsubscribe).call(sub); if (trial === errorObject) { hasErrors = true; errors = errors || []; let err = errorObject.e; if (err instanceof UnsubscriptionError) { errors = errors.concat(flattenUnsubscriptionErrors(err.errors)); } else { errors.push(err); } } } } } if (hasErrors) { throw new UnsubscriptionError(errors); } } /** * Adds a tear down to be called during the unsubscribe() of this * Subscription. * * If the tear down being added is a subscription that is already * unsubscribed, is the same reference `add` is being called on, or is * `Subscription.EMPTY`, it will not be added. * * If this subscription is already in an `closed` state, the passed * tear down logic will be executed immediately. * * @param {TeardownLogic} teardown The additional logic to execute on * teardown. * @return {Subscription} Returns the Subscription used or created to be * added to the inner subscriptions list. This Subscription can be used with * `remove()` to remove the passed teardown logic from the inner subscriptions * list. */ add(teardown) { if (!teardown || (teardown === Subscription.EMPTY)) { return Subscription.EMPTY; } if (teardown === this) { return this; } let subscription = teardown; switch (typeof teardown) { case 'function': subscription = new Subscription(teardown); case 'object': if (subscription.closed || typeof subscription.unsubscribe !== 'function') { return subscription; } else if (this.closed) { subscription.unsubscribe(); return subscription; } else if (typeof subscription._addParent !== 'function' /* quack quack */) { const tmp = subscription; subscription = new Subscription(); subscription._subscriptions = [tmp]; } break; default: throw new Error('unrecognized teardown ' + teardown + ' added to Subscription.'); } const subscriptions = this._subscriptions || (this._subscriptions = []); subscriptions.push(subscription); subscription._addParent(this); return subscription; } /** * Removes a Subscription from the internal list of subscriptions that will * unsubscribe during the unsubscribe process of this Subscription. * @param {Subscription} subscription The subscription to remove. * @return {void} */ remove(subscription) { const subscriptions = this._subscriptions; if (subscriptions) { const subscriptionIndex = subscriptions.indexOf(subscription); if (subscriptionIndex !== -1) { subscriptions.splice(subscriptionIndex, 1); } } } _addParent(parent) { let { _parent, _parents } = this; if (!_parent || _parent === parent) { // If we don't have a parent, or the new parent is the same as the // current parent, then set this._parent to the new parent. this._parent = parent; } else if (!_parents) { // If there's already one parent, but not multiple, allocate an Array to // store the rest of the parent Subscriptions. this._parents = [parent]; } else if (_parents.indexOf(parent) === -1) { // Only add the new parent to the _parents list if it's not already there. _parents.push(parent); } } } Subscription.EMPTY = (function (empty) { empty.closed = true; return empty; }(new Subscription())); function flattenUnsubscriptionErrors(errors) { return errors.reduce((errs, err) => errs.concat((err instanceof UnsubscriptionError) ? err.errors : err), []); } //# sourceMappingURL=Subscription.js.map