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:
pewdiepie-archdaemon
2026-06-05 17:05:04 +09:00
parent 2ba77e3aa3
commit e0e250d023

View File

@@ -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.