@timekeeper-countdown/core 0.1.3 → 0.2.0

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
@@ -32,9 +32,11 @@ Supported runtimes:
32
32
 
33
33
  ```ts
34
34
  import { Countdown, TimerState } from '@timekeeper-countdown/core';
35
+ import { formatTime } from '@timekeeper-countdown/core/format';
35
36
 
36
37
  const countdown = Countdown(300, {
37
- onUpdate: (minutes, seconds) => {
38
+ onSnapshot: snapshot => {
39
+ const { minutes, seconds } = formatTime(snapshot);
38
40
  timerElement.textContent = `${minutes}:${seconds}`;
39
41
  },
40
42
  onStateChange: state => {
@@ -42,6 +44,9 @@ const countdown = Countdown(300, {
42
44
  console.log('Finished!');
43
45
  }
44
46
  },
47
+ onError: error => {
48
+ console.error('Countdown error:', error);
49
+ },
45
50
  });
46
51
 
47
52
  countdown.start(); // Begin the countdown
@@ -116,6 +121,40 @@ interface CountdownSnapshot {
116
121
 
117
122
  ---
118
123
 
124
+ ## State Transitions
125
+
126
+ The engine enforces a strict state machine. Invalid transitions are silently ignored and return `false`.
127
+
128
+ ```text
129
+ start()
130
+ IDLE ──────────────► RUNNING
131
+ ▲ │ │
132
+ │ │ │
133
+ │ reset() pause() stop() / complete()
134
+ │ │ │
135
+ │ ▼ │
136
+ │ PAUSED │
137
+ │ │ │
138
+ │ reset() │ │
139
+ │◄───────────────────┘ │
140
+ │ │
141
+ │ reset() ▼
142
+ │◄──────────────────── STOPPED
143
+ ```
144
+
145
+ | From | Action | To |
146
+ | --------- | ---------- | --------- |
147
+ | `IDLE` | `start()` | `RUNNING` |
148
+ | `RUNNING` | `pause()` | `PAUSED` |
149
+ | `RUNNING` | `reset()` | `IDLE` |
150
+ | `RUNNING` | `stop()` | `STOPPED` |
151
+ | `PAUSED` | `resume()` | `RUNNING` |
152
+ | `PAUSED` | `reset()` | `IDLE` |
153
+ | `PAUSED` | `stop()` | `STOPPED` |
154
+ | `STOPPED` | `reset()` | `IDLE` |
155
+
156
+ ---
157
+
119
158
  ## Formatting Helpers
120
159
 
121
160
  Use the helpers exported at `@timekeeper-countdown/core/format` to avoid reimplementing `padStart` logic.
@@ -145,28 +184,140 @@ import {
145
184
  createFakeTimeProvider,
146
185
  toTimeProvider,
147
186
  buildSnapshot,
187
+ buildSnapshotSequence,
148
188
  assertSnapshotState,
189
+ assertSnapshotCompleted,
190
+ assertRemainingSeconds,
191
+ TimerState,
149
192
  } from '@timekeeper-countdown/core/testing-utils';
193
+ ```
150
194
 
151
- const fake = createFakeTimeProvider({ startMs: 0, tickMs: 1000 });
152
- const engine = CountdownEngine(5, {
153
- timeProvider: toTimeProvider(fake),
154
- tickIntervalMs: 5,
155
- });
195
+ ### `createFakeTimeProvider(options?)`
156
196
 
157
- engine.start();
158
- fake.advance(3000);
197
+ Provides a controllable clock for deterministic testing.
198
+
199
+ ```ts
200
+ interface FakeTimeOptions {
201
+ startMs?: number; // default: 0
202
+ tickMs?: number; // default: 1000 — default step for advance()
203
+ highResolution?: boolean; // default: true
204
+ }
205
+
206
+ interface FakeTimeProvider extends TimeProvider {
207
+ advance(ms?: number): number; // advances by ms (or tickMs if omitted), returns current time
208
+ set(ms: number): number; // sets clock to absolute value, returns current time
209
+ reset(): number; // resets to startMs, returns current time
210
+ getTime(): number; // returns current time without advancing
211
+ now(): number; // same as getTime (inherited from TimeProvider)
212
+ isHighResolution: boolean;
213
+ type: 'fake';
214
+ }
215
+
216
+ function createFakeTimeProvider(options?: FakeTimeOptions): FakeTimeProvider;
217
+ ```
218
+
219
+ - Negative or non-finite values are clamped to `0`.
220
+ - Values above `Number.MAX_SAFE_INTEGER` are clamped to `Number.MAX_SAFE_INTEGER`.
221
+ - `advance()` without a parameter uses `tickMs` as the default step.
222
+
223
+ ### `toTimeProvider(fake)`
224
+
225
+ ```ts
226
+ function toTimeProvider(fake: FakeTimeProvider): TimeProvider;
227
+ ```
228
+
229
+ Converts a `FakeTimeProvider` to a read-only `TimeProvider`, used to pass to `CountdownEngine` or `useCountdown`.
230
+
231
+ ### `buildSnapshot(options?)`
232
+
233
+ ```ts
234
+ interface SnapshotOptions {
235
+ initialSeconds?: number; // fallback: totalSeconds, then 0
236
+ totalSeconds?: number; // fallback: initialSeconds, then 0
237
+ state?: TimerState; // fallback: IDLE if totalSeconds > 0, else STOPPED
238
+ }
239
+
240
+ function buildSnapshot(options?: SnapshotOptions): CountdownSnapshot;
241
+ ```
159
242
 
160
- expect(engine.getSnapshot().totalSeconds).toBe(2);
243
+ ```ts
244
+ const snapshot = buildSnapshot({ totalSeconds: 90, state: TimerState.RUNNING });
245
+ // snapshot.parts.minutes === 1
246
+ // snapshot.parts.seconds === 30
247
+ // snapshot.isRunning === true
248
+ ```
249
+
250
+ ### `buildSnapshotSequence(options?)`
251
+
252
+ ```ts
253
+ interface SequenceOptions extends SnapshotOptions {
254
+ step?: number; // default: 1 — decrement per snapshot
255
+ count?: number; // default: 1 — number of snapshots
256
+ }
257
+
258
+ function buildSnapshotSequence(options?: SequenceOptions): CountdownSnapshot[];
259
+ ```
161
260
 
162
- const snapshot = buildSnapshot({ totalSeconds: 42 });
163
- assertSnapshotState(snapshot, 'RUNNING');
261
+ Generates `count` snapshots, decrementing `totalSeconds` by `step` each iteration. The last snapshot with `remaining === 0` gets `state: STOPPED`; all others get `state: RUNNING`.
262
+
263
+ ```ts
264
+ const sequence = buildSnapshotSequence({ totalSeconds: 4, step: 2, count: 3 });
265
+ // sequence[0].totalSeconds === 4 (RUNNING)
266
+ // sequence[1].totalSeconds === 2 (RUNNING)
267
+ // sequence[2].totalSeconds === 0 (STOPPED)
268
+ ```
269
+
270
+ ### `assertSnapshotState(snapshot, expected, message?)`
271
+
272
+ ```ts
273
+ function assertSnapshotState(
274
+ snapshot: CountdownSnapshot,
275
+ expected: TimerState,
276
+ message?: string // default: "Unexpected countdown state"
277
+ ): void;
164
278
  ```
165
279
 
166
- Utilities include:
280
+ ### `assertSnapshotCompleted(snapshot, message?)`
167
281
 
168
- - `createFakeTimeProvider` / `toTimeProvider` for manual clock control.
169
- - `buildSnapshot`, `assertSnapshotState` helpers for quick snapshot fabrication.
282
+ ```ts
283
+ function assertSnapshotCompleted(
284
+ snapshot: CountdownSnapshot,
285
+ message?: string // default: "Countdown should be completed"
286
+ ): void;
287
+ ```
288
+
289
+ Throws if `snapshot.isCompleted === false` OR `snapshot.totalSeconds !== 0`.
290
+
291
+ ### `assertRemainingSeconds(snapshot, expected, tolerance?, message?)`
292
+
293
+ ```ts
294
+ function assertRemainingSeconds(
295
+ snapshot: CountdownSnapshot,
296
+ expected: number,
297
+ tolerance?: number, // default: 0
298
+ message?: string // default: "Unexpected remaining seconds"
299
+ ): void;
300
+ ```
301
+
302
+ Throws if `Math.abs(snapshot.totalSeconds - Math.floor(expected)) > tolerance` or if `expected` is not a finite number.
303
+
304
+ ```ts
305
+ assertRemainingSeconds(snapshot, 5); // exact
306
+ assertRemainingSeconds(snapshot, 5, 0.5); // accepts 4.5–5.5
307
+ ```
308
+
309
+ ### `TimerState` re-export
310
+
311
+ `TimerState` is re-exported via `testing-utils`, avoiding a double import:
312
+
313
+ ```ts
314
+ // Instead of two separate imports:
315
+ import { TimerState } from '@timekeeper-countdown/core';
316
+ import { buildSnapshot } from '@timekeeper-countdown/core/testing-utils';
317
+
318
+ // You can import everything from one place:
319
+ import { buildSnapshot, TimerState } from '@timekeeper-countdown/core/testing-utils';
320
+ ```
170
321
 
171
322
  ---
172
323
 
@@ -56,6 +56,7 @@ interface CountdownEngineInstance {
56
56
  subscribe: (listener: (snapshot: CountdownSnapshot) => void) => CountdownSubscription;
57
57
  destroy: () => void;
58
58
  }
59
+ declare function buildSnapshot(initialSeconds: number, totalSeconds: number, state: TimerState): CountdownSnapshot;
59
60
  declare function CountdownEngine(initialSecondsInput: number, options?: CountdownEngineOptions): CountdownEngineInstance;
60
61
 
61
- export { type CountdownSnapshot as C, TimerState as T, CountdownEngine as a, type CountdownEngineInstance as b, type CountdownEngineOptions as c, type CountdownParts as d, type TimeProvider as e };
62
+ export { type CountdownSnapshot as C, TimerState as T, CountdownEngine as a, buildSnapshot as b, type CountdownEngineInstance as c, type CountdownEngineOptions as d, type CountdownParts as e, type TimeProvider as f };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Time providers with fallbacks for critical APIs
3
+ * Ensures the library works even without modern browser APIs
4
+ */
5
+ interface TimeProvider {
6
+ now(): number;
7
+ isHighResolution: boolean;
8
+ type: string;
9
+ }
10
+
11
+ declare const TimerState: Readonly<{
12
+ IDLE: "IDLE";
13
+ RUNNING: "RUNNING";
14
+ PAUSED: "PAUSED";
15
+ STOPPED: "STOPPED";
16
+ }>;
17
+ type TimerState = (typeof TimerState)[keyof typeof TimerState];
18
+
19
+ interface CountdownParts {
20
+ years: number;
21
+ weeks: number;
22
+ days: number;
23
+ hours: number;
24
+ minutes: number;
25
+ seconds: number;
26
+ totalDays: number;
27
+ totalHours: number;
28
+ totalMinutes: number;
29
+ }
30
+ interface CountdownSnapshot {
31
+ initialSeconds: number;
32
+ totalSeconds: number;
33
+ parts: CountdownParts;
34
+ state: TimerState;
35
+ isRunning: boolean;
36
+ isCompleted: boolean;
37
+ }
38
+ interface CountdownSubscription {
39
+ unsubscribe: () => void;
40
+ }
41
+ interface CountdownEngineOptions {
42
+ onSnapshot?: (snapshot: CountdownSnapshot) => void;
43
+ onStateChange?: (state: TimerState, snapshot: CountdownSnapshot) => void;
44
+ onError?: (error: Error) => void;
45
+ timeProvider?: TimeProvider | (() => number);
46
+ tickIntervalMs?: number;
47
+ }
48
+ interface CountdownEngineInstance {
49
+ start: () => boolean;
50
+ pause: () => boolean;
51
+ resume: () => boolean;
52
+ reset: (nextInitialSeconds?: number) => boolean;
53
+ stop: () => boolean;
54
+ setSeconds: (seconds: number) => void;
55
+ getSnapshot: () => CountdownSnapshot;
56
+ subscribe: (listener: (snapshot: CountdownSnapshot) => void) => CountdownSubscription;
57
+ destroy: () => void;
58
+ }
59
+ declare function buildSnapshot(initialSeconds: number, totalSeconds: number, state: TimerState): CountdownSnapshot;
60
+ declare function CountdownEngine(initialSecondsInput: number, options?: CountdownEngineOptions): CountdownEngineInstance;
61
+
62
+ export { type CountdownSnapshot as C, TimerState as T, CountdownEngine as a, buildSnapshot as b, type CountdownEngineInstance as c, type CountdownEngineOptions as d, type CountdownParts as e, type TimeProvider as f };
@@ -0,0 +1 @@
1
+ 'use strict';function f(t){return typeof t=="number"?t:t&&typeof t.totalSeconds=="number"?t.totalSeconds:0}function i(t){return typeof t!="number"||!Number.isFinite(t)||t<=0?0:t>=Number.MAX_SAFE_INTEGER?Number.MAX_SAFE_INTEGER:Math.floor(t)}function o(t,a=2){return !Number.isFinite(t)||t<0?"0".repeat(a):Math.floor(t).toString().padStart(a,"0")}function s(t){return Math.floor(t/60)}function c(t){return s(t)%60}function d(t){return Math.floor(t/3600)%24}function g(t){return Math.floor(t/86400)%7}function p(t){return Math.floor(t/604800)%52}function S(t){return Math.floor(t/31536e3)}function m(){let t=r=>i(f(r));return {formatTime:r=>{let n=t(r);return {minutes:o(s(n)),seconds:o(n%60)}},formatMinutes:r=>{let n=t(r);return o(c(n))},formatSeconds:r=>{let n=t(r);return o(n%60)},formatHours:r=>{let n=t(r);return o(d(n))},formatDays:r=>{let n=t(r);return o(g(n))},formatWeeks:r=>{let n=t(r);return o(p(n))},formatYears:r=>{let n=t(r);return o(S(n))}}}var e=m();function l(t){return e.formatTime(t)}function E(t){return e.formatMinutes(t)}function T(t){return e.formatSeconds(t)}function x(t){return e.formatHours(t)}function F(t){return e.formatDays(t)}function b(t){return e.formatWeeks(t)}function _(t){return e.formatYears(t)}exports.Formatter=m;exports.defaultFormatter=e;exports.formatDays=F;exports.formatHours=x;exports.formatMinutes=E;exports.formatSeconds=T;exports.formatTime=l;exports.formatWeeks=b;exports.formatYears=_;
@@ -0,0 +1,49 @@
1
+ import { C as CountdownSnapshot } from './countdown-engine-qnWxt-ed.cjs';
2
+
3
+ type FormatTarget = number | Pick<CountdownSnapshot, 'totalSeconds'> | null | undefined;
4
+ declare function Formatter(): {
5
+ formatTime: (target: FormatTarget) => {
6
+ minutes: string;
7
+ seconds: string;
8
+ };
9
+ formatMinutes: (target: FormatTarget) => string;
10
+ formatSeconds: (target: FormatTarget) => string;
11
+ formatHours: (target: FormatTarget) => string;
12
+ formatDays: (target: FormatTarget) => string;
13
+ formatWeeks: (target: FormatTarget) => string;
14
+ formatYears: (target: FormatTarget) => string;
15
+ };
16
+ declare const defaultFormatter: {
17
+ formatTime: (target: FormatTarget) => {
18
+ minutes: string;
19
+ seconds: string;
20
+ };
21
+ formatMinutes: (target: FormatTarget) => string;
22
+ formatSeconds: (target: FormatTarget) => string;
23
+ formatHours: (target: FormatTarget) => string;
24
+ formatDays: (target: FormatTarget) => string;
25
+ formatWeeks: (target: FormatTarget) => string;
26
+ formatYears: (target: FormatTarget) => string;
27
+ };
28
+ declare function formatTime(target: number): {
29
+ minutes: string;
30
+ seconds: string;
31
+ };
32
+ declare function formatTime(target: Pick<CountdownSnapshot, 'totalSeconds'> | null | undefined): {
33
+ minutes: string;
34
+ seconds: string;
35
+ };
36
+ declare function formatMinutes(target: number): string;
37
+ declare function formatMinutes(target: Pick<CountdownSnapshot, 'totalSeconds'> | null | undefined): string;
38
+ declare function formatSeconds(target: number): string;
39
+ declare function formatSeconds(target: Pick<CountdownSnapshot, 'totalSeconds'> | null | undefined): string;
40
+ declare function formatHours(target: number): string;
41
+ declare function formatHours(target: Pick<CountdownSnapshot, 'totalSeconds'> | null | undefined): string;
42
+ declare function formatDays(target: number): string;
43
+ declare function formatDays(target: Pick<CountdownSnapshot, 'totalSeconds'> | null | undefined): string;
44
+ declare function formatWeeks(target: number): string;
45
+ declare function formatWeeks(target: Pick<CountdownSnapshot, 'totalSeconds'> | null | undefined): string;
46
+ declare function formatYears(target: number): string;
47
+ declare function formatYears(target: Pick<CountdownSnapshot, 'totalSeconds'> | null | undefined): string;
48
+
49
+ export { type FormatTarget, Formatter, defaultFormatter, formatDays, formatHours, formatMinutes, formatSeconds, formatTime, formatWeeks, formatYears };
package/dist/format.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { C as CountdownSnapshot } from './countdown-engine-tLXf4OVD.js';
1
+ import { C as CountdownSnapshot } from './countdown-engine-qnWxt-ed.js';
2
2
 
3
3
  type FormatTarget = number | Pick<CountdownSnapshot, 'totalSeconds'> | null | undefined;
4
4
  declare function Formatter(): {
@@ -25,15 +25,25 @@ declare const defaultFormatter: {
25
25
  formatWeeks: (target: FormatTarget) => string;
26
26
  formatYears: (target: FormatTarget) => string;
27
27
  };
28
- declare const formatTime: (target: FormatTarget) => {
28
+ declare function formatTime(target: number): {
29
29
  minutes: string;
30
30
  seconds: string;
31
31
  };
32
- declare const formatMinutes: (target: FormatTarget) => string;
33
- declare const formatSeconds: (target: FormatTarget) => string;
34
- declare const formatHours: (target: FormatTarget) => string;
35
- declare const formatDays: (target: FormatTarget) => string;
36
- declare const formatWeeks: (target: FormatTarget) => string;
37
- declare const formatYears: (target: FormatTarget) => string;
32
+ declare function formatTime(target: Pick<CountdownSnapshot, 'totalSeconds'> | null | undefined): {
33
+ minutes: string;
34
+ seconds: string;
35
+ };
36
+ declare function formatMinutes(target: number): string;
37
+ declare function formatMinutes(target: Pick<CountdownSnapshot, 'totalSeconds'> | null | undefined): string;
38
+ declare function formatSeconds(target: number): string;
39
+ declare function formatSeconds(target: Pick<CountdownSnapshot, 'totalSeconds'> | null | undefined): string;
40
+ declare function formatHours(target: number): string;
41
+ declare function formatHours(target: Pick<CountdownSnapshot, 'totalSeconds'> | null | undefined): string;
42
+ declare function formatDays(target: number): string;
43
+ declare function formatDays(target: Pick<CountdownSnapshot, 'totalSeconds'> | null | undefined): string;
44
+ declare function formatWeeks(target: number): string;
45
+ declare function formatWeeks(target: Pick<CountdownSnapshot, 'totalSeconds'> | null | undefined): string;
46
+ declare function formatYears(target: number): string;
47
+ declare function formatYears(target: Pick<CountdownSnapshot, 'totalSeconds'> | null | undefined): string;
38
48
 
39
49
  export { type FormatTarget, Formatter, defaultFormatter, formatDays, formatHours, formatMinutes, formatSeconds, formatTime, formatWeeks, formatYears };
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 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};
1
+ function f(t){return typeof t=="number"?t:t&&typeof t.totalSeconds=="number"?t.totalSeconds:0}function i(t){return typeof t!="number"||!Number.isFinite(t)||t<=0?0:t>=Number.MAX_SAFE_INTEGER?Number.MAX_SAFE_INTEGER:Math.floor(t)}function o(t,a=2){return !Number.isFinite(t)||t<0?"0".repeat(a):Math.floor(t).toString().padStart(a,"0")}function s(t){return Math.floor(t/60)}function c(t){return s(t)%60}function d(t){return Math.floor(t/3600)%24}function g(t){return Math.floor(t/86400)%7}function p(t){return Math.floor(t/604800)%52}function S(t){return Math.floor(t/31536e3)}function m(){let t=r=>i(f(r));return {formatTime:r=>{let n=t(r);return {minutes:o(s(n)),seconds:o(n%60)}},formatMinutes:r=>{let n=t(r);return o(c(n))},formatSeconds:r=>{let n=t(r);return o(n%60)},formatHours:r=>{let n=t(r);return o(d(n))},formatDays:r=>{let n=t(r);return o(g(n))},formatWeeks:r=>{let n=t(r);return o(p(n))},formatYears:r=>{let n=t(r);return o(S(n))}}}var e=m();function l(t){return e.formatTime(t)}function E(t){return e.formatMinutes(t)}function T(t){return e.formatSeconds(t)}function x(t){return e.formatHours(t)}function F(t){return e.formatDays(t)}function b(t){return e.formatWeeks(t)}function _(t){return e.formatYears(t)}export{m as Formatter,e as defaultFormatter,F as formatDays,x as formatHours,E as formatMinutes,T as formatSeconds,l as formatTime,b as formatWeeks,_ as formatYears};
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ 'use strict';function Y(){return {now:()=>performance.now(),isHighResolution:true,type:"performance"}}function W(){return {now:()=>Date.now(),isHighResolution:false,type:"date"}}function w(){let t,e=W();function s(){try{if(typeof performance<"u"&&typeof performance.now=="function"){let n=performance.now();if(typeof n=="number"&&Number.isFinite(n)&&!isNaN(n)&&n>=0&&n<=Number.MAX_SAFE_INTEGER){t=Y();return}}throw new Error("performance.now() not available or invalid")}catch{t=e;}}return s(),{now:()=>{try{let n=t.now();if(typeof n=="number"&&Number.isFinite(n)&&!isNaN(n)&&n>=0&&n<=Number.MAX_SAFE_INTEGER)return n;throw new Error("Invalid time value returned")}catch{t!==e&&(t=e);try{let n=e.now();if(typeof n=="number"&&Number.isFinite(n)&&!isNaN(n)&&n>=0)return n}catch{}return 0}},get isHighResolution(){return t.isHighResolution},get type(){return t.type}}}w();var L=10,V=100;function D(t,e,s={}){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");let n=Math.floor(Math.max(0,t)),m=null,i=null,u=0,c=n,r=n,a=Math.max(L,Math.floor(s.tickIntervalMs??V)),b=w(),d=s.timeProvider??(()=>b.now()),p=()=>setInterval(()=>{try{if(i===null)return;let E=d()-i-u,x=Math.floor(E/1e3),o=Math.max(0,r-x);n=o,o!==c&&(c=o,e.onTick(o)),o===0&&(l(),e.onComplete());}catch(S){l(),e.onError(S instanceof Error?S:new Error(String(S)));}},a),P=()=>{if(m||n<=0)return false;try{if(i!==null&&n<r){let S=(r-n)*1e3;u=d()-i-S;}else i=d(),u=0,c=n;return m=p(),!0}catch(S){return e.onError(S instanceof Error?S:new Error(String(S))),false}},l=()=>{m&&(clearInterval(m),m=null);},g=()=>{l(),n=r,i=null,u=0,c=r;},v=S=>{if(typeof S!="number"||!Number.isFinite(S))return;let E=Math.floor(Math.max(0,Math.min(S,Number.MAX_SAFE_INTEGER)));n=E,i=null,u=0,c=E;};return {start:P,stop:l,reset:g,setSeconds:v,setInitialValue:S=>{if(typeof S!="number"||!Number.isFinite(S))return;let E=Math.floor(Math.max(0,Math.min(S,Number.MAX_SAFE_INTEGER)));r=E,v(E);},getTotalSeconds:()=>n,getInitialValue:()=>r,isRunning:()=>m!==null,destroy:()=>{l(),n=0,i=null,u=0;}}}var f=Object.freeze({IDLE:"IDLE",RUNNING:"RUNNING",PAUSED:"PAUSED",STOPPED:"STOPPED"}),M={[f.IDLE]:{start:f.RUNNING},[f.RUNNING]:{pause:f.PAUSED,reset:f.IDLE,stop:f.STOPPED,complete:f.STOPPED},[f.PAUSED]:{resume:f.RUNNING,reset:f.IDLE,stop:f.STOPPED},[f.STOPPED]:{reset:f.IDLE}};function _(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=f.IDLE;function s(b){let d=M[e]?.[b];if(!d)return false;e=d;try{t?.onStateChange?.(d);}catch{}return true}function n(){return s("start")}function m(){return s("resume")}function i(){return s("pause")}function u(){return s("reset")}function c(){return s("stop")}function r(){return s("complete")}function a(){e===f.IDLE||e===f.STOPPED||s("stop");}return {start:n,resume:m,pause:i,reset:u,stop:c,complete:r,getCurrentState:()=>e,canStart:()=>!!M[e]?.start,canResume:()=>!!M[e]?.resume,canPause:()=>!!M[e]?.pause,isRunning:()=>e===f.RUNNING,destroy:a}}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=w();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)),s=Math.floor(e/31536e3),n=Math.floor(e/604800)%52,m=Math.floor(e/86400)%7,i=Math.floor(e/3600)%24,u=Math.floor(e/60)%60,c=e%60,r=Math.floor(e/86400),a=Math.floor(e/3600),b=Math.floor(e/60);return {years:s,weeks:n,days:m,hours:i,minutes:u,seconds:c,totalDays:r,totalHours:a,totalMinutes:b}}function y(t,e,s){let n=Q(e);return {initialSeconds:t,totalSeconds:e,parts:n,state:s,isRunning:s===f.RUNNING,isCompleted:e===0&&s===f.STOPPED}}function R(t,e={}){let s=O(t),n=J(e.timeProvider),m=s,i=y(s,s,f.IDLE),u=new Set,c=o=>{try{e.onSnapshot?.(o);}catch{}for(let N of u)try{N(o);}catch{}},r=o=>{i=y(m,i.totalSeconds,o);try{e.onStateChange?.(o,i);}catch{}c(i);},a=o=>{i=y(m,o,d.getCurrentState()),c(i);},b=o=>{try{e.onError?.(o);}catch{}},d=_({onStateChange:o=>{r(o);}}),p=D(s,{onTick:o=>{a(o);},onComplete:()=>{d.complete(),a(0);},onError:o=>{b(o),d.stop();}},{timeProvider:n,tickIntervalMs:e.tickIntervalMs});return {start:()=>{if(!d.canStart())return false;let o=p.start();return o&&(d.start(),a(p.getTotalSeconds())),o},pause:()=>{if(!d.canPause())return false;p.stop();let o=d.pause();return o&&a(p.getTotalSeconds()),o},resume:()=>{if(!d.canResume())return false;let o=p.start();return o&&(d.resume(),a(p.getTotalSeconds())),o},reset:o=>{if(p.stop(),typeof o=="number"){let N=O(o);m=N,p.setInitialValue(N);}else m=s,p.reset();return d.reset(),a(p.getTotalSeconds()),true},stop:()=>{p.stop();let o=d.stop();return o&&(p.setSeconds(0),a(0)),o},setSeconds:o=>{p.setSeconds(o),a(p.getTotalSeconds());},getSnapshot:()=>i,subscribe:o=>{u.add(o);try{o(i);}catch{}return {unsubscribe:()=>{u.delete(o);}}},destroy:()=>{p.destroy(),d.destroy(),u.clear(),i=y(m,0,f.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 h(t,e=2){return !Number.isFinite(t)||t<0?"0".repeat(e):Math.floor(t).toString().padStart(e,"0")}function k(t){return Math.floor(t/60)}function tt(t){return k(t)%60}function et(t){return Math.floor(t/3600)%24}function nt(t){return Math.floor(t/86400)%7}function ot(t){return Math.floor(t/604800)%52}function rt(t){return Math.floor(t/31536e3)}function I(){let t=r=>$(Z(r));return {formatTime:r=>{let a=t(r);return {minutes:h(k(a)),seconds:h(a%60)}},formatMinutes:r=>{let a=t(r);return h(tt(a))},formatSeconds:r=>{let a=t(r);return h(a%60)},formatHours:r=>{let a=t(r);return h(et(a))},formatDays:r=>{let a=t(r);return h(nt(a))},formatWeeks:r=>{let a=t(r);return h(ot(a))},formatYears:r=>{let a=t(r);return h(rt(a))}}}var T=I();function at(t){return T.formatTime(t)}function st(t){return T.formatMinutes(t)}function it(t){return T.formatSeconds(t)}function ut(t){return T.formatHours(t)}function ct(t){return T.formatDays(t)}function mt(t){return T.formatWeeks(t)}function ft(t){return T.formatYears(t)}function dt(t,e={}){let{onSnapshot:s,onStateChange:n,onError:m}=e;if(s!==void 0&&typeof s!="function")throw new Error("onSnapshot must be a function");if(n!==void 0&&typeof n!="function")throw new Error("onStateChange must be a function");if(m!==void 0&&typeof m!="function")throw new Error("onError must be a function");let i=I(),u=R(t,{onError:m}),c=u.getSnapshot(),r=c.state,a=Number.NaN,b=l=>{if(m)try{m(l);}catch{}},d=l=>{if(s)try{s(l);}catch(g){b(g instanceof Error?g:new Error(String(g)));}},p=l=>{if(n)try{n(l);}catch(g){b(g instanceof Error?g:new Error(String(g)));}},P=u.subscribe(l=>{c=l,l.state!==r&&(r=l.state,p(l.state)),l.totalSeconds!==a&&(a=l.totalSeconds,d(l));});return {start:()=>{u.start();},pause:()=>{u.pause();},resume:()=>{u.resume();},reset:l=>{u.reset(l);},stop:()=>{u.stop();},getSeconds:()=>i.formatSeconds(c.totalSeconds),getMinutes:()=>i.formatMinutes(c.totalSeconds),getHours:()=>i.formatHours(c.totalSeconds),getDays:()=>i.formatDays(c.totalSeconds),getWeeks:()=>i.formatWeeks(c.totalSeconds),getYears:()=>i.formatYears(c.totalSeconds),getCurrentState:()=>c.state,getSnapshot:()=>c,destroy:()=>{try{P.unsubscribe();}finally{u.destroy();}}}}exports.Countdown=dt;exports.CountdownEngine=R;exports.Formatter=I;exports.TimerState=f;exports.buildSnapshot=y;exports.defaultFormatter=T;exports.formatDays=ct;exports.formatHours=ut;exports.formatMinutes=st;exports.formatSeconds=it;exports.formatTime=at;exports.formatWeeks=mt;exports.formatYears=ft;
@@ -0,0 +1,28 @@
1
+ import { C as CountdownSnapshot, T as TimerState } from './countdown-engine-qnWxt-ed.cjs';
2
+ export { a as CountdownEngine, c as CountdownEngineInstance, d as CountdownEngineOptions, e as CountdownParts, b as buildSnapshot } from './countdown-engine-qnWxt-ed.cjs';
3
+ export { FormatTarget, Formatter, defaultFormatter, formatDays, formatHours, formatMinutes, formatSeconds, formatTime, formatWeeks, formatYears } from './format.cjs';
4
+
5
+ interface CountdownOptions {
6
+ onSnapshot?: (snapshot: CountdownSnapshot) => void;
7
+ onStateChange?: (state: TimerState) => void;
8
+ onError?: (error: Error) => void;
9
+ }
10
+ interface CountdownInstance {
11
+ start: () => void;
12
+ pause: () => void;
13
+ resume: () => void;
14
+ reset: (newInitialSeconds?: number) => void;
15
+ stop: () => void;
16
+ getSeconds: () => string;
17
+ getMinutes: () => string;
18
+ getHours: () => string;
19
+ getDays: () => string;
20
+ getWeeks: () => string;
21
+ getYears: () => string;
22
+ getCurrentState: () => TimerState;
23
+ getSnapshot: () => CountdownSnapshot;
24
+ destroy: () => void;
25
+ }
26
+ declare function Countdown(initialSeconds: number, options?: CountdownOptions): CountdownInstance;
27
+
28
+ export { Countdown, type CountdownInstance, type CountdownOptions, CountdownSnapshot, TimerState };
package/dist/index.d.ts CHANGED
@@ -1,10 +1,11 @@
1
- import { T as TimerState, C as CountdownSnapshot } from './countdown-engine-tLXf4OVD.js';
2
- export { a as CountdownEngine, b as CountdownEngineInstance, c as CountdownEngineOptions, d as CountdownParts } from './countdown-engine-tLXf4OVD.js';
1
+ import { C as CountdownSnapshot, T as TimerState } from './countdown-engine-qnWxt-ed.js';
2
+ export { a as CountdownEngine, c as CountdownEngineInstance, d as CountdownEngineOptions, e as CountdownParts, b as buildSnapshot } from './countdown-engine-qnWxt-ed.js';
3
3
  export { FormatTarget, Formatter, defaultFormatter, formatDays, formatHours, formatMinutes, formatSeconds, formatTime, formatWeeks, formatYears } from './format.js';
4
4
 
5
5
  interface CountdownOptions {
6
- onUpdate?: (minutes: string, seconds: string) => void;
6
+ onSnapshot?: (snapshot: CountdownSnapshot) => void;
7
7
  onStateChange?: (state: TimerState) => void;
8
+ onError?: (error: Error) => void;
8
9
  }
9
10
  interface CountdownInstance {
10
11
  start: () => void;
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 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};
1
+ function Y(){return {now:()=>performance.now(),isHighResolution:true,type:"performance"}}function W(){return {now:()=>Date.now(),isHighResolution:false,type:"date"}}function w(){let t,e=W();function s(){try{if(typeof performance<"u"&&typeof performance.now=="function"){let n=performance.now();if(typeof n=="number"&&Number.isFinite(n)&&!isNaN(n)&&n>=0&&n<=Number.MAX_SAFE_INTEGER){t=Y();return}}throw new Error("performance.now() not available or invalid")}catch{t=e;}}return s(),{now:()=>{try{let n=t.now();if(typeof n=="number"&&Number.isFinite(n)&&!isNaN(n)&&n>=0&&n<=Number.MAX_SAFE_INTEGER)return n;throw new Error("Invalid time value returned")}catch{t!==e&&(t=e);try{let n=e.now();if(typeof n=="number"&&Number.isFinite(n)&&!isNaN(n)&&n>=0)return n}catch{}return 0}},get isHighResolution(){return t.isHighResolution},get type(){return t.type}}}w();var L=10,V=100;function D(t,e,s={}){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");let n=Math.floor(Math.max(0,t)),m=null,i=null,u=0,c=n,r=n,a=Math.max(L,Math.floor(s.tickIntervalMs??V)),b=w(),d=s.timeProvider??(()=>b.now()),p=()=>setInterval(()=>{try{if(i===null)return;let E=d()-i-u,x=Math.floor(E/1e3),o=Math.max(0,r-x);n=o,o!==c&&(c=o,e.onTick(o)),o===0&&(l(),e.onComplete());}catch(S){l(),e.onError(S instanceof Error?S:new Error(String(S)));}},a),P=()=>{if(m||n<=0)return false;try{if(i!==null&&n<r){let S=(r-n)*1e3;u=d()-i-S;}else i=d(),u=0,c=n;return m=p(),!0}catch(S){return e.onError(S instanceof Error?S:new Error(String(S))),false}},l=()=>{m&&(clearInterval(m),m=null);},g=()=>{l(),n=r,i=null,u=0,c=r;},v=S=>{if(typeof S!="number"||!Number.isFinite(S))return;let E=Math.floor(Math.max(0,Math.min(S,Number.MAX_SAFE_INTEGER)));n=E,i=null,u=0,c=E;};return {start:P,stop:l,reset:g,setSeconds:v,setInitialValue:S=>{if(typeof S!="number"||!Number.isFinite(S))return;let E=Math.floor(Math.max(0,Math.min(S,Number.MAX_SAFE_INTEGER)));r=E,v(E);},getTotalSeconds:()=>n,getInitialValue:()=>r,isRunning:()=>m!==null,destroy:()=>{l(),n=0,i=null,u=0;}}}var f=Object.freeze({IDLE:"IDLE",RUNNING:"RUNNING",PAUSED:"PAUSED",STOPPED:"STOPPED"}),M={[f.IDLE]:{start:f.RUNNING},[f.RUNNING]:{pause:f.PAUSED,reset:f.IDLE,stop:f.STOPPED,complete:f.STOPPED},[f.PAUSED]:{resume:f.RUNNING,reset:f.IDLE,stop:f.STOPPED},[f.STOPPED]:{reset:f.IDLE}};function _(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=f.IDLE;function s(b){let d=M[e]?.[b];if(!d)return false;e=d;try{t?.onStateChange?.(d);}catch{}return true}function n(){return s("start")}function m(){return s("resume")}function i(){return s("pause")}function u(){return s("reset")}function c(){return s("stop")}function r(){return s("complete")}function a(){e===f.IDLE||e===f.STOPPED||s("stop");}return {start:n,resume:m,pause:i,reset:u,stop:c,complete:r,getCurrentState:()=>e,canStart:()=>!!M[e]?.start,canResume:()=>!!M[e]?.resume,canPause:()=>!!M[e]?.pause,isRunning:()=>e===f.RUNNING,destroy:a}}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=w();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)),s=Math.floor(e/31536e3),n=Math.floor(e/604800)%52,m=Math.floor(e/86400)%7,i=Math.floor(e/3600)%24,u=Math.floor(e/60)%60,c=e%60,r=Math.floor(e/86400),a=Math.floor(e/3600),b=Math.floor(e/60);return {years:s,weeks:n,days:m,hours:i,minutes:u,seconds:c,totalDays:r,totalHours:a,totalMinutes:b}}function y(t,e,s){let n=Q(e);return {initialSeconds:t,totalSeconds:e,parts:n,state:s,isRunning:s===f.RUNNING,isCompleted:e===0&&s===f.STOPPED}}function R(t,e={}){let s=O(t),n=J(e.timeProvider),m=s,i=y(s,s,f.IDLE),u=new Set,c=o=>{try{e.onSnapshot?.(o);}catch{}for(let N of u)try{N(o);}catch{}},r=o=>{i=y(m,i.totalSeconds,o);try{e.onStateChange?.(o,i);}catch{}c(i);},a=o=>{i=y(m,o,d.getCurrentState()),c(i);},b=o=>{try{e.onError?.(o);}catch{}},d=_({onStateChange:o=>{r(o);}}),p=D(s,{onTick:o=>{a(o);},onComplete:()=>{d.complete(),a(0);},onError:o=>{b(o),d.stop();}},{timeProvider:n,tickIntervalMs:e.tickIntervalMs});return {start:()=>{if(!d.canStart())return false;let o=p.start();return o&&(d.start(),a(p.getTotalSeconds())),o},pause:()=>{if(!d.canPause())return false;p.stop();let o=d.pause();return o&&a(p.getTotalSeconds()),o},resume:()=>{if(!d.canResume())return false;let o=p.start();return o&&(d.resume(),a(p.getTotalSeconds())),o},reset:o=>{if(p.stop(),typeof o=="number"){let N=O(o);m=N,p.setInitialValue(N);}else m=s,p.reset();return d.reset(),a(p.getTotalSeconds()),true},stop:()=>{p.stop();let o=d.stop();return o&&(p.setSeconds(0),a(0)),o},setSeconds:o=>{p.setSeconds(o),a(p.getTotalSeconds());},getSnapshot:()=>i,subscribe:o=>{u.add(o);try{o(i);}catch{}return {unsubscribe:()=>{u.delete(o);}}},destroy:()=>{p.destroy(),d.destroy(),u.clear(),i=y(m,0,f.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 h(t,e=2){return !Number.isFinite(t)||t<0?"0".repeat(e):Math.floor(t).toString().padStart(e,"0")}function k(t){return Math.floor(t/60)}function tt(t){return k(t)%60}function et(t){return Math.floor(t/3600)%24}function nt(t){return Math.floor(t/86400)%7}function ot(t){return Math.floor(t/604800)%52}function rt(t){return Math.floor(t/31536e3)}function I(){let t=r=>$(Z(r));return {formatTime:r=>{let a=t(r);return {minutes:h(k(a)),seconds:h(a%60)}},formatMinutes:r=>{let a=t(r);return h(tt(a))},formatSeconds:r=>{let a=t(r);return h(a%60)},formatHours:r=>{let a=t(r);return h(et(a))},formatDays:r=>{let a=t(r);return h(nt(a))},formatWeeks:r=>{let a=t(r);return h(ot(a))},formatYears:r=>{let a=t(r);return h(rt(a))}}}var T=I();function at(t){return T.formatTime(t)}function st(t){return T.formatMinutes(t)}function it(t){return T.formatSeconds(t)}function ut(t){return T.formatHours(t)}function ct(t){return T.formatDays(t)}function mt(t){return T.formatWeeks(t)}function ft(t){return T.formatYears(t)}function dt(t,e={}){let{onSnapshot:s,onStateChange:n,onError:m}=e;if(s!==void 0&&typeof s!="function")throw new Error("onSnapshot must be a function");if(n!==void 0&&typeof n!="function")throw new Error("onStateChange must be a function");if(m!==void 0&&typeof m!="function")throw new Error("onError must be a function");let i=I(),u=R(t,{onError:m}),c=u.getSnapshot(),r=c.state,a=Number.NaN,b=l=>{if(m)try{m(l);}catch{}},d=l=>{if(s)try{s(l);}catch(g){b(g instanceof Error?g:new Error(String(g)));}},p=l=>{if(n)try{n(l);}catch(g){b(g instanceof Error?g:new Error(String(g)));}},P=u.subscribe(l=>{c=l,l.state!==r&&(r=l.state,p(l.state)),l.totalSeconds!==a&&(a=l.totalSeconds,d(l));});return {start:()=>{u.start();},pause:()=>{u.pause();},resume:()=>{u.resume();},reset:l=>{u.reset(l);},stop:()=>{u.stop();},getSeconds:()=>i.formatSeconds(c.totalSeconds),getMinutes:()=>i.formatMinutes(c.totalSeconds),getHours:()=>i.formatHours(c.totalSeconds),getDays:()=>i.formatDays(c.totalSeconds),getWeeks:()=>i.formatWeeks(c.totalSeconds),getYears:()=>i.formatYears(c.totalSeconds),getCurrentState:()=>c.state,getSnapshot:()=>c,destroy:()=>{try{P.unsubscribe();}finally{u.destroy();}}}}export{dt as Countdown,R as CountdownEngine,I as Formatter,f as TimerState,y as buildSnapshot,T as defaultFormatter,ct as formatDays,ut as formatHours,st as formatMinutes,it as formatSeconds,at as formatTime,mt as formatWeeks,ft as formatYears};
@@ -0,0 +1 @@
1
+ 'use strict';var c=t=>!Number.isFinite(t)||t<0?0:t>Number.MAX_SAFE_INTEGER?Number.MAX_SAFE_INTEGER:Math.floor(t);function h(t={}){let{startMs:e=0,tickMs:r=1e3,highResolution:a=true}=t,o=c(e),i=c(r);return {now:()=>o,advance:(s=i)=>(o=c(o+c(s)),o),set:s=>(o=c(s),o),reset:()=>(o=c(e),o),getTime:()=>o,isHighResolution:a,type:"fake"}}function T(t){return {now:()=>t.now(),isHighResolution:t.isHighResolution,type:t.type}}var n=Object.freeze({IDLE:"IDLE",RUNNING:"RUNNING",PAUSED:"PAUSED",STOPPED:"STOPPED"});({[n.IDLE]:{start:n.RUNNING},[n.RUNNING]:{pause:n.PAUSED,reset:n.IDLE,stop:n.STOPPED,complete:n.STOPPED},[n.PAUSED]:{resume:n.RUNNING,reset:n.IDLE,stop:n.STOPPED},[n.STOPPED]:{reset:n.IDLE}});var m=t=>typeof t!="number"||!Number.isFinite(t)||t<=0?0:t>=Number.MAX_SAFE_INTEGER?Number.MAX_SAFE_INTEGER:Math.floor(t),l=t=>{let e=m(t),r=Math.floor(e/31536e3),a=Math.floor(e/604800)%52,o=Math.floor(e/86400)%7,i=Math.floor(e/3600)%24,S=Math.floor(e/60)%60,d=e%60,p=Math.floor(e/86400),u=Math.floor(e/3600),s=Math.floor(e/60);return {years:r,weeks:a,days:o,hours:i,minutes:S,seconds:d,totalDays:p,totalHours:u,totalMinutes:s}};function b(t={}){let e=m(t.initialSeconds??t.totalSeconds??0),r=m(t.totalSeconds??t.initialSeconds??0),a=t.state??(r>0?n.IDLE:n.STOPPED),o=l(r);return {initialSeconds:e,totalSeconds:r,parts:o,state:a,isRunning:a===n.RUNNING,isCompleted:r===0&&a===n.STOPPED}}function w(t={}){let{totalSeconds:e=0,step:r=1,count:a=1,initialSeconds:o}=t,i=m(e),S=m(r),d=m(a)||1,p=[];for(let u=0;u<d;u+=1){let s=Math.max(i-S*u,0);p.push(b({initialSeconds:o??i,totalSeconds:s,state:s===0?n.STOPPED:n.RUNNING}));}return p}var f=(t,e)=>e?`${t}: ${e}`:t;function x(t,e,r){if(t.state!==e)throw new Error(f(r??"Unexpected countdown state",`expected ${e} but received ${t.state}`))}function I(t,e){if(!t.isCompleted||t.totalSeconds!==0)throw new Error(f(e??"Countdown should be completed"))}function y(t,e,r=0,a){if(typeof e!="number"||!Number.isFinite(e))throw new Error("Expected remaining seconds must be a finite number");if(Math.abs(t.totalSeconds-Math.floor(e))>r)throw new Error(f(a??"Unexpected remaining seconds",`expected ${e}\xB1${r} but received ${t.totalSeconds}`))}exports.TimerState=n;exports.assertRemainingSeconds=y;exports.assertSnapshotCompleted=I;exports.assertSnapshotState=x;exports.buildSnapshot=b;exports.buildSnapshotSequence=w;exports.createFakeTimeProvider=h;exports.toTimeProvider=T;
@@ -0,0 +1,33 @@
1
+ import { f as TimeProvider, T as TimerState, C as CountdownSnapshot } from './countdown-engine-qnWxt-ed.cjs';
2
+
3
+ interface FakeTimeProvider extends TimeProvider {
4
+ advance: (ms?: number) => number;
5
+ set: (ms: number) => number;
6
+ reset: () => number;
7
+ getTime: () => number;
8
+ }
9
+ interface FakeTimeOptions {
10
+ startMs?: number;
11
+ tickMs?: number;
12
+ highResolution?: boolean;
13
+ }
14
+ declare function createFakeTimeProvider(options?: FakeTimeOptions): FakeTimeProvider;
15
+ declare function toTimeProvider(fake: FakeTimeProvider): TimeProvider;
16
+
17
+ interface SnapshotOptions {
18
+ initialSeconds?: number;
19
+ totalSeconds?: number;
20
+ state?: TimerState;
21
+ }
22
+ declare function buildSnapshot(options?: SnapshotOptions): CountdownSnapshot;
23
+ interface SequenceOptions extends SnapshotOptions {
24
+ step?: number;
25
+ count?: number;
26
+ }
27
+ declare function buildSnapshotSequence(options?: SequenceOptions): CountdownSnapshot[];
28
+
29
+ declare function assertSnapshotState(snapshot: CountdownSnapshot, expected: TimerState, message?: string): void;
30
+ declare function assertSnapshotCompleted(snapshot: CountdownSnapshot, message?: string): void;
31
+ declare function assertRemainingSeconds(snapshot: CountdownSnapshot, expected: number, tolerance?: number, message?: string): void;
32
+
33
+ export { type FakeTimeOptions, type FakeTimeProvider, type SequenceOptions, type SnapshotOptions, TimerState, assertRemainingSeconds, assertSnapshotCompleted, assertSnapshotState, buildSnapshot, buildSnapshotSequence, createFakeTimeProvider, toTimeProvider };
@@ -1,4 +1,4 @@
1
- import { e as TimeProvider, T as TimerState, C as CountdownSnapshot } from './countdown-engine-tLXf4OVD.js';
1
+ import { f as TimeProvider, T as TimerState, C as CountdownSnapshot } from './countdown-engine-qnWxt-ed.js';
2
2
 
3
3
  interface FakeTimeProvider extends TimeProvider {
4
4
  advance: (ms?: number) => number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timekeeper-countdown/core",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "Lightweight countdown engine that powers the Timekeeper packages. Pure TypeScript with no runtime dependencies.",
5
5
  "keywords": [
6
6
  "countdown",
@@ -29,15 +29,18 @@
29
29
  "exports": {
30
30
  ".": {
31
31
  "types": "./dist/index.d.ts",
32
- "import": "./dist/index.js"
32
+ "import": "./dist/index.js",
33
+ "require": "./dist/index.cjs"
33
34
  },
34
35
  "./format": {
35
36
  "types": "./dist/format.d.ts",
36
- "import": "./dist/format.js"
37
+ "import": "./dist/format.js",
38
+ "require": "./dist/format.cjs"
37
39
  },
38
40
  "./testing-utils": {
39
41
  "types": "./dist/testing-utils.d.ts",
40
- "import": "./dist/testing-utils.js"
42
+ "import": "./dist/testing-utils.js",
43
+ "require": "./dist/testing-utils.cjs"
41
44
  }
42
45
  },
43
46
  "files": [