rn-persistent-timer 1.1.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.
Files changed (55) hide show
  1. package/README.md +607 -0
  2. package/android/.gradle/7.4.2/checksums/checksums.lock +0 -0
  3. package/android/.gradle/7.4.2/fileChanges/last-build.bin +0 -0
  4. package/android/.gradle/7.4.2/fileHashes/fileHashes.lock +0 -0
  5. package/android/.gradle/7.4.2/gc.properties +0 -0
  6. package/android/.gradle/vcs-1/gc.properties +0 -0
  7. package/android/build.gradle +15 -0
  8. package/android/src/main/java/com/rnpersistenttimer/RNPersistentTimerModule.java +164 -0
  9. package/android/src/main/java/com/rnpersistenttimer/RNPersistentTimerPackage.java +27 -0
  10. package/android/src/main/java/com/rnpersistenttimer/TimerForegroundService.java +280 -0
  11. package/ios/RNPersistentTimer.h +10 -0
  12. package/ios/RNPersistentTimer.m +221 -0
  13. package/lib/commonjs/NativeTimerModule.js +46 -0
  14. package/lib/commonjs/NativeTimerModule.js.map +1 -0
  15. package/lib/commonjs/PersistentTimerManager.js +337 -0
  16. package/lib/commonjs/PersistentTimerManager.js.map +1 -0
  17. package/lib/commonjs/index.js +76 -0
  18. package/lib/commonjs/index.js.map +1 -0
  19. package/lib/commonjs/types.js +2 -0
  20. package/lib/commonjs/types.js.map +1 -0
  21. package/lib/commonjs/usePersistentTimer.js +159 -0
  22. package/lib/commonjs/usePersistentTimer.js.map +1 -0
  23. package/lib/commonjs/utils.js +112 -0
  24. package/lib/commonjs/utils.js.map +1 -0
  25. package/lib/module/NativeTimerModule.js +40 -0
  26. package/lib/module/NativeTimerModule.js.map +1 -0
  27. package/lib/module/PersistentTimerManager.js +329 -0
  28. package/lib/module/PersistentTimerManager.js.map +1 -0
  29. package/lib/module/index.js +17 -0
  30. package/lib/module/index.js.map +1 -0
  31. package/lib/module/types.js +2 -0
  32. package/lib/module/types.js.map +1 -0
  33. package/lib/module/usePersistentTimer.js +153 -0
  34. package/lib/module/usePersistentTimer.js.map +1 -0
  35. package/lib/module/utils.js +100 -0
  36. package/lib/module/utils.js.map +1 -0
  37. package/lib/typescript/NativeTimerModule.d.ts +31 -0
  38. package/lib/typescript/NativeTimerModule.d.ts.map +1 -0
  39. package/lib/typescript/PersistentTimerManager.d.ts +37 -0
  40. package/lib/typescript/PersistentTimerManager.d.ts.map +1 -0
  41. package/lib/typescript/index.d.ts +7 -0
  42. package/lib/typescript/index.d.ts.map +1 -0
  43. package/lib/typescript/types.d.ts +167 -0
  44. package/lib/typescript/types.d.ts.map +1 -0
  45. package/lib/typescript/usePersistentTimer.d.ts +16 -0
  46. package/lib/typescript/usePersistentTimer.d.ts.map +1 -0
  47. package/lib/typescript/utils.d.ts +36 -0
  48. package/lib/typescript/utils.d.ts.map +1 -0
  49. package/package.json +98 -0
  50. package/src/NativeTimerModule.ts +73 -0
  51. package/src/PersistentTimerManager.ts +410 -0
  52. package/src/index.ts +41 -0
  53. package/src/types.ts +198 -0
  54. package/src/usePersistentTimer.tsx +173 -0
  55. package/src/utils.ts +91 -0
@@ -0,0 +1,410 @@
1
+ // rn-persistent-timer — PersistentTimerManager
2
+ // Handles foreground, background, and killed-state timer logic
3
+ // ─────────────────────────────────────────────────────────────────────────────
4
+
5
+ import { AppState as RNAppState, AppStateStatus } from 'react-native';
6
+ import AsyncStorage from '@react-native-async-storage/async-storage';
7
+ import { NativeTimerModule } from './NativeTimerModule';
8
+ import { formatTime } from './utils';
9
+ import type {
10
+ TimerConfig,
11
+ TimerState,
12
+ AppState,
13
+ TimerSnapshot,
14
+ TimerCallbacks,
15
+ PersistedTimerRecord,
16
+ RestoredTimerData,
17
+ } from './types';
18
+
19
+ const STORAGE_KEY_PREFIX = '@rn_persistent_timer_';
20
+
21
+ type EventName = keyof TimerCallbacks | 'stop' | 'error';
22
+ type HandlerMap = Partial<Record<EventName, Function[]>>;
23
+
24
+ // ─────────────────────────────────────────────────────────────────────────────
25
+
26
+ export class PersistentTimerManager {
27
+ private _config: Required<TimerConfig>;
28
+ private _state: TimerState = 'idle';
29
+ private _elapsed = 0;
30
+ private _startedAt: number | null = null;
31
+ private _pausedAt: number | null = null;
32
+ private _tickInterval: ReturnType<typeof setInterval> | null = null;
33
+ private _handlers: HandlerMap = {};
34
+ private _appState: AppState = 'foreground';
35
+ private _appStateSubscription: ReturnType<
36
+ typeof RNAppState.addEventListener
37
+ > | null = null;
38
+
39
+ constructor(config: TimerConfig) {
40
+ this._config = {
41
+ timerId: config.timerId,
42
+ mode: config.mode ?? 'stopwatch',
43
+ duration: config.duration ?? 0,
44
+ runInBackground: config.runInBackground !== false, // default true
45
+ runInKilledState: config.runInKilledState === true, // default false
46
+ pauseOnBackground: config.pauseOnBackground === true,
47
+ resetOnForeground: config.resetOnForeground === true,
48
+ interval: config.interval ?? 1000,
49
+ showNotification: config.showNotification !== false,
50
+ notification: config.notification ?? {},
51
+ };
52
+ }
53
+
54
+ // ─── Public API ─────────────────────────────────────────────────────────────
55
+
56
+ start(): void {
57
+ if (this._state === 'running') {
58
+ return;
59
+ }
60
+ if (this._state === 'completed') {
61
+ console.warn(
62
+ '[rn-persistent-timer] Timer already completed. Call reset() first.',
63
+ );
64
+ return;
65
+ }
66
+
67
+ this._startedAt = Date.now() - this._elapsed * 1000;
68
+ this._state = 'running';
69
+
70
+ this._startJSTick();
71
+ this._listenAppState();
72
+
73
+ if (this._config.runInBackground) {
74
+ this._startNativeBackground();
75
+ }
76
+ if (this._config.runInKilledState) {
77
+ void this._persistState();
78
+ }
79
+
80
+ this._emit('onStart', this.getSnapshot());
81
+ }
82
+
83
+ pause(): void {
84
+ if (this._state !== 'running') {
85
+ return;
86
+ }
87
+ this._pausedAt = Date.now();
88
+ this._elapsed = Math.floor((Date.now() - (this._startedAt ?? Date.now())) / 1000);
89
+ this._state = 'paused';
90
+
91
+ this._stopJSTick();
92
+ this._stopNativeBackground();
93
+
94
+ if (this._config.runInKilledState) {
95
+ void this._persistState();
96
+ }
97
+ this._emit('onPause', this.getSnapshot());
98
+ }
99
+
100
+ resume(): void {
101
+ if (this._state !== 'paused') {
102
+ return;
103
+ }
104
+ this._startedAt = Date.now() - this._elapsed * 1000;
105
+ this._pausedAt = null;
106
+ this._state = 'running';
107
+
108
+ this._startJSTick();
109
+ if (this._config.runInBackground) {
110
+ this._startNativeBackground();
111
+ }
112
+ if (this._config.runInKilledState) {
113
+ void this._persistState();
114
+ }
115
+ this._emit('onResume', this.getSnapshot());
116
+ }
117
+
118
+ stop(): void {
119
+ if (this._state === 'idle') {
120
+ return;
121
+ }
122
+ this._stopJSTick();
123
+ this._stopNativeBackground();
124
+ this._state = 'idle';
125
+ void this._clearPersistedState();
126
+ this._emit('stop', this.getSnapshot());
127
+ }
128
+
129
+ reset(): void {
130
+ this._stopJSTick();
131
+ this._stopNativeBackground();
132
+ this._elapsed = 0;
133
+ this._startedAt = null;
134
+ this._pausedAt = null;
135
+ this._state = 'idle';
136
+ void this._clearPersistedState();
137
+ this._emit('onReset', this.getSnapshot());
138
+ }
139
+
140
+ getSnapshot(): TimerSnapshot {
141
+ const elapsed = this._getElapsed();
142
+ const remaining =
143
+ this._config.mode === 'countdown'
144
+ ? Math.max(0, this._config.duration - elapsed)
145
+ : null;
146
+ const progress =
147
+ this._config.mode === 'countdown' && this._config.duration > 0
148
+ ? 1 - (remaining ?? 0) / this._config.duration
149
+ : null;
150
+
151
+ return {
152
+ timerId: this._config.timerId,
153
+ elapsed,
154
+ remaining,
155
+ state: this._state,
156
+ appState: this._appState,
157
+ startedAt: this._startedAt,
158
+ pausedAt: this._pausedAt,
159
+ formattedElapsed: formatTime(elapsed),
160
+ formattedRemaining: remaining !== null ? formatTime(remaining) : null,
161
+ progress,
162
+ };
163
+ }
164
+
165
+ destroy(): void {
166
+ this.stop();
167
+ if (this._appStateSubscription) {
168
+ this._appStateSubscription.remove();
169
+ this._appStateSubscription = null;
170
+ }
171
+ this._handlers = {};
172
+ }
173
+
174
+ on(event: EventName, handler: Function): void {
175
+ if (!this._handlers[event]) {
176
+ this._handlers[event] = [];
177
+ }
178
+ this._handlers[event]!.push(handler);
179
+ }
180
+
181
+ off(event: EventName, handler: Function): void {
182
+ if (!this._handlers[event]) {
183
+ return;
184
+ }
185
+ this._handlers[event] = this._handlers[event]!.filter((h) => h !== handler);
186
+ }
187
+
188
+ // ─── Internal Tick ───────────────────────────────────────────────────────────
189
+
190
+ private _startJSTick(): void {
191
+ this._stopJSTick();
192
+ this._tickInterval = setInterval(() => {
193
+ this._onTick();
194
+ }, this._config.interval);
195
+ }
196
+
197
+ private _stopJSTick(): void {
198
+ if (this._tickInterval !== null) {
199
+ clearInterval(this._tickInterval);
200
+ this._tickInterval = null;
201
+ }
202
+ }
203
+
204
+ private _onTick(): void {
205
+ const snap = this.getSnapshot();
206
+
207
+ if (this._config.mode === 'countdown' && snap.remaining === 0) {
208
+ this._state = 'completed';
209
+ this._stopJSTick();
210
+ this._stopNativeBackground();
211
+ void this._clearPersistedState();
212
+ this._emit('onComplete', snap);
213
+ return;
214
+ }
215
+
216
+ this._emit('onTick', snap);
217
+ }
218
+
219
+ private _getElapsed(): number {
220
+ if (this._state === 'running' && this._startedAt !== null) {
221
+ return Math.floor((Date.now() - this._startedAt) / 1000);
222
+ }
223
+ return this._elapsed;
224
+ }
225
+
226
+ // ─── App State ───────────────────────────────────────────────────────────────
227
+
228
+ private _listenAppState(): void {
229
+ if (this._appStateSubscription) {
230
+ return;
231
+ }
232
+
233
+ this._appStateSubscription = RNAppState.addEventListener(
234
+ 'change',
235
+ (nextAppState: AppStateStatus) => {
236
+ const prev = this._appState;
237
+
238
+ if (nextAppState === 'active') {
239
+ this._appState = 'foreground';
240
+
241
+ if (prev === 'background' && this._config.runInBackground) {
242
+ this._syncFromNative();
243
+ }
244
+
245
+ if (this._config.resetOnForeground) {
246
+ this.reset();
247
+ return;
248
+ }
249
+
250
+ if (this._state === 'running') {
251
+ this._startJSTick();
252
+ }
253
+
254
+ this._emit('onForeground', this.getSnapshot());
255
+ } else if (
256
+ nextAppState === 'background' ||
257
+ nextAppState === 'inactive'
258
+ ) {
259
+ this._appState = 'background';
260
+
261
+ if (this._config.pauseOnBackground) {
262
+ this.pause();
263
+ } else {
264
+ this._stopJSTick();
265
+ }
266
+
267
+ this._emit('onBackground', this.getSnapshot());
268
+ }
269
+ },
270
+ );
271
+ }
272
+
273
+ // ─── Native Background ───────────────────────────────────────────────────────
274
+
275
+ private _startNativeBackground(): void {
276
+ if (!NativeTimerModule) {
277
+ return;
278
+ }
279
+ try {
280
+ NativeTimerModule.startTimer({
281
+ timerId: this._config.timerId,
282
+ mode: this._config.mode,
283
+ duration: this._config.duration,
284
+ elapsed: this._getElapsed(),
285
+ startedAt: this._startedAt ?? Date.now(),
286
+ showNotification: this._config.showNotification,
287
+ notification: this._config.notification as Record<string, unknown>,
288
+ });
289
+ } catch (e) {
290
+ this._emit('error', e);
291
+ }
292
+ }
293
+
294
+ private _stopNativeBackground(): void {
295
+ if (!NativeTimerModule) {
296
+ return;
297
+ }
298
+ try {
299
+ NativeTimerModule.stopTimer(this._config.timerId);
300
+ } catch (e) {
301
+ this._emit('error', e);
302
+ }
303
+ }
304
+
305
+ private _syncFromNative(): void {
306
+ if (!NativeTimerModule) {
307
+ return;
308
+ }
309
+ NativeTimerModule.getElapsed(this._config.timerId)
310
+ .then((nativeElapsed) => {
311
+ if (nativeElapsed != null) {
312
+ this._elapsed = nativeElapsed;
313
+ this._startedAt = Date.now() - nativeElapsed * 1000;
314
+ }
315
+ })
316
+ .catch(() => {
317
+ // Fallback: wall-clock diff handles it via _startedAt
318
+ });
319
+ }
320
+
321
+ // ─── Killed-State Persistence ────────────────────────────────────────────────
322
+
323
+ private async _persistState(): Promise<void> {
324
+ try {
325
+ const record: PersistedTimerRecord = {
326
+ timerId: this._config.timerId,
327
+ config: this._config,
328
+ state: this._state,
329
+ elapsed: this._getElapsed(),
330
+ startedAt: this._startedAt,
331
+ pausedAt: this._pausedAt,
332
+ savedAt: Date.now(),
333
+ };
334
+ await AsyncStorage.setItem(
335
+ `${STORAGE_KEY_PREFIX}${this._config.timerId}`,
336
+ JSON.stringify(record),
337
+ );
338
+ } catch (e) {
339
+ console.warn('[rn-persistent-timer] Failed to persist state:', e);
340
+ }
341
+ }
342
+
343
+ private async _clearPersistedState(): Promise<void> {
344
+ try {
345
+ await AsyncStorage.removeItem(
346
+ `${STORAGE_KEY_PREFIX}${this._config.timerId}`,
347
+ );
348
+ } catch {
349
+ // Swallow
350
+ }
351
+ }
352
+
353
+ // ─── Static Restore ──────────────────────────────────────────────────────────
354
+
355
+ static async restore(
356
+ timerId: string,
357
+ ): Promise<RestoredTimerData | null> {
358
+ try {
359
+ const raw = await AsyncStorage.getItem(
360
+ `${STORAGE_KEY_PREFIX}${timerId}`,
361
+ );
362
+ if (!raw) {
363
+ return null;
364
+ }
365
+
366
+ const saved: PersistedTimerRecord = JSON.parse(raw);
367
+ const now = Date.now();
368
+ const secondsSinceSave = Math.floor((now - saved.savedAt) / 1000);
369
+
370
+ let elapsed = saved.elapsed;
371
+ if (saved.state === 'running') {
372
+ elapsed = saved.elapsed + secondsSinceSave;
373
+ }
374
+
375
+ // Cap countdown timers at their duration
376
+ if (
377
+ saved.config.mode === 'countdown' &&
378
+ elapsed >= saved.config.duration
379
+ ) {
380
+ elapsed = saved.config.duration;
381
+ saved.state = 'completed';
382
+ }
383
+
384
+ return {
385
+ config: saved.config,
386
+ state: saved.state,
387
+ elapsed,
388
+ secondsLost: secondsSinceSave,
389
+ };
390
+ } catch {
391
+ return null;
392
+ }
393
+ }
394
+
395
+ // ─── Event Emitter ───────────────────────────────────────────────────────────
396
+
397
+ private _emit(event: EventName, data: unknown): void {
398
+ const handlers = this._handlers[event] ?? [];
399
+ for (const h of handlers) {
400
+ try {
401
+ h(data);
402
+ } catch (e) {
403
+ console.error(
404
+ `[rn-persistent-timer] Handler error for "${event}":`,
405
+ e,
406
+ );
407
+ }
408
+ }
409
+ }
410
+ }
package/src/index.ts ADDED
@@ -0,0 +1,41 @@
1
+ // rn-persistent-timer — Public API Barrel
2
+ // ─────────────────────────────────────────────────────────────────────────────
3
+
4
+ // Hook
5
+ export { usePersistentTimer } from './usePersistentTimer';
6
+
7
+ // Class
8
+ export { PersistentTimerManager } from './PersistentTimerManager';
9
+
10
+ // Utilities
11
+ export {
12
+ formatTime,
13
+ parseTime,
14
+ isBackgroundTimerSupported,
15
+ isKilledStateTimerSupported,
16
+ getActiveTimers,
17
+ cancelAllTimers,
18
+ } from './utils';
19
+
20
+ // Types — re-export everything so consumers only need one import path
21
+ export type {
22
+ TimerMode,
23
+ TimerState,
24
+ AppState,
25
+ TimerConfig,
26
+ AndroidNotificationConfig,
27
+ TimerSnapshot,
28
+ TimerCallbacks,
29
+ PersistentTimerControls,
30
+ UsePersistentTimerReturn,
31
+ PersistedTimerRecord,
32
+ RestoredTimerData,
33
+ } from './types';
34
+
35
+ // Native internals (advanced use only)
36
+ export {
37
+ NativeTimerModule,
38
+ NativeTimerEmitter,
39
+ NATIVE_EVENTS,
40
+ } from './NativeTimerModule';
41
+ export type { INativeTimerModule, NativeStartOptions, NativeEventName } from './NativeTimerModule';
package/src/types.ts ADDED
@@ -0,0 +1,198 @@
1
+ // rn-persistent-timer — Shared TypeScript Types
2
+ // ─────────────────────────────────────────────────────────────────────────────
3
+
4
+ export type TimerMode = 'countdown' | 'stopwatch';
5
+ export type TimerState = 'idle' | 'running' | 'paused' | 'completed';
6
+ export type AppState = 'foreground' | 'background' | 'killed';
7
+
8
+ // ─── Configuration ────────────────────────────────────────────────────────────
9
+
10
+ export interface AndroidNotificationConfig {
11
+ /** Notification title shown in status bar */
12
+ title?: string;
13
+ /** Notification body. Use {time} as a placeholder for the current timer value */
14
+ body?: string;
15
+ /** Android notification channel ID */
16
+ channelId?: string;
17
+ /** Android notification channel name */
18
+ channelName?: string;
19
+ /** Drawable resource name for the notification icon */
20
+ icon?: string;
21
+ /** Accent color (hex, e.g. '#FF5733') */
22
+ color?: string;
23
+ /** Show elapsed / remaining time in the notification */
24
+ showTime?: boolean;
25
+ /** Add pause / resume action buttons to the notification */
26
+ showActions?: boolean;
27
+ }
28
+
29
+ export interface TimerConfig {
30
+ /**
31
+ * Unique ID for this timer instance. Required.
32
+ * Used as the key for persistence and native interop.
33
+ */
34
+ timerId: string;
35
+
36
+ /**
37
+ * Timer mode.
38
+ * - `'countdown'` — counts down from `duration` to 0.
39
+ * - `'stopwatch'` — counts up from 0.
40
+ * @default 'stopwatch'
41
+ */
42
+ mode?: TimerMode;
43
+
44
+ /**
45
+ * Duration in seconds. Required when `mode === 'countdown'`.
46
+ */
47
+ duration?: number;
48
+
49
+ /**
50
+ * Keep the timer running when the app goes to the **background**.
51
+ * On Android this starts a Foreground Service; on iOS it uses
52
+ * `UIBackgroundTaskIdentifier` + `BGTaskScheduler`.
53
+ * @default true
54
+ */
55
+ runInBackground?: boolean;
56
+
57
+ /**
58
+ * Keep the timer alive even after the app is **killed** from the
59
+ * task manager. State is persisted via AsyncStorage and restored
60
+ * when the app is re-opened.
61
+ * Requires additional native setup — see README.
62
+ * @default false
63
+ */
64
+ runInKilledState?: boolean;
65
+
66
+ /**
67
+ * Tick interval in milliseconds.
68
+ * @default 1000
69
+ */
70
+ interval?: number;
71
+
72
+ /**
73
+ * Show a persistent notification while the timer is active (Android).
74
+ * Required for reliable background execution on Android 8+.
75
+ * @default true (when runInBackground is true)
76
+ */
77
+ showNotification?: boolean;
78
+
79
+ /** Android notification customisation. */
80
+ notification?: AndroidNotificationConfig;
81
+
82
+ /**
83
+ * Automatically pause when the app moves to the background.
84
+ * Overrides `runInBackground` when `true`.
85
+ * @default false
86
+ */
87
+ pauseOnBackground?: boolean;
88
+
89
+ /**
90
+ * Automatically reset when the app returns to the foreground from a
91
+ * killed state.
92
+ * @default false
93
+ */
94
+ resetOnForeground?: boolean;
95
+ }
96
+
97
+ // ─── Snapshot ─────────────────────────────────────────────────────────────────
98
+
99
+ export interface TimerSnapshot {
100
+ /** Timer ID */
101
+ timerId: string;
102
+ /** Elapsed seconds since the timer last started */
103
+ elapsed: number;
104
+ /** Remaining seconds — countdown mode only, otherwise `null` */
105
+ remaining: number | null;
106
+ /** Current timer state */
107
+ state: TimerState;
108
+ /** Current app state at the time the snapshot was taken */
109
+ appState: AppState;
110
+ /** Unix-ms timestamp when the timer was (last) started */
111
+ startedAt: number | null;
112
+ /** Unix-ms timestamp when the timer was paused */
113
+ pausedAt: number | null;
114
+ /** Elapsed time formatted as `HH:MM:SS` */
115
+ formattedElapsed: string;
116
+ /** Remaining time formatted as `HH:MM:SS` — countdown only, otherwise `null` */
117
+ formattedRemaining: string | null;
118
+ /** Progress from 0 to 1 — countdown only, otherwise `null` */
119
+ progress: number | null;
120
+ }
121
+
122
+ // ─── Callbacks ────────────────────────────────────────────────────────────────
123
+
124
+ export interface TimerCallbacks {
125
+ /** Fires every tick (based on `interval`). */
126
+ onTick?: (snapshot: TimerSnapshot) => void;
127
+ /** Fires when the timer starts. */
128
+ onStart?: (snapshot: TimerSnapshot) => void;
129
+ /** Fires when the timer is paused. */
130
+ onPause?: (snapshot: TimerSnapshot) => void;
131
+ /** Fires when the timer is resumed. */
132
+ onResume?: (snapshot: TimerSnapshot) => void;
133
+ /** Fires when the timer is reset. */
134
+ onReset?: (snapshot: TimerSnapshot) => void;
135
+ /** Fires when a countdown timer reaches zero. */
136
+ onComplete?: (snapshot: TimerSnapshot) => void;
137
+ /** Fires when the app moves to the background. */
138
+ onBackground?: (snapshot: TimerSnapshot) => void;
139
+ /** Fires when the app returns to the foreground. */
140
+ onForeground?: (snapshot: TimerSnapshot) => void;
141
+ /** Fires when timer state is restored after an app kill. */
142
+ onRestore?: (snapshot: TimerSnapshot) => void;
143
+ /** Fires on any internal error. */
144
+ onError?: (error: Error) => void;
145
+ }
146
+
147
+ // ─── Controls ─────────────────────────────────────────────────────────────────
148
+
149
+ export interface PersistentTimerControls {
150
+ /** Start (or restart from 0) the timer. */
151
+ start: () => void;
152
+ /** Pause the timer. */
153
+ pause: () => void;
154
+ /** Resume from a paused state. */
155
+ resume: () => void;
156
+ /** Stop the timer and reset elapsed to 0. */
157
+ reset: () => void;
158
+ /** Stop the timer without resetting elapsed time. */
159
+ stop: () => void;
160
+ /** Get the current snapshot synchronously. */
161
+ getSnapshot: () => TimerSnapshot;
162
+ /** Destroy the timer and remove all listeners. */
163
+ destroy: () => void;
164
+ }
165
+
166
+ // ─── Hook Return ──────────────────────────────────────────────────────────────
167
+
168
+ export interface UsePersistentTimerReturn extends PersistentTimerControls {
169
+ /** Latest reactive timer snapshot. */
170
+ snapshot: TimerSnapshot;
171
+ /** `true` when `state === 'running'`. */
172
+ isRunning: boolean;
173
+ /** `true` when `state === 'paused'`. */
174
+ isPaused: boolean;
175
+ /** `true` when `state === 'completed'` (countdown). */
176
+ isCompleted: boolean;
177
+ /** Current app state. */
178
+ appState: AppState;
179
+ }
180
+
181
+ // ─── Persistence Record ───────────────────────────────────────────────────────
182
+
183
+ export interface PersistedTimerRecord {
184
+ timerId: string;
185
+ config: Required<TimerConfig>;
186
+ state: TimerState;
187
+ elapsed: number;
188
+ startedAt: number | null;
189
+ pausedAt: number | null;
190
+ savedAt: number;
191
+ }
192
+
193
+ export interface RestoredTimerData {
194
+ config: Required<TimerConfig>;
195
+ state: TimerState;
196
+ elapsed: number;
197
+ secondsLost: number;
198
+ }