-
-
Notifications
You must be signed in to change notification settings - Fork 130
/
format-time-in-words.pipe.ts
76 lines (67 loc) · 3.1 KB
/
format-time-in-words.pipe.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
import { ChangeDetectorRef, OnDestroy, Pipe, PipeTransform } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { interval, Observable, of } from 'rxjs';
import { delayWhen, map, repeatWhen, takeWhile, tap } from 'rxjs/operators';
import { Options } from 'date-fns';
// FIXME: esm modules not working with Jest Tests
import { differenceInMinutes, formatDistance } from 'date-fns/esm';
// import { formatDistance, differenceInMinutes } from 'date-fns';
const defaultConfig: Options = { addSuffix: true };
/**
* impure pipe, which in general can lead to bad performance
* but the backoff function limits the frequency the pipe checks for updates
* so the performance is close to that of a pure pipe
* the downside of this is that if you change the value of the input, the pipe might not notice for a while
* so this pipe is intended for static data
*
* expected input is a time (number, string or Date)
* output is a string expressing distance from that time to now, plus the suffix 'ago'
* output refreshes at dynamic intervals, with refresh rate slowing down as the input time gets further away from now
*/
@Pipe({ name: 'formatTimeInWords', pure: false })
export class FormatTimeInWordsPipe implements PipeTransform, OnDestroy {
static readonly NO_ARGS_ERROR = 'formatTimeInWords: missing required arguments';
private readonly async: AsyncPipe;
private isDestroyed = false;
private agoExpression: Observable<string>;
constructor(private cdr: ChangeDetectorRef) {
this.async = new AsyncPipe(this.cdr);
}
ngOnDestroy() {
this.isDestroyed = true; // pipe will stop executing after next iteration
}
transform(date: string | number | Date, options?: Options): string {
if (date == null) {
throw new Error(FormatTimeInWordsPipe.NO_ARGS_ERROR);
}
// set the pipe to the Observable if not yet done, and return an async pipe
if (!this.agoExpression) {
this.agoExpression = this.timeAgo(date, { ...defaultConfig, ...options });
}
return this.async.transform(this.agoExpression);
}
private timeAgo(date: string | number | Date, options?: Options): Observable<string> {
let nextBackoff = this.backoff(date);
return of(true).pipe(
// will not recheck input until delay completes
repeatWhen(notify => notify.pipe(delayWhen( () => interval(nextBackoff)))),
takeWhile(_ => !this.isDestroyed),
map(_ => formatDistance(date, new Date(), options)),
tap(_ => nextBackoff = this.backoff(date)),
);
}
private backoff(date: string | number | Date): number {
const minutesElapsed = Math.abs(differenceInMinutes(new Date(), date)); // this will always be positive
let backoffAmountInSeconds: number;
if (minutesElapsed < 2) {
backoffAmountInSeconds = 5;
} else if (minutesElapsed >= 2 && minutesElapsed < 5) {
backoffAmountInSeconds = 15;
} else if (minutesElapsed >= 5 && minutesElapsed < 60) {
backoffAmountInSeconds = 30;
} else if (minutesElapsed >= 60) {
backoffAmountInSeconds = 300; // 5 minutes
}
return backoffAmountInSeconds * 1000; // return an amount of milliseconds
}
}