@oscarpalmer/timer 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ import type {Timer} from './timer';
2
+
3
+ /**
4
+ * A set of all active timers
5
+ */
6
+ export const activeTimers = new Set<Timer>();
7
+
8
+ /**
9
+ * A set of timers that were paused due to the document being hidden
10
+ */
11
+ export const hiddenTimers = new Set<Timer>();
12
+
13
+ /**
14
+ * Milliseconds in a frame, probably ;-)
15
+ */
16
+ export const milliseconds = 1_000 / 60;
@@ -0,0 +1,151 @@
1
+ import {activeTimers, milliseconds} from './constants';
2
+ import type {TimerOptions, TimerState, WorkType} from './models';
3
+ import type {Timer} from './timer';
4
+
5
+ export function getOptions(
6
+ options: Partial<TimerOptions>,
7
+ isRepeated: boolean,
8
+ ): TimerOptions {
9
+ return {
10
+ afterCallback: options.afterCallback,
11
+ count: getValueOrDefault(
12
+ options.count,
13
+ isRepeated ? Number.POSITIVE_INFINITY : 1,
14
+ ),
15
+ errorCallback: options.errorCallback,
16
+ interval: getValueOrDefault(options.interval, milliseconds, milliseconds),
17
+ timeout: getValueOrDefault(
18
+ options.timeout,
19
+ isRepeated ? Number.POSITIVE_INFINITY : 30_000,
20
+ ),
21
+ };
22
+ }
23
+
24
+ export function getValueOrDefault(
25
+ value: unknown,
26
+ defaultValue: number,
27
+ minimum?: number,
28
+ ): number {
29
+ return typeof value === 'number' && value > (minimum ?? 0)
30
+ ? value
31
+ : defaultValue;
32
+ }
33
+
34
+ export function work(
35
+ type: WorkType,
36
+ timer: Timer,
37
+ state: TimerState,
38
+ options: TimerOptions,
39
+ ): Timer {
40
+ if (
41
+ (['continue', 'start'].includes(type) && state.active) ||
42
+ (['pause', 'stop'].includes(type) && !state.active)
43
+ ) {
44
+ return timer;
45
+ }
46
+
47
+ const {count, interval, timeout} = options;
48
+ const {isRepeated, minimum} = state;
49
+
50
+ if (['pause', 'restart', 'stop'].includes(type)) {
51
+ const isStop = type === 'stop';
52
+
53
+ activeTimers.delete(timer);
54
+
55
+ cancelAnimationFrame(state.frame as never);
56
+
57
+ if (isStop) {
58
+ options.afterCallback?.(false);
59
+ }
60
+
61
+ state.active = false;
62
+ state.frame = undefined;
63
+ state.paused = !isStop;
64
+
65
+ if (isStop) {
66
+ state.elapsed = undefined;
67
+ state.index = undefined;
68
+ }
69
+
70
+ return type === 'restart' ? work('start', timer, state, options) : timer;
71
+ }
72
+
73
+ state.active = true;
74
+ state.paused = false;
75
+
76
+ const elapsed = type === 'continue' ? +(state.elapsed ?? 0) : 0;
77
+
78
+ let index = type === 'continue' ? +(state.index ?? 0) : 0;
79
+
80
+ state.elapsed = elapsed;
81
+ state.index = index;
82
+
83
+ const total =
84
+ (count === Number.POSITIVE_INFINITY
85
+ ? Number.POSITIVE_INFINITY
86
+ : (count - index) * (interval > 0 ? interval : milliseconds)) - elapsed;
87
+
88
+ let current: DOMHighResTimeStamp | null;
89
+ let start: DOMHighResTimeStamp | null;
90
+
91
+ function finish(finished: boolean, error: boolean) {
92
+ activeTimers.delete(timer);
93
+
94
+ state.active = false;
95
+ state.elapsed = undefined;
96
+ state.frame = undefined;
97
+ state.index = undefined;
98
+
99
+ if (error) {
100
+ options.errorCallback?.();
101
+ }
102
+
103
+ options.afterCallback?.(finished);
104
+ }
105
+
106
+ function step(timestamp: DOMHighResTimeStamp): void {
107
+ if (!state.active) {
108
+ return;
109
+ }
110
+
111
+ current ??= timestamp;
112
+ start ??= timestamp;
113
+
114
+ const time = timestamp - current;
115
+
116
+ state.elapsed = elapsed + (current - start);
117
+
118
+ const finished = time - elapsed >= total;
119
+
120
+ if (timestamp - start >= timeout - elapsed) {
121
+ finish(finished, !finished);
122
+
123
+ return;
124
+ }
125
+
126
+ if (finished || time >= minimum) {
127
+ if (state.active) {
128
+ state.callback((isRepeated ? index : undefined) as never);
129
+ }
130
+
131
+ index += 1;
132
+
133
+ state.index = index;
134
+
135
+ if (!finished && index < count) {
136
+ current = null;
137
+ } else {
138
+ finish(true, false);
139
+ return;
140
+ }
141
+ }
142
+
143
+ state.frame = requestAnimationFrame(step);
144
+ }
145
+
146
+ activeTimers.add(timer);
147
+
148
+ state.frame = requestAnimationFrame(step);
149
+
150
+ return timer;
151
+ }
package/src/global.ts ADDED
@@ -0,0 +1,14 @@
1
+ import {activeTimers} from './constants';
2
+
3
+ declare global {
4
+ var _oscarpalmer_timer_debug: boolean | undefined;
5
+ var _oscarpalmer_timers: Timer[] | undefined;
6
+ }
7
+
8
+ if (globalThis._oscarpalmer_timers == null) {
9
+ Object.defineProperty(globalThis, '_oscarpalmer_timers', {
10
+ get() {
11
+ return globalThis._oscarpalmer_timer_debug ? [...activeTimers] : [];
12
+ },
13
+ });
14
+ }
package/src/index.ts CHANGED
@@ -1,230 +1,37 @@
1
- type Callbacks = {
2
- after: AfterCallback | undefined;
3
- default: (index?: number) => void;
4
- };
5
-
6
- type Configuration = {
7
- count: number;
8
- time: number;
9
- };
1
+ import {noop} from '@oscarpalmer/atoms/function';
2
+ import {activeTimers, hiddenTimers} from './constants';
3
+ import './global';
4
+ import {wait} from './timer';
10
5
 
11
6
  /**
12
- * @param {boolean} finished Did the timer finish?
7
+ * Creates a delayed promise that resolves after a certain amount of time _(or rejects when timed out)_
13
8
  */
14
- export type AfterCallback = (finished: boolean) => void;
15
-
16
- /**
17
- * @param {number} index The index of the current iteration
18
- */
19
- export type RepeatedCallback = (index: number) => void;
20
-
21
- type State = {
22
- active: boolean;
23
- finished: boolean;
24
- frame?: number;
25
- };
26
-
27
- const callbacks = new WeakMap<Timed<never, never>, Callbacks>();
28
- const configuration = new WeakMap<Timed<never, never>, Configuration>();
29
- const state = new WeakMap<Timed<never, never>, State>();
30
-
31
- const milliseconds = Math.round(1000 / 60);
32
-
33
- function run(timed: Timed<never, never>): void {
34
- const timedConfiguration = configuration.get(timed)!;
35
- const timedCallbacks = callbacks.get(timed)!;
36
- const timedState = state.get(timed)!;
37
-
38
- timedState.active = true;
39
- timedState.finished = false;
40
-
41
- const isRepeated = timed instanceof Repeated;
42
-
43
- let index = 0;
44
-
45
- let start;
46
-
47
- function step(timestamp: DOMHighResTimeStamp): void {
48
- if (!timedState.active) {
49
- return;
50
- }
51
-
52
- start ??= timestamp;
53
-
54
- const elapsed = timestamp - start;
55
-
56
- const elapsedMinimum = elapsed - milliseconds;
57
- const elapsedMaximum = elapsed + milliseconds;
58
-
59
- if (
60
- elapsedMinimum < timedConfiguration.time &&
61
- timedConfiguration.time < elapsedMaximum
62
- ) {
63
- if (timedState.active) {
64
- timedCallbacks.default(isRepeated ? index : undefined);
65
- }
66
-
67
- index += 1;
68
-
69
- if (isRepeated && index < timedConfiguration.count) {
70
- start = undefined;
71
- } else {
72
- timedState.finished = true;
73
-
74
- timed.stop();
75
-
76
- return;
77
- }
78
- }
79
-
80
- timedState.frame = globalThis.requestAnimationFrame(step);
81
- }
82
-
83
- timedState.frame = globalThis.requestAnimationFrame(step);
84
- }
85
-
86
- class Timed<Type, Callback> {
87
- get active(): boolean {
88
- return state.get(this as never)?.active ?? false;
89
- }
90
-
91
- get finished(): boolean {
92
- return !this.active && (state.get(this as never)?.finished ?? false);
93
- }
94
-
95
- /**
96
- * @param {Callback} callback
97
- * @param {number} time
98
- * @param {number} count
99
- * @param {AfterCallback=} afterCallback
100
- */
101
- constructor(
102
- callback: Callback,
103
- time: number,
104
- count: number,
105
- afterCallback?: AfterCallback,
106
- ) {
107
- const isRepeated = this instanceof Repeated;
108
-
109
- const type = isRepeated ? 'repeated' : 'waited';
110
-
111
- if (typeof callback !== 'function') {
112
- throw new TypeError(`A ${type} timer must have a callback function`);
113
- }
114
-
115
- if (typeof time !== 'number' || time < 0) {
116
- throw new TypeError(
117
- `A ${type} timer must have a non-negative number as its time`,
118
- );
119
- }
120
-
121
- if (isRepeated && (typeof count !== 'number' || count < 2)) {
122
- throw new TypeError(
123
- 'A repeated timer must have a number above 1 as its repeat count',
124
- );
125
- }
126
-
127
- if (
128
- isRepeated &&
129
- afterCallback !== undefined &&
130
- typeof afterCallback !== 'function'
131
- ) {
132
- throw new TypeError(
133
- "A repeated timer's after-callback must be a function",
134
- );
135
- }
136
-
137
- callbacks.set(this as never, {
138
- after: afterCallback,
139
- default: callback as never,
9
+ export function delay(time: number, timeout?: number): Promise<void> {
10
+ return new Promise((resolve, reject) => {
11
+ wait(resolve ?? noop, {
12
+ timeout,
13
+ errorCallback: reject ?? noop,
14
+ interval: time,
140
15
  });
16
+ });
17
+ }
141
18
 
142
- configuration.set(this as never, {count, time});
143
-
144
- state.set(this as never, {
145
- active: false,
146
- finished: false,
147
- });
148
- }
149
-
150
- restart(): Type {
151
- this.stop();
152
-
153
- run(this as never);
154
-
155
- return this as never;
156
- }
157
-
158
- start(): Type {
159
- if (!this.active) {
160
- run(this as never);
19
+ document.addEventListener('visibilitychange', () => {
20
+ if (document.hidden) {
21
+ for (const timer of activeTimers) {
22
+ hiddenTimers.add(timer);
23
+ timer.pause();
161
24
  }
162
-
163
- return this as never;
164
- }
165
-
166
- stop(): Type {
167
- const timedCallbacks = callbacks.get(this as never)!;
168
- const timedState = state.get(this as never)!;
169
-
170
- timedState.active = false;
171
-
172
- if (timedState.frame === undefined) {
173
- return this as never;
25
+ } else {
26
+ for (const timer of hiddenTimers) {
27
+ timer.continue();
174
28
  }
175
29
 
176
- globalThis.cancelAnimationFrame(timedState.frame);
177
-
178
- timedCallbacks.after?.(this.finished);
179
-
180
- timedState.frame = undefined;
181
-
182
- return this as never;
183
- }
184
- }
185
-
186
- /**
187
- * A timer that waits and runs repeatedly
188
- */
189
- export class Repeated extends Timed<Repeated, RepeatedCallback> {}
190
-
191
- /**
192
- * A timer that waits and runs once
193
- */
194
- export class Waited extends Timed<Waited, () => void> {
195
- /**
196
- * Creates a new waited timer
197
- * @param {() => void} callback
198
- * @param {number} time
199
- */
200
- constructor(callback: () => void, time: number) {
201
- super(callback, time, 1);
30
+ hiddenTimers.clear();
202
31
  }
203
- }
32
+ });
204
33
 
205
- /**
206
- * Creates and starts a new repeated timer
207
- * @param {RepeatedCallback} callback
208
- * @param {number} time
209
- * @param {number} count
210
- * @param {AfterCallback=} afterCallback
211
- * @return {Repeated}
212
- */
213
- export function repeat(
214
- callback: RepeatedCallback,
215
- time: number,
216
- count: number,
217
- afterCallback?: AfterCallback,
218
- ): Repeated {
219
- return new Repeated(callback as never, time, count, afterCallback).start();
220
- }
34
+ export {isRepeated, isTimer, isWaited, isWhen} from './is';
35
+ export {repeat, wait, type Timer} from './timer';
36
+ export {when, type When} from './when';
221
37
 
222
- /**
223
- * Creates and starts a new waited timer
224
- * @param {() => void} callback
225
- * @param {number} time
226
- * @return {Waited}
227
- */
228
- export function wait(callback: () => void, time: number): Waited {
229
- return new Waited(callback, time).start();
230
- }
package/src/is.ts ADDED
@@ -0,0 +1,35 @@
1
+ import type {PlainObject} from '@oscarpalmer/atoms/models';
2
+ import type {Timer} from './timer';
3
+ import type {When} from './when';
4
+
5
+ function is(pattern: RegExp, value: unknown) {
6
+ return pattern.test((value as PlainObject)?.$timer as string);
7
+ }
8
+
9
+ /**
10
+ * Is the value a repeating timer?
11
+ */
12
+ export function isRepeated(value: unknown): value is Timer {
13
+ return is(/^repeat$/, value);
14
+ }
15
+
16
+ /**
17
+ * Is the value a timer?
18
+ */
19
+ export function isTimer(value: unknown): value is Timer {
20
+ return is(/^repeat|wait$/, value);
21
+ }
22
+
23
+ /**
24
+ * Is the value a waiting timer?
25
+ */
26
+ export function isWaited(value: unknown): value is Timer {
27
+ return is(/^wait$/, value);
28
+ }
29
+
30
+ /**
31
+ * Is the value a conditional timer?
32
+ */
33
+ export function isWhen(value: unknown): value is When {
34
+ return is(/^when$/, value) && typeof (value as When).then === 'function';
35
+ }
package/src/models.ts ADDED
@@ -0,0 +1,76 @@
1
+ import type {Timer} from './timer';
2
+
3
+ /**
4
+ * Callback that runs after the timer has finished (or is stopped)
5
+ * - `finished` is `true` if the timer was allowed to finish, and `false` if it was stopped
6
+ */
7
+ export type AfterCallback = (finished: boolean) => void;
8
+
9
+ export type AnyCallback = (() => void) | IndexedCallback;
10
+
11
+ /**
12
+ * Callback that runs for each iteration of the timer
13
+ */
14
+ export type IndexedCallback = (index: number) => void;
15
+
16
+ export type BaseOptions = {
17
+ /**
18
+ * Interval between each callback
19
+ */
20
+ interval: number;
21
+ /**
22
+ * Maximum amount of time the timer may run for
23
+ */
24
+ timeout: number;
25
+ };
26
+
27
+ export type OptionsWithCount = {
28
+ /**
29
+ * How many times the timer should repeat
30
+ */
31
+ count: number;
32
+ } & BaseOptions;
33
+
34
+ export type OptionsWithError = {
35
+ /**
36
+ * Callback to run when an error occurs _(usually a timeout)_
37
+ */
38
+ errorCallback?: () => void;
39
+ };
40
+
41
+ export type RepeatOptions = {
42
+ /**
43
+ * Callback to run after the timer has finished (or is stopped)
44
+ * - `finished` is `true` if the timer was allowed to finish, and `false` if it was stopped
45
+ */
46
+ afterCallback?: AfterCallback;
47
+ } & OptionsWithCount &
48
+ OptionsWithError;
49
+
50
+ export type TimerOptions = {} & RepeatOptions;
51
+
52
+ export type TimerState = {
53
+ active: boolean;
54
+ callback: AnyCallback;
55
+ count?: number;
56
+ elapsed?: number;
57
+ frame?: number;
58
+ index?: number;
59
+ isRepeated: boolean;
60
+ minimum: number;
61
+ paused: boolean;
62
+ trace: unknown;
63
+ };
64
+
65
+ export type WaitOptions = {} & BaseOptions & OptionsWithError;
66
+
67
+ export type WhenOptions = {} & OptionsWithCount;
68
+
69
+ export type WhenState = {
70
+ promise: Promise<void>;
71
+ rejecter?: () => void;
72
+ resolver?: () => void;
73
+ timer: Timer;
74
+ };
75
+
76
+ export type WorkType = 'continue' | 'pause' | 'restart' | 'start' | 'stop';