Skip to content

Commit c84549c

Browse files
committed
refactor(timer): share schedule runtime
1 parent 406c510 commit c84549c

24 files changed

Lines changed: 943 additions & 830 deletions

README.md

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,17 @@
1515

1616
## Why this exists
1717

18-
Timer hooks look simple until real apps need pause/resume semantics, Strict Mode cleanup, async callbacks, polling that does not overlap, and lists with dozens of independent timers.
18+
Timers get messy when a product needs pause and resume, countdowns tied to server time, async work, or a screen full of independent rows.
1919

20-
`@crup/react-timer-hook` starts with a ~1.2 kB timer core and lets your app compose the heavier pieces only when it needs them:
20+
`@crup/react-timer-hook` keeps the default import small and lets you add only the pieces your screen needs:
2121

2222
- ⏱️ `useTimer()` from the root package for one lifecycle: stopwatch, countdown, clock, or custom flow.
23-
- 🔋 Batteries are optional: schedules, timer groups, duration helpers, and diagnostics live in subpath imports.
23+
- 🔋 Add-ons are opt-in: schedules, timer groups, duration helpers, and diagnostics live in subpath imports.
2424
- 🧭 `useTimerGroup()` from `/group` for many keyed lifecycles with one shared scheduler.
25-
- 📡 `useScheduledTimer()` from `/schedules` for polling, overdue timing context, and opt-in diagnostics.
26-
- 🧩 `durationParts()` from `/duration` for display math without locale or timezone opinions.
27-
- 🧼 No formatting, timezone, audio, retry, cache, or data-fetching policy baked in.
28-
- 🧪 Built for rerenders, Strict Mode, async callbacks, cleanup, and many timers.
29-
- 🤖 Agent-friendly docs through hosted `llms.txt`, `llms-full.txt`, and an optional MCP docs helper.
25+
- 📡 `useScheduledTimer()` from `/schedules` for polling and timing context.
26+
- 🧩 `durationParts()` from `/duration` for common display math.
27+
- 🧪 Tested for React Strict Mode, rerenders, async callbacks, cleanup, and multi-timer screens.
28+
- 🤖 AI-ready docs are available through hosted `llms.txt`, `llms-full.txt`, and an optional MCP docs helper.
3029

3130
## Install
3231

@@ -49,7 +48,7 @@ import { useScheduledTimer } from '@crup/react-timer-hook/schedules';
4948
Each recipe has a live playground and a focused code sample:
5049

5150
- Basic: [wall clock](https://crup.github.io/react-timer-hook/recipes/basic/wall-clock/), [stopwatch](https://crup.github.io/react-timer-hook/recipes/basic/stopwatch/), [absolute countdown](https://crup.github.io/react-timer-hook/recipes/basic/absolute-countdown/), [pausable countdown](https://crup.github.io/react-timer-hook/recipes/basic/pausable-countdown/), [manual controls](https://crup.github.io/react-timer-hook/recipes/basic/manual-controls/)
52-
- Intermediate: [once-only onEnd](https://crup.github.io/react-timer-hook/recipes/intermediate/once-only-on-end/), [polling schedule](https://crup.github.io/react-timer-hook/recipes/intermediate/polling-schedule/), [poll and cancel](https://crup.github.io/react-timer-hook/recipes/intermediate/poll-and-cancel/), [backend event stop](https://crup.github.io/react-timer-hook/recipes/intermediate/backend-event-stop/), [debug logs](https://crup.github.io/react-timer-hook/recipes/intermediate/debug-logs/)
51+
- Intermediate: [once-only onEnd](https://crup.github.io/react-timer-hook/recipes/intermediate/once-only-on-end/), [polling schedule](https://crup.github.io/react-timer-hook/recipes/intermediate/polling-schedule/), [poll and cancel](https://crup.github.io/react-timer-hook/recipes/intermediate/poll-and-cancel/), [backend event stop](https://crup.github.io/react-timer-hook/recipes/intermediate/backend-event-stop/), [diagnostics](https://crup.github.io/react-timer-hook/recipes/intermediate/debug-logs/)
5352
- Advanced: [many display countdowns](https://crup.github.io/react-timer-hook/recipes/advanced/many-display-countdowns/), [timer group](https://crup.github.io/react-timer-hook/recipes/advanced/timer-group/), [group controls](https://crup.github.io/react-timer-hook/recipes/advanced/group-controls/), [per-item polling](https://crup.github.io/react-timer-hook/recipes/advanced/per-item-polling/), [dynamic items](https://crup.github.io/react-timer-hook/recipes/advanced/dynamic-items/)
5453

5554
## Quick examples
@@ -143,7 +142,7 @@ const timers = useTimerGroup({
143142
});
144143
```
145144

146-
## API tables
145+
## API reference
147146

148147
### `useTimer()` settings
149148

@@ -165,13 +164,13 @@ Import from `@crup/react-timer-hook/schedules` when you need polling or schedule
165164
| `endWhen` | `(snapshot) => boolean` | No | Ends the lifecycle when it returns `true`. |
166165
| `onEnd` | `(snapshot, controls) => void \| Promise<void>` | No | Called once per generation when `endWhen` ends the lifecycle. |
167166
| `schedules` | `TimerSchedule[]` | No | Scheduled side effects that run while the timer is active. Async overlap defaults to `skip`. |
168-
| `debug` | `TimerDebug` | No | Opt-in semantic diagnostics. No logs are emitted by default. |
167+
| `diagnostics` | `TimerDiagnostics` | No | Optional lifecycle and schedule events. No logs are emitted unless you pass a logger. |
169168

170169
### `TimerSchedule`
171170

172171
| Key | Type | Required | Description |
173172
| --- | --- | --- | --- |
174-
| `id` | `string` | No | Stable identifier used in debug events and schedule context. Falls back to the array index. |
173+
| `id` | `string` | No | Stable identifier used in diagnostics events and schedule context. Falls back to the array index. |
175174
| `everyMs` | `number` | Yes | Schedule cadence in milliseconds. Must be positive and finite. |
176175
| `leading` | `boolean` | No | Runs the schedule immediately when the timer starts or resumes into a new generation. Defaults to `false`. |
177176
| `overlap` | `'skip' \| 'allow'` | No | Controls async overlap. Defaults to `skip`, so a pending callback prevents another run. |
@@ -185,7 +184,7 @@ Import from `@crup/react-timer-hook/group` when many keyed items need independen
185184
| --- | --- | --- | --- |
186185
| `updateIntervalMs` | `number` | No | Shared scheduler cadence for the group. Defaults to `1000`. |
187186
| `items` | `TimerGroupItem[]` | No | Initial/synced timer item definitions. Each item has its own lifecycle state. |
188-
| `debug` | `TimerDebug` | No | Opt-in semantic diagnostics for group lifecycle and schedule events. |
187+
| `diagnostics` | `TimerDiagnostics` | No | Optional lifecycle and schedule events for group timers. |
189188

190189
### `TimerGroupItem`
191190

@@ -224,15 +223,15 @@ Import from `@crup/react-timer-hook/group` when many keyed items need independen
224223

225224
## Bundle size
226225

227-
The core import stays small. Extra capabilities are opt-in batteries.
226+
The default import stays small. Add the other pieces only when that screen needs them.
228227

229-
| Entry | Raw | Gzip | Brotli |
230-
| --- | ---: | ---: | ---: |
231-
| core | 3.82 kB | 1.31 kB | 1.21 kB |
232-
| timer group add-on | 8.94 kB | 2.97 kB | 2.70 kB |
233-
| schedules add-on | 6.88 kB | 2.32 kB | 2.13 kB |
234-
| duration helper | 318 B | 224 B | 192 B |
235-
| diagnostics helper | 105 B | 115 B | 99 B |
228+
| Piece | Import | Best for | Raw | Gzip | Brotli |
229+
| --- | --- | --- | ---: | ---: | ---: |
230+
| ⏱️ Core | `@crup/react-timer-hook` | Stopwatch, countdown, clock, custom lifecycle | 3.82 kB | 1.31 kB | 1.21 kB |
231+
| 🧭 Timer group | `@crup/react-timer-hook/group` | Many independent row/item timers | 9.75 kB | 3.42 kB | 3.13 kB |
232+
| 📡 Schedules | `@crup/react-timer-hook/schedules` | Polling, cadence callbacks, overdue timing context | 7.41 kB | 2.57 kB | 2.36 kB |
233+
| 🧩 Duration | `@crup/react-timer-hook/duration` | `days`, `hours`, `minutes`, `seconds`, `milliseconds` | 318 B | 224 B | 192 B |
234+
| 🔎 Diagnostics | `@crup/react-timer-hook/diagnostics` | Optional lifecycle and schedule event logging | 105 B | 115 B | 90 B |
236235

237236
CI writes a size summary to the GitHub Actions UI and posts bundle-size reports on pull requests.
238237

docs-site/docs/api/types.mdx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,16 @@ type TimerSchedule = {
5252

5353
`scheduledAt` is the intended fire time. `firedAt` is when the browser actually ran the callback. `overdueCount` reports how many schedule windows were missed before this callback because the browser, tab, or previous work delayed execution.
5454

55-
## Debug
55+
## Diagnostics
5656

57-
Debug is opt-in. The package does not emit logs by default.
57+
Diagnostics are opt-in. The package does not emit logs by default.
5858

5959
```ts
60-
type TimerDebug =
61-
| boolean
62-
| TimerDebugLogger
60+
type TimerDiagnostics =
61+
| TimerDiagnosticsLogger
6362
| {
6463
enabled?: boolean;
65-
logger?: TimerDebugLogger;
64+
logger: TimerDiagnosticsLogger;
6665
includeTicks?: boolean;
6766
label?: string;
6867
};

docs-site/docs/api/use-scheduled-timer.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ description: Optional schedule-enabled timer API for polling and timing context.
99
import { useScheduledTimer } from '@crup/react-timer-hook/schedules';
1010
```
1111

12-
`useScheduledTimer()` includes the lifecycle API from `useTimer()` plus schedule callbacks and opt-in debug events. It lives in a subpath so the root import stays small for clocks, stopwatches, and countdowns that do not need polling.
12+
`useScheduledTimer()` includes the lifecycle API from `useTimer()` plus schedule callbacks and optional diagnostics. It lives in a subpath so the root import stays small for clocks, stopwatches, and countdowns that do not need polling.
1313

1414
```ts
1515
type UseScheduledTimerOptions = UseTimerOptions & {
1616
schedules?: TimerSchedule[];
17-
debug?: TimerDebug;
17+
diagnostics?: TimerDiagnostics;
1818
};
1919
```
2020

docs-site/docs/api/use-timer-group.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Item schedules use the same `TimerSchedule` contract as `useScheduledTimer()`, i
1919
type UseTimerGroupOptions = {
2020
updateIntervalMs?: number;
2121
items?: TimerGroupItem[];
22-
debug?: TimerDebug;
22+
diagnostics?: TimerDiagnostics;
2323
};
2424

2525
type TimerGroupItem = {

docs-site/docs/project/contributing.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,5 @@ Timer rules:
3030
- Do not schedule work during render.
3131
- Use recursive `setTimeout`, not `setInterval`.
3232
- Keep timezone and formatting outside the library.
33-
- Keep debug logs opt-in.
33+
- Keep diagnostics opt-in.
3434
- Add tests for Strict Mode and async callbacks when lifecycle behavior changes.
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
---
2-
title: Debug logs
2+
title: Diagnostics
33
description: Opt-in semantic diagnostics for timer lifecycles and schedules.
44
---
55

6-
# Debug logs
6+
# Diagnostics
77

8-
Debug logs are off by default.
8+
Diagnostics are off by default.
99

1010
```tsx
1111
import { useScheduledTimer } from '@crup/react-timer-hook/schedules';
1212
import { consoleTimerDiagnostics } from '@crup/react-timer-hook/diagnostics';
1313

1414
useScheduledTimer({
1515
autoStart: true,
16-
debug: consoleTimerDiagnostics({ label: 'auction-card' }),
16+
diagnostics: consoleTimerDiagnostics({ label: 'auction-card' }),
1717
});
1818
```
1919

20-
Debug events are semantic: `timer:start`, `timer:pause`, `timer:resume`, `timer:reset`, `timer:restart`, `timer:cancel`, `timer:end`, `scheduler:start`, `scheduler:stop`, `schedule:start`, `schedule:skip`, `schedule:end`, `schedule:error`, and `callback:error`.
20+
Events are semantic: `timer:start`, `timer:pause`, `timer:resume`, `timer:reset`, `timer:restart`, `timer:cancel`, `timer:end`, `scheduler:start`, `scheduler:stop`, `schedule:start`, `schedule:skip`, `schedule:end`, `schedule:error`, and `callback:error`.
2121

2222
Raw timeout handles are not exposed.
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
---
2-
title: Debug logs
2+
title: Diagnostics
33
description: Opt into semantic lifecycle and schedule events.
44
---
55

66
import RecipePlayground from '@site/src/components/RecipePlayground';
77
import { DebugLogsSample } from '../../../samples/recipes';
88

9-
# Debug logs
9+
# Diagnostics
1010

11-
Debug logs are opt-in and semantic. They do not expose timeout handles.
11+
Diagnostics are opt-in and semantic. They do not expose timeout handles.
1212

13-
<RecipePlayground title="Debug logs">
13+
<RecipePlayground title="Diagnostics">
1414
<DebugLogsSample />
1515
</RecipePlayground>
1616

@@ -19,6 +19,6 @@ import { consoleTimerDiagnostics } from '@crup/react-timer-hook/diagnostics';
1919
import { useScheduledTimer } from '@crup/react-timer-hook/schedules';
2020

2121
useScheduledTimer({
22-
debug: consoleTimerDiagnostics({ label: 'auction-card' }),
22+
diagnostics: consoleTimerDiagnostics({ label: 'auction-card' }),
2323
});
2424
```
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: Intermediate recipes
3-
description: End callbacks, schedules, early cancellation, backend events, and debug logs.
3+
description: End callbacks, schedules, early cancellation, backend events, and diagnostics.
44
---
55

66
# Intermediate recipes
@@ -11,5 +11,4 @@ Use these patterns when the timer coordinates with side effects or external app
1111
- [Polling schedule](./polling-schedule)
1212
- [Poll and cancel](./poll-and-cancel)
1313
- [Backend event stop](./backend-event-stop)
14-
- [Debug logs](./debug-logs)
15-
14+
- [Diagnostics](./debug-logs)

docs-site/samples/recipes.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -254,15 +254,15 @@ export function DebugLogsSample() {
254254
const [logs, setLogs] = useState<string[]>([]);
255255
const timer = useScheduledTimer({
256256
updateIntervalMs: 1000,
257-
debug: {
257+
diagnostics: {
258258
label: 'docs-demo',
259259
includeTicks: false,
260260
logger: event => setLogs(previous => [`${event.type} (${event.status})`, ...previous].slice(0, 5)),
261261
},
262262
});
263263

264264
return (
265-
<DemoShell eyebrow="Debug events" title={logs[0] ?? 'No events yet'} status={timer.status}>
265+
<DemoShell eyebrow="Diagnostics" title={logs[0] ?? 'No events yet'} status={timer.status}>
266266
<EventStream events={logs.length ? logs : ['start, pause, resume, cancel, or restart to emit events']} />
267267
<TimerControlsPanel timer={timer} allowCancel />
268268
</DemoShell>

docs-site/static/llms-full.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,6 @@ Local docs MCP server:
7878
}
7979
```
8080

81-
## Non-goals
81+
## Boundaries
8282

83-
No formatting, no timezone handling, no mode enums, no data-fetching cache, and no raw timeout handle exposure.
83+
Use the hook for timer lifecycle, elapsed time, schedules, and controls. Keep UI display and data fetching in your app.

0 commit comments

Comments
 (0)