@crup/react-timer-hook 0.0.1-alpha.2

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