@crup/react-timer-hook 0.0.1-alpha.4 → 0.0.1-alpha.5

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/index.js CHANGED
@@ -1,812 +1 @@
1
- // src/durationParts.ts
2
- var SECOND = 1e3;
3
- var MINUTE = 60 * SECOND;
4
- var HOUR = 60 * MINUTE;
5
- var DAY = 24 * HOUR;
6
- function durationParts(milliseconds) {
7
- const totalMilliseconds = Math.max(0, Math.trunc(Number.isFinite(milliseconds) ? milliseconds : 0));
8
- const days = Math.floor(totalMilliseconds / DAY);
9
- const afterDays = totalMilliseconds % DAY;
10
- const hours = Math.floor(afterDays / HOUR);
11
- const afterHours = afterDays % HOUR;
12
- const minutes = Math.floor(afterHours / MINUTE);
13
- const afterMinutes = afterHours % MINUTE;
14
- const seconds = Math.floor(afterMinutes / SECOND);
15
- return {
16
- totalMilliseconds,
17
- totalSeconds: Math.floor(totalMilliseconds / SECOND),
18
- milliseconds: afterMinutes % SECOND,
19
- seconds,
20
- minutes,
21
- hours,
22
- days
23
- };
24
- }
25
-
26
- // src/useTimer.ts
27
- import { useCallback, useEffect, useMemo, useReducer, useRef } from "react";
28
-
29
- // src/debug.ts
30
- function resolveDebug(debug) {
31
- if (!debug) return { enabled: false, includeTicks: false };
32
- if (debug === true) return { enabled: true, includeTicks: false, logger: console.debug };
33
- if (typeof debug === "function") return { enabled: true, includeTicks: false, logger: debug };
34
- return {
35
- enabled: debug.enabled !== false,
36
- includeTicks: debug.includeTicks ?? false,
37
- label: debug.label,
38
- logger: debug.logger ?? console.debug
39
- };
40
- }
41
- function emitDebug(debug, event) {
42
- const config = resolveDebug(debug);
43
- if (!config.enabled || !config.logger) return;
44
- if (event.type === "timer:tick" && !config.includeTicks) return;
45
- config.logger({
46
- ...event,
47
- label: event.label ?? config.label
48
- });
49
- }
50
- function baseDebugEvent(snapshot, generation) {
51
- return {
52
- generation,
53
- tick: snapshot.tick,
54
- now: snapshot.now,
55
- elapsedMilliseconds: snapshot.elapsedMilliseconds,
56
- status: snapshot.status
57
- };
58
- }
59
-
60
- // src/clocks.ts
61
- function readClock() {
62
- const wallNow = Date.now();
63
- const monotonicNow = typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : wallNow;
64
- return { wallNow, monotonicNow };
65
- }
66
- function validatePositiveFinite(value, name) {
67
- if (!Number.isFinite(value) || value <= 0) {
68
- throw new RangeError(`${name} must be a finite number greater than 0`);
69
- }
70
- }
71
-
72
- // src/state.ts
73
- function createTimerState(clock) {
74
- return {
75
- status: "idle",
76
- generation: 0,
77
- tick: 0,
78
- startedAt: null,
79
- pausedAt: null,
80
- endedAt: null,
81
- cancelledAt: null,
82
- cancelReason: null,
83
- baseElapsedMilliseconds: 0,
84
- activeStartedAtMonotonic: null,
85
- now: clock.wallNow
86
- };
87
- }
88
- function getElapsedMilliseconds(state, clock) {
89
- if (state.status !== "running" || state.activeStartedAtMonotonic === null) {
90
- return state.baseElapsedMilliseconds;
91
- }
92
- return Math.max(0, state.baseElapsedMilliseconds + clock.monotonicNow - state.activeStartedAtMonotonic);
93
- }
94
- function toSnapshot(state, clock) {
95
- const elapsedMilliseconds = getElapsedMilliseconds(state, clock);
96
- return {
97
- status: state.status,
98
- now: clock.wallNow,
99
- tick: state.tick,
100
- startedAt: state.startedAt,
101
- pausedAt: state.pausedAt,
102
- endedAt: state.endedAt,
103
- cancelledAt: state.cancelledAt,
104
- cancelReason: state.cancelReason,
105
- elapsedMilliseconds,
106
- isIdle: state.status === "idle",
107
- isRunning: state.status === "running",
108
- isPaused: state.status === "paused",
109
- isEnded: state.status === "ended",
110
- isCancelled: state.status === "cancelled"
111
- };
112
- }
113
- function startTimerState(state, clock) {
114
- if (state.status !== "idle") return false;
115
- state.status = "running";
116
- state.startedAt = clock.wallNow;
117
- state.pausedAt = null;
118
- state.endedAt = null;
119
- state.cancelledAt = null;
120
- state.cancelReason = null;
121
- state.activeStartedAtMonotonic = clock.monotonicNow;
122
- state.now = clock.wallNow;
123
- return true;
124
- }
125
- function pauseTimerState(state, clock) {
126
- if (state.status !== "running") return false;
127
- state.baseElapsedMilliseconds = getElapsedMilliseconds(state, clock);
128
- state.activeStartedAtMonotonic = null;
129
- state.status = "paused";
130
- state.pausedAt = clock.wallNow;
131
- state.now = clock.wallNow;
132
- return true;
133
- }
134
- function resumeTimerState(state, clock) {
135
- if (state.status !== "paused") return false;
136
- state.status = "running";
137
- state.pausedAt = null;
138
- state.activeStartedAtMonotonic = clock.monotonicNow;
139
- state.now = clock.wallNow;
140
- return true;
141
- }
142
- function resetTimerState(state, clock, options = {}) {
143
- state.generation += 1;
144
- state.tick = 0;
145
- state.status = options.autoStart ? "running" : "idle";
146
- state.startedAt = options.autoStart ? clock.wallNow : null;
147
- state.pausedAt = null;
148
- state.endedAt = null;
149
- state.cancelledAt = null;
150
- state.cancelReason = null;
151
- state.baseElapsedMilliseconds = 0;
152
- state.activeStartedAtMonotonic = options.autoStart ? clock.monotonicNow : null;
153
- state.now = clock.wallNow;
154
- return true;
155
- }
156
- function restartTimerState(state, clock) {
157
- return resetTimerState(state, clock, { autoStart: true });
158
- }
159
- function cancelTimerState(state, clock, reason) {
160
- if (state.status === "ended" || state.status === "cancelled") return false;
161
- state.baseElapsedMilliseconds = getElapsedMilliseconds(state, clock);
162
- state.activeStartedAtMonotonic = null;
163
- state.status = "cancelled";
164
- state.cancelledAt = clock.wallNow;
165
- state.cancelReason = reason ?? null;
166
- state.now = clock.wallNow;
167
- return true;
168
- }
169
- function endTimerState(state, clock) {
170
- if (state.status !== "running") return false;
171
- state.baseElapsedMilliseconds = getElapsedMilliseconds(state, clock);
172
- state.activeStartedAtMonotonic = null;
173
- state.status = "ended";
174
- state.endedAt = clock.wallNow;
175
- state.now = clock.wallNow;
176
- return true;
177
- }
178
- function tickTimerState(state, clock) {
179
- if (state.status !== "running") return false;
180
- state.tick += 1;
181
- state.now = clock.wallNow;
182
- return true;
183
- }
184
-
185
- // src/useTimer.ts
186
- function useTimer(options = {}) {
187
- const updateIntervalMs = options.updateIntervalMs ?? 1e3;
188
- validatePositiveFinite(updateIntervalMs, "updateIntervalMs");
189
- validateSchedules(options.schedules);
190
- const optionsRef = useRef(options);
191
- optionsRef.current = options;
192
- const stateRef = useRef(null);
193
- if (stateRef.current === null) {
194
- stateRef.current = createTimerState(readClock());
195
- }
196
- const mountedRef = useRef(false);
197
- const timeoutRef = useRef(null);
198
- const schedulesRef = useRef(/* @__PURE__ */ new Map());
199
- const endCalledGenerationRef = useRef(null);
200
- const [, rerender] = useReducer((value) => value + 1, 0);
201
- const clearScheduledTick = useCallback(() => {
202
- if (timeoutRef.current !== null) {
203
- clearTimeout(timeoutRef.current);
204
- timeoutRef.current = null;
205
- }
206
- }, []);
207
- const getSnapshot = useCallback((clock = readClock()) => {
208
- return toSnapshot(stateRef.current, clock);
209
- }, []);
210
- const emit = useCallback(
211
- (type, snapshot2, extra = {}) => {
212
- emitDebug(optionsRef.current.debug, {
213
- type,
214
- scope: "timer",
215
- ...baseDebugEvent(snapshot2, stateRef.current.generation),
216
- ...extra
217
- });
218
- },
219
- []
220
- );
221
- const controlsRef = useRef(null);
222
- const callOnEnd = useCallback(
223
- (snapshot2) => {
224
- const generation2 = stateRef.current.generation;
225
- if (endCalledGenerationRef.current === generation2) return;
226
- endCalledGenerationRef.current = generation2;
227
- try {
228
- void optionsRef.current.onEnd?.(snapshot2, controlsRef.current);
229
- } catch (error) {
230
- emitDebug(optionsRef.current.debug, {
231
- type: "callback:error",
232
- scope: "timer",
233
- ...baseDebugEvent(snapshot2, generation2),
234
- error
235
- });
236
- }
237
- },
238
- []
239
- );
240
- const runSchedule = useCallback(
241
- (schedule, key, scheduleState, snapshot2, generation2) => {
242
- if (scheduleState.pending && (schedule.overlap ?? "skip") === "skip") {
243
- emitDebug(optionsRef.current.debug, {
244
- type: "schedule:skip",
245
- scope: "timer",
246
- scheduleId: schedule.id ?? key,
247
- reason: "overlap",
248
- ...baseDebugEvent(snapshot2, generation2)
249
- });
250
- return;
251
- }
252
- scheduleState.lastRunAt = snapshot2.now;
253
- scheduleState.pending = true;
254
- emitDebug(optionsRef.current.debug, {
255
- type: "schedule:start",
256
- scope: "timer",
257
- scheduleId: schedule.id ?? key,
258
- ...baseDebugEvent(snapshot2, generation2)
259
- });
260
- Promise.resolve().then(() => schedule.callback(snapshot2, controlsRef.current)).then(
261
- () => {
262
- emitDebug(optionsRef.current.debug, {
263
- type: "schedule:end",
264
- scope: "timer",
265
- scheduleId: schedule.id ?? key,
266
- ...baseDebugEvent(snapshot2, generation2)
267
- });
268
- },
269
- (error) => {
270
- emitDebug(optionsRef.current.debug, {
271
- type: "schedule:error",
272
- scope: "timer",
273
- scheduleId: schedule.id ?? key,
274
- error,
275
- ...baseDebugEvent(snapshot2, generation2)
276
- });
277
- }
278
- ).finally(() => {
279
- if (stateRef.current?.generation === generation2) {
280
- scheduleState.pending = false;
281
- }
282
- });
283
- },
284
- []
285
- );
286
- const evaluateSchedules = useCallback(
287
- (snapshot2, generation2, activation = false) => {
288
- const schedules = optionsRef.current.schedules ?? [];
289
- const liveKeys = /* @__PURE__ */ new Set();
290
- schedules.forEach((schedule, index) => {
291
- const key = schedule.id ?? String(index);
292
- liveKeys.add(key);
293
- let scheduleState = schedulesRef.current.get(key);
294
- if (!scheduleState) {
295
- scheduleState = { lastRunAt: null, pending: false, leadingGeneration: null };
296
- schedulesRef.current.set(key, scheduleState);
297
- }
298
- if (activation && schedule.leading && scheduleState.leadingGeneration !== generation2) {
299
- scheduleState.leadingGeneration = generation2;
300
- runSchedule(schedule, key, scheduleState, snapshot2, generation2);
301
- return;
302
- }
303
- if (scheduleState.lastRunAt === null) {
304
- scheduleState.lastRunAt = snapshot2.now;
305
- return;
306
- }
307
- if (snapshot2.now - scheduleState.lastRunAt >= schedule.everyMs) {
308
- runSchedule(schedule, key, scheduleState, snapshot2, generation2);
309
- }
310
- });
311
- for (const key of schedulesRef.current.keys()) {
312
- if (!liveKeys.has(key)) schedulesRef.current.delete(key);
313
- }
314
- },
315
- [runSchedule]
316
- );
317
- const processRunningState = useCallback(
318
- (clock = readClock(), activation = false) => {
319
- const state = stateRef.current;
320
- if (state.status !== "running") return;
321
- const snapshot2 = toSnapshot(state, clock);
322
- const generation2 = state.generation;
323
- if (optionsRef.current.endWhen?.(snapshot2)) {
324
- if (endTimerState(state, clock)) {
325
- const endedSnapshot = toSnapshot(state, clock);
326
- emit("timer:end", endedSnapshot);
327
- clearScheduledTick();
328
- callOnEnd(endedSnapshot);
329
- rerender();
330
- }
331
- return;
332
- }
333
- evaluateSchedules(snapshot2, generation2, activation);
334
- },
335
- [callOnEnd, clearScheduledTick, emit, evaluateSchedules]
336
- );
337
- const start = useCallback(() => {
338
- const clock = readClock();
339
- if (!startTimerState(stateRef.current, clock)) return;
340
- const snapshot2 = toSnapshot(stateRef.current, clock);
341
- emit("timer:start", snapshot2);
342
- processRunningState(clock, true);
343
- rerender();
344
- }, [emit, processRunningState]);
345
- const pause = useCallback(() => {
346
- const clock = readClock();
347
- if (!pauseTimerState(stateRef.current, clock)) return;
348
- clearScheduledTick();
349
- const snapshot2 = toSnapshot(stateRef.current, clock);
350
- emit("timer:pause", snapshot2);
351
- rerender();
352
- }, [clearScheduledTick, emit]);
353
- const resume = useCallback(() => {
354
- const clock = readClock();
355
- if (!resumeTimerState(stateRef.current, clock)) return;
356
- const snapshot2 = toSnapshot(stateRef.current, clock);
357
- emit("timer:resume", snapshot2);
358
- processRunningState(clock, true);
359
- rerender();
360
- }, [emit, processRunningState]);
361
- const reset = useCallback(
362
- (resetOptions = {}) => {
363
- const clock = readClock();
364
- clearScheduledTick();
365
- resetTimerState(stateRef.current, clock, resetOptions);
366
- schedulesRef.current.clear();
367
- endCalledGenerationRef.current = null;
368
- const snapshot2 = toSnapshot(stateRef.current, clock);
369
- emit("timer:reset", snapshot2);
370
- if (resetOptions.autoStart) processRunningState(clock, true);
371
- rerender();
372
- },
373
- [clearScheduledTick, emit, processRunningState]
374
- );
375
- const restart = useCallback(() => {
376
- const clock = readClock();
377
- clearScheduledTick();
378
- restartTimerState(stateRef.current, clock);
379
- schedulesRef.current.clear();
380
- endCalledGenerationRef.current = null;
381
- const snapshot2 = toSnapshot(stateRef.current, clock);
382
- emit("timer:restart", snapshot2);
383
- processRunningState(clock, true);
384
- rerender();
385
- }, [clearScheduledTick, emit, processRunningState]);
386
- const cancel = useCallback(
387
- (reason) => {
388
- const clock = readClock();
389
- if (!cancelTimerState(stateRef.current, clock, reason)) return;
390
- clearScheduledTick();
391
- const snapshot2 = toSnapshot(stateRef.current, clock);
392
- emit("timer:cancel", snapshot2, { reason });
393
- rerender();
394
- },
395
- [clearScheduledTick, emit]
396
- );
397
- controlsRef.current = useMemo(
398
- () => ({ start, pause, resume, reset, restart, cancel }),
399
- [cancel, pause, reset, restart, resume, start]
400
- );
401
- useEffect(() => {
402
- mountedRef.current = true;
403
- if (optionsRef.current.autoStart && stateRef.current.status === "idle") {
404
- controlsRef.current.start();
405
- }
406
- return () => {
407
- mountedRef.current = false;
408
- clearScheduledTick();
409
- };
410
- }, [clearScheduledTick]);
411
- const snapshot = getSnapshot();
412
- const generation = stateRef.current.generation;
413
- const status = snapshot.status;
414
- useEffect(() => {
415
- if (!mountedRef.current || status !== "running") {
416
- clearScheduledTick();
417
- return;
418
- }
419
- clearScheduledTick();
420
- emit("scheduler:start", getSnapshot());
421
- timeoutRef.current = setTimeout(() => {
422
- if (!mountedRef.current) return;
423
- if (stateRef.current.generation !== generation) return;
424
- if (stateRef.current.status !== "running") return;
425
- const clock = readClock();
426
- tickTimerState(stateRef.current, clock);
427
- const tickSnapshot = toSnapshot(stateRef.current, clock);
428
- emit("timer:tick", tickSnapshot);
429
- processRunningState(clock);
430
- rerender();
431
- }, optionsRef.current.updateIntervalMs ?? 1e3);
432
- return () => {
433
- if (timeoutRef.current !== null) {
434
- emit("scheduler:stop", getSnapshot());
435
- }
436
- clearScheduledTick();
437
- };
438
- }, [clearScheduledTick, emit, generation, getSnapshot, processRunningState, snapshot.tick, status]);
439
- return {
440
- ...snapshot,
441
- ...controlsRef.current
442
- };
443
- }
444
- function validateSchedules(schedules) {
445
- schedules?.forEach((schedule) => validatePositiveFinite(schedule.everyMs, "schedule.everyMs"));
446
- }
447
-
448
- // src/useTimerGroup.ts
449
- import { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useReducer as useReducer2, useRef as useRef2 } from "react";
450
- function useTimerGroup(options = {}) {
451
- const updateIntervalMs = options.updateIntervalMs ?? 1e3;
452
- validatePositiveFinite(updateIntervalMs, "updateIntervalMs");
453
- validateItems(options.items);
454
- const optionsRef = useRef2(options);
455
- optionsRef.current = options;
456
- const itemsRef = useRef2(/* @__PURE__ */ new Map());
457
- const mountedRef = useRef2(false);
458
- const timeoutRef = useRef2(null);
459
- const [, rerender] = useReducer2((value) => value + 1, 0);
460
- const clearScheduledTick = useCallback2(() => {
461
- if (timeoutRef.current !== null) {
462
- clearTimeout(timeoutRef.current);
463
- timeoutRef.current = null;
464
- }
465
- }, []);
466
- const getItemSnapshot = useCallback2((item, clock = readClock()) => {
467
- return toSnapshot(item.state, clock);
468
- }, []);
469
- const emit = useCallback2(
470
- (type, item, snapshot, extra = {}) => {
471
- emitDebug(optionsRef.current.debug, {
472
- type,
473
- scope: "timer-group",
474
- timerId: item?.id,
475
- ...baseDebugEvent(snapshot, item?.state.generation ?? 0),
476
- ...extra
477
- });
478
- },
479
- []
480
- );
481
- const controlsFor = useCallback2((id) => {
482
- return {
483
- start: () => start(id),
484
- pause: () => pause(id),
485
- resume: () => resume(id),
486
- reset: (resetOptions) => reset(id, resetOptions),
487
- restart: () => restart(id),
488
- cancel: (reason) => cancel(id, reason)
489
- };
490
- }, []);
491
- const callOnEnd = useCallback2(
492
- (item, snapshot) => {
493
- const generation = item.state.generation;
494
- if (item.endCalledGeneration === generation) return;
495
- item.endCalledGeneration = generation;
496
- try {
497
- void item.definition.onEnd?.(snapshot, controlsFor(item.id));
498
- } catch (error) {
499
- emitDebug(optionsRef.current.debug, {
500
- type: "callback:error",
501
- scope: "timer-group",
502
- timerId: item.id,
503
- error,
504
- ...baseDebugEvent(snapshot, generation)
505
- });
506
- }
507
- },
508
- [controlsFor]
509
- );
510
- const runSchedule = useCallback2(
511
- (item, schedule, key, scheduleState, snapshot, generation) => {
512
- if (scheduleState.pending && (schedule.overlap ?? "skip") === "skip") {
513
- emitDebug(optionsRef.current.debug, {
514
- type: "schedule:skip",
515
- scope: "timer-group",
516
- timerId: item.id,
517
- scheduleId: schedule.id ?? key,
518
- reason: "overlap",
519
- ...baseDebugEvent(snapshot, generation)
520
- });
521
- return;
522
- }
523
- scheduleState.lastRunAt = snapshot.now;
524
- scheduleState.pending = true;
525
- emitDebug(optionsRef.current.debug, {
526
- type: "schedule:start",
527
- scope: "timer-group",
528
- timerId: item.id,
529
- scheduleId: schedule.id ?? key,
530
- ...baseDebugEvent(snapshot, generation)
531
- });
532
- Promise.resolve().then(() => schedule.callback(snapshot, controlsFor(item.id))).then(
533
- () => {
534
- emitDebug(optionsRef.current.debug, {
535
- type: "schedule:end",
536
- scope: "timer-group",
537
- timerId: item.id,
538
- scheduleId: schedule.id ?? key,
539
- ...baseDebugEvent(snapshot, generation)
540
- });
541
- },
542
- (error) => {
543
- emitDebug(optionsRef.current.debug, {
544
- type: "schedule:error",
545
- scope: "timer-group",
546
- timerId: item.id,
547
- scheduleId: schedule.id ?? key,
548
- error,
549
- ...baseDebugEvent(snapshot, generation)
550
- });
551
- }
552
- ).finally(() => {
553
- const liveItem = itemsRef.current.get(item.id);
554
- if (liveItem?.state.generation === generation) {
555
- scheduleState.pending = false;
556
- }
557
- });
558
- },
559
- [controlsFor]
560
- );
561
- const evaluateItemSchedules = useCallback2(
562
- (item, snapshot, activation = false) => {
563
- const schedules = item.definition.schedules ?? [];
564
- const liveKeys = /* @__PURE__ */ new Set();
565
- schedules.forEach((schedule, index) => {
566
- const key = schedule.id ?? String(index);
567
- liveKeys.add(key);
568
- let scheduleState = item.schedules.get(key);
569
- if (!scheduleState) {
570
- scheduleState = { lastRunAt: null, pending: false, leadingGeneration: null };
571
- item.schedules.set(key, scheduleState);
572
- }
573
- if (activation && schedule.leading && scheduleState.leadingGeneration !== item.state.generation) {
574
- scheduleState.leadingGeneration = item.state.generation;
575
- runSchedule(item, schedule, key, scheduleState, snapshot, item.state.generation);
576
- return;
577
- }
578
- if (scheduleState.lastRunAt === null) {
579
- scheduleState.lastRunAt = item.state.startedAt ?? snapshot.now;
580
- if (snapshot.now - scheduleState.lastRunAt >= schedule.everyMs) {
581
- runSchedule(item, schedule, key, scheduleState, snapshot, item.state.generation);
582
- }
583
- return;
584
- }
585
- if (snapshot.now - scheduleState.lastRunAt >= schedule.everyMs) {
586
- runSchedule(item, schedule, key, scheduleState, snapshot, item.state.generation);
587
- }
588
- });
589
- for (const key of item.schedules.keys()) {
590
- if (!liveKeys.has(key)) item.schedules.delete(key);
591
- }
592
- },
593
- [runSchedule]
594
- );
595
- const processItem = useCallback2(
596
- (item, clock = readClock(), activation = false) => {
597
- if (item.state.status !== "running") return;
598
- const snapshot = toSnapshot(item.state, clock);
599
- if (item.definition.endWhen?.(snapshot)) {
600
- if (endTimerState(item.state, clock)) {
601
- const endedSnapshot = toSnapshot(item.state, clock);
602
- emit("timer:end", item, endedSnapshot);
603
- callOnEnd(item, endedSnapshot);
604
- }
605
- return;
606
- }
607
- evaluateItemSchedules(item, snapshot, activation);
608
- },
609
- [callOnEnd, emit, evaluateItemSchedules]
610
- );
611
- const ensureItem = useCallback2((definition) => {
612
- const existing = itemsRef.current.get(definition.id);
613
- if (existing) {
614
- existing.definition = definition;
615
- return { item: existing, added: false };
616
- }
617
- const item = {
618
- id: definition.id,
619
- state: createTimerState(readClock()),
620
- definition,
621
- schedules: /* @__PURE__ */ new Map(),
622
- endCalledGeneration: null
623
- };
624
- itemsRef.current.set(definition.id, item);
625
- if (definition.autoStart) {
626
- startTimerState(item.state, readClock());
627
- }
628
- return { item, added: true };
629
- }, []);
630
- const syncItems = useCallback2(() => {
631
- const definitions = optionsRef.current.items ?? [];
632
- const liveIds = /* @__PURE__ */ new Set();
633
- let changed = false;
634
- definitions.forEach((definition) => {
635
- liveIds.add(definition.id);
636
- const { item, added } = ensureItem(definition);
637
- changed = changed || added;
638
- if (definition.autoStart && item.state.status === "idle") {
639
- changed = startTimerState(item.state, readClock()) || changed;
640
- }
641
- });
642
- for (const id of itemsRef.current.keys()) {
643
- if (!liveIds.has(id)) {
644
- itemsRef.current.delete(id);
645
- changed = true;
646
- }
647
- }
648
- return changed;
649
- }, [ensureItem]);
650
- useEffect2(() => {
651
- if (syncItems()) rerender();
652
- }, [syncItems, options.items]);
653
- const add = useCallback2((item) => {
654
- validateItems([item]);
655
- if (itemsRef.current.has(item.id)) throw new Error(`Timer item "${item.id}" already exists`);
656
- ensureItem(item);
657
- rerender();
658
- }, [ensureItem]);
659
- const update = useCallback2((id, item) => {
660
- const existing = itemsRef.current.get(id);
661
- if (!existing) return;
662
- const next = { ...existing.definition, ...item, id };
663
- validateItems([next]);
664
- existing.definition = next;
665
- rerender();
666
- }, []);
667
- const remove = useCallback2((id) => {
668
- itemsRef.current.delete(id);
669
- rerender();
670
- }, []);
671
- const clear = useCallback2(() => {
672
- itemsRef.current.clear();
673
- clearScheduledTick();
674
- rerender();
675
- }, [clearScheduledTick]);
676
- const start = useCallback2((id) => {
677
- const item = itemsRef.current.get(id);
678
- if (!item) return;
679
- const clock = readClock();
680
- if (!startTimerState(item.state, clock)) return;
681
- emit("timer:start", item, toSnapshot(item.state, clock));
682
- processItem(item, clock, true);
683
- rerender();
684
- }, [emit, processItem]);
685
- const pause = useCallback2((id) => {
686
- const item = itemsRef.current.get(id);
687
- if (!item) return;
688
- const clock = readClock();
689
- if (!pauseTimerState(item.state, clock)) return;
690
- emit("timer:pause", item, toSnapshot(item.state, clock));
691
- rerender();
692
- }, [emit]);
693
- const resume = useCallback2((id) => {
694
- const item = itemsRef.current.get(id);
695
- if (!item) return;
696
- const clock = readClock();
697
- if (!resumeTimerState(item.state, clock)) return;
698
- emit("timer:resume", item, toSnapshot(item.state, clock));
699
- processItem(item, clock, true);
700
- rerender();
701
- }, [emit, processItem]);
702
- const reset = useCallback2((id, resetOptions = {}) => {
703
- const item = itemsRef.current.get(id);
704
- if (!item) return;
705
- const clock = readClock();
706
- resetTimerState(item.state, clock, resetOptions);
707
- item.schedules.clear();
708
- item.endCalledGeneration = null;
709
- emit("timer:reset", item, toSnapshot(item.state, clock));
710
- if (resetOptions.autoStart) processItem(item, clock, true);
711
- rerender();
712
- }, [emit, processItem]);
713
- const restart = useCallback2((id) => {
714
- const item = itemsRef.current.get(id);
715
- if (!item) return;
716
- const clock = readClock();
717
- restartTimerState(item.state, clock);
718
- item.schedules.clear();
719
- item.endCalledGeneration = null;
720
- emit("timer:restart", item, toSnapshot(item.state, clock));
721
- processItem(item, clock, true);
722
- rerender();
723
- }, [emit, processItem]);
724
- const cancel = useCallback2((id, reason) => {
725
- const item = itemsRef.current.get(id);
726
- if (!item) return;
727
- const clock = readClock();
728
- if (!cancelTimerState(item.state, clock, reason)) return;
729
- emit("timer:cancel", item, toSnapshot(item.state, clock), { reason });
730
- rerender();
731
- }, [emit]);
732
- const ids = Array.from(itemsRef.current.keys());
733
- const activeSignature = ids.map((id) => `${id}:${itemsRef.current.get(id).state.status}:${itemsRef.current.get(id).state.generation}:${itemsRef.current.get(id).state.tick}`).join("|");
734
- useEffect2(() => {
735
- mountedRef.current = true;
736
- const runningItems = Array.from(itemsRef.current.values()).filter((item) => item.state.status === "running");
737
- if (runningItems.length === 0) {
738
- clearScheduledTick();
739
- return;
740
- }
741
- clearScheduledTick();
742
- const first = runningItems[0];
743
- emit("scheduler:start", first, toSnapshot(first.state, readClock()));
744
- timeoutRef.current = setTimeout(() => {
745
- if (!mountedRef.current) return;
746
- const clock = readClock();
747
- for (const item of itemsRef.current.values()) {
748
- if (item.state.status !== "running") continue;
749
- tickTimerState(item.state, clock);
750
- const snapshot = toSnapshot(item.state, clock);
751
- emit("timer:tick", item, snapshot);
752
- processItem(item, clock);
753
- }
754
- rerender();
755
- }, optionsRef.current.updateIntervalMs ?? 1e3);
756
- return () => {
757
- if (timeoutRef.current !== null) {
758
- emit("scheduler:stop", first, toSnapshot(first.state, readClock()));
759
- }
760
- clearScheduledTick();
761
- mountedRef.current = false;
762
- };
763
- }, [activeSignature, clearScheduledTick, emit, processItem]);
764
- const get = useCallback2(
765
- (id) => {
766
- const item = itemsRef.current.get(id);
767
- if (!item) return void 0;
768
- return getItemSnapshot(item);
769
- },
770
- [getItemSnapshot]
771
- );
772
- const now = readClock().wallNow;
773
- return useMemo2(
774
- () => ({
775
- now,
776
- size: itemsRef.current.size,
777
- ids: Array.from(itemsRef.current.keys()),
778
- get,
779
- add,
780
- update,
781
- remove,
782
- clear,
783
- start,
784
- pause,
785
- resume,
786
- reset,
787
- restart,
788
- cancel,
789
- startAll: () => Array.from(itemsRef.current.keys()).forEach(start),
790
- pauseAll: () => Array.from(itemsRef.current.keys()).forEach(pause),
791
- resumeAll: () => Array.from(itemsRef.current.keys()).forEach(resume),
792
- resetAll: (resetOptions) => Array.from(itemsRef.current.keys()).forEach((id) => reset(id, resetOptions)),
793
- restartAll: () => Array.from(itemsRef.current.keys()).forEach(restart),
794
- cancelAll: (reason) => Array.from(itemsRef.current.keys()).forEach((id) => cancel(id, reason))
795
- }),
796
- [add, cancel, clear, get, now, pause, remove, reset, restart, resume, start, update]
797
- );
798
- }
799
- function validateItems(items) {
800
- const ids = /* @__PURE__ */ new Set();
801
- items?.forEach((item) => {
802
- if (ids.has(item.id)) throw new Error(`Duplicate timer item id "${item.id}"`);
803
- ids.add(item.id);
804
- item.schedules?.forEach((schedule) => validatePositiveFinite(schedule.everyMs, "schedule.everyMs"));
805
- });
806
- }
807
- export {
808
- durationParts,
809
- useTimer,
810
- useTimerGroup
811
- };
812
- //# sourceMappingURL=index.js.map
1
+ function ie(e){let u=Math.max(0,Math.trunc(Number.isFinite(e)?e:0)),i=Math.floor(u/864e5),o=u%864e5,C=Math.floor(o/36e5),h=o%36e5,p=Math.floor(h/6e4),I=h%6e4,v=Math.floor(I/1e3);return{totalMilliseconds:u,totalSeconds:Math.floor(u/1e3),milliseconds:I%1e3,seconds:v,minutes:p,hours:C,days:i}}import{useCallback as E,useEffect as se,useMemo as ce,useReducer as de,useRef as H}from"react";function le(e){return e?e===!0?{enabled:!0,includeTicks:!1,logger:console.debug}:typeof e=="function"?{enabled:!0,includeTicks:!1,logger:e}:{enabled:e.enabled!==!1,includeTicks:e.includeTicks??!1,label:e.label,logger:e.logger??console.debug}:{enabled:!1,includeTicks:!1}}function A(e,u){let i=le(e);!i.enabled||!i.logger||u.type==="timer:tick"&&!i.includeTicks||i.logger({...u,label:u.label??i.label})}function k(e,u){return{generation:u,tick:e.tick,now:e.now,elapsedMilliseconds:e.elapsedMilliseconds,status:e.status}}function d(){let e=Date.now(),u=typeof performance<"u"&&typeof performance.now=="function"?performance.now():e;return{wallNow:e,monotonicNow:u}}function Y(e,u){if(!Number.isFinite(e)||e<=0)throw new RangeError(`${u} must be a finite number greater than 0`)}function V(e){return{status:"idle",generation:0,tick:0,startedAt:null,pausedAt:null,endedAt:null,cancelledAt:null,cancelReason:null,baseElapsedMilliseconds:0,activeStartedAtMonotonic:null,now:e.wallNow}}function X(e,u){return e.status!=="running"||e.activeStartedAtMonotonic===null?e.baseElapsedMilliseconds:Math.max(0,e.baseElapsedMilliseconds+u.monotonicNow-e.activeStartedAtMonotonic)}function m(e,u){let i=X(e,u);return{status:e.status,now:u.wallNow,tick:e.tick,startedAt:e.startedAt,pausedAt:e.pausedAt,endedAt:e.endedAt,cancelledAt:e.cancelledAt,cancelReason:e.cancelReason,elapsedMilliseconds:i,isIdle:e.status==="idle",isRunning:e.status==="running",isPaused:e.status==="paused",isEnded:e.status==="ended",isCancelled:e.status==="cancelled"}}function z(e,u){return e.status!=="idle"?!1:(e.status="running",e.startedAt=u.wallNow,e.pausedAt=null,e.endedAt=null,e.cancelledAt=null,e.cancelReason=null,e.activeStartedAtMonotonic=u.monotonicNow,e.now=u.wallNow,!0)}function Z(e,u){return e.status!=="running"?!1:(e.baseElapsedMilliseconds=X(e,u),e.activeStartedAtMonotonic=null,e.status="paused",e.pausedAt=u.wallNow,e.now=u.wallNow,!0)}function _(e,u){return e.status!=="paused"?!1:(e.status="running",e.pausedAt=null,e.activeStartedAtMonotonic=u.monotonicNow,e.now=u.wallNow,!0)}function J(e,u,i={}){return e.generation+=1,e.tick=0,e.status=i.autoStart?"running":"idle",e.startedAt=i.autoStart?u.wallNow:null,e.pausedAt=null,e.endedAt=null,e.cancelledAt=null,e.cancelReason=null,e.baseElapsedMilliseconds=0,e.activeStartedAtMonotonic=i.autoStart?u.monotonicNow:null,e.now=u.wallNow,!0}function ee(e,u){return J(e,u,{autoStart:!0})}function te(e,u,i){return e.status==="ended"||e.status==="cancelled"?!1:(e.baseElapsedMilliseconds=X(e,u),e.activeStartedAtMonotonic=null,e.status="cancelled",e.cancelledAt=u.wallNow,e.cancelReason=i??null,e.now=u.wallNow,!0)}function re(e,u){return e.status!=="running"?!1:(e.baseElapsedMilliseconds=X(e,u),e.activeStartedAtMonotonic=null,e.status="ended",e.endedAt=u.wallNow,e.now=u.wallNow,!0)}function ne(e,u){return e.status!=="running"?!1:(e.tick+=1,e.now=u.wallNow,!0)}function me(e={}){let u=e.updateIntervalMs??1e3;Y(u,"updateIntervalMs"),fe(e.schedules);let i=H(e);i.current=e;let o=H(null);o.current===null&&(o.current=V(d()));let C=H(!1),h=H(null),p=H(new Map),I=H(null),[,v]=de(s=>s+1,0),l=E(()=>{h.current!==null&&(clearTimeout(h.current),h.current=null)},[]),N=E((s=d())=>m(o.current,s),[]),g=E((s,c,f={})=>{A(i.current.debug,{type:s,scope:"timer",...k(c,o.current.generation),...f})},[]),D=H(null),K=E(s=>{let c=o.current.generation;if(I.current!==c){I.current=c;try{i.current.onEnd?.(s,D.current)}catch(f){A(i.current.debug,{type:"callback:error",scope:"timer",...k(s,c),error:f})}}},[]),y=E((s,c,f,w,M)=>{if(f.pending&&(s.overlap??"skip")==="skip"){A(i.current.debug,{type:"schedule:skip",scope:"timer",scheduleId:s.id??c,reason:"overlap",...k(w,M)});return}f.lastRunAt=w.now,f.pending=!0,A(i.current.debug,{type:"schedule:start",scope:"timer",scheduleId:s.id??c,...k(w,M)}),Promise.resolve().then(()=>s.callback(w,D.current)).then(()=>{A(i.current.debug,{type:"schedule:end",scope:"timer",scheduleId:s.id??c,...k(w,M)})},t=>{A(i.current.debug,{type:"schedule:error",scope:"timer",scheduleId:s.id??c,error:t,...k(w,M)})}).finally(()=>{o.current?.generation===M&&(f.pending=!1)})},[]),P=E((s,c,f=!1)=>{let w=i.current.schedules??[],M=new Set;w.forEach((t,r)=>{let n=t.id??String(r);M.add(n);let a=p.current.get(n);if(a||(a={lastRunAt:null,pending:!1,leadingGeneration:null},p.current.set(n,a)),f&&t.leading&&a.leadingGeneration!==c){a.leadingGeneration=c,y(t,n,a,s,c);return}if(a.lastRunAt===null){a.lastRunAt=s.now;return}s.now-a.lastRunAt>=t.everyMs&&y(t,n,a,s,c)});for(let t of p.current.keys())M.has(t)||p.current.delete(t)},[y]),R=E((s=d(),c=!1)=>{let f=o.current;if(f.status!=="running")return;let w=m(f,s),M=f.generation;if(i.current.endWhen?.(w)){if(re(f,s)){let t=m(f,s);g("timer:end",t),l(),K(t),v()}return}P(w,M,c)},[K,l,g,P]),W=E(()=>{let s=d();if(!z(o.current,s))return;let c=m(o.current,s);g("timer:start",c),R(s,!0),v()},[g,R]),j=E(()=>{let s=d();if(!Z(o.current,s))return;l();let c=m(o.current,s);g("timer:pause",c),v()},[l,g]),q=E(()=>{let s=d();if(!_(o.current,s))return;let c=m(o.current,s);g("timer:resume",c),R(s,!0),v()},[g,R]),B=E((s={})=>{let c=d();l(),J(o.current,c,s),p.current.clear(),I.current=null;let f=m(o.current,c);g("timer:reset",f),s.autoStart&&R(c,!0),v()},[l,g,R]),O=E(()=>{let s=d();l(),ee(o.current,s),p.current.clear(),I.current=null;let c=m(o.current,s);g("timer:restart",c),R(s,!0),v()},[l,g,R]),U=E(s=>{let c=d();if(!te(o.current,c,s))return;l();let f=m(o.current,c);g("timer:cancel",f,{reason:s}),v()},[l,g]);D.current=ce(()=>({start:W,pause:j,resume:q,reset:B,restart:O,cancel:U}),[U,j,B,O,q,W]),se(()=>(C.current=!0,i.current.autoStart&&o.current.status==="idle"&&D.current.start(),()=>{C.current=!1,l()}),[l]);let x=N(),$=o.current.generation,F=x.status;return se(()=>{if(!C.current||F!=="running"){l();return}return l(),g("scheduler:start",N()),h.current=setTimeout(()=>{if(!C.current||o.current.generation!==$||o.current.status!=="running")return;let s=d();ne(o.current,s);let c=m(o.current,s);g("timer:tick",c),R(s),v()},i.current.updateIntervalMs??1e3),()=>{h.current!==null&&g("scheduler:stop",N()),l()}},[l,g,$,N,R,x.tick,F]),{...x,...D.current}}function fe(e){e?.forEach(u=>Y(u.everyMs,"schedule.everyMs"))}import{useCallback as T,useEffect as ae,useMemo as pe,useReducer as ge,useRef as oe}from"react";function Te(e={}){let u=e.updateIntervalMs??1e3;Y(u,"updateIntervalMs"),ue(e.items);let i=oe(e);i.current=e;let o=oe(new Map),C=oe(!1),h=oe(null),[,p]=ge(t=>t+1,0),I=T(()=>{h.current!==null&&(clearTimeout(h.current),h.current=null)},[]),v=T((t,r=d())=>m(t.state,r),[]),l=T((t,r,n,a={})=>{A(i.current.debug,{type:t,scope:"timer-group",timerId:r?.id,...k(n,r?.state.generation??0),...a})},[]),N=T(t=>({start:()=>O(t),pause:()=>U(t),resume:()=>x(t),reset:r=>$(t,r),restart:()=>F(t),cancel:r=>s(t,r)}),[]),g=T((t,r)=>{let n=t.state.generation;if(t.endCalledGeneration!==n){t.endCalledGeneration=n;try{t.definition.onEnd?.(r,N(t.id))}catch(a){A(i.current.debug,{type:"callback:error",scope:"timer-group",timerId:t.id,error:a,...k(r,n)})}}},[N]),D=T((t,r,n,a,b,S)=>{if(a.pending&&(r.overlap??"skip")==="skip"){A(i.current.debug,{type:"schedule:skip",scope:"timer-group",timerId:t.id,scheduleId:r.id??n,reason:"overlap",...k(b,S)});return}a.lastRunAt=b.now,a.pending=!0,A(i.current.debug,{type:"schedule:start",scope:"timer-group",timerId:t.id,scheduleId:r.id??n,...k(b,S)}),Promise.resolve().then(()=>r.callback(b,N(t.id))).then(()=>{A(i.current.debug,{type:"schedule:end",scope:"timer-group",timerId:t.id,scheduleId:r.id??n,...k(b,S)})},Q=>{A(i.current.debug,{type:"schedule:error",scope:"timer-group",timerId:t.id,scheduleId:r.id??n,error:Q,...k(b,S)})}).finally(()=>{o.current.get(t.id)?.state.generation===S&&(a.pending=!1)})},[N]),K=T((t,r,n=!1)=>{let a=t.definition.schedules??[],b=new Set;a.forEach((S,Q)=>{let L=S.id??String(Q);b.add(L);let G=t.schedules.get(L);if(G||(G={lastRunAt:null,pending:!1,leadingGeneration:null},t.schedules.set(L,G)),n&&S.leading&&G.leadingGeneration!==t.state.generation){G.leadingGeneration=t.state.generation,D(t,S,L,G,r,t.state.generation);return}if(G.lastRunAt===null){G.lastRunAt=t.state.startedAt??r.now,r.now-G.lastRunAt>=S.everyMs&&D(t,S,L,G,r,t.state.generation);return}r.now-G.lastRunAt>=S.everyMs&&D(t,S,L,G,r,t.state.generation)});for(let S of t.schedules.keys())b.has(S)||t.schedules.delete(S)},[D]),y=T((t,r=d(),n=!1)=>{if(t.state.status!=="running")return;let a=m(t.state,r);if(t.definition.endWhen?.(a)){if(re(t.state,r)){let b=m(t.state,r);l("timer:end",t,b),g(t,b)}return}K(t,a,n)},[g,l,K]),P=T(t=>{let r=o.current.get(t.id);if(r)return r.definition=t,{item:r,added:!1};let n={id:t.id,state:V(d()),definition:t,schedules:new Map,endCalledGeneration:null};return o.current.set(t.id,n),t.autoStart&&z(n.state,d()),{item:n,added:!0}},[]),R=T(()=>{let t=i.current.items??[],r=new Set,n=!1;t.forEach(a=>{r.add(a.id);let{item:b,added:S}=P(a);n=n||S,a.autoStart&&b.state.status==="idle"&&(n=z(b.state,d())||n)});for(let a of o.current.keys())r.has(a)||(o.current.delete(a),n=!0);return n},[P]);ae(()=>{R()&&p()},[R,e.items]);let W=T(t=>{if(ue([t]),o.current.has(t.id))throw new Error(`Timer item "${t.id}" already exists`);P(t),p()},[P]),j=T((t,r)=>{let n=o.current.get(t);if(!n)return;let a={...n.definition,...r,id:t};ue([a]),n.definition=a,p()},[]),q=T(t=>{o.current.delete(t),p()},[]),B=T(()=>{o.current.clear(),I(),p()},[I]),O=T(t=>{let r=o.current.get(t);if(!r)return;let n=d();z(r.state,n)&&(l("timer:start",r,m(r.state,n)),y(r,n,!0),p())},[l,y]),U=T(t=>{let r=o.current.get(t);if(!r)return;let n=d();Z(r.state,n)&&(l("timer:pause",r,m(r.state,n)),p())},[l]),x=T(t=>{let r=o.current.get(t);if(!r)return;let n=d();_(r.state,n)&&(l("timer:resume",r,m(r.state,n)),y(r,n,!0),p())},[l,y]),$=T((t,r={})=>{let n=o.current.get(t);if(!n)return;let a=d();J(n.state,a,r),n.schedules.clear(),n.endCalledGeneration=null,l("timer:reset",n,m(n.state,a)),r.autoStart&&y(n,a,!0),p()},[l,y]),F=T(t=>{let r=o.current.get(t);if(!r)return;let n=d();ee(r.state,n),r.schedules.clear(),r.endCalledGeneration=null,l("timer:restart",r,m(r.state,n)),y(r,n,!0),p()},[l,y]),s=T((t,r)=>{let n=o.current.get(t);if(!n)return;let a=d();te(n.state,a,r)&&(l("timer:cancel",n,m(n.state,a),{reason:r}),p())},[l]),f=Array.from(o.current.keys()).map(t=>`${t}:${o.current.get(t).state.status}:${o.current.get(t).state.generation}:${o.current.get(t).state.tick}`).join("|");ae(()=>{C.current=!0;let t=Array.from(o.current.values()).filter(n=>n.state.status==="running");if(t.length===0){I();return}I();let r=t[0];return l("scheduler:start",r,m(r.state,d())),h.current=setTimeout(()=>{if(!C.current)return;let n=d();for(let a of o.current.values()){if(a.state.status!=="running")continue;ne(a.state,n);let b=m(a.state,n);l("timer:tick",a,b),y(a,n)}p()},i.current.updateIntervalMs??1e3),()=>{h.current!==null&&l("scheduler:stop",r,m(r.state,d())),I(),C.current=!1}},[f,I,l,y]);let w=T(t=>{let r=o.current.get(t);if(r)return v(r)},[v]),M=d().wallNow;return pe(()=>({now:M,size:o.current.size,ids:Array.from(o.current.keys()),get:w,add:W,update:j,remove:q,clear:B,start:O,pause:U,resume:x,reset:$,restart:F,cancel:s,startAll:()=>Array.from(o.current.keys()).forEach(O),pauseAll:()=>Array.from(o.current.keys()).forEach(U),resumeAll:()=>Array.from(o.current.keys()).forEach(x),resetAll:t=>Array.from(o.current.keys()).forEach(r=>$(r,t)),restartAll:()=>Array.from(o.current.keys()).forEach(F),cancelAll:t=>Array.from(o.current.keys()).forEach(r=>s(r,t))}),[W,s,B,w,M,U,q,$,F,x,O,j])}function ue(e){let u=new Set;e?.forEach(i=>{if(u.has(i.id))throw new Error(`Duplicate timer item id "${i.id}"`);u.add(i.id),i.schedules?.forEach(o=>Y(o.everyMs,"schedule.everyMs"))})}export{ie as durationParts,me as useTimer,Te as useTimerGroup};