@timekeeper-countdown/core 0.1.0 → 0.1.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/README.md CHANGED
@@ -1,3 +1,232 @@
1
- # Timekeeper Countdown Core
1
+ # @timekeeper-countdown/core
2
2
 
3
- This package contains the framework-agnostic countdown engine that backs the `timekeeper-countdown` distribution. For full documentation, usage guides, and examples, see the root [README](../../README.md).
3
+ Lightweight countdown engine used by Timekeeper Countdown. This package exposes the finite-state machine, snapshot APIs, formatting helpers, and testing utilities that power the React hook and future framework adapters.
4
+
5
+ - Written in TypeScript with zero runtime dependencies.
6
+ - Ships modern ESM bundles and type definitions.
7
+ - Designed to run in browsers, Node.js, or custom runtimes.
8
+ - Tested with deterministic fake timers for reliable behavior.
9
+
10
+ > **Looking for the React hook?** Install [`@timekeeper-countdown/react`](https://www.npmjs.com/package/@timekeeper-countdown/react) for an idiomatic React API that wraps this engine.
11
+
12
+ ---
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @timekeeper-countdown/core
18
+ ```
19
+
20
+ The published bundle is pure ESM. When targeting CommonJS environments use a bundler that understands `type: "module"` packages.
21
+
22
+ Supported runtimes:
23
+
24
+ - Node.js 18+
25
+ - Modern browsers (ES2022 modules)
26
+
27
+ ---
28
+
29
+ ## Quick Start
30
+
31
+ ### High-level helper (`Countdown`)
32
+
33
+ ```ts
34
+ import { Countdown, TimerState } from '@timekeeper-countdown/core';
35
+
36
+ const countdown = Countdown(300, {
37
+ onUpdate: (minutes, seconds) => {
38
+ timerElement.textContent = `${minutes}:${seconds}`;
39
+ },
40
+ onStateChange: state => {
41
+ if (state === TimerState.STOPPED) {
42
+ console.log('Finished!');
43
+ }
44
+ },
45
+ });
46
+
47
+ countdown.start(); // Begin the countdown
48
+ ```
49
+
50
+ `Countdown` wraps the lower-level engine and returns convenient methods:
51
+
52
+ ```ts
53
+ countdown.start(); // boolean (false when invalid transition)
54
+ countdown.pause();
55
+ countdown.resume();
56
+ countdown.reset(nextInitialSeconds?);
57
+ countdown.stop();
58
+ countdown.destroy(); // dispose timers and listeners
59
+
60
+ countdown.getSnapshot(); // CountdownSnapshot
61
+ countdown.getCurrentState(); // TimerState
62
+ countdown.getMinutes(); // string e.g. "05"
63
+ ```
64
+
65
+ ### Low-level engine (`CountdownEngine`)
66
+
67
+ ```ts
68
+ import { CountdownEngine, TimerState } from '@timekeeper-countdown/core';
69
+
70
+ const engine = CountdownEngine(90, {
71
+ tickIntervalMs: 50,
72
+ onSnapshot: snapshot => {
73
+ console.log(snapshot.totalSeconds);
74
+ },
75
+ onStateChange: (state, snapshot) => {
76
+ if (state === TimerState.STOPPED && snapshot.isCompleted) {
77
+ console.log('Done!');
78
+ }
79
+ },
80
+ onError: error => {
81
+ console.error('Timer failure', error);
82
+ },
83
+ });
84
+
85
+ engine.start();
86
+ ```
87
+
88
+ `CountdownEngine` exposes fine-grained control:
89
+
90
+ - `start`, `pause`, `resume`, `reset(nextInitialSeconds?)`, `stop`, `setSeconds(value)`, `destroy`
91
+ - `getSnapshot()` returns the latest snapshot.
92
+ - `subscribe(listener)` emits the current snapshot immediately and on every tick.
93
+
94
+ Snapshot structure:
95
+
96
+ ```ts
97
+ interface CountdownSnapshot {
98
+ initialSeconds: number; // starting value
99
+ totalSeconds: number; // remaining seconds, floor-clamped
100
+ parts: {
101
+ years: number;
102
+ weeks: number;
103
+ days: number;
104
+ hours: number;
105
+ minutes: number;
106
+ seconds: number;
107
+ totalDays: number;
108
+ totalHours: number;
109
+ totalMinutes: number;
110
+ };
111
+ state: TimerState; // IDLE | RUNNING | PAUSED | STOPPED
112
+ isRunning: boolean;
113
+ isCompleted: boolean;
114
+ }
115
+ ```
116
+
117
+ ---
118
+
119
+ ## Formatting Helpers
120
+
121
+ Use the helpers exported at `@timekeeper-countdown/core/format` to avoid reimplementing `padStart` logic.
122
+
123
+ ```ts
124
+ import { formatTime, formatMinutes, formatSeconds, Formatter } from '@timekeeper-countdown/core/format';
125
+
126
+ const snapshot = engine.getSnapshot();
127
+
128
+ formatTime(snapshot); // { minutes: "01", seconds: "30" }
129
+ formatMinutes(snapshot); // "01"
130
+
131
+ const formatter = Formatter();
132
+ formatter.formatHours(snapshot); // memoised string helpers
133
+ ```
134
+
135
+ All helpers accept either a snapshot or any object that exposes `totalSeconds`.
136
+
137
+ ---
138
+
139
+ ## Testing Utilities
140
+
141
+ The package includes utilities under `@timekeeper-countdown/core/testing-utils` to make unit tests deterministic.
142
+
143
+ ```ts
144
+ import {
145
+ createFakeTimeProvider,
146
+ toTimeProvider,
147
+ buildSnapshot,
148
+ assertSnapshotState,
149
+ } from '@timekeeper-countdown/core/testing-utils';
150
+
151
+ const fake = createFakeTimeProvider({ startMs: 0, tickMs: 1000 });
152
+ const engine = CountdownEngine(5, {
153
+ timeProvider: toTimeProvider(fake),
154
+ tickIntervalMs: 5,
155
+ });
156
+
157
+ engine.start();
158
+ fake.advance(3000);
159
+
160
+ expect(engine.getSnapshot().totalSeconds).toBe(2);
161
+
162
+ const snapshot = buildSnapshot({ totalSeconds: 42 });
163
+ assertSnapshotState(snapshot, 'RUNNING');
164
+ ```
165
+
166
+ Utilities include:
167
+
168
+ - `createFakeTimeProvider` / `toTimeProvider` for manual clock control.
169
+ - `buildSnapshot`, `assertSnapshotState` helpers for quick snapshot fabrication.
170
+
171
+ ---
172
+
173
+ ## Custom Time Providers
174
+
175
+ `CountdownEngine` accepts either:
176
+
177
+ - A function: `() => number` returning milliseconds.
178
+ - An object implementing the `TimeProvider` interface: `{ now(): number; isHighResolution: boolean; type: string }`.
179
+
180
+ This allows plugging in custom schedulers or synchronizing multiple engines.
181
+
182
+ ```ts
183
+ const provider = {
184
+ now: () => performance.now(),
185
+ isHighResolution: true,
186
+ type: 'custom',
187
+ };
188
+ CountdownEngine(60, { timeProvider: provider });
189
+ ```
190
+
191
+ ---
192
+
193
+ ## TypeScript Support
194
+
195
+ All exports are fully typed. Useful entry points:
196
+
197
+ ```ts
198
+ import type {
199
+ CountdownSnapshot,
200
+ CountdownEngineOptions,
201
+ CountdownEngineInstance,
202
+ TimerState,
203
+ } from '@timekeeper-countdown/core';
204
+ ```
205
+
206
+ ---
207
+
208
+ ## Documentation & Examples
209
+
210
+ - Monorepo overview: [GitHub repository](https://github.com/eagle-head/timekeeper-countdown)
211
+ - React hook guide: [`@timekeeper-countdown/react` README](https://github.com/eagle-head/timekeeper-countdown/tree/main/packages/react#readme)
212
+ - Complete docs site (guides, API reference, roadmap): <https://eagle-head.github.io/timekeeper-countdown/>
213
+
214
+ ---
215
+
216
+ ## Contributing
217
+
218
+ Issues and pull requests are welcome. Please review the [repository guidelines](https://github.com/eagle-head/timekeeper-countdown/blob/main/AGENTS.md) for more details and run:
219
+
220
+ ```bash
221
+ npm run lint --workspaces
222
+ npm run test --workspaces
223
+ npm run typecheck --workspaces
224
+ ```
225
+
226
+ before submitting changes.
227
+
228
+ ---
229
+
230
+ ## License
231
+
232
+ MIT © [Eduardo Kohn](https://www.linkedin.com/in/eduardo-kohn-56817b195/)
package/dist/format.js CHANGED
@@ -1 +1 @@
1
- function u(t){return typeof t=="number"?t:t&&typeof t.totalSeconds=="number"?t.totalSeconds:0}function c(t){return typeof t!="number"||!Number.isFinite(t)||t<=0?0:t>=Number.MAX_SAFE_INTEGER?Number.MAX_SAFE_INTEGER:Math.floor(t)}function e(t,a=2){try{return !Number.isFinite(t)||t<0?"0".repeat(a):Math.floor(t).toString().padStart(a,"0")}catch{return "0".repeat(a)}}function s(t){return Math.floor(t/60)}function i(t){return Math.floor(t/3600)%24}function S(t){return Math.floor(t/86400)%7}function p(t){return Math.floor(t/604800)%52}function E(t){return Math.floor(t/31536e3)}function f(){let t=r=>c(u(r));return {formatTime:r=>{let o=t(r);return {minutes:e(s(o)),seconds:e(o%60)}},formatMinutes:r=>{let o=t(r);return e(s(o))},formatSeconds:r=>{let o=t(r);return e(o%60)},formatHours:r=>{let o=t(r);return e(i(o))},formatDays:r=>{let o=t(r);return e(S(o))},formatWeeks:r=>{let o=t(r);return e(p(o))},formatYears:r=>{let o=t(r);return e(E(o))}}}var n=f(),F=t=>n.formatTime(t),T=t=>n.formatMinutes(t),d=t=>n.formatSeconds(t),_=t=>n.formatHours(t),b=t=>n.formatDays(t),g=t=>n.formatWeeks(t),M=t=>n.formatYears(t);export{f as Formatter,n as defaultFormatter,b as formatDays,_ as formatHours,T as formatMinutes,d as formatSeconds,F as formatTime,g as formatWeeks,M as formatYears};
1
+ function u(t){return typeof t=="number"?t:t&&typeof t.totalSeconds=="number"?t.totalSeconds:0}function c(t){return typeof t!="number"||!Number.isFinite(t)||t<=0?0:t>=Number.MAX_SAFE_INTEGER?Number.MAX_SAFE_INTEGER:Math.floor(t)}function e(t,a=2){try{return !Number.isFinite(t)||t<0?"0".repeat(a):Math.floor(t).toString().padStart(a,"0")}catch{return "0".repeat(a)}}function s(t){return Math.floor(t/60)}function i(t){return s(t)%60}function S(t){return Math.floor(t/3600)%24}function p(t){return Math.floor(t/86400)%7}function E(t){return Math.floor(t/604800)%52}function T(t){return Math.floor(t/31536e3)}function f(){let t=r=>c(u(r));return {formatTime:r=>{let o=t(r);return {minutes:e(s(o)),seconds:e(o%60)}},formatMinutes:r=>{let o=t(r);return e(i(o))},formatSeconds:r=>{let o=t(r);return e(o%60)},formatHours:r=>{let o=t(r);return e(S(o))},formatDays:r=>{let o=t(r);return e(p(o))},formatWeeks:r=>{let o=t(r);return e(E(o))},formatYears:r=>{let o=t(r);return e(T(o))}}}var n=f(),F=t=>n.formatTime(t),_=t=>n.formatMinutes(t),d=t=>n.formatSeconds(t),b=t=>n.formatHours(t),M=t=>n.formatDays(t),g=t=>n.formatWeeks(t),R=t=>n.formatYears(t);export{f as Formatter,n as defaultFormatter,M as formatDays,b as formatHours,_ as formatMinutes,d as formatSeconds,F as formatTime,g as formatWeeks,R as formatYears};
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- function Y(){return {now:()=>performance.now(),isHighResolution:true,type:"performance"}}function G(){return {now:()=>Date.now(),isHighResolution:false,type:"date"}}function y(){let t,e=G();function i(){try{if(typeof performance<"u"&&typeof performance.now=="function"){let o=performance.now();if(typeof o=="number"&&Number.isFinite(o)&&!isNaN(o)&&o>=0&&o<=Number.MAX_SAFE_INTEGER){t=Y();return}}throw new Error("performance.now() not available or invalid")}catch{t=e;}}return i(),{now:()=>{try{let o=t.now();if(typeof o=="number"&&Number.isFinite(o)&&!isNaN(o)&&o>=0&&o<=Number.MAX_SAFE_INTEGER)return o;throw new Error("Invalid time value returned")}catch{t!==e&&(t=e);try{let o=e.now();if(typeof o=="number"&&Number.isFinite(o)&&!isNaN(o)&&o>=0)return o}catch{}return 0}},get isHighResolution(){return t.isHighResolution},get type(){return t.type}}}y();var L=10,V=100;function _(t,e,i={}){if(typeof t!="number"||!Number.isFinite(t))throw new Error("initialSeconds must be a finite number");if(t<0)throw new Error("initialSeconds must be non-negative");if(t>Number.MAX_SAFE_INTEGER)throw new Error("initialSeconds exceeds maximum safe integer");if(!e||typeof e!="object")throw new Error("events must be an object");if(typeof e.onTick!="function")throw new Error("events.onTick must be a function");if(typeof e.onComplete!="function")throw new Error("events.onComplete must be a function");if(typeof e.onError!="function")throw new Error("events.onError must be a function");let o=Math.floor(Math.max(0,t)),c=null,r=null,u=0,S=o,a=o,s=Math.max(L,Math.floor(i.tickIntervalMs??V)),b=y(),d=i.timeProvider??(()=>b.now()),f=()=>setInterval(()=>{try{if(r===null)return;let h=d()-r-u,D=Math.floor(h/1e3),n=Math.max(0,a-D);o=n,n!==S&&(S=n,e.onTick(n)),n===0&&(E(),e.onComplete());}catch(l){E(),e.onError(l instanceof Error?l:new Error(String(l)));}},s),p=()=>{if(c||o<=0)return false;try{if(r!==null&&o<a){let l=(a-o)*1e3;u=d()-r-l;}else r=d(),u=0,S=o;return c=f(),!0}catch(l){return e.onError(l instanceof Error?l:new Error(String(l))),false}},E=()=>{c&&(clearInterval(c),c=null);},C=()=>{E(),o=a,r=null,u=0,S=a;},v=l=>{if(typeof l!="number"||!Number.isFinite(l))return;let h=Math.floor(Math.max(0,Math.min(l,Number.MAX_SAFE_INTEGER)));o=h,r=null,u=0,S=h;};return {start:p,stop:E,reset:C,setSeconds:v,setInitialValue:l=>{if(typeof l!="number"||!Number.isFinite(l))return;let h=Math.floor(Math.max(0,Math.min(l,Number.MAX_SAFE_INTEGER)));a=h,v(h);},getTotalSeconds:()=>o,getInitialValue:()=>a,isRunning:()=>c!==null,destroy:()=>{E(),o=0,r=null,u=0;}}}var m=Object.freeze({IDLE:"IDLE",RUNNING:"RUNNING",PAUSED:"PAUSED",STOPPED:"STOPPED"}),P={[m.IDLE]:{start:m.RUNNING},[m.RUNNING]:{pause:m.PAUSED,reset:m.IDLE,stop:m.STOPPED,complete:m.STOPPED},[m.PAUSED]:{resume:m.RUNNING,reset:m.IDLE,stop:m.STOPPED},[m.STOPPED]:{reset:m.IDLE}};function x(t){if(t!==void 0){if(typeof t!="object"||t===null)throw new Error("events must be an object");if(t.onStateChange!==void 0&&typeof t.onStateChange!="function")throw new Error("events.onStateChange must be a function")}let e=m.IDLE;function i(b){let d=P[e]?.[b];if(!d)return false;e=d;try{t?.onStateChange?.(d);}catch{}return true}function o(){return i("start")}function c(){return i("resume")}function r(){return i("pause")}function u(){return i("reset")}function S(){return i("stop")}function a(){return i("complete")}function s(){e===m.IDLE||e===m.STOPPED||i("stop");}return {start:o,resume:c,pause:r,reset:u,stop:S,complete:a,getCurrentState:()=>e,canStart:()=>!!P[e]?.start,canResume:()=>!!P[e]?.resume,canPause:()=>!!P[e]?.pause,isRunning:()=>e===m.RUNNING,destroy:s}}function O(t){if(typeof t!="number"||!Number.isFinite(t)||!Number.isInteger(t))throw new Error("initialSeconds must be a finite, non-negative integer");if(t<0)throw new Error("initialSeconds must be non-negative");if(t>Number.MAX_SAFE_INTEGER)throw new Error("initialSeconds exceeds maximum safe integer");return t}function q(t){if(!t){let e=y();return ()=>e.now()}if(typeof t=="function")return t;if(typeof t.now=="function"){let e=t;return ()=>e.now()}throw new Error("timeProvider must implement a now(): number method")}function J(t){let e=Math.max(0,Math.floor(t)),i=Math.floor(e/31536e3),o=Math.floor(e/604800)%52,c=Math.floor(e/86400)%7,r=Math.floor(e/3600)%24,u=Math.floor(e/60)%60,S=e%60,a=Math.floor(e/86400),s=Math.floor(e/3600),b=Math.floor(e/60);return {years:i,weeks:o,days:c,hours:r,minutes:u,seconds:S,totalDays:a,totalHours:s,totalMinutes:b}}function I(t,e,i){let o=J(e);return {initialSeconds:t,totalSeconds:e,parts:o,state:i,isRunning:i===m.RUNNING,isCompleted:e===0&&i===m.STOPPED}}function R(t,e={}){let i=O(t),o=q(e.timeProvider),c=i,r=I(i,i,m.IDLE),u=new Set,S=n=>{try{e.onSnapshot?.(n);}catch{}for(let N of u)try{N(n);}catch{}},a=n=>{r=I(c,r.totalSeconds,n);try{e.onStateChange?.(n,r);}catch{}S(r);},s=n=>{r=I(c,n,d.getCurrentState()),S(r);},b=n=>{try{e.onError?.(n);}catch{}},d=x({onStateChange:n=>{a(n);}}),f=_(i,{onTick:n=>{s(n);},onComplete:()=>{d.complete(),s(0);},onError:n=>{b(n),d.stop();}},{timeProvider:o,tickIntervalMs:e.tickIntervalMs});return {start:()=>{if(!d.canStart())return false;let n=f.start();return n&&(d.start(),s(f.getTotalSeconds())),n},pause:()=>{if(!d.canPause())return false;f.stop();let n=d.pause();return n&&s(f.getTotalSeconds()),n},resume:()=>{if(!d.canResume())return false;let n=f.start();return n&&(d.resume(),s(f.getTotalSeconds())),n},reset:n=>{if(f.stop(),typeof n=="number"){let N=O(n);c=N,f.setInitialValue(N);}else c=i,f.reset();return d.reset(),s(f.getTotalSeconds()),true},stop:()=>{f.stop();let n=d.stop();return n&&(f.setSeconds(0),s(0)),n},setSeconds:n=>{f.setSeconds(n),s(f.getTotalSeconds());},getSnapshot:()=>r,subscribe:n=>{u.add(n);try{n(r);}catch{}return {unsubscribe:()=>{u.delete(n);}}},destroy:()=>{f.destroy(),d.destroy(),u.clear(),r=I(c,0,m.STOPPED);}}}function Q(t){return typeof t=="number"?t:t&&typeof t.totalSeconds=="number"?t.totalSeconds:0}function Z(t){return typeof t!="number"||!Number.isFinite(t)||t<=0?0:t>=Number.MAX_SAFE_INTEGER?Number.MAX_SAFE_INTEGER:Math.floor(t)}function T(t,e=2){try{return !Number.isFinite(t)||t<0?"0".repeat(e):Math.floor(t).toString().padStart(e,"0")}catch{return "0".repeat(e)}}function k(t){return Math.floor(t/60)}function $(t){return Math.floor(t/3600)%24}function tt(t){return Math.floor(t/86400)%7}function et(t){return Math.floor(t/604800)%52}function ot(t){return Math.floor(t/31536e3)}function M(){let t=a=>Z(Q(a));return {formatTime:a=>{let s=t(a);return {minutes:T(k(s)),seconds:T(s%60)}},formatMinutes:a=>{let s=t(a);return T(k(s))},formatSeconds:a=>{let s=t(a);return T(s%60)},formatHours:a=>{let s=t(a);return T($(s))},formatDays:a=>{let s=t(a);return T(tt(s))},formatWeeks:a=>{let s=t(a);return T(et(s))},formatYears:a=>{let s=t(a);return T(ot(s))}}}var g=M(),nt=t=>g.formatTime(t),rt=t=>g.formatMinutes(t),at=t=>g.formatSeconds(t),st=t=>g.formatHours(t),it=t=>g.formatDays(t),ut=t=>g.formatWeeks(t),ct=t=>g.formatYears(t);function mt(t,e={}){let{onUpdate:i,onStateChange:o}=e;if(i!==void 0&&typeof i!="function")throw new Error("onUpdate must be a function");if(o!==void 0&&typeof o!="function")throw new Error("onStateChange must be a function");let c=M(),r=R(t),u=r.getSnapshot(),S=u.state,a=Number.NaN,s=p=>{if(i)try{let{minutes:E,seconds:C}=c.formatTime(p.totalSeconds);i(E,C);}catch{}},b=p=>{if(o)try{o(p);}catch{}},d=r.subscribe(p=>{u=p,p.state!==S&&(S=p.state,b(p.state)),p.totalSeconds!==a&&(a=p.totalSeconds,s(p));}),f=p=>{try{p();}catch{}};return {start:()=>{f(()=>{r.start();});},pause:()=>{f(()=>{r.pause();});},resume:()=>{f(()=>{r.resume();});},reset:p=>{f(()=>{r.reset(p);});},stop:()=>{f(()=>{r.stop();});},getSeconds:()=>{try{return c.formatSeconds(u.totalSeconds)}catch{return "00"}},getMinutes:()=>{try{return c.formatMinutes(u.totalSeconds)}catch{return "00"}},getHours:()=>{try{return c.formatHours(u.totalSeconds)}catch{return "00"}},getDays:()=>{try{return c.formatDays(u.totalSeconds)}catch{return "00"}},getWeeks:()=>{try{return c.formatWeeks(u.totalSeconds)}catch{return "00"}},getYears:()=>{try{return c.formatYears(u.totalSeconds)}catch{return "00"}},getCurrentState:()=>{try{return u.state}catch{return m.IDLE}},getSnapshot:()=>u,destroy:()=>{f(()=>{d.unsubscribe(),r.destroy();});}}}export{mt as Countdown,R as CountdownEngine,M as Formatter,m as TimerState,g as defaultFormatter,it as formatDays,st as formatHours,rt as formatMinutes,at as formatSeconds,nt as formatTime,ut as formatWeeks,ct as formatYears};
1
+ function Y(){return {now:()=>performance.now(),isHighResolution:true,type:"performance"}}function G(){return {now:()=>Date.now(),isHighResolution:false,type:"date"}}function y(){let t,e=G();function i(){try{if(typeof performance<"u"&&typeof performance.now=="function"){let o=performance.now();if(typeof o=="number"&&Number.isFinite(o)&&!isNaN(o)&&o>=0&&o<=Number.MAX_SAFE_INTEGER){t=Y();return}}throw new Error("performance.now() not available or invalid")}catch{t=e;}}return i(),{now:()=>{try{let o=t.now();if(typeof o=="number"&&Number.isFinite(o)&&!isNaN(o)&&o>=0&&o<=Number.MAX_SAFE_INTEGER)return o;throw new Error("Invalid time value returned")}catch{t!==e&&(t=e);try{let o=e.now();if(typeof o=="number"&&Number.isFinite(o)&&!isNaN(o)&&o>=0)return o}catch{}return 0}},get isHighResolution(){return t.isHighResolution},get type(){return t.type}}}y();var L=10,V=100;function D(t,e,i={}){if(typeof t!="number"||!Number.isFinite(t))throw new Error("initialSeconds must be a finite number");if(t<0)throw new Error("initialSeconds must be non-negative");if(t>Number.MAX_SAFE_INTEGER)throw new Error("initialSeconds exceeds maximum safe integer");if(!e||typeof e!="object")throw new Error("events must be an object");if(typeof e.onTick!="function")throw new Error("events.onTick must be a function");if(typeof e.onComplete!="function")throw new Error("events.onComplete must be a function");if(typeof e.onError!="function")throw new Error("events.onError must be a function");let o=Math.floor(Math.max(0,t)),c=null,r=null,u=0,S=o,a=o,s=Math.max(L,Math.floor(i.tickIntervalMs??V)),b=y(),d=i.timeProvider??(()=>b.now()),f=()=>setInterval(()=>{try{if(r===null)return;let h=d()-r-u,_=Math.floor(h/1e3),n=Math.max(0,a-_);o=n,n!==S&&(S=n,e.onTick(n)),n===0&&(E(),e.onComplete());}catch(l){E(),e.onError(l instanceof Error?l:new Error(String(l)));}},s),p=()=>{if(c||o<=0)return false;try{if(r!==null&&o<a){let l=(a-o)*1e3;u=d()-r-l;}else r=d(),u=0,S=o;return c=f(),!0}catch(l){return e.onError(l instanceof Error?l:new Error(String(l))),false}},E=()=>{c&&(clearInterval(c),c=null);},C=()=>{E(),o=a,r=null,u=0,S=a;},v=l=>{if(typeof l!="number"||!Number.isFinite(l))return;let h=Math.floor(Math.max(0,Math.min(l,Number.MAX_SAFE_INTEGER)));o=h,r=null,u=0,S=h;};return {start:p,stop:E,reset:C,setSeconds:v,setInitialValue:l=>{if(typeof l!="number"||!Number.isFinite(l))return;let h=Math.floor(Math.max(0,Math.min(l,Number.MAX_SAFE_INTEGER)));a=h,v(h);},getTotalSeconds:()=>o,getInitialValue:()=>a,isRunning:()=>c!==null,destroy:()=>{E(),o=0,r=null,u=0;}}}var m=Object.freeze({IDLE:"IDLE",RUNNING:"RUNNING",PAUSED:"PAUSED",STOPPED:"STOPPED"}),P={[m.IDLE]:{start:m.RUNNING},[m.RUNNING]:{pause:m.PAUSED,reset:m.IDLE,stop:m.STOPPED,complete:m.STOPPED},[m.PAUSED]:{resume:m.RUNNING,reset:m.IDLE,stop:m.STOPPED},[m.STOPPED]:{reset:m.IDLE}};function x(t){if(t!==void 0){if(typeof t!="object"||t===null)throw new Error("events must be an object");if(t.onStateChange!==void 0&&typeof t.onStateChange!="function")throw new Error("events.onStateChange must be a function")}let e=m.IDLE;function i(b){let d=P[e]?.[b];if(!d)return false;e=d;try{t?.onStateChange?.(d);}catch{}return true}function o(){return i("start")}function c(){return i("resume")}function r(){return i("pause")}function u(){return i("reset")}function S(){return i("stop")}function a(){return i("complete")}function s(){e===m.IDLE||e===m.STOPPED||i("stop");}return {start:o,resume:c,pause:r,reset:u,stop:S,complete:a,getCurrentState:()=>e,canStart:()=>!!P[e]?.start,canResume:()=>!!P[e]?.resume,canPause:()=>!!P[e]?.pause,isRunning:()=>e===m.RUNNING,destroy:s}}function O(t){if(typeof t!="number"||!Number.isFinite(t)||!Number.isInteger(t))throw new Error("initialSeconds must be a finite, non-negative integer");if(t<0)throw new Error("initialSeconds must be non-negative");if(t>Number.MAX_SAFE_INTEGER)throw new Error("initialSeconds exceeds maximum safe integer");return t}function J(t){if(!t){let e=y();return ()=>e.now()}if(typeof t=="function")return t;if(typeof t.now=="function"){let e=t;return ()=>e.now()}throw new Error("timeProvider must implement a now(): number method")}function Q(t){let e=Math.max(0,Math.floor(t)),i=Math.floor(e/31536e3),o=Math.floor(e/604800)%52,c=Math.floor(e/86400)%7,r=Math.floor(e/3600)%24,u=Math.floor(e/60)%60,S=e%60,a=Math.floor(e/86400),s=Math.floor(e/3600),b=Math.floor(e/60);return {years:i,weeks:o,days:c,hours:r,minutes:u,seconds:S,totalDays:a,totalHours:s,totalMinutes:b}}function I(t,e,i){let o=Q(e);return {initialSeconds:t,totalSeconds:e,parts:o,state:i,isRunning:i===m.RUNNING,isCompleted:e===0&&i===m.STOPPED}}function R(t,e={}){let i=O(t),o=J(e.timeProvider),c=i,r=I(i,i,m.IDLE),u=new Set,S=n=>{try{e.onSnapshot?.(n);}catch{}for(let N of u)try{N(n);}catch{}},a=n=>{r=I(c,r.totalSeconds,n);try{e.onStateChange?.(n,r);}catch{}S(r);},s=n=>{r=I(c,n,d.getCurrentState()),S(r);},b=n=>{try{e.onError?.(n);}catch{}},d=x({onStateChange:n=>{a(n);}}),f=D(i,{onTick:n=>{s(n);},onComplete:()=>{d.complete(),s(0);},onError:n=>{b(n),d.stop();}},{timeProvider:o,tickIntervalMs:e.tickIntervalMs});return {start:()=>{if(!d.canStart())return false;let n=f.start();return n&&(d.start(),s(f.getTotalSeconds())),n},pause:()=>{if(!d.canPause())return false;f.stop();let n=d.pause();return n&&s(f.getTotalSeconds()),n},resume:()=>{if(!d.canResume())return false;let n=f.start();return n&&(d.resume(),s(f.getTotalSeconds())),n},reset:n=>{if(f.stop(),typeof n=="number"){let N=O(n);c=N,f.setInitialValue(N);}else c=i,f.reset();return d.reset(),s(f.getTotalSeconds()),true},stop:()=>{f.stop();let n=d.stop();return n&&(f.setSeconds(0),s(0)),n},setSeconds:n=>{f.setSeconds(n),s(f.getTotalSeconds());},getSnapshot:()=>r,subscribe:n=>{u.add(n);try{n(r);}catch{}return {unsubscribe:()=>{u.delete(n);}}},destroy:()=>{f.destroy(),d.destroy(),u.clear(),r=I(c,0,m.STOPPED);}}}function Z(t){return typeof t=="number"?t:t&&typeof t.totalSeconds=="number"?t.totalSeconds:0}function $(t){return typeof t!="number"||!Number.isFinite(t)||t<=0?0:t>=Number.MAX_SAFE_INTEGER?Number.MAX_SAFE_INTEGER:Math.floor(t)}function T(t,e=2){try{return !Number.isFinite(t)||t<0?"0".repeat(e):Math.floor(t).toString().padStart(e,"0")}catch{return "0".repeat(e)}}function H(t){return Math.floor(t/60)}function tt(t){return H(t)%60}function et(t){return Math.floor(t/3600)%24}function ot(t){return Math.floor(t/86400)%7}function nt(t){return Math.floor(t/604800)%52}function rt(t){return Math.floor(t/31536e3)}function M(){let t=a=>$(Z(a));return {formatTime:a=>{let s=t(a);return {minutes:T(H(s)),seconds:T(s%60)}},formatMinutes:a=>{let s=t(a);return T(tt(s))},formatSeconds:a=>{let s=t(a);return T(s%60)},formatHours:a=>{let s=t(a);return T(et(s))},formatDays:a=>{let s=t(a);return T(ot(s))},formatWeeks:a=>{let s=t(a);return T(nt(s))},formatYears:a=>{let s=t(a);return T(rt(s))}}}var g=M(),at=t=>g.formatTime(t),st=t=>g.formatMinutes(t),it=t=>g.formatSeconds(t),ut=t=>g.formatHours(t),ct=t=>g.formatDays(t),mt=t=>g.formatWeeks(t),ft=t=>g.formatYears(t);function dt(t,e={}){let{onUpdate:i,onStateChange:o}=e;if(i!==void 0&&typeof i!="function")throw new Error("onUpdate must be a function");if(o!==void 0&&typeof o!="function")throw new Error("onStateChange must be a function");let c=M(),r=R(t),u=r.getSnapshot(),S=u.state,a=Number.NaN,s=p=>{if(i)try{let{minutes:E,seconds:C}=c.formatTime(p.totalSeconds);i(E,C);}catch{}},b=p=>{if(o)try{o(p);}catch{}},d=r.subscribe(p=>{u=p,p.state!==S&&(S=p.state,b(p.state)),p.totalSeconds!==a&&(a=p.totalSeconds,s(p));}),f=p=>{try{p();}catch{}};return {start:()=>{f(()=>{r.start();});},pause:()=>{f(()=>{r.pause();});},resume:()=>{f(()=>{r.resume();});},reset:p=>{f(()=>{r.reset(p);});},stop:()=>{f(()=>{r.stop();});},getSeconds:()=>{try{return c.formatSeconds(u.totalSeconds)}catch{return "00"}},getMinutes:()=>{try{return c.formatMinutes(u.totalSeconds)}catch{return "00"}},getHours:()=>{try{return c.formatHours(u.totalSeconds)}catch{return "00"}},getDays:()=>{try{return c.formatDays(u.totalSeconds)}catch{return "00"}},getWeeks:()=>{try{return c.formatWeeks(u.totalSeconds)}catch{return "00"}},getYears:()=>{try{return c.formatYears(u.totalSeconds)}catch{return "00"}},getCurrentState:()=>{try{return u.state}catch{return m.IDLE}},getSnapshot:()=>u,destroy:()=>{f(()=>{d.unsubscribe(),r.destroy();});}}}export{dt as Countdown,R as CountdownEngine,M as Formatter,m as TimerState,g as defaultFormatter,ct as formatDays,ut as formatHours,st as formatMinutes,it as formatSeconds,at as formatTime,mt as formatWeeks,ft as formatYears};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timekeeper-countdown/core",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Lightweight countdown engine that powers the Timekeeper packages. Pure TypeScript with no runtime dependencies.",
5
5
  "keywords": [
6
6
  "countdown",
@@ -41,7 +41,8 @@
41
41
  }
42
42
  },
43
43
  "files": [
44
- "dist"
44
+ "dist",
45
+ "README.md"
45
46
  ],
46
47
  "sideEffects": false,
47
48
  "scripts": {