-
-
Notifications
You must be signed in to change notification settings - Fork 4.5k
[calendar] fix: prevent excessive fetching on client reload and refactor calendarfetcherutils.js
#3976
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…ezone support - Simplify 'getMomentsFromRecurringEvent' using modern syntax - Improve handling of full-day events across different timezones
This change separates the expansion of recurring events from the main filtering loop into a new helper function 'expandRecurringEvent'. It resolves two TODOs: - 'This should be a separate function' - 'This should create an event per moment so we can change anything we want' This improves code readability, reduces complexity in 'filterEvents', and allows for cleaner handling of individual recurrence instances.
- Extract filtering logic from `shouldEventBeExcluded` into new helper `checkEventAgainstFilter` - Simplify the main loop in `shouldEventBeExcludedrefactor(calendar): simplify event exclusion logic
|
But 1 is how we get calendar updates. Resending the cached events doesn’t keep the calendar up to date |
f4cd8c0 to
182d3dd
Compare
Very good catch! Many thanks! In the meantime I've updated the implementation in commit 182d3dd to address exactly that concern. Now when the calendar is reloaded, it always broadcasts the cached events immediately (for fast UI response), and then checks if the data is older than 3 minutes - if so, it fetches fresh data in the background. This way you get instant display with cached data, plus automatic updates when needed, while still preventing rate limit issues from rapid reloads. I couldn't think of a simpler approach. I'm thinking about that I should split up this PR since it touches several points. Then we could look at each individual point more easily and decide whether we merge them or not. What do you think? |
|
Do you know what the impact of the multiple broadcasts does to the using modules? Ext3, calendarMonthly ext3 has a 10 min (default) refresh cycle |
- Add lastFetch timestamp tracking to CalendarFetcher - Add shouldRefetch() method with configurable minimum interval (default: 3 minutes) - When reusing existing fetcher: fetch if data is stale (>3 min), otherwise broadcast cached events - Prevents double broadcasts to consuming modules while maintaining fresh data - Balances rate limit prevention (Issue MagicMirrorOrg#3971) with data freshness on user reload - Prevents excessive fetching during rapid reloads (e.g., Fully Kiosk screensaver use case) - Allows fresh calendar data when enough time has passed since last fetch Fixes MagicMirrorOrg#3971
182d3dd to
8892cd3
Compare
Again a good point! I've just made a small adjustment to optimize this. As it stands now, it shouldn't have any noticeable effect on ext3 and similar modules. Calendar Fetch Strategy ComparisonKey Differences
Impact on Ext3 and Other ModulesBefore:
After:
|
|
Since calendars don't change that often, we could even set the default cache limit time higher. Maybe 5 or 10 minutes. And perhaps even offer it as an option that users can adjust. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On this
if (isFullDayEvent && rule.options?.until) {
rule.options.until = moment(rule.options.until).endOf("day").toDate();
}
I used start of day vs end of, cause end of could appear as tomorrow in some timezones. Start of day will never be wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point about timezones. But I think endOf("day") shouldn't cause issues in this case because it operates on the same calendar day - it just moves the time to 23:59:59 of that same day, never into the next day.
So even if someone is in a timezone like UTC+12, endOf("day") on May 5th stays May 5th 23:59:59 - it can't "appear as tomorrow" because we're not adding time, just extending to the end of the current day.
That said, this is admittedly a workaround for a deeper issue in rrule.js itself. The library has known problems with UNTIL and timezone handling (see rrule#596, open since Aug 2023), and unfortunately rrule.js appears to be unmaintained at this point (~180 open issues).
I'm working on replacing rrule.js with rrule-temporal in node-ical to solve these problems at the root. rrule-temporal is built on the Temporal API which handles timezones correctly by design. Since this is a major change, it will take a while to implement it, so unfortunately I think we will need to use workarounds like this for the time being.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remember full day doesn’t have time, I ran into that here and in the front end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it's still necessary. Here's a test script that demonstrates the bug:
const { RRule } = require('rrule');
const moment = require('moment-timezone');
// Full-day event: daily at 00:00 UTC, starting March 10
const dtstartUTC = new Date(Date.UTC(2024, 2, 10, 0, 0, 0));
// UNTIL=20240315 parsed in Europe/Berlin (UTC+1)
// "March 15 00:00 Berlin" = "March 14 23:00 UTC"
const untilRaw = moment.tz('2024-03-15', 'Europe/Berlin').toDate();
const untilFixed = moment.tz('2024-03-15', 'Europe/Berlin').endOf('day').toDate();
const rule1 = new RRule({ freq: RRule.DAILY, dtstart: dtstartUTC, until: untilRaw });
const rule2 = new RRule({ freq: RRule.DAILY, dtstart: dtstartUTC, until: untilFixed });
console.log('WITHOUT endOf("day"):', rule1.all().length, 'days, last:', rule1.all().pop()?.toISOString().slice(0,10));
console.log('WITH endOf("day"): ', rule2.all().length, 'days, last:', rule2.all().pop()?.toISOString().slice(0,10));Output:
WITHOUT endOf("day"): 5 days, last: 2024-03-14
WITH endOf("day"): 6 days, last: 2024-03-15
The issue: When rrule.js checks if March 15 should be included, it compares:
- Event time:
00:00:00 UTCon March 15 - UNTIL:
23:00:00 UTCon March 14 (because of timezone conversion)
Since 23:00 < 00:00, March 15th is excluded. The endOf("day") fix ensures UNTIL is always after the event time on that day.
Remove unnecessary parameter from shouldRefetch() and use the fetcher's reloadInterval directly. This respects the user's configured fetchInterval as the threshold for rate limit protection.
|
Seems ok. |
Great. It would be awesome if you'd merge it 🙂 |
Thanks to: @Blackspirits, @Crazylegstoo, @jarnoml, @jboucly, @JHWelch, @khassel, @KristjanESPERANTO, @rejas, @sdetweil, @xsorifc28⚠️ This release needs nodejs version >=22.21.1 <23 || >=24 [Compare to previous Release v2.33.0](v2.33.0...develop) [core] Prepare Release 2.34.0 (#3998) dependency update + adjust Playwright setup + fix linter issue (#3994) demo with gif (#3995) [core] fix: allow browser globals in config files (#3992) [core] fix: restore --ozone-platform=wayland flag for reliable Wayland support (#3989) [core] auto create release notes with every push on develop (#3985) [core] chore: simplify Wayland start script (#3974) [gitignore] restore the old Git behavior for the default modules (#3968) [core] configure cspell to check default modules only and fix typos (#3955) [gitignore] restoring the old Git behavior for the CSS directory (#3954) feat(core): add server:watch script with automatic restart on file changes (#3920) [check_config] refactor: improve error handling (#3927) refactor: replace express-ipfilter with lightweight custom middleware (#3917) refactor: replace module-alias dependency with internal alias resolver (#3893) [dependencies] [chore] update dependencies and min. node version (#3986) [core] bump dependencies into december (#3982) Bump actions/checkout from 5 to 6 (#3972) Update deps, unpin parse5 (#3934) [core] Update deps and pin jsdom to v27.0.0 (#3925) chore: update dependencies (#3921) update deps, exclude node v23 (#3916) remove eslint warnings, add npm publish process to Collaboration.md (#3913) feat: add ESlint rule no-sparse-arrays for config check (#3911) chore: bump dependencies into october (#3909) [logging] logger: add calling filename as prefix on server side (#3926) [logger] Add prefixes to most Log messages (#3923) [modules/alert] Add new pt and pt-BR translations for Alert module and update global PT strings (#3965) [modules/calendar] add checksum to test whether calendar event list changed (#3988) [calendar] fix: prevent excessive fetching on client reload and refactor calendarfetcherutils.js (#3976) [calendar] refactor: migrate CalendarFetcher to ES6 class and improve error handling (#3959) [calendar] Show repeatingCountTitle only if yearDiff > 0 (#3949) [tests] suppress debug logs in CI environment + improve calendar symbol test stability (#3941) [calendar] chore: remove requiresVersion: "2.1.0" (#3932) [calendar] test: remove "Recurring event per timezone" test (#3929) [modules/compliments] [compliments] refactor: optimize loadComplimentFile method and add unit tests (#3969) fix: set compliments remote file minimum delay to 15 minutes (#3970) [compliments] fix: duplicate query param "?" in compliments module refresh url (#3967) [modules/newsfeed] [newsfeed] fix header layout issue (#3946) [modules/weather] [weatherprovider] update subclass language use override (#3914) [weather] fix wind-icon not showing in pirateweather (#3957) [weather] add error handling to weather fetch functions, including cors (#3791) remove deprecated ukmetoffice datapoint provider, cleanup .gitignore (#3952) fixes problems with daylight-saving-time in weather provider openmeteo (#3931) Fix for envcanada Provider to use updated Env Canada URL (#3919) [weather] feat: add configurable forecast date format option (#3918) [testing] testing: update "Enforce Pull-Request Rules" workflow (#3987) [core] refactor: replace XMLHttpRequest with fetch and migrate e2e tests to Playwright (#3950) [test] replace node-libgpiod with serialport in electron-rebuild workflow (#3945) [ci] Add concurrency to automated tests workflow to cancel outdated runs (#3943) [tests] migrate from jest to vitest (#3940) --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Karsten Hassel <hassel@gmx.de> Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Co-authored-by: Ryan Williams <65094007+ryan-d-williams@users.noreply.github.com> Co-authored-by: Veeck <github@veeck.de> Co-authored-by: Bugsounet - Cédric <github@bugsounet.fr> Co-authored-by: Marc Landis <dirk.rettschlag@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: HeikoGr <20295490+HeikoGr@users.noreply.github.com> Co-authored-by: Pedro Lamas <pedrolamas@gmail.com> Co-authored-by: veeck <gitkraken@veeck.de> Co-authored-by: Magnus <34011212+MagMar94@users.noreply.github.com> Co-authored-by: Ikko Eltociear Ashimine <eltociear@gmail.com> Co-authored-by: DevIncomin <56730075+Developer-Incoming@users.noreply.github.com> Co-authored-by: Nathan <n8nyoung@gmail.com> Co-authored-by: mixasgr <mixasgr@users.noreply.github.com> Co-authored-by: Savvas Adamtziloglou <savvas-gr@greeklug.gr> Co-authored-by: Konstantinos <geraki@gmail.com> Co-authored-by: OWL4C <124401812+OWL4C@users.noreply.github.com> Co-authored-by: BugHaver <43462320+bughaver@users.noreply.github.com> Co-authored-by: BugHaver <43462320+lsaadeh@users.noreply.github.com> Co-authored-by: Koen Konst <koenspero@gmail.com> Co-authored-by: Koen Konst <c.h.konst@avisi.nl> Co-authored-by: dathbe <github@beffa.us> Co-authored-by: Marcel <m-idler@users.noreply.github.com> Co-authored-by: Kevin G. <crazylegstoo@gmail.com> Co-authored-by: Jboucly <33218155+jboucly@users.noreply.github.com> Co-authored-by: Jboucly <contact@jboucly.fr> Co-authored-by: Jarno <54169345+jarnoml@users.noreply.github.com> Co-authored-by: Jordan Welch <JordanHWelch@gmail.com> Co-authored-by: Blackspirits <blackspirits@gmail.com> Co-authored-by: Samed Ozdemir <samed@xsor.io>
…ss all timezones (#4004) Fixes **full-day recurring events showing on wrong day** in timezones west of UTC (reported in #4003). **Root cause**: `moment.tz(date, eventTimezone).startOf("day")` interprets UTC midnight as local time: - `2025-11-03T00:00:00.000Z` in America/Chicago (UTC-6) - → Converts to `2025-11-02 18:00:00` (6 hours back) - → `.startOf("day")` → `2025-11-02 00:00:00` ❌ **Wrong day!** **Impact**: The bug affects: - All timezones west of UTC (UTC-1 through UTC-12): Americas, Pacific - Timezones east of UTC (UTC+1 through UTC+12): Europe, Asia, Africa - work correctly - UTC itself - works correctly The issue was introduced with commit c2ec6fc (#3976), which fixed the time but broke the date. This PR fixes both. | | Result | Day | Time | Notes | |----------|--------|-----|------|-------| | **Before c2ec6fc** | `2025-11-03 05:00:00 Monday` | ✅ | ❌ | Wrong time, but correct day | | **Current (c2ec6fc)** | `2025-11-02 00:00:00 Sunday` | ❌ (west of UTC)<br>✅ (east of UTC) | ✅ | Wrong day - visible bug! | | **This fix** | `2025-11-03 00:00:00 Monday` | ✅ | ✅ | Correct in all timezones | Note: While the old logic had incorrect timing, it produced the correct calendar day due to how it handled UTC offsets. The current logic fixed the timing issue but introduced the more visible calendar day bug. ### Solution Extract UTC date components and interpret as local calendar dates: ```javascript const utcYear = date.getUTCFullYear(); const utcMonth = date.getUTCMonth(); const utcDate = date.getUTCDate(); return moment.tz([utcYear, utcMonth, utcDate], eventTimezone); ``` ### Testing To prevent this from happening again in future refactorings, I wrote a test for it. ```bash npm test -- tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.js ```
The bottom line of this PR is, that it fixes an issue and simplifies the code by dealing with the TODOs in the code.
For review, I suggest looking at each commit individually. If there are too many changes for a PR, let me know and I'll split it up 🙂
1. fix(calendar): prevent excessive fetching with smart refresh strategy
2. refactor(calendar): simplify event exclusion logic
shouldEventBeExcludedinto new helpercheckEventAgainstFilterIt resolves a TODO from the comments in the code:
This seems like an overly complicated way to exclude events based on the title.3. refactor(calendar): extract recurring event expansion logic
This change separates the expansion of recurring events from the main filtering loop into a new helper function 'expandRecurringEvent'.
It resolves two TODOs from the comments in the code:
This should be a separate functionThis should create an event per moment so we can change anything we wantThis improves code readability, reduces complexity in 'filterEvents', and allows for cleaner handling of individual recurrence instances.
4. refactor(calendar): simplify recurring event handling
5. test(calendar): fix UNTIL date in fullday_until.ics fixture
The issue was with the UNTIL date being May 4th while DTSTART was May 5th. This created an invalid recurrence rule where the end date came before the start date.
The fix only adjusts the UNTIL date from May 4th to May 5th, so it matches the start date.