fix(reminders): RMW race, reliable removal, stable poll interval (N113/N114/N115)
- N113: mutations compute from a local ref kept in sync with server echoes, and writes serialize through a promise queue, so rapid add/remove no longer reads a stale baseline and clobbers a prior write. - N114: ReminderMonitor shows each toast once (firedRef) but retries the account-data removal on later ticks if it fails (removingRef released on error) — a failed removal no longer permanently swallows the reminder. - N115: the 30s poll interval reads reminders/mDirects via refs and drops them from the effect deps, so it's created once instead of resetting its countdown on every reminder sync (which could indefinitely defer a near-due reminder). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -390,16 +390,26 @@ function ReminderMonitor() {
|
||||
const setToast = useSetAtom(toastQueueAtom);
|
||||
const mDirects = useAtomValue(mDirectAtom);
|
||||
const firedRef = useRef<Set<string>>(new Set());
|
||||
const removingRef = useRef<Set<string>>(new Set());
|
||||
// Read the latest reminders / DM map via refs so the poll interval below is
|
||||
// created once — not torn down and restarted (which resets its 30s countdown
|
||||
// and can indefinitely defer a near-due reminder) on every reminder sync (N115).
|
||||
const remindersRef = useRef(reminders);
|
||||
remindersRef.current = reminders;
|
||||
const mDirectsRef = useRef(mDirects);
|
||||
mDirectsRef.current = mDirects;
|
||||
|
||||
useEffect(() => {
|
||||
const check = () => {
|
||||
const now = Date.now();
|
||||
reminders.forEach((r) => {
|
||||
remindersRef.current.forEach((r) => {
|
||||
if (r.timestamp > now) return;
|
||||
const key = `${r.eventId}-${r.timestamp}`;
|
||||
if (r.timestamp <= now && !firedRef.current.has(key)) {
|
||||
// Show the toast exactly once.
|
||||
if (!firedRef.current.has(key)) {
|
||||
firedRef.current.add(key);
|
||||
const room = mx.getRoom(r.roomId);
|
||||
const hashPath = mDirects.has(r.roomId)
|
||||
const hashPath = mDirectsRef.current.has(r.roomId)
|
||||
? getDirectRoomPath(r.roomId, r.eventId)
|
||||
: getHomeRoomPath(r.roomId, r.eventId);
|
||||
setToast({
|
||||
@@ -410,7 +420,15 @@ function ReminderMonitor() {
|
||||
roomId: r.roomId,
|
||||
hashPath,
|
||||
});
|
||||
removeReminder(r.eventId, r.timestamp);
|
||||
}
|
||||
// Persist the removal, retrying on a later tick if it fails — without
|
||||
// re-showing the toast (N114). The server echo drops it from
|
||||
// `reminders` once the write lands.
|
||||
if (!removingRef.current.has(key)) {
|
||||
removingRef.current.add(key);
|
||||
removeReminder(r.eventId, r.timestamp).catch(() => {
|
||||
removingRef.current.delete(key);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -425,7 +443,7 @@ function ReminderMonitor() {
|
||||
clearInterval(interval);
|
||||
document.removeEventListener('visibilitychange', onVisible);
|
||||
};
|
||||
}, [mx, reminders, setToast, removeReminder, mDirects]);
|
||||
}, [mx, setToast, removeReminder]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user