'use strict'; // ---- Changes made with no signal: adding a task, ticking one off, adding to the shopping list and writing a note are kept on this device and sent when the connection is back. // Only these few safe changes are queued (nothing that could clash with what someone else did meanwhile); anything else says it needs a connection. Tasks show the waiting changes. const OFFQ_RULES = [[/^\/api\/tasks$/, b => !b.id && String(b.list || '').startsWith('local:')], [/^\/api\/tasks\/toggle$/, b => String(b.list || '').startsWith('local:')], [/^\/api\/recipes\/shopping\/add$/, () => true], [/^\/api\/notes\/$/, b => !b.id]]; const offqKey = () => 'offq:' + (typeof ME !== 'undefined' && ME ? ME.id : ''); const offqRead = () => { try { const a = JSON.parse(localStorage.getItem(offqKey())); return Array.isArray(a) ? a : []; } catch { return []; } }; const offqWrite = l => { try { if (l.length) localStorage.setItem(offqKey(), JSON.stringify(l)); else localStorage.removeItem(offqKey()); } catch { /* storage full or blocked */ } offqNote(); }; const offPending = () => offqRead().length; function offqNote() { const n = offPending(); let el = document.getElementById('offnote'); if (!el && n) { el = Object.assign(document.createElement('div'), { id: 'offnote', role: 'status' }); document.body.appendChild(el); } if (!el) return; if (n || !navigator.onLine) { el.hidden = false; el.textContent = (navigator.onLine ? '' : 'You are offline. Showing what was last saved. ') + (n ? n + ' change' + (n === 1 ? '' : 's') + ' waiting to send.' : 'Changes need a connection.'); } else el.hidden = true; } /** Called by jfetch when a request failed on the network: keep it for later if it is one of the safe kinds. Returns the answer to give, or null (the error stands). */ function offQueued(path, opts) { const method = String((opts && opts.method) || 'GET').toUpperCase(); if (method !== 'POST' || typeof (opts && opts.body) !== 'string') return null; let b; try { b = JSON.parse(opts.body); } catch { return null; } const rule = OFFQ_RULES.find(r => r[0].test(path)); if (!rule || !b || !rule[1](b)) return null; const l = offqRead(); if (l.length >= 100) return null; l.push({ id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6), path, body: opts.body, t: Date.now() }); offqWrite(l); toast('Saved on this device. It sends when you are back online.'); return { ok: true, queued: true }; } let offqBusy = false; /** Send what is waiting, oldest first; stops at the first network failure, and drops a change the server refuses (saying so). */ async function offqFlush() { if (offqBusy || !navigator.onLine) return; offqBusy = true; let sent = 0; try { for (;;) { const l = offqRead(); if (!l.length) break; const it = l[0]; let r; try { r = await fetch(it.path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: it.body }); } catch { break; } if (!r.ok && r.status >= 500) break; // (the server is having trouble: try again later) offqWrite(offqRead().filter(x => x.id !== it.id)); if (r.ok) sent++; else { const j = await r.json().catch(() => ({})); toast('A change made offline was not accepted: ' + (j.error || r.status)); } } } finally { offqBusy = false; } if (sent) { toast(sent + ' change' + (sent === 1 ? '' : 's') + ' sent'); if (/^#\/(tasks|planner)/.test(location.hash) && typeof route === 'function') route(); } } /** What the Tasks list should show while changes are waiting: queued ticks applied, and queued new tasks added (marked as waiting). */ function offApplyTasks(tasks) { const q = offqRead(); if (!q.length) return tasks; const out = tasks.map(t => ({ ...t })); for (const it of q) { let b; try { b = JSON.parse(it.body); } catch { continue; } if (/toggle$/.test(it.path)) { const t = out.find(x => x.id === b.id); if (t) t.done = !!b.done; } else if (it.path === '/api/tasks' && b.fields) out.push({ id: 'queued:' + it.id, list: b.list, title: b.fields.title, notes: b.fields.notes || '', due: b.fields.due || '', dueTime: b.fields.dueTime || '', done: !!b.fields.done, parent: '', repeat: 'none', _ln: 'waiting to send' }); } return out; } addEventListener('online', () => { offqNote(); setTimeout(offqFlush, 800); }); addEventListener('offline', offqNote); setTimeout(offqFlush, 4000); setInterval(offqFlush, 60e3);