@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.js ADDED
@@ -0,0 +1,812 @@
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