feat(web): wire TimerChain[] into Zustand store with auto-start

- chains[] persisted alongside timers in localStorage
- addChain/removeChain with linkedTimerId bookkeeping
- removeTimer auto-cleans chain references
- complete() auto-starts next timer in chain via getNextInChain
- 373/373 tests pass
This commit is contained in:
saravanakumardb1 2026-02-28 13:52:20 -08:00
parent 233fde8f99
commit ad4bc946a8

View File

@ -2,6 +2,8 @@
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import type { Timer, CreateAlarmParams, CreateCountdownParams, CreatePomodoroParams, CreateEventParams } from './timer-engine';
import type { TimerChain } from './linked-timers';
import { buildChain, getNextInChain, findChainForTimer } from './linked-timers';
import {
createAlarm,
createCountdown,
@ -23,6 +25,7 @@ import { trackTimerCreated, trackTimerCompleted, trackCascadeFired } from './ana
export interface TimerStore {
timers: Timer[];
chains: TimerChain[];
now: number; // current time for reactivity
// CRUD
@ -32,6 +35,11 @@ export interface TimerStore {
addEvent: (params: CreateEventParams) => Timer;
removeTimer: (id: string) => void;
// Chain operations
addChain: (name: string, timerIds: string[], delayMs?: number) => TimerChain;
removeChain: (chainId: string) => void;
getChainForTimer: (timerId: string) => TimerChain | null;
// State transitions
pause: (id: string) => void;
resume: (id: string) => void;
@ -58,6 +66,7 @@ export const useTimerStore = create<TimerStore>()(
persist(
(set, get) => ({
timers: [],
chains: [],
now: Date.now(),
addAlarm: (params) => {
@ -89,7 +98,41 @@ export const useTimerStore = create<TimerStore>()(
},
removeTimer: (id) => {
set((s) => ({ timers: s.timers.filter((t) => t.id !== id) }));
set((s) => ({
timers: s.timers.filter((t) => t.id !== id),
chains: s.chains
.map((c) => ({
...c,
timerIds: c.timerIds.filter((tid) => tid !== id),
links: c.links.filter((l) => l.fromId !== id && l.toId !== id),
}))
.filter((c) => c.timerIds.length > 1),
}));
},
addChain: (name, timerIds, delayMs = 0) => {
const chain = buildChain(name, timerIds, delayMs);
set((s) => ({
chains: [...s.chains, chain],
timers: s.timers.map((t) =>
timerIds.includes(t.id) ? { ...t, linkedTimerId: chain.id } : t
),
}));
return chain;
},
removeChain: (chainId) => {
const chain = get().chains.find((c) => c.id === chainId);
set((s) => ({
chains: s.chains.filter((c) => c.id !== chainId),
timers: s.timers.map((t) =>
chain && chain.timerIds.includes(t.id) ? { ...t, linkedTimerId: null } : t
),
}));
},
getChainForTimer: (timerId) => {
return findChainForTimer(get().chains, timerId);
},
pause: (id) => {
@ -118,6 +161,23 @@ export const useTimerStore = create<TimerStore>()(
trackTimerCompleted(timer.type, timer.targetTime - timer.createdAt);
}
set((s) => ({ timers: updateTimer(s.timers, id, completeTimer) }));
// Auto-start next timer in chain
const { chains, timers } = get();
const link = getNextInChain(chains, id);
if (link) {
const nextTimer = timers.find((t) => t.id === link.toId);
if (nextTimer && nextTimer.state === 'idle') {
const now = Date.now();
const targetTime = now + (nextTimer.duration ?? 0) + link.delay;
set((s) => ({
timers: s.timers.map((t) =>
t.id === link.toId
? { ...t, state: 'active' as const, startedAt: now + link.delay, targetTime }
: t
),
}));
}
}
},
advancePom: (id) => {
@ -201,7 +261,7 @@ export const useTimerStore = create<TimerStore>()(
}
return localStorage;
}),
partialize: (state) => ({ timers: state.timers }),
partialize: (state) => ({ timers: state.timers, chains: state.chains }),
}
)
);