@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/LICENSE +21 -0
- package/README.md +273 -0
- package/dist/index.cjs +841 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +129 -0
- package/dist/index.d.ts +129 -0
- package/dist/index.js +812 -0
- package/dist/index.js.map +1 -0
- package/package.json +71 -0
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
|