mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-12-12 09:52:37 +00:00
1. Convert CalendarFetcher from legacy constructor function pattern to ES6 class (which simplifies future migration from CommonJS to ES modules). 2. Implement targeted HTTP error handling with smart retry strategies for common calendar feed issues: - 401/403: Extended retry delay (5× interval, min 30 min) - 429: Retry-After header parsing with 15 min fallback - 5xx: Exponential backoff (2^count, max 3 retries) - 4xx: Extended retry (2× interval, min 15 min) - Add serverErrorCount tracking for exponential backoff - Error messages now include specific HTTP status codes and calculated retry delays for better debugging and user feedback Previously, CalendarFetcher did not respond appropriately to HTTP errors, continuing to hammer endpoints without backoff, potentially overloading servers and triggering rate limits. This refactoring implements respectful retry strategies that adapt to server responses and reduce unnecessary load. Maybe we could later centralize the HTTP error handling and use it for weather and newsfeed as well. The PR was inspired by having worked on the calendar fetcher for MMM-CalendarExt2, where there was already better error handling.
95 lines
3.7 KiB
JavaScript
95 lines
3.7 KiB
JavaScript
const NodeHelper = require("node_helper");
|
|
const Log = require("logger");
|
|
const CalendarFetcher = require("./calendarfetcher");
|
|
|
|
module.exports = NodeHelper.create({
|
|
// Override start method.
|
|
start () {
|
|
Log.log(`Starting node helper for: ${this.name}`);
|
|
this.fetchers = [];
|
|
},
|
|
|
|
// Override socketNotificationReceived method.
|
|
socketNotificationReceived (notification, payload) {
|
|
if (notification === "ADD_CALENDAR") {
|
|
this.createFetcher(payload.url, payload.fetchInterval, payload.excludedEvents, payload.maximumEntries, payload.maximumNumberOfDays, payload.auth, payload.broadcastPastEvents, payload.selfSignedCert, payload.id);
|
|
} else if (notification === "FETCH_CALENDAR") {
|
|
const key = payload.id + payload.url;
|
|
if (typeof this.fetchers[key] === "undefined") {
|
|
Log.error("No fetcher exists with key: ", key);
|
|
this.sendSocketNotification("CALENDAR_ERROR", { error_type: "MODULE_ERROR_UNSPECIFIED" });
|
|
return;
|
|
}
|
|
this.fetchers[key].fetchCalendar();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Creates a fetcher for a new url if it doesn't exist yet.
|
|
* Otherwise it reuses the existing one.
|
|
* @param {string} url The url of the calendar
|
|
* @param {number} fetchInterval How often does the calendar needs to be fetched in ms
|
|
* @param {string[]} excludedEvents An array of words / phrases from event titles that will be excluded from being shown.
|
|
* @param {number} maximumEntries The maximum number of events fetched.
|
|
* @param {number} maximumNumberOfDays The maximum number of days an event should be in the future.
|
|
* @param {object} auth The object containing options for authentication against the calendar.
|
|
* @param {boolean} broadcastPastEvents If true events from the past maximumNumberOfDays will be included in event broadcasts
|
|
* @param {boolean} selfSignedCert If true, the server certificate is not verified against the list of supplied CAs.
|
|
* @param {string} identifier ID of the module
|
|
*/
|
|
createFetcher (url, fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert, identifier) {
|
|
try {
|
|
new URL(url);
|
|
} catch (error) {
|
|
Log.error("Malformed calendar url: ", url, error);
|
|
this.sendSocketNotification("CALENDAR_ERROR", { error_type: "MODULE_ERROR_MALFORMED_URL" });
|
|
return;
|
|
}
|
|
|
|
let fetcher;
|
|
let fetchIntervalCorrected;
|
|
if (typeof this.fetchers[identifier + url] === "undefined") {
|
|
if (fetchInterval < 60000) {
|
|
Log.warn(`fetchInterval for url ${url} must be >= 60000`);
|
|
fetchIntervalCorrected = 60000;
|
|
}
|
|
Log.log(`Create new calendarfetcher for url: ${url} - Interval: ${fetchIntervalCorrected || fetchInterval}`);
|
|
fetcher = new CalendarFetcher(url, fetchIntervalCorrected || fetchInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, broadcastPastEvents, selfSignedCert);
|
|
|
|
fetcher.onReceive((fetcher) => {
|
|
this.broadcastEvents(fetcher, identifier);
|
|
});
|
|
|
|
fetcher.onError((fetcher, error) => {
|
|
Log.error("Calendar Error. Could not fetch calendar: ", fetcher.url, error);
|
|
let error_type = NodeHelper.checkFetchError(error);
|
|
this.sendSocketNotification("CALENDAR_ERROR", {
|
|
id: identifier,
|
|
error_type
|
|
});
|
|
});
|
|
|
|
this.fetchers[identifier + url] = fetcher;
|
|
} else {
|
|
Log.log(`Use existing calendarfetcher for url: ${url}`);
|
|
fetcher = this.fetchers[identifier + url];
|
|
fetcher.broadcastEvents();
|
|
}
|
|
|
|
fetcher.fetchCalendar();
|
|
},
|
|
|
|
/**
|
|
*
|
|
* @param {object} fetcher the fetcher associated with the calendar
|
|
* @param {string} identifier the identifier of the calendar
|
|
*/
|
|
broadcastEvents (fetcher, identifier) {
|
|
this.sendSocketNotification("CALENDAR_EVENTS", {
|
|
id: identifier,
|
|
url: fetcher.url,
|
|
events: fetcher.events
|
|
});
|
|
}
|
|
});
|