Calendar: cross-session delete sync — 404 = success, refetch on tab focus
A stale event deleted on one device stayed undeletable on every other session: the cached row showed up, the DELETE call returned 404 (server already removed it), the optimistic catch-block restored the row, and the user could never clear it. - Treat HTTP 404 on DELETE as success — the event is already gone, which is the state we wanted. Skip the optimistic restore. - Re-fetch the visible range on document `visibilitychange` (mobile app returns to foreground) and on window `focus` (desktop alt-tab), throttled to once per 10s so rapid tab-flipping doesn't hammer the API. Without a focus refresh, mobile only got fresh server state at page-load and lived on stale data until a full reload.
This commit is contained in:
@@ -328,7 +328,11 @@ async function _deleteEvent(uid) {
|
||||
fetch(`${API_BASE}/api/calendar/events/${encodeURIComponent(uid)}`, {
|
||||
method: 'DELETE', credentials: 'same-origin',
|
||||
}).then(r => {
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
// 404 = the event was already deleted by another session/device. That's
|
||||
// exactly the state we want, so treat it as success — don't restore the
|
||||
// row, otherwise the user can never clear stale cached events that were
|
||||
// deleted from desktop while mobile was open (and vice versa).
|
||||
if (!r.ok && r.status !== 404) throw new Error('HTTP ' + r.status);
|
||||
if (isRecurring) {
|
||||
_fetchedRanges = [];
|
||||
localStorage.removeItem(LS_KEY);
|
||||
@@ -3435,6 +3439,44 @@ window.addEventListener('calendar-refresh', () => {
|
||||
.catch(() => {});
|
||||
});
|
||||
|
||||
// Cross-session catch-up: when the tab/app becomes visible again (you alt-tab
|
||||
// back, the mobile app comes to the foreground, or you switch back from
|
||||
// another browser session), drop the range cache and re-fetch. Without this,
|
||||
// a delete or add on desktop never propagates to the still-open mobile tab
|
||||
// until the user does a full reload — so stale events sit there undeletable
|
||||
// (they 404 on the server). Triggers on every visibility change but the
|
||||
// fetch is cheap and already de-duped by _fetchPromise on line ~120.
|
||||
let _lastVisRefetchAt = 0;
|
||||
const _VIS_REFETCH_MIN_MS = 10 * 1000; // throttle if user is rapidly tab-flipping
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState !== 'visible') return;
|
||||
const now = Date.now();
|
||||
if (now - _lastVisRefetchAt < _VIS_REFETCH_MIN_MS) return;
|
||||
_lastVisRefetchAt = now;
|
||||
_fetchedRanges = [];
|
||||
const range = (_view === 'year')
|
||||
? [`${_currentDate.getFullYear()}-01-01`, `${_currentDate.getFullYear() + 1}-01-01`]
|
||||
: (_view === 'week') ? _weekRange(_currentDate) : _monthRange(_currentDate);
|
||||
_fetchEvents(range[0], range[1], /*force*/ true)
|
||||
.then(() => { if (_open) _render(); _updateBadge(); })
|
||||
.catch(() => {});
|
||||
});
|
||||
|
||||
// Same idea for window-level focus — covers desktop alt-tabbing back to a
|
||||
// browser that already had the tab visible (visibilitychange won't fire).
|
||||
window.addEventListener('focus', () => {
|
||||
const now = Date.now();
|
||||
if (now - _lastVisRefetchAt < _VIS_REFETCH_MIN_MS) return;
|
||||
_lastVisRefetchAt = now;
|
||||
_fetchedRanges = [];
|
||||
const range = (_view === 'year')
|
||||
? [`${_currentDate.getFullYear()}-01-01`, `${_currentDate.getFullYear() + 1}-01-01`]
|
||||
: (_view === 'week') ? _weekRange(_currentDate) : _monthRange(_currentDate);
|
||||
_fetchEvents(range[0], range[1], /*force*/ true)
|
||||
.then(() => { if (_open) _render(); _updateBadge(); })
|
||||
.catch(() => {});
|
||||
});
|
||||
|
||||
// Calendar reminders are stored as Notes. The Notes reminder loop owns
|
||||
// notification dispatch so calendar reminders do not fire twice.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user