forked from ReactiveX/rxjs
/
Subscription.ts
214 lines (194 loc) · 7.32 KB
/
Subscription.ts
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
import { isFunction } from './util/isFunction';
import { UnsubscriptionError } from './util/UnsubscriptionError';
import { SubscriptionLike, TeardownLogic, Unsubscribable } from './types';
import { arrRemove } from './util/arrRemove';
/**
* 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 = (() => {
const empty = new Subscription();
empty.closed = true;
return empty;
})();
/**
* A flag to indicate whether this Subscription has already been unsubscribed.
*/
public closed = false;
private _parentage: Subscription[] | Subscription | null = null;
/**
* The list of registered teardowns to execute upon unsubscription. Adding and removing from this
* list occurs in the {@link #add} and {@link #remove} methods.
*/
private _teardowns: Exclude<TeardownLogic, void>[] | null = null;
/**
* @param initialTeardown A function executed first as part of the teardown
* process that is kicked off when {@link #unsubscribe} is called.
*/
constructor(private initialTeardown?: () => void) {}
/**
* 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[] | undefined;
if (!this.closed) {
this.closed = true;
// Remove this from it's parents.
const { _parentage } = this;
this._parentage = null;
if (Array.isArray(_parentage)) {
for (const parent of _parentage) {
parent.remove(this);
}
} else {
_parentage?.remove(this);
}
const { initialTeardown } = this;
if (isFunction(initialTeardown)) {
try {
initialTeardown();
} catch (e) {
errors = e instanceof UnsubscriptionError ? e.errors : [e];
}
}
const { _teardowns } = this;
if (_teardowns) {
this._teardowns = null;
for (const teardown of _teardowns) {
try {
execTeardown(teardown);
} catch (err) {
errors = errors ?? [];
if (err instanceof UnsubscriptionError) {
errors = [...errors, ...err.errors];
} else {
errors.push(err);
}
}
}
}
if (errors) {
throw new UnsubscriptionError(errors);
}
}
}
/**
* Adds a teardown to this subscription, so that teardown will be unsubscribed/called
* when this subscription is unsubscribed. If this subscription is already {@link #closed},
* because it has already been unsubscribed, then whatever teardown is passed to it
* will automatically be executed (unless the teardown itself is also a closed subscription).
*
* Closed Subscriptions cannot be added as teardowns to any subscription. Adding a closed
* subscription to a any subscription will result in no operation. (A noop).
*
* Adding a subscription to itself, or adding `null` or `undefined` will not perform any
* operation at all. (A noop).
*
* `Subscription` instances that are added to this instance will automatically remove themselves
* if they are unsubscribed. Functions and {@link Unsubscribable} objects that you wish to remove
* will need to be removed manually with {@link #remove}
*
* @param teardown The teardown logic to add to this subscription.
*/
add(teardown: TeardownLogic): void {
// Only add the teardown if it's not undefined
// and don't add a subscription to itself.
if (teardown && teardown !== this) {
if (this.closed) {
// If this subscription is already closed,
// execute whatever teardown is handed to it automatically.
execTeardown(teardown);
} else {
if (teardown instanceof Subscription) {
// We don't add closed subscriptions, and we don't add the same subscription
// twice. Subscription unsubscribe is idempotent.
if (teardown.closed || teardown._hasParent(this)) {
return;
}
teardown._addParent(this);
}
(this._teardowns = this._teardowns ?? []).push(teardown);
}
}
}
/**
* Checks to see if a this subscription already has a particular parent.
* This will signal that this subscription has already been added to the parent in question.
* @param parent the parent to check for
*/
private _hasParent(parent: Subscription) {
const { _parentage } = this;
return _parentage === parent || (Array.isArray(_parentage) && _parentage.includes(parent));
}
/**
* Adds a parent to this subscription so it can be removed from the parent if it
* unsubscribes on it's own.
*
* NOTE: THIS ASSUMES THAT {@link _hasParent} HAS ALREADY BEEN CHECKED.
* @param parent The parent subscription to add
*/
private _addParent(parent: Subscription) {
const { _parentage } = this;
this._parentage = Array.isArray(_parentage) ? (_parentage.push(parent), _parentage) : _parentage ? [_parentage, parent] : parent;
}
/**
* Called on a child when it is removed via {@link #remove}.
* @param parent The parent to remove
*/
private _removeParent(parent: Subscription) {
const { _parentage } = this;
if (_parentage === parent) {
this._parentage = null;
} else if (Array.isArray(_parentage)) {
arrRemove(_parentage, parent);
}
}
/**
* Removes a teardown from this subscription that was previously added with the {@link #add} method.
*
* Note that `Subscription` instances, when unsubscribed, will automatically remove themselves
* from every other `Subscription` they have been added to. This means that using the `remove` method
* is not a common thing and should be used thoughtfully.
*
* If you add the same teardown instance of a function or an unsubscribable object to a `Subcription` instance
* more than once, you will need to call `remove` the same number of times to remove all instances.
*
* All teardown instances are removed to free up memory upon unsubscription.
*
* @param teardown The teardown to remove from this subscription
*/
remove(teardown: Exclude<TeardownLogic, void>): void {
const { _teardowns } = this;
_teardowns && arrRemove(_teardowns, teardown);
if (teardown instanceof Subscription) {
teardown._removeParent(this);
}
}
}
export const EMPTY_SUBSCRIPTION = Subscription.EMPTY;
export function isSubscription(value: any): value is Subscription {
return (
value instanceof Subscription ||
(value && 'closed' in value && isFunction(value.remove) && isFunction(value.add) && isFunction(value.unsubscribe))
);
}
function execTeardown(teardown: Unsubscribable | (() => void)) {
if (isFunction(teardown)) {
teardown();
} else {
teardown.unsubscribe();
}
}