@oscarpalmer/timer 0.20.0 → 0.21.1

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.
package/dist/timer.js CHANGED
@@ -1,111 +1,292 @@
1
- // src/index.ts
2
- var run = function(timer) {
3
- const { _configuration, _state } = timer;
4
- _state.active = true;
5
- _state.finished = false;
6
- const isRepeated = _configuration.count > 1;
7
- let index = 0;
1
+ // node_modules/@oscarpalmer/atoms/dist/js/function.mjs
2
+ function noop() {
3
+ }
4
+
5
+ // src/constants.ts
6
+ var activeTimers = new Set;
7
+ var hiddenTimers = new Set;
8
+ var milliseconds = 1000 / 60;
9
+
10
+ // src/global.ts
11
+ if (globalThis._oscarpalmer_timers == null) {
12
+ Object.defineProperty(globalThis, "_oscarpalmer_timers", {
13
+ get() {
14
+ return globalThis._oscarpalmer_timer_debug ? [...activeTimers] : [];
15
+ }
16
+ });
17
+ }
18
+
19
+ // src/functions.ts
20
+ function getOptions(options, isRepeated) {
21
+ return {
22
+ afterCallback: options.afterCallback,
23
+ count: getValueOrDefault(options.count, isRepeated ? Number.POSITIVE_INFINITY : 1),
24
+ errorCallback: options.errorCallback,
25
+ interval: getValueOrDefault(options.interval, milliseconds, milliseconds),
26
+ timeout: getValueOrDefault(options.timeout, isRepeated ? Number.POSITIVE_INFINITY : 30000)
27
+ };
28
+ }
29
+ function getValueOrDefault(value, defaultValue, minimum) {
30
+ return typeof value === "number" && value > (minimum ?? 0) ? value : defaultValue;
31
+ }
32
+ function work(type, timer, state, options) {
33
+ if (["continue", "start"].includes(type) && state.active || ["pause", "stop"].includes(type) && !state.active) {
34
+ return timer;
35
+ }
36
+ const { count, interval, timeout } = options;
37
+ const { isRepeated, minimum } = state;
38
+ if (["pause", "restart", "stop"].includes(type)) {
39
+ const isStop = type === "stop";
40
+ activeTimers.delete(timer);
41
+ cancelAnimationFrame(state.frame);
42
+ if (isStop) {
43
+ options.afterCallback?.(false);
44
+ }
45
+ state.active = false;
46
+ state.frame = undefined;
47
+ state.paused = !isStop;
48
+ if (isStop) {
49
+ state.elapsed = undefined;
50
+ state.index = undefined;
51
+ }
52
+ return type === "restart" ? work("start", timer, state, options) : timer;
53
+ }
54
+ state.active = true;
55
+ state.paused = false;
56
+ const elapsed = type === "continue" ? +(state.elapsed ?? 0) : 0;
57
+ let index = type === "continue" ? +(state.index ?? 0) : 0;
58
+ state.elapsed = elapsed;
59
+ state.index = index;
60
+ const total = (count === Number.POSITIVE_INFINITY ? Number.POSITIVE_INFINITY : (count - index) * (interval > 0 ? interval : milliseconds)) - elapsed;
61
+ let current;
8
62
  let start;
63
+ function finish(finished, error) {
64
+ activeTimers.delete(timer);
65
+ state.active = false;
66
+ state.elapsed = undefined;
67
+ state.frame = undefined;
68
+ state.index = undefined;
69
+ if (error) {
70
+ options.errorCallback?.();
71
+ }
72
+ options.afterCallback?.(finished);
73
+ }
9
74
  function step(timestamp) {
10
- if (!_state.active) {
75
+ if (!state.active) {
11
76
  return;
12
77
  }
78
+ current ??= timestamp;
13
79
  start ??= timestamp;
14
- const elapsed = timestamp - start;
15
- const elapsedMinimum = elapsed - milliseconds;
16
- const elapsedMaximum = elapsed + milliseconds;
17
- if (elapsedMinimum < _configuration.time && _configuration.time < elapsedMaximum) {
18
- if (_state.active) {
19
- _configuration.callbacks.default(index);
80
+ const time = timestamp - current;
81
+ state.elapsed = elapsed + (current - start);
82
+ const finished = time - elapsed >= total;
83
+ if (timestamp - start >= timeout - elapsed) {
84
+ finish(finished, !finished);
85
+ return;
86
+ }
87
+ if (finished || time >= minimum) {
88
+ if (state.active) {
89
+ state.callback(isRepeated ? index : undefined);
20
90
  }
21
91
  index += 1;
22
- if (isRepeated && index < _configuration.count) {
23
- start = undefined;
92
+ state.index = index;
93
+ if (!finished && index < count) {
94
+ current = null;
24
95
  } else {
25
- _state.finished = true;
26
- timer.stop();
96
+ finish(true, false);
27
97
  return;
28
98
  }
29
99
  }
30
- _state.frame = requestAnimationFrame(step);
100
+ state.frame = requestAnimationFrame(step);
31
101
  }
32
- _state.frame = requestAnimationFrame(step);
33
- };
34
- function repeat(callback, count, afterOrTime, after) {
35
- if (typeof count !== "number" || count < 2) {
36
- throw new TypeError("A repeated timer must have a number greater than or equal to 2 as its run count");
102
+ activeTimers.add(timer);
103
+ state.frame = requestAnimationFrame(step);
104
+ return timer;
105
+ }
106
+
107
+ // src/timer.ts
108
+ function repeat(callback, options) {
109
+ return timer("repeat", callback, options ?? {}, true);
110
+ }
111
+ function timer(type, callback, partial, start) {
112
+ const isRepeated = type === "repeat";
113
+ const options = getOptions(partial, isRepeated);
114
+ const instance = new Timer(type, {
115
+ callback,
116
+ isRepeated,
117
+ active: false,
118
+ minimum: options.interval - options.interval % milliseconds / 2,
119
+ paused: false,
120
+ trace: new TimerTrace
121
+ }, options);
122
+ if (start) {
123
+ instance.start();
37
124
  }
38
- const afterOrTimeIsFunction = typeof afterOrTime === "function";
39
- return new Timer(callback, afterOrTimeIsFunction ? 0 : afterOrTime, count, afterOrTimeIsFunction ? afterOrTime : after).start();
125
+ return instance;
40
126
  }
41
- function wait(callback, time) {
42
- return new Timer(callback, time).start();
127
+ function wait(callback, options) {
128
+ return timer("wait", callback, options == null || typeof options === "number" ? {
129
+ interval: options
130
+ } : options, true);
43
131
  }
44
- var milliseconds = Math.round(16.666666666666668);
45
132
 
46
- class Timer {
133
+ class BasicTimer {
134
+ constructor(type, state) {
135
+ this.$timer = type;
136
+ this.state = state;
137
+ }
138
+ }
139
+
140
+ class Timer extends BasicTimer {
47
141
  get active() {
48
- return this._state.active;
142
+ return this.state.active;
49
143
  }
50
- get finished() {
51
- return this._state.finished;
144
+ get paused() {
145
+ return this.state.paused;
52
146
  }
53
- constructor(callback, time, count, afterCallback) {
54
- if (typeof callback !== "function") {
55
- throw new TypeError("A timer must have a callback function");
56
- }
57
- const actualTime = typeof time === "number" ? time : 0;
58
- if (actualTime < 0) {
59
- throw new TypeError("A timer must have a non-negative number as its time");
60
- }
61
- const actualCount = typeof count === "number" ? count : 1;
62
- if (actualCount < 1) {
63
- throw new TypeError("A timer must have a number greater than or equal to 1 as its run count");
64
- }
65
- if (actualCount > 1 && afterCallback !== undefined && typeof afterCallback !== "function") {
66
- throw new TypeError("A repeated timer's after-callback must be a function");
67
- }
68
- Object.defineProperty(this, "_configuration", {
69
- value: {
70
- callbacks: {
71
- after: afterCallback,
72
- default: callback
73
- },
74
- count: actualCount,
75
- time: actualTime
76
- }
77
- });
78
- Object.defineProperty(this, "_state", {
79
- value: {
80
- active: false,
81
- finished: false
82
- }
83
- });
147
+ get trace() {
148
+ return globalThis._oscarpalmer_timer_debug ? this.state.trace : undefined;
149
+ }
150
+ constructor(type, state, options) {
151
+ super(type, state);
152
+ this.options = options;
153
+ }
154
+ continue() {
155
+ return work("continue", this, this.state, this.options);
156
+ }
157
+ pause() {
158
+ return work("pause", this, this.state, this.options);
84
159
  }
85
160
  restart() {
86
- this.stop();
87
- run(this);
88
- return this;
161
+ return work("restart", this, this.state, this.options);
89
162
  }
90
163
  start() {
91
- if (!this._state.active) {
92
- run(this);
164
+ return work("start", this, this.state, this.options);
165
+ }
166
+ stop() {
167
+ return work("stop", this, this.state, this.options);
168
+ }
169
+ }
170
+
171
+ class TimerTrace extends Error {
172
+ constructor() {
173
+ super();
174
+ this.name = "TimerTrace";
175
+ }
176
+ }
177
+
178
+ // src/index.ts
179
+ function delay(time, timeout) {
180
+ return new Promise((resolve, reject) => {
181
+ wait(resolve ?? noop, {
182
+ timeout,
183
+ errorCallback: reject ?? noop,
184
+ interval: time
185
+ });
186
+ });
187
+ }
188
+
189
+ // src/is.ts
190
+ function is(pattern, value) {
191
+ return pattern.test(value?.$timer);
192
+ }
193
+ function isRepeated(value) {
194
+ return is(/^repeat$/, value);
195
+ }
196
+ function isTimer(value) {
197
+ return is(/^repeat|wait$/, value);
198
+ }
199
+ function isWaited(value) {
200
+ return is(/^wait$/, value);
201
+ }
202
+ function isWhen(value) {
203
+ return is(/^when$/, value) && typeof value.then === "function";
204
+ }
205
+ // src/when.ts
206
+ function when(condition, options) {
207
+ const repeated = timer("repeat", () => {
208
+ if (condition()) {
209
+ repeated.stop();
210
+ state.resolver?.();
93
211
  }
212
+ }, {
213
+ afterCallback() {
214
+ if (!repeated.paused) {
215
+ if (condition()) {
216
+ state.resolver?.();
217
+ } else {
218
+ state.rejecter?.();
219
+ }
220
+ }
221
+ },
222
+ errorCallback() {
223
+ state.rejecter?.();
224
+ },
225
+ count: options?.count,
226
+ interval: options?.interval,
227
+ timeout: options?.timeout
228
+ }, false);
229
+ const state = {};
230
+ state.promise = new Promise((resolve, reject) => {
231
+ state.resolver = resolve;
232
+ state.rejecter = reject;
233
+ });
234
+ state.timer = repeated;
235
+ return new When(state);
236
+ }
237
+
238
+ class When extends BasicTimer {
239
+ get active() {
240
+ return this.state.timer.active;
241
+ }
242
+ get paused() {
243
+ return this.state.timer.paused;
244
+ }
245
+ constructor(state) {
246
+ super("when", state);
247
+ }
248
+ continue() {
249
+ this.state.timer.continue();
250
+ return this;
251
+ }
252
+ pause() {
253
+ this.state.timer.pause();
94
254
  return this;
95
255
  }
96
256
  stop() {
97
- this._state.active = false;
98
- if (this._state.frame === undefined) {
99
- return this;
257
+ if (this.state.timer.active) {
258
+ this.state.timer.stop();
259
+ this.state.rejecter?.();
100
260
  }
101
- cancelAnimationFrame(this._state.frame);
102
- this._configuration.callbacks.after?.(this._state.finished);
103
- this._state.frame = undefined;
104
261
  return this;
105
262
  }
263
+ then(resolve, reject) {
264
+ this.state.timer.start();
265
+ return this.state.promise.then(resolve ?? noop, reject ?? noop);
266
+ }
106
267
  }
268
+
269
+ // src/index.ts
270
+ document.addEventListener("visibilitychange", () => {
271
+ if (document.hidden) {
272
+ for (const timer4 of activeTimers) {
273
+ hiddenTimers.add(timer4);
274
+ timer4.pause();
275
+ }
276
+ } else {
277
+ for (const timer4 of hiddenTimers) {
278
+ timer4.continue();
279
+ }
280
+ hiddenTimers.clear();
281
+ }
282
+ });
107
283
  export {
284
+ when,
108
285
  wait,
109
286
  repeat,
110
- Timer
287
+ isWhen,
288
+ isWaited,
289
+ isTimer,
290
+ isRepeated,
291
+ delay
111
292
  };
package/dist/timer.mjs ADDED
@@ -0,0 +1,40 @@
1
+ // src/index.ts
2
+ import {noop} from "@oscarpalmer/atoms/function";
3
+ import {activeTimers, hiddenTimers} from "./constants";
4
+ import"./global";
5
+ import {wait} from "./timer";
6
+ function delay(time, timeout) {
7
+ return new Promise((resolve, reject) => {
8
+ wait(resolve ?? noop, {
9
+ timeout,
10
+ errorCallback: reject ?? noop,
11
+ interval: time
12
+ });
13
+ });
14
+ }
15
+ import {isRepeated, isTimer, isWaited, isWhen} from "./is";
16
+ import {repeat, wait as wait2} from "./timer";
17
+ import {when} from "./when";
18
+ document.addEventListener("visibilitychange", () => {
19
+ if (document.hidden) {
20
+ for (const timer2 of activeTimers) {
21
+ hiddenTimers.add(timer2);
22
+ timer2.pause();
23
+ }
24
+ } else {
25
+ for (const timer2 of hiddenTimers) {
26
+ timer2.continue();
27
+ }
28
+ hiddenTimers.clear();
29
+ }
30
+ });
31
+ export {
32
+ when,
33
+ wait2 as wait,
34
+ repeat,
35
+ isWhen,
36
+ isWaited,
37
+ isTimer,
38
+ isRepeated,
39
+ delay
40
+ };
package/package.json CHANGED
@@ -3,39 +3,53 @@
3
3
  "name": "Oscar Palmér",
4
4
  "url": "https://oscarpalmer.se"
5
5
  },
6
+ "dependencies": {
7
+ "@oscarpalmer/atoms": "0.69.0"
8
+ },
6
9
  "description": "A better solution for timeout- and interval-based timers.",
7
10
  "devDependencies": {
8
- "@biomejs/biome": "^1.4",
9
- "@happy-dom/global-registrator": "^12.10",
10
- "bun": "^1.0",
11
- "typescript": "^5.3"
11
+ "@biomejs/biome": "^1.8.3",
12
+ "@happy-dom/global-registrator": "^14.12.3",
13
+ "@types/bun": "^1.1.6",
14
+ "bun": "^1.1.21",
15
+ "dts-bundle-generator": "9.5.1",
16
+ "typescript": "^5.5.4"
17
+ },
18
+ "exports": {
19
+ ".": {
20
+ "bun": "./src/index.ts",
21
+ "import": {
22
+ "types": "./types/index.d.ts",
23
+ "default": "./dist/timer.mjs"
24
+ },
25
+ "require": {
26
+ "types": "./types/index.d.cts",
27
+ "default": "./dist/timer.js"
28
+ }
29
+ }
12
30
  },
13
31
  "files": ["dist", "src", "types"],
14
32
  "keywords": ["timer", "setTimeout", "setInterval", "requestAnimationFrame"],
15
33
  "license": "MIT",
16
34
  "main": "dist/timer.js",
17
- "module": "dist/timer.js",
35
+ "module": "dist/timer.mjs",
18
36
  "name": "@oscarpalmer/timer",
19
- "prettier": {
20
- "arrowParens": "avoid",
21
- "bracketSpacing": false,
22
- "singleQuote": true,
23
- "switchIndent": true,
24
- "trailingComma": "all",
25
- "useTabs": true
26
- },
27
37
  "repository": {
28
38
  "type": "git",
29
39
  "url": "git+https://github.com/oscarpalmer/timer.git"
30
40
  },
31
41
  "scripts": {
32
- "build": "bun build ./src/index.ts --outfile ./dist/timer.js",
33
- "test": "bun test --coverage",
34
- "types": "bunx tsc -p ./tsconfig.json",
35
- "watch": "rollup -c -w",
36
- "xo": "xo ./src/*.ts --env browser"
42
+ "build": "bun run clean && bun run build:cjs && bun run build:esm && bun run types",
43
+ "build:cjs": "bun build ./src/index.ts --outfile ./dist/timer.js",
44
+ "build:esm": "bun build ./src/index.ts --external '*' --outfile ./dist/timer.mjs",
45
+ "clean": "rm -rf ./dist && rm -rf ./types && rm -f ./tsconfig.tsbuildinfo",
46
+ "test": "bun test",
47
+ "types": "bun run types:cjs && bun run types:esm",
48
+ "types:cjs": "bunx dts-bundle-generator --out-file ./types/index.d.cts --external-inlines '@oscarpalmer/atoms' --no-check --silent ./src/index.ts",
49
+ "types:esm": "bunx tsc -p ./tsconfig.json",
50
+ "watch": "bun build ./src/index.ts --outfile ./dist/timer.js --watch"
37
51
  },
38
52
  "type": "module",
39
- "types": "src/index.d.ts",
40
- "version": "0.20.0"
53
+ "types": "types/index.d.cts",
54
+ "version": "0.21.1"
41
55
  }
@@ -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
+ }