mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
fix(notifications): cap pre-scheduled recurring reminders (#7850)
iOS caps pending local notifications at 64 globally across every reminder effect and silently drops the overflow. Bound the recurring pre-scheduler to the soonest-firing REPEAT_MAX_SCHEDULED occurrences (matching iOS's own keep-soonest behaviour) and log any truncation rather than letting the OS drop an arbitrary subset. Add a regression test covering the cap.
This commit is contained in:
parent
5e0be5f97f
commit
e84e75d854
2 changed files with 49 additions and 0 deletions
|
|
@ -745,5 +745,32 @@ describe('MobileNotificationEffects', () => {
|
|||
generateNotificationId(tomorrowId),
|
||||
);
|
||||
}));
|
||||
|
||||
it('caps pre-scheduled reminders, keeping the soonest-firing (iOS 64-pending limit)', fakeAsync(() => {
|
||||
// More configs than the cap (REPEAT_MAX_SCHEDULED = 32 in the effect),
|
||||
// each due tomorrow at a distinct clock time 30 min apart (00:00 → 19:30).
|
||||
const cfgs = Array.from({ length: 40 }, (_, i) => {
|
||||
const h = String(Math.floor(i / 2)).padStart(2, '0');
|
||||
const m = i % 2 === 0 ? '00' : '30';
|
||||
return mkDailyCfg({ id: `cfg${i}`, startTime: `${h}:${m}` });
|
||||
});
|
||||
store.overrideSelector(selectActiveTaskRepeatCfgs, cfgs);
|
||||
subscribeRepeatReminders();
|
||||
|
||||
tick(REPEAT_SETTLE_MS + 1);
|
||||
|
||||
// Bounded to the cap, not the full 40.
|
||||
expect(reminderServiceSpy.scheduleReminder).toHaveBeenCalledTimes(32);
|
||||
// Soonest kept, last-within-cap kept, first-over-cap dropped.
|
||||
expect(reminderServiceSpy.scheduleReminder).toHaveBeenCalledWith(
|
||||
jasmine.objectContaining({ triggerAtMs: triggerFor(1, '00:00') }),
|
||||
);
|
||||
expect(reminderServiceSpy.scheduleReminder).toHaveBeenCalledWith(
|
||||
jasmine.objectContaining({ triggerAtMs: triggerFor(1, '15:30') }),
|
||||
);
|
||||
expect(reminderServiceSpy.scheduleReminder).not.toHaveBeenCalledWith(
|
||||
jasmine.objectContaining({ triggerAtMs: triggerFor(1, '16:00') }),
|
||||
);
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -37,6 +37,16 @@ const DELAY_SCHEDULE = 5000;
|
|||
// this window. See #7850.
|
||||
const REPEAT_LOOKAHEAD_DAYS = 14;
|
||||
|
||||
// Upper bound on how many recurring reminders we pre-schedule in one pass.
|
||||
// iOS caps pending local notifications at 64 GLOBALLY (across every reminder
|
||||
// effect), silently dropping the rest, and it keeps the soonest-firing ones.
|
||||
// We pre-schedule at most one occurrence per config, so this only bites a user
|
||||
// with an unusually large number of timed recurring tasks — but when it does we
|
||||
// want to deterministically keep the most imminent ones (matching iOS's own
|
||||
// keep-soonest behaviour) and leave headroom for the live/due-date effects,
|
||||
// rather than letting the OS choose for us. Truncation is logged, never silent.
|
||||
const REPEAT_MAX_SCHEDULED = 32;
|
||||
|
||||
// Settle window for the recurring-reminder scheduler. Instance creation
|
||||
// dispatches addTask → updateTaskRepeatCfg → scheduleTaskWithTime back-to-back,
|
||||
// which the store surfaces as several intermediate emissions. Debouncing
|
||||
|
|
@ -563,6 +573,18 @@ export class MobileNotificationEffects {
|
|||
}
|
||||
}
|
||||
|
||||
// Keep the soonest-firing occurrences when over the cap (see
|
||||
// REPEAT_MAX_SCHEDULED). The day loop already yields roughly ascending
|
||||
// order, but sort explicitly so the cap is deterministic.
|
||||
result.sort((a, b) => a.triggerAtMs - b.triggerAtMs);
|
||||
if (result.length > REPEAT_MAX_SCHEDULED) {
|
||||
Log.log('MobileEffects: capping pre-scheduled repeat reminders', {
|
||||
total: result.length,
|
||||
cap: REPEAT_MAX_SCHEDULED,
|
||||
});
|
||||
return result.slice(0, REPEAT_MAX_SCHEDULED);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue