Skip to content

Commit b37c8e5

Browse files
authored
feat: initial implementation
1 parent a1e4f1e commit b37c8e5

File tree

4 files changed

+502
-7
lines changed

4 files changed

+502
-7
lines changed

src/async-tracker-factory.service.ts

Lines changed: 171 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,180 @@
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+
163
export class AsyncTracker {
264

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+
3171
}
4172

173+
@Injectable()
5174
export class AsyncTrackerFactory {
6175

7-
create(): AsyncTracker {
8-
return new AsyncTracker();
176+
create(trackerOptions?: AsyncTrackerOptions): AsyncTracker {
177+
return new AsyncTracker(trackerOptions);
9178
}
10179

11180
}

src/async-tracker.module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { NgModule, ModuleWithProviders } from '@angular/core';
22
import { AsyncTrackerFactory } from './async-tracker-factory.service';
33

4-
@NgModule()
4+
@NgModule({})
55
export class AsyncTrackerModule {
66

77
static forRoot(): ModuleWithProviders {

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
export * from './async-tracker.module';
2-
export * from './async-tracker-factory.service';
2+
export { AsyncTracker, AsyncTrackerFactory } from './async-tracker-factory.service';

0 commit comments

Comments
 (0)