function createTodaySessionSync({ fetchJson, getDeviceId, timer, onRemote = () => {}, onTransferred = () => {}, onStatus = () => {}, setInterval = globalThis.setInterval, clearInterval = globalThis.clearInterval, }) { let current = null; let ownedRevision = 0; let pollTimer = null; const endpoint = 'api/v1/today/session'; const deviceId = () => String(getDeviceId?.() || '').trim(); function adopt(session) { if (!session || !Number.isInteger(session.revision)) return null; const previousOwned = current?.device_id === deviceId() && current?.running; current = session; if (session.device_id === deviceId()) { ownedRevision = session.revision; onRemote(null); } else if (session.running && session.identity) { if (previousOwned) { timer?.pause?.(); onTransferred(session); } onRemote(session); } else { onRemote(null); } return session; } async function refresh() { try { const session = await fetchJson(endpoint); onStatus('online'); return adopt(session); } catch (error) { onStatus('offline', error); return null; } } async function claim() { if (!current?.running || !current.identity || current.device_id === deviceId() || !deviceId()) return null; try { const session = await fetchJson(endpoint, { method:'PATCH', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ base_revision:current.revision, device_id:deviceId(), identity:current.identity, elapsed_ms:current.elapsed_ms, running:true, }), }); adopt(session); timer?.adopt?.(session.identity, session.elapsed_ms, session.running); return session; } catch (error) { onStatus('conflict', error); await refresh(); return null; } } async function publish(snapshot = timer?.snapshot?.()) { if (!snapshot?.identity || !deviceId()) return null; try { const session = await fetchJson(endpoint, { method:'PATCH', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ base_revision:ownedRevision, device_id:deviceId(), identity:snapshot.identity, elapsed_ms:Math.max(0, Math.floor(Number(snapshot.elapsed_ms) || 0)), running:Boolean(snapshot.running), }), }); adopt(session); return session; } catch (error) { onStatus('offline', error); return null; } } const pulse = () => current?.device_id === deviceId() && current?.running ? publish() : refresh(); return { refresh, claim, publish, session:() => current, start(intervalMs = 15000) { if (pollTimer !== null) return false; pulse(); pollTimer = setInterval?.(pulse, intervalMs); pollTimer?.unref?.(); return true; }, stop() { if (pollTimer === null) return false; clearInterval?.(pollTimer); pollTimer = null; return true; }, }; } function attachTodaySessionHandoff({ fetchJson, storage, timer, qs, items, identity, selectToday, startItem, announce, renderTimer, }) { const deviceKey = 'stackchain.today-session-device.v1'; const getDeviceId = () => { let value = storage.getItem(deviceKey); if (!value) { value = globalThis.crypto?.randomUUID?.() || Math.random().toString(36).slice(2); storage.setItem(deviceKey, value); } return value; }; let offered = null; const sync = createTodaySessionSync({ fetchJson, getDeviceId, timer, onRemote:session => { offered = session; const handoff = qs('#today-session-handoff'); handoff.hidden = !session; if (!session) return; const item = items().find(entry => identity(entry) === session.identity); const minutes = Math.max(0, Math.floor(Number(session.elapsed_ms || 0) / 60000)); qs('#today-session-handoff-summary').textContent = `${item?.title || 'Current Today item'} ยท ${minutes} min elapsed`; }, onTransferred:() => { announce('Today continued on another device. Timer paused here.'); renderTimer(); }, }); qs('#continue-today-session').addEventListener('click', async () => { const target = offered; const claimed = await sync.claim(); if (!claimed || !target) return; selectToday(); const item = items().find(entry => identity(entry) === claimed.identity); if (item) startItem(item); else announce('Today session moved here; refresh work to open its item.'); renderTimer(); }); sync.start(5000); return sync; } if (typeof module !== 'undefined' && module.exports) module.exports = { createTodaySessionSync, attachTodaySessionHandoff, };