1
+ import { Injectable } from '@angular/core' ;
2
+ import { Subscription } from 'rxjs/Subscription' ;
3
+ import { Subject } from 'rxjs/Subject' ;
4
+
5
+ const isActive : symbol = Symbol ( 'isActive' ) ;
6
+ const tracking : symbol = Symbol ( 'tracking' ) ;
7
+ const options : symbol = Symbol ( 'options' ) ;
8
+ const activationDelayTimeout : symbol = Symbol ( 'activationDelayTimeout' ) ;
9
+ const minDurationTimeout : symbol = Symbol ( 'minDurationTimeout' ) ;
10
+
11
+ function isPromise ( value : any ) : boolean {
12
+ return typeof value . then === 'function' && typeof value . catch === 'function' ;
13
+ }
14
+
15
+ function isSubscription ( value : any ) : boolean {
16
+ return value instanceof Subscription ;
17
+ }
18
+
19
+ function removeFromTracking ( tracker : AsyncTracker , promiseOrSubscription : PromiseOrSubscription ) : void {
20
+ if ( tracker [ minDurationTimeout ] ) {
21
+ tracker [ minDurationTimeout ] . promise . then ( ( ) => {
22
+ removeFromTracking ( tracker , promiseOrSubscription ) ;
23
+ } ) ;
24
+ } else {
25
+ tracker [ tracking ] = tracker [ tracking ] . filter ( item => item !== promiseOrSubscription ) ;
26
+ if ( tracker [ tracking ] . length === 0 && tracker [ activationDelayTimeout ] ) {
27
+ tracker [ activationDelayTimeout ] . cancel ( ) ;
28
+ delete tracker [ activationDelayTimeout ] ;
29
+ }
30
+ updateIsActive ( tracker ) ;
31
+ }
32
+ }
33
+
34
+ function updateIsActive ( tracker : AsyncTracker ) : void {
35
+ if ( ! tracker [ activationDelayTimeout ] && ( ! tracker [ minDurationTimeout ] || ! tracker [ isActive ] ) ) {
36
+ const oldValue : boolean = tracker [ isActive ] ;
37
+ tracker [ isActive ] = tracker [ tracking ] . length > 0 ;
38
+ if ( oldValue !== tracker [ isActive ] ) {
39
+ tracker . active$ . next ( tracker [ isActive ] ) ;
40
+ }
41
+ }
42
+ }
43
+
44
+ function timeoutPromise ( duration : number ) : { promise : Promise < any > , cancel : Function } {
45
+ let cancel : Function ;
46
+ const promise : Promise < any > = new Promise ( ( resolve ) => {
47
+ const timerId : any = setTimeout ( ( ) => resolve ( ) , duration ) ;
48
+ cancel = ( ) => {
49
+ clearTimeout ( timerId ) ;
50
+ resolve ( ) ;
51
+ } ;
52
+ } ) ;
53
+ return { cancel, promise} ;
54
+ }
55
+
56
+ export type PromiseOrSubscription = Promise < any > | Subscription ;
57
+
58
+ export interface AsyncTrackerOptions {
59
+ activationDelay ?: number ;
60
+ minDuration ?: number ;
61
+ }
62
+
1
63
export class AsyncTracker {
2
64
65
+ /**
66
+ * An observable that emits true or false as the value for `active` changes
67
+ */
68
+ active$ : Subject < boolean > = new Subject ( ) ;
69
+
70
+ /**
71
+ * @param trackerOptions.activationDelay - number of milliseconds that an added promise needs to be pending before this tracker is active.
72
+ * @param trackerOptions.minDuration - Minimum number of milliseconds that a tracker will stay active.
73
+ */
74
+ constructor ( trackerOptions : AsyncTrackerOptions = { } ) {
75
+ this [ tracking ] = [ ] ;
76
+ this [ options ] = trackerOptions ;
77
+ updateIsActive ( this ) ;
78
+ }
79
+
80
+ /**
81
+ * Returns whether this tracker is currently active. That is, whether any of the promises added to/created by this tracker
82
+ * are still pending. Note: if the `activationDelay` has not elapsed yet, this will return false.
83
+ */
84
+ get active ( ) : boolean {
85
+ return this [ isActive ] ;
86
+ }
87
+
88
+ /**
89
+ * The count of promises or subscriptions currently being tracked.
90
+ */
91
+ get trackingCount ( ) : number {
92
+ return this [ tracking ] . length ;
93
+ }
94
+
95
+ /**
96
+ * Returns whether this tracker is currently tracking a request.
97
+ * That is, whether any of the promises / subscriptions added to/created by this tracker are still pending.
98
+ * This method has no regard for `activationDelay`.
99
+ */
100
+ get tracking ( ) : boolean {
101
+ return this [ tracking ] . length > 0 ;
102
+ }
103
+
104
+ /**
105
+ * Add any arbitrary promise or observable subscription to the tracker.
106
+ * `tracker.active` will be true until a promise is resolved or rejected or a subscription emits the first value.
107
+ */
108
+ add ( promiseOrSubscription : PromiseOrSubscription | PromiseOrSubscription [ ] ) : void {
109
+
110
+ const startMinDuration : ( ) => void = ( ) => {
111
+ if ( this [ options ] . minDuration && ! this [ minDurationTimeout ] && this [ tracking ] . length > 0 ) {
112
+ this [ minDurationTimeout ] = timeoutPromise ( this [ options ] . minDuration ) ;
113
+ this [ minDurationTimeout ] . promise . then ( ( ) => {
114
+ delete this [ minDurationTimeout ] ;
115
+ updateIsActive ( this ) ;
116
+ } ) ;
117
+ }
118
+ } ;
119
+
120
+ if ( Array . isArray ( promiseOrSubscription ) ) {
121
+ promiseOrSubscription . forEach ( arrayItem => this . add ( arrayItem ) ) ;
122
+ } else {
123
+ this [ tracking ] . push ( promiseOrSubscription ) ;
124
+ if ( this [ tracking ] . length === 1 ) {
125
+ if ( this [ options ] . activationDelay ) {
126
+ this [ activationDelayTimeout ] = timeoutPromise ( this [ options ] . activationDelay ) ;
127
+ this [ activationDelayTimeout ] . promise . then ( ( ) => {
128
+ delete this [ activationDelayTimeout ] ;
129
+ startMinDuration ( ) ;
130
+ updateIsActive ( this ) ;
131
+ } ) ;
132
+ } else {
133
+ startMinDuration ( ) ;
134
+ }
135
+ }
136
+ updateIsActive ( this ) ;
137
+ if ( isPromise ( promiseOrSubscription ) ) {
138
+ const promise : Promise < any > = promiseOrSubscription as Promise < any > ;
139
+ promise . then ( ( ) => {
140
+ removeFromTracking ( this , promiseOrSubscription ) ;
141
+ } , ( ) => {
142
+ removeFromTracking ( this , promiseOrSubscription ) ;
143
+ } ) ;
144
+ } else if ( isSubscription ( promiseOrSubscription ) ) {
145
+ const subscription : Subscription = promiseOrSubscription as Subscription ;
146
+ subscription . add ( ( ) => {
147
+ removeFromTracking ( this , promiseOrSubscription ) ;
148
+ } ) ;
149
+ } else {
150
+ throw new Error ( 'asyncTracker.add expects either a promise or an observable subscription.' ) ;
151
+ }
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Causes a tracker to immediately become inactive and stop tracking all current promises and subscriptions.
157
+ */
158
+ clear ( ) : void {
159
+ if ( this [ activationDelayTimeout ] ) {
160
+ this [ activationDelayTimeout ] . cancel ( ) ;
161
+ delete this [ activationDelayTimeout ] ;
162
+ }
163
+ if ( this [ minDurationTimeout ] ) {
164
+ this [ minDurationTimeout ] . cancel ( ) ;
165
+ delete this [ minDurationTimeout ] ;
166
+ }
167
+ this [ tracking ] = [ ] ;
168
+ updateIsActive ( this ) ;
169
+ }
170
+
3
171
}
4
172
173
+ @Injectable ( )
5
174
export class AsyncTrackerFactory {
6
175
7
- create ( ) : AsyncTracker {
8
- return new AsyncTracker ( ) ;
176
+ create ( trackerOptions ?: AsyncTrackerOptions ) : AsyncTracker {
177
+ return new AsyncTracker ( trackerOptions ) ;
9
178
}
10
179
11
180
}
0 commit comments