@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rajender Joshi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,273 @@
1
+ # @crup/react-timer-hook
2
+
3
+ A small React hook library for deterministic timer lifecycles.
4
+
5
+ This package is planned as a replacement for the previous `react-timer-hook` API. It intentionally does not ship formatting, timezone conversion, `ampm` fields, or countdown/stopwatch/clock mode enums. It gives you raw time data and lifecycle controls so your app can decide what the timer means.
6
+
7
+ ## Status
8
+
9
+ Alpha-ready v1 implementation. The package is intended to be published under the `@crup` npm scope.
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ pnpm add @crup/react-timer-hook
15
+ npm install @crup/react-timer-hook
16
+ ```
17
+
18
+ ## Planned Public API
19
+
20
+ ```ts
21
+ import { useTimer, useTimerGroup, durationParts } from '@crup/react-timer-hook';
22
+ ```
23
+
24
+ V1 should expose only:
25
+
26
+ - `useTimer()` for one timer lifecycle.
27
+ - `useTimerGroup()` for many keyed independent timer lifecycles.
28
+ - `durationParts()` as a pure numeric helper.
29
+
30
+ ## Why This Shape
31
+
32
+ Most timer libraries mix three concerns:
33
+
34
+ - scheduling
35
+ - lifecycle state
36
+ - presentation formatting
37
+
38
+ This library should only own scheduling and lifecycle mechanics.
39
+
40
+ Consumers own:
41
+
42
+ - countdown math
43
+ - stopwatch display
44
+ - clock formatting
45
+ - timezone and locale behavior
46
+ - API polling behavior
47
+ - audio or notification side effects
48
+
49
+ ## Single Timer
50
+
51
+ ```tsx
52
+ function Stopwatch() {
53
+ const timer = useTimer({
54
+ autoStart: false,
55
+ updateIntervalMs: 100,
56
+ });
57
+
58
+ return (
59
+ <>
60
+ <span>{Math.floor(timer.elapsedMilliseconds / 1000)}s</span>
61
+ <button onClick={timer.start}>Start</button>
62
+ <button onClick={timer.pause}>Pause</button>
63
+ <button onClick={timer.resume}>Resume</button>
64
+ <button onClick={timer.restart}>Restart</button>
65
+ <button onClick={() => timer.reset()}>Reset</button>
66
+ </>
67
+ );
68
+ }
69
+ ```
70
+
71
+ ## Absolute Deadline Countdown
72
+
73
+ Use this for auctions, server deadlines, reservations, or any timer where the end timestamp comes from outside the UI.
74
+
75
+ ```tsx
76
+ function AuctionTimer({ auctionId, expiresAt }: {
77
+ auctionId: string;
78
+ expiresAt: number;
79
+ }) {
80
+ const timer = useTimer({
81
+ autoStart: true,
82
+ updateIntervalMs: 1000,
83
+ endWhen: snapshot => snapshot.now >= expiresAt,
84
+ onEnd: () => api.closeAuction(auctionId),
85
+ });
86
+
87
+ const remainingMs = Math.max(0, expiresAt - timer.now);
88
+
89
+ if (timer.isEnded) {
90
+ return <span>Auction ended</span>;
91
+ }
92
+
93
+ return <span>{Math.ceil(remainingMs / 1000)}s left</span>;
94
+ }
95
+ ```
96
+
97
+ For absolute deadlines, `pause()` pauses the local timer lifecycle and schedules, but it does not change the external server deadline. On `resume()`, the next `now` value catches up to wall time.
98
+
99
+ ## Pausable Duration Countdown
100
+
101
+ Use this when pausing should freeze the remaining duration.
102
+
103
+ ```tsx
104
+ function BreakTimer() {
105
+ const durationMs = 5 * 60 * 1000;
106
+
107
+ const timer = useTimer({
108
+ autoStart: true,
109
+ updateIntervalMs: 1000,
110
+ endWhen: snapshot => snapshot.elapsedMilliseconds >= durationMs,
111
+ });
112
+
113
+ const remainingMs = Math.max(0, durationMs - timer.elapsedMilliseconds);
114
+
115
+ return <span>{Math.ceil(remainingMs / 1000)}s left</span>;
116
+ }
117
+ ```
118
+
119
+ ## Clock
120
+
121
+ ```tsx
122
+ function Clock() {
123
+ const timer = useTimer({
124
+ autoStart: true,
125
+ updateIntervalMs: 1000,
126
+ });
127
+
128
+ return <span>{new Date(timer.now).toLocaleTimeString()}</span>;
129
+ }
130
+ ```
131
+
132
+ The hook does not format time. Use native `Intl`, `Date`, or your preferred date library.
133
+
134
+ ## Schedules and Polling
135
+
136
+ Schedules are optional side effects that run while a timer is active. They are useful for polling, audio cues, analytics pings, or other app-owned side effects.
137
+
138
+ ```tsx
139
+ function AuctionTimer({ auctionId, expiresAt }: Props) {
140
+ const timer = useTimer({
141
+ autoStart: true,
142
+ updateIntervalMs: 1000,
143
+ endWhen: snapshot => snapshot.now >= expiresAt,
144
+ onEnd: () => api.closeAuction(auctionId),
145
+ schedules: [
146
+ {
147
+ id: 'poll-auction',
148
+ everyMs: 5000,
149
+ overlap: 'skip',
150
+ callback: async (_snapshot, controls) => {
151
+ const auction = await api.getAuction(auctionId);
152
+
153
+ if (auction.status === 'sold') {
154
+ controls.cancel('sold');
155
+ }
156
+ },
157
+ },
158
+ ],
159
+ });
160
+
161
+ if (timer.isCancelled) {
162
+ return <span>Auction closed</span>;
163
+ }
164
+
165
+ const remainingMs = Math.max(0, expiresAt - timer.now);
166
+ return <span>{Math.ceil(remainingMs / 1000)}s left</span>;
167
+ }
168
+ ```
169
+
170
+ `overlap: 'skip'` is the default because it prevents slow async callbacks from piling up.
171
+
172
+ ## Many Independent Timers
173
+
174
+ For lists where each item has its own pause, resume, cancel, and end lifecycle, use `useTimerGroup`.
175
+
176
+ ```tsx
177
+ function AuctionList({ auctions }: { auctions: Auction[] }) {
178
+ const timers = useTimerGroup({
179
+ updateIntervalMs: 1000,
180
+ items: auctions.map(auction => ({
181
+ id: auction.id,
182
+ autoStart: true,
183
+ endWhen: snapshot => snapshot.now >= auction.expiresAt,
184
+ onEnd: () => api.closeAuction(auction.id),
185
+ })),
186
+ });
187
+
188
+ return (
189
+ <>
190
+ {auctions.map(auction => {
191
+ const timer = timers.get(auction.id);
192
+ const remainingMs = Math.max(0, auction.expiresAt - (timer?.now ?? timers.now));
193
+
194
+ return (
195
+ <AuctionRow
196
+ key={auction.id}
197
+ auction={auction}
198
+ remainingMs={remainingMs}
199
+ isPaused={timer?.isPaused ?? false}
200
+ isEnded={timer?.isEnded ?? false}
201
+ onPause={() => timers.pause(auction.id)}
202
+ onResume={() => timers.resume(auction.id)}
203
+ onCancel={() => timers.cancel(auction.id, 'sold')}
204
+ />
205
+ );
206
+ })}
207
+ </>
208
+ );
209
+ }
210
+ ```
211
+
212
+ `useTimerGroup()` should use one scheduler internally, not one timeout loop per item.
213
+
214
+ ## Debug Logs
215
+
216
+ Debug logging is planned for v1, but it is opt-in.
217
+
218
+ ```tsx
219
+ const timer = useTimer({
220
+ autoStart: true,
221
+ updateIntervalMs: 1000,
222
+ debug: event => {
223
+ console.debug('[timer]', event);
224
+ },
225
+ });
226
+ ```
227
+
228
+ No logs should be emitted by default.
229
+
230
+ Debug events should be semantic, for example `timer:start`, `timer:tick`, `scheduler:start`, `schedule:skip`, and `timer:end`. The library should not expose raw `setTimeout` handles.
231
+
232
+ ## Bundle Size
233
+
234
+ Current local build size:
235
+
236
+ | File | Raw | Gzip | Brotli |
237
+ | --- | ---: | ---: | ---: |
238
+ | `dist/index.js` | 27.32 kB | 4.69 kB | 4.18 kB |
239
+ | `dist/index.cjs` | 29.18 kB | 5.08 kB | 4.50 kB |
240
+ | `dist/index.d.ts` | 3.95 kB | 992 B | 888 B |
241
+
242
+ Run this after `pnpm build`:
243
+
244
+ ```sh
245
+ pnpm size
246
+ ```
247
+
248
+ CI compares PR bundle size against `main` and writes a size summary to the workflow output.
249
+
250
+ ## Implementation Notes
251
+
252
+ - Use recursive `setTimeout`, not `setInterval`.
253
+ - Never schedule timers during render.
254
+ - Use `Date.now()` for wall-clock `now`.
255
+ - Use `performance.now()` internally for active elapsed duration, with a `Date.now()` fallback.
256
+ - Keep controls stable for React dependency arrays.
257
+ - Keep latest callbacks and options in refs so rerenders do not restart the scheduler unnecessarily.
258
+ - Guard async work with generation IDs.
259
+ - Clean up on unmount.
260
+ - Test with fake timers and React Strict Mode.
261
+
262
+ See:
263
+
264
+ - [Requirements](./REQUIREMENTS.md)
265
+ - [API Specification](./docs/API.md)
266
+ - [Design Decisions](./docs/DECISIONS.md)
267
+ - [Recipes](./docs/RECIPES.md)
268
+ - [Branching and Commits](./docs/BRANCHING_AND_COMMITS.md)
269
+ - [Implementation Plan](./docs/IMPLEMENTATION.md)
270
+ - [Task Plan](./docs/TASKS.md)
271
+ - [OSS and GTM Plan](./docs/OSS_GTM.md)
272
+ - [Release and Docs Plan](./docs/RELEASE_AND_DOCS.md)
273
+ - [Agent Task Cards](./docs/AGENT_TASKS.md)