@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/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)
|