Subscription.ts
7.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
import { isArray } from './util/isArray';
import { isObject } from './util/isObject';
import { isFunction } from './util/isFunction';
import { UnsubscriptionError } from './util/UnsubscriptionError';
import { SubscriptionLike, TeardownLogic } from './types';
/**
* 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 implements SubscriptionLike {
/** @nocollapse */
public static EMPTY: Subscription = (function(empty: any) {
empty.closed = true;
return empty;
}(new Subscription()));
/**
* A flag to indicate whether this Subscription has already been unsubscribed.
* @type {boolean}
*/
public closed: boolean = false;
/** @internal */
protected _parentOrParents: Subscription | Subscription[] = null;
/** @internal */
private _subscriptions: SubscriptionLike[] = null;
/**
* @param {function(): void} [unsubscribe] A function describing how to
* perform the disposal of resources when the `unsubscribe` method is called.
*/
constructor(unsubscribe?: () => void) {
if (unsubscribe) {
(this as any)._ctorUnsubscribe = true;
(this as any)._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(): void {
let errors: any[];
if (this.closed) {
return;
}
let { _parentOrParents, _ctorUnsubscribe, _unsubscribe, _subscriptions } = (this as any);
this.closed = true;
this._parentOrParents = null;
// null out _subscriptions first so any child subscriptions that attempt
// to remove themselves from this subscription will noop
this._subscriptions = null;
if (_parentOrParents instanceof Subscription) {
_parentOrParents.remove(this);
} else if (_parentOrParents !== null) {
for (let index = 0; index < _parentOrParents.length; ++index) {
const parent = _parentOrParents[index];
parent.remove(this);
}
}
if (isFunction(_unsubscribe)) {
// It's only possible to null _unsubscribe - to release the reference to
// any teardown function passed in the constructor - if the property was
// actually assigned in the constructor, as there are some classes that
// are derived from Subscriber (which derives from Subscription) that
// implement an _unsubscribe method as a mechanism for obtaining
// unsubscription notifications and some of those subscribers are
// recycled. Also, in some of those subscribers, _unsubscribe switches
// from a prototype method to an instance property - see notifyNext in
// RetryWhenSubscriber.
if (_ctorUnsubscribe) {
(this as any)._unsubscribe = undefined;
}
try {
_unsubscribe.call(this);
} catch (e) {
errors = e instanceof UnsubscriptionError ? flattenUnsubscriptionErrors(e.errors) : [e];
}
}
if (isArray(_subscriptions)) {
let index = -1;
let len = _subscriptions.length;
while (++index < len) {
const sub = _subscriptions[index];
if (isObject(sub)) {
try {
sub.unsubscribe();
} catch (e) {
errors = errors || [];
if (e instanceof UnsubscriptionError) {
errors = errors.concat(flattenUnsubscriptionErrors(e.errors));
} else {
errors.push(e);
}
}
}
}
}
if (errors) {
throw new UnsubscriptionError(errors);
}
}
/**
* Adds a tear down to be called during the unsubscribe() of this
* Subscription. Can also be used to add a child 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.
*
* When a parent subscription is unsubscribed, any child subscriptions that were added to it are also unsubscribed.
*
* @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: TeardownLogic): Subscription {
let subscription = (<Subscription>teardown);
if (!teardown) {
return Subscription.EMPTY;
}
switch (typeof teardown) {
case 'function':
subscription = new Subscription(<(() => void)>teardown);
case 'object':
if (subscription === this || subscription.closed || typeof subscription.unsubscribe !== 'function') {
// This also covers the case where `subscription` is `Subscription.EMPTY`, which is always in `closed` state.
return subscription;
} else if (this.closed) {
subscription.unsubscribe();
return subscription;
} else if (!(subscription instanceof Subscription)) {
const tmp = subscription;
subscription = new Subscription();
subscription._subscriptions = [tmp];
}
break;
default: {
throw new Error('unrecognized teardown ' + teardown + ' added to Subscription.');
}
}
// Add `this` as parent of `subscription` if that's not already the case.
let { _parentOrParents } = subscription;
if (_parentOrParents === null) {
// If we don't have a parent, then set `subscription._parents` to
// the `this`, which is the common case that we optimize for.
subscription._parentOrParents = this;
} else if (_parentOrParents instanceof Subscription) {
if (_parentOrParents === this) {
// The `subscription` already has `this` as a parent.
return subscription;
}
// If there's already one parent, but not multiple, allocate an
// Array to store the rest of the parent Subscriptions.
subscription._parentOrParents = [_parentOrParents, this];
} else if (_parentOrParents.indexOf(this) === -1) {
// Only add `this` to the _parentOrParents list if it's not already there.
_parentOrParents.push(this);
} else {
// The `subscription` already has `this` as a parent.
return subscription;
}
// Optimize for the common case when adding the first subscription.
const subscriptions = this._subscriptions;
if (subscriptions === null) {
this._subscriptions = [subscription];
} else {
subscriptions.push(subscription);
}
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: Subscription): void {
const subscriptions = this._subscriptions;
if (subscriptions) {
const subscriptionIndex = subscriptions.indexOf(subscription);
if (subscriptionIndex !== -1) {
subscriptions.splice(subscriptionIndex, 1);
}
}
}
}
function flattenUnsubscriptionErrors(errors: any[]) {
return errors.reduce((errs, err) => errs.concat((err instanceof UnsubscriptionError) ? err.errors : err), []);
}