Files
MagicMirror/js/node_helper.js
Kristjan ESPERANTO 3c4d69ea84 [calendar] refactor: migrate CalendarFetcher to ES6 class and improve error handling (#3959)
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.
2025-11-14 20:14:23 +01:00

130 lines
3.3 KiB
JavaScript

const express = require("express");
const Log = require("logger");
const Class = require("./class");
const NodeHelper = Class.extend({
init () {
Log.log("Initializing new module helper ...");
},
loaded () {
Log.log(`Module helper loaded: ${this.name}`);
},
start () {
Log.log(`Starting module helper: ${this.name}`);
},
/**
* Called when the MagicMirror² server receives a `SIGINT`
* Close any open connections, stop any sub-processes and
* gracefully exit the module.
*/
stop () {
Log.log(`Stopping module helper: ${this.name}`);
},
/**
* This method is called when a socket notification arrives.
* @param {string} notification The identifier of the notification.
* @param {object} payload The payload of the notification.
*/
socketNotificationReceived (notification, payload) {
Log.log(`${this.name} received a socket notification: ${notification} - Payload: ${payload}`);
},
/**
* Set the module name.
* @param {string} name Module name.
*/
setName (name) {
this.name = name;
},
/**
* Set the module path.
* @param {string} path Module path.
*/
setPath (path) {
this.path = path;
},
/*
* sendSocketNotification(notification, payload)
* Send a socket notification to the node helper.
*
* argument notification string - The identifier of the notification.
* argument payload mixed - The payload of the notification.
*/
sendSocketNotification (notification, payload) {
this.io.of(this.name).emit(notification, payload);
},
/*
* setExpressApp(app)
* Sets the express app object for this module.
* This allows you to host files from the created webserver.
*
* argument app Express app - The Express app object.
*/
setExpressApp (app) {
this.expressApp = app;
app.use(`/${this.name}`, express.static(`${this.path}/public`));
},
/*
* setSocketIO(io)
* Sets the socket io object for this module.
* Binds message receiver.
*
* argument io Socket.io - The Socket io object.
*/
setSocketIO (io) {
this.io = io;
Log.log(`Connecting socket for: ${this.name}`);
io.of(this.name).on("connection", (socket) => {
// register catch all.
socket.onAny((notification, payload) => {
this.socketNotificationReceived(notification, payload);
});
});
}
});
NodeHelper.checkFetchStatus = function (response) {
// response.status >= 200 && response.status < 300
if (response.ok) {
return response;
} else {
throw Error(response.statusText);
}
};
/**
* Look at the specified error and return an appropriate error type, that
* can be translated to a detailed error message
* @param {Error} error the error from fetching something
* @returns {string} the string of the detailed error message in the translations
*/
NodeHelper.checkFetchError = function (error) {
let error_type = "MODULE_ERROR_UNSPECIFIED";
if (error.code === "EAI_AGAIN") {
error_type = "MODULE_ERROR_NO_CONNECTION";
} else {
const message = typeof error.message === "string" ? error.message.toLowerCase() : "";
if (message.includes("unauthorized") || message.includes("http 401") || message.includes("http 403")) {
error_type = "MODULE_ERROR_UNAUTHORIZED";
}
}
return error_type;
};
NodeHelper.create = function (moduleDefinition) {
return NodeHelper.extend(moduleDefinition);
};
module.exports = NodeHelper;