mirror of
https://github.com/MagicMirrorOrg/MagicMirror.git
synced 2026-03-12 17:51:41 +08:00
[core] refactor: extract and centralize HTTP fetcher (#4016)
## Summary PR [#3976](https://github.com/MagicMirrorOrg/MagicMirror/pull/3976) introduced smart HTTP error handling for the Calendar module. This PR extracts that HTTP logic into a central `HTTPFetcher` class. Calendar is the first module to use it. Follow-up PRs would migrate Newsfeed and maybe even Weather. **Before this change:** - ❌ Each module had to implemented its own `fetch()` calls - ❌ No centralized retry logic or backoff strategies - ❌ No timeout handling for hanging requests - ❌ Error detection relied on fragile string parsing **What this PR adds:** - ✅ Unified HTTPFetcher class with intelligent retry strategies - ✅ Modern AbortController with configurable timeout (default 30s) - ✅ Proper undici Agent for self-signed certificates - ✅ Structured error objects with translation keys - ✅ Calendar module migrated as first consumer - ✅ Comprehensive unit tests with msw (Mock Service Worker) ## Architecture **Before - Decentralized HTTP handling:** ``` Calendar Module Newsfeed Module Weather Module ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ fetch() own │ │ fetch() own │ │ fetch() own │ │ retry logic │ │ basic error │ │ no retry │ │ error parse │ │ handling │ │ client-side │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ └───────────────────────┴───────────────────────┘ ▼ External APIs ``` **After - Centralized with HTTPFetcher:** ``` ┌─────────────────────────────────────────────────────┐ │ HTTPFetcher │ │ • Unified retry strategies (401/403, 429, 5xx) │ │ • AbortController timeout (30s) │ │ • Structured errors with translation keys │ │ • undici Agent for self-signed certs │ └────────────┬──────────────┬──────────────┬──────────┘ │ │ │ ┌───────▼───────┐ ┌────▼─────┐ ┌──────▼──────┐ │ Calendar │ │ Newsfeed │ │ Weather │ │ ✅ This PR │ │ future │ │ future │ └───────────────┘ └──────────┘ └─────────────┘ │ │ │ └──────────────┴──────────────┘ ▼ External APIs ``` ## Complexity Considerations **Does HTTPFetcher add complexity?** Even if it may look more complex, it actually **reduces overall complexity**: - **Calendar already has this logic** (PR #3976) - we're extracting, not adding - **Alternative is worse:** Each module implementing own logic = 3× the code - **Better testability:** 443 lines of tests once vs. duplicating tests for each module - **Standards-based:** Retry-After is RFC 7231, not custom logic ## Future Benefits **Weather migration (future PR):** Moving Weather from client-side to server-side will enable: - **Same robust error handling** - Weather gets 429 rate-limiting, 5xx backoff for free - **Simpler architecture** - No proxy layer needed Moving the weather modules from client-side to server-side will be a big undertaking, but I think it's a good strategy. Even if we only move the calendar and newsfeed to the new HTTP fetcher and leave the weather as it is, this PR still makes sense, I think. ## Breaking Changes **None** ---- I am eager to hear your opinion on this 🙂
This commit is contained in:
committed by
GitHub
parent
23f0290139
commit
34913bfb9f
300
js/http_fetcher.js
Normal file
300
js/http_fetcher.js
Normal file
@@ -0,0 +1,300 @@
|
||||
const { EventEmitter } = require("node:events");
|
||||
const { Agent } = require("undici");
|
||||
const Log = require("logger");
|
||||
const { getUserAgent } = require("#server_functions");
|
||||
|
||||
const FIFTEEN_MINUTES = 15 * 60 * 1000;
|
||||
const THIRTY_MINUTES = 30 * 60 * 1000;
|
||||
const MAX_SERVER_BACKOFF = 3;
|
||||
const DEFAULT_TIMEOUT = 30000; // 30 seconds
|
||||
|
||||
/**
|
||||
* Maps errorType to MagicMirror translation keys.
|
||||
* This allows HTTPFetcher to provide ready-to-use translation keys,
|
||||
* eliminating the need to call NodeHelper.checkFetchError().
|
||||
*/
|
||||
const ERROR_TYPE_TO_TRANSLATION = {
|
||||
AUTH_FAILURE: "MODULE_ERROR_UNAUTHORIZED",
|
||||
RATE_LIMITED: "MODULE_ERROR_RATE_LIMITED",
|
||||
SERVER_ERROR: "MODULE_ERROR_SERVER_ERROR",
|
||||
CLIENT_ERROR: "MODULE_ERROR_CLIENT_ERROR",
|
||||
NETWORK_ERROR: "MODULE_ERROR_NO_CONNECTION",
|
||||
UNKNOWN_ERROR: "MODULE_ERROR_UNSPECIFIED"
|
||||
};
|
||||
|
||||
/**
|
||||
* HTTPFetcher - Centralized HTTP fetching with intelligent error handling
|
||||
*
|
||||
* Features:
|
||||
* - Automatic retry strategies based on HTTP status codes
|
||||
* - Exponential backoff for server errors
|
||||
* - Retry-After header parsing for rate limiting
|
||||
* - Authentication support (Basic, Bearer)
|
||||
* - Self-signed certificate support
|
||||
* @augments EventEmitter
|
||||
* @fires HTTPFetcher#response - When fetch succeeds with ok response
|
||||
* @fires HTTPFetcher#error - When fetch fails or returns non-ok response
|
||||
* @example
|
||||
* const fetcher = new HTTPFetcher(url, { reloadInterval: 60000 });
|
||||
* fetcher.on('response', (response) => { ... });
|
||||
* fetcher.on('error', (errorInfo) => { ... });
|
||||
* fetcher.startPeriodicFetch();
|
||||
*/
|
||||
class HTTPFetcher extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Creates a new HTTPFetcher instance
|
||||
* @param {string} url - The URL to fetch
|
||||
* @param {object} options - Configuration options
|
||||
* @param {number} [options.reloadInterval] - Time in ms between fetches (default: 5 min)
|
||||
* @param {object} [options.auth] - Authentication options
|
||||
* @param {string} [options.auth.method] - 'basic' or 'bearer'
|
||||
* @param {string} [options.auth.user] - Username for basic auth
|
||||
* @param {string} [options.auth.pass] - Password or token
|
||||
* @param {boolean} [options.selfSignedCert] - Accept self-signed certificates
|
||||
* @param {object} [options.headers] - Additional headers to send
|
||||
* @param {number} [options.maxRetries] - Max retries for 5xx errors (default: 3)
|
||||
* @param {number} [options.timeout] - Request timeout in ms (default: 30000)
|
||||
*/
|
||||
constructor (url, options = {}) {
|
||||
super();
|
||||
|
||||
this.url = url;
|
||||
this.reloadInterval = options.reloadInterval || 5 * 60 * 1000;
|
||||
this.auth = options.auth || null;
|
||||
this.selfSignedCert = options.selfSignedCert || false;
|
||||
this.customHeaders = options.headers || {};
|
||||
this.maxRetries = options.maxRetries || MAX_SERVER_BACKOFF;
|
||||
this.timeout = options.timeout || DEFAULT_TIMEOUT;
|
||||
|
||||
this.reloadTimer = null;
|
||||
this.serverErrorCount = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears any pending reload timer
|
||||
*/
|
||||
clearTimer () {
|
||||
if (this.reloadTimer) {
|
||||
clearTimeout(this.reloadTimer);
|
||||
this.reloadTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules the next fetch.
|
||||
* If no delay is provided, uses reloadInterval.
|
||||
* If delay is provided but very short (< 1 second), clamps to reloadInterval
|
||||
* to prevent hammering servers.
|
||||
* @param {number} [delay] - Delay in milliseconds
|
||||
*/
|
||||
scheduleNextFetch (delay) {
|
||||
let nextDelay = delay ?? this.reloadInterval;
|
||||
|
||||
// Only clamp if delay is unreasonably short (< 1 second)
|
||||
// This allows respecting Retry-After headers while preventing abuse
|
||||
if (nextDelay < 1000) {
|
||||
nextDelay = this.reloadInterval;
|
||||
}
|
||||
|
||||
// Don't schedule in test mode
|
||||
if (process.env.mmTestMode === "true") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.reloadTimer = setTimeout(() => this.fetch(), nextDelay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts periodic fetching
|
||||
*/
|
||||
startPeriodicFetch () {
|
||||
this.fetch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the options object for fetch
|
||||
* @returns {object} Options object containing headers (and dispatcher if needed)
|
||||
*/
|
||||
getRequestOptions () {
|
||||
const headers = {
|
||||
"User-Agent": getUserAgent(),
|
||||
...this.customHeaders
|
||||
};
|
||||
const options = { headers };
|
||||
|
||||
if (this.selfSignedCert) {
|
||||
options.dispatcher = new Agent({
|
||||
connect: {
|
||||
rejectUnauthorized: false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this.auth) {
|
||||
if (this.auth.method === "bearer") {
|
||||
headers.Authorization = `Bearer ${this.auth.pass}`;
|
||||
} else {
|
||||
headers.Authorization = `Basic ${Buffer.from(`${this.auth.user}:${this.auth.pass}`).toString("base64")}`;
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the Retry-After header value
|
||||
* @param {string} retryAfter - The Retry-After header value
|
||||
* @returns {number|null} Milliseconds to wait or null if parsing failed
|
||||
*/
|
||||
#parseRetryAfter (retryAfter) {
|
||||
// Try parsing as seconds
|
||||
const seconds = Number(retryAfter);
|
||||
if (!Number.isNaN(seconds) && seconds >= 0) {
|
||||
return seconds * 1000;
|
||||
}
|
||||
|
||||
// Try parsing as HTTP-date
|
||||
const retryDate = Date.parse(retryAfter);
|
||||
if (!Number.isNaN(retryDate)) {
|
||||
return Math.max(0, retryDate - Date.now());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the retry delay for a non-ok response
|
||||
* @param {Response} response - The fetch Response object
|
||||
* @returns {{delay: number, errorInfo: object}} Computed retry delay and error info
|
||||
*/
|
||||
#getDelayForResponse (response) {
|
||||
const { status } = response;
|
||||
let delay = this.reloadInterval;
|
||||
let message = "";
|
||||
let errorType = "UNKNOWN_ERROR";
|
||||
|
||||
if (status === 401 || status === 403) {
|
||||
errorType = "AUTH_FAILURE";
|
||||
delay = Math.max(this.reloadInterval * 5, THIRTY_MINUTES);
|
||||
message = `Authentication failed (${status}). Waiting ${Math.round(delay / 60000)} minutes before retry.`;
|
||||
Log.error(`${this.url} - ${message}`);
|
||||
} else if (status === 429) {
|
||||
errorType = "RATE_LIMITED";
|
||||
const retryAfter = response.headers.get("retry-after");
|
||||
const parsed = retryAfter ? this.#parseRetryAfter(retryAfter) : null;
|
||||
delay = parsed !== null ? Math.max(parsed, this.reloadInterval) : Math.max(this.reloadInterval * 2, FIFTEEN_MINUTES);
|
||||
message = `Rate limited (429). Retrying in ${Math.round(delay / 60000)} minutes.`;
|
||||
Log.warn(`${this.url} - ${message}`);
|
||||
} else if (status >= 500) {
|
||||
errorType = "SERVER_ERROR";
|
||||
this.serverErrorCount = Math.min(this.serverErrorCount + 1, this.maxRetries);
|
||||
delay = this.reloadInterval * Math.pow(2, this.serverErrorCount);
|
||||
message = `Server error (${status}). Retry #${this.serverErrorCount} in ${Math.round(delay / 60000)} minutes.`;
|
||||
Log.error(`${this.url} - ${message}`);
|
||||
} else if (status >= 400) {
|
||||
errorType = "CLIENT_ERROR";
|
||||
delay = Math.max(this.reloadInterval * 2, FIFTEEN_MINUTES);
|
||||
message = `Client error (${status}). Retrying in ${Math.round(delay / 60000)} minutes.`;
|
||||
Log.error(`${this.url} - ${message}`);
|
||||
} else {
|
||||
message = `Unexpected HTTP status ${status}.`;
|
||||
Log.error(`${this.url} - ${message}`);
|
||||
}
|
||||
|
||||
return {
|
||||
delay,
|
||||
errorInfo: this.#createErrorInfo(message, status, errorType, delay)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a standardized error info object
|
||||
* @param {string} message - Error message
|
||||
* @param {number|null} status - HTTP status code or null for network errors
|
||||
* @param {string} errorType - Error type: AUTH_FAILURE, RATE_LIMITED, SERVER_ERROR, CLIENT_ERROR, NETWORK_ERROR
|
||||
* @param {number} retryAfter - Delay until next retry in ms
|
||||
* @param {Error} [originalError] - The original error if any
|
||||
* @returns {object} Error info object with translationKey for direct use
|
||||
*/
|
||||
#createErrorInfo (message, status, errorType, retryAfter, originalError = null) {
|
||||
return {
|
||||
message,
|
||||
status,
|
||||
errorType,
|
||||
translationKey: ERROR_TYPE_TO_TRANSLATION[errorType] || "MODULE_ERROR_UNSPECIFIED",
|
||||
retryAfter,
|
||||
retryCount: this.serverErrorCount,
|
||||
url: this.url,
|
||||
originalError
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the HTTP fetch and emits appropriate events
|
||||
* @fires HTTPFetcher#response
|
||||
* @fires HTTPFetcher#error
|
||||
*/
|
||||
async fetch () {
|
||||
this.clearTimer();
|
||||
|
||||
let nextDelay = this.reloadInterval;
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
||||
|
||||
try {
|
||||
const response = await fetch(this.url, {
|
||||
...this.getRequestOptions(),
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const { delay, errorInfo } = this.#getDelayForResponse(response);
|
||||
nextDelay = delay;
|
||||
this.emit("error", errorInfo);
|
||||
} else {
|
||||
// Reset server error count on success
|
||||
this.serverErrorCount = 0;
|
||||
|
||||
/**
|
||||
* Response event - fired when fetch succeeds
|
||||
* @event HTTPFetcher#response
|
||||
* @type {Response}
|
||||
*/
|
||||
this.emit("response", response);
|
||||
}
|
||||
} catch (error) {
|
||||
const isTimeout = error.name === "AbortError";
|
||||
const message = isTimeout ? `Request timeout after ${this.timeout}ms` : `Network error: ${error.message}`;
|
||||
|
||||
Log.error(`${this.url} - ${message}`);
|
||||
|
||||
const errorInfo = this.#createErrorInfo(
|
||||
message,
|
||||
null,
|
||||
"NETWORK_ERROR",
|
||||
this.reloadInterval,
|
||||
error
|
||||
);
|
||||
|
||||
/**
|
||||
* Error event - fired when fetch fails
|
||||
* @event HTTPFetcher#error
|
||||
* @type {object}
|
||||
* @property {string} message - Error description
|
||||
* @property {number|null} statusCode - HTTP status or null for network errors
|
||||
* @property {number} retryDelay - Ms until next retry
|
||||
* @property {number} retryCount - Number of consecutive server errors
|
||||
* @property {string} url - The URL that was fetched
|
||||
* @property {Error|null} originalError - The original error
|
||||
*/
|
||||
this.emit("error", errorInfo);
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
this.scheduleNextFetch(nextDelay);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HTTPFetcher;
|
||||
@@ -2,15 +2,11 @@ const ical = require("node-ical");
|
||||
const Log = require("logger");
|
||||
const { Agent } = require("undici");
|
||||
const CalendarFetcherUtils = require("./calendarfetcherutils");
|
||||
const { getUserAgent } = require("#server_functions");
|
||||
|
||||
|
||||
const FIFTEEN_MINUTES = 15 * 60 * 1000;
|
||||
const THIRTY_MINUTES = 30 * 60 * 1000;
|
||||
const MAX_SERVER_BACKOFF = 3;
|
||||
const HTTPFetcher = require("#http_fetcher");
|
||||
|
||||
/**
|
||||
* CalendarFetcher - Fetches and parses iCal calendar data with MagicMirror-focused error handling
|
||||
* CalendarFetcher - Fetches and parses iCal calendar data
|
||||
* Uses HTTPFetcher for HTTP handling with intelligent error handling
|
||||
* @class
|
||||
*/
|
||||
class CalendarFetcher {
|
||||
@@ -28,162 +24,68 @@ class CalendarFetcher {
|
||||
*/
|
||||
constructor (url, reloadInterval, excludedEvents, maximumEntries, maximumNumberOfDays, auth, includePastEvents, selfSignedCert) {
|
||||
this.url = url;
|
||||
this.reloadInterval = reloadInterval;
|
||||
this.excludedEvents = excludedEvents;
|
||||
this.maximumEntries = maximumEntries;
|
||||
this.maximumNumberOfDays = maximumNumberOfDays;
|
||||
this.auth = auth;
|
||||
this.includePastEvents = includePastEvents;
|
||||
this.selfSignedCert = selfSignedCert;
|
||||
|
||||
this.events = [];
|
||||
this.reloadTimer = null;
|
||||
this.serverErrorCount = 0;
|
||||
this.lastFetch = null;
|
||||
this.fetchFailedCallback = () => {};
|
||||
this.eventsReceivedCallback = () => {};
|
||||
|
||||
// Use HTTPFetcher for HTTP handling (Composition)
|
||||
this.httpFetcher = new HTTPFetcher(url, {
|
||||
reloadInterval,
|
||||
auth,
|
||||
selfSignedCert
|
||||
});
|
||||
|
||||
// Wire up HTTPFetcher events
|
||||
this.httpFetcher.on("response", (response) => this.#handleResponse(response));
|
||||
this.httpFetcher.on("error", (errorInfo) => this.fetchFailedCallback(this, errorInfo));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears any pending reload timer
|
||||
* Handles successful HTTP response
|
||||
* @param {Response} response - The fetch Response object
|
||||
*/
|
||||
clearReloadTimer () {
|
||||
if (this.reloadTimer) {
|
||||
clearTimeout(this.reloadTimer);
|
||||
this.reloadTimer = null;
|
||||
}
|
||||
}
|
||||
async #handleResponse (response) {
|
||||
try {
|
||||
const responseData = await response.text();
|
||||
const parsed = ical.parseICS(responseData);
|
||||
|
||||
/**
|
||||
* Schedules the next fetch respecting MagicMirror test mode
|
||||
* @param {number} delay - Delay in milliseconds
|
||||
*/
|
||||
scheduleNextFetch (delay) {
|
||||
const nextDelay = Math.max(delay || this.reloadInterval, this.reloadInterval);
|
||||
if (process.env.mmTestMode === "true") {
|
||||
return;
|
||||
}
|
||||
this.reloadTimer = setTimeout(() => this.fetchCalendar(), nextDelay);
|
||||
}
|
||||
Log.debug(`Parsed iCal data from ${this.url} with ${Object.keys(parsed).length} entries.`);
|
||||
|
||||
/**
|
||||
* Builds the options object for fetch
|
||||
* @returns {object} Options object containing headers (and agent if needed)
|
||||
*/
|
||||
getRequestOptions () {
|
||||
const headers = { "User-Agent": getUserAgent() };
|
||||
const options = { headers };
|
||||
this.events = CalendarFetcherUtils.filterEvents(parsed, {
|
||||
excludedEvents: this.excludedEvents,
|
||||
includePastEvents: this.includePastEvents,
|
||||
maximumEntries: this.maximumEntries,
|
||||
maximumNumberOfDays: this.maximumNumberOfDays
|
||||
});
|
||||
|
||||
if (this.selfSignedCert) {
|
||||
options.dispatcher = new Agent({
|
||||
connect: {
|
||||
rejectUnauthorized: false
|
||||
}
|
||||
this.lastFetch = Date.now();
|
||||
this.broadcastEvents();
|
||||
} catch (error) {
|
||||
Log.error(`${this.url} - iCal parsing failed: ${error.message}`);
|
||||
this.fetchFailedCallback(this, {
|
||||
message: `iCal parsing failed: ${error.message}`,
|
||||
status: null,
|
||||
errorType: "PARSE_ERROR",
|
||||
translationKey: "MODULE_ERROR_UNSPECIFIED",
|
||||
retryAfter: this.httpFetcher.reloadInterval,
|
||||
retryCount: 0,
|
||||
url: this.url,
|
||||
originalError: error
|
||||
});
|
||||
}
|
||||
|
||||
if (this.auth) {
|
||||
if (this.auth.method === "bearer") {
|
||||
headers.Authorization = `Bearer ${this.auth.pass}`;
|
||||
} else {
|
||||
headers.Authorization = `Basic ${Buffer.from(`${this.auth.user}:${this.auth.pass}`).toString("base64")}`;
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the Retry-After header value
|
||||
* @param {string} retryAfter - The Retry-After header value
|
||||
* @returns {number|null} Milliseconds to wait or null if parsing failed
|
||||
* Starts fetching calendar data
|
||||
*/
|
||||
parseRetryAfter (retryAfter) {
|
||||
const seconds = Number(retryAfter);
|
||||
if (!Number.isNaN(seconds) && seconds >= 0) {
|
||||
return seconds * 1000;
|
||||
}
|
||||
|
||||
const retryDate = Date.parse(retryAfter);
|
||||
if (!Number.isNaN(retryDate)) {
|
||||
return Math.max(0, retryDate - Date.now());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the retry delay for a non-ok response
|
||||
* @param {Response} response - The fetch Response object
|
||||
* @returns {{delay: number, error: Error}} Error describing the issue and computed retry delay
|
||||
*/
|
||||
getDelayForResponse (response) {
|
||||
const { status, statusText = "" } = response;
|
||||
let delay = this.reloadInterval;
|
||||
|
||||
if (status === 401 || status === 403) {
|
||||
delay = Math.max(this.reloadInterval * 5, THIRTY_MINUTES);
|
||||
Log.error(`${this.url} - Authentication failed (${status}). Waiting ${Math.round(delay / 60000)} minutes before retry.`);
|
||||
} else if (status === 429) {
|
||||
const retryAfter = response.headers.get("retry-after");
|
||||
const parsed = retryAfter ? this.parseRetryAfter(retryAfter) : null;
|
||||
delay = parsed !== null ? Math.max(parsed, this.reloadInterval) : Math.max(this.reloadInterval * 2, FIFTEEN_MINUTES);
|
||||
Log.warn(`${this.url} - Rate limited (429). Retrying in ${Math.round(delay / 60000)} minutes.`);
|
||||
} else if (status >= 500) {
|
||||
this.serverErrorCount = Math.min(this.serverErrorCount + 1, MAX_SERVER_BACKOFF);
|
||||
delay = this.reloadInterval * Math.pow(2, this.serverErrorCount);
|
||||
Log.error(`${this.url} - Server error (${status}). Retry #${this.serverErrorCount} in ${Math.round(delay / 60000)} minutes.`);
|
||||
} else if (status >= 400) {
|
||||
delay = Math.max(this.reloadInterval * 2, FIFTEEN_MINUTES);
|
||||
Log.error(`${this.url} - Client error (${status}). Retrying in ${Math.round(delay / 60000)} minutes.`);
|
||||
} else {
|
||||
Log.error(`${this.url} - Unexpected HTTP status ${status}.`);
|
||||
}
|
||||
|
||||
return {
|
||||
delay,
|
||||
error: new Error(`HTTP ${status} ${statusText}`.trim())
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and processes calendar data
|
||||
*/
|
||||
async fetchCalendar () {
|
||||
this.clearReloadTimer();
|
||||
|
||||
let nextDelay = this.reloadInterval;
|
||||
try {
|
||||
const response = await fetch(this.url, this.getRequestOptions());
|
||||
if (!response.ok) {
|
||||
const { delay, error } = this.getDelayForResponse(response);
|
||||
nextDelay = delay;
|
||||
this.fetchFailedCallback(this, error);
|
||||
} else {
|
||||
this.serverErrorCount = 0;
|
||||
const responseData = await response.text();
|
||||
try {
|
||||
const parsed = ical.parseICS(responseData);
|
||||
Log.debug(`Parsed iCal data from ${this.url} with ${Object.keys(parsed).length} entries.`);
|
||||
this.events = CalendarFetcherUtils.filterEvents(parsed, {
|
||||
excludedEvents: this.excludedEvents,
|
||||
includePastEvents: this.includePastEvents,
|
||||
maximumEntries: this.maximumEntries,
|
||||
maximumNumberOfDays: this.maximumNumberOfDays
|
||||
});
|
||||
this.lastFetch = Date.now();
|
||||
this.broadcastEvents();
|
||||
} catch (error) {
|
||||
Log.error(`${this.url} - iCal parsing failed: ${error.message}`);
|
||||
this.fetchFailedCallback(this, error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
Log.error(`${this.url} - Fetch failed: ${error.message}`);
|
||||
this.fetchFailedCallback(this, error);
|
||||
}
|
||||
|
||||
this.scheduleNextFetch(nextDelay);
|
||||
fetchCalendar () {
|
||||
this.httpFetcher.startPeriodicFetch();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -196,7 +98,7 @@ class CalendarFetcher {
|
||||
return true;
|
||||
}
|
||||
const timeSinceLastFetch = Date.now() - this.lastFetch;
|
||||
return timeSinceLastFetch >= this.reloadInterval;
|
||||
return timeSinceLastFetch >= this.httpFetcher.reloadInterval;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -61,12 +61,11 @@ module.exports = NodeHelper.create({
|
||||
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);
|
||||
fetcher.onError((fetcher, errorInfo) => {
|
||||
Log.error("Calendar Error. Could not fetch calendar: ", fetcher.url, errorInfo.message || errorInfo);
|
||||
this.sendSocketNotification("CALENDAR_ERROR", {
|
||||
id: identifier,
|
||||
error_type
|
||||
error_type: errorInfo.translationKey
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
532
package-lock.json
generated
532
package-lock.json
generated
@@ -51,6 +51,7 @@
|
||||
"jsdom": "^27.4.0",
|
||||
"lint-staged": "^16.2.7",
|
||||
"markdownlint-cli2": "^0.20.0",
|
||||
"msw": "^2.12.7",
|
||||
"playwright": "^1.57.0",
|
||||
"prettier": "^3.7.4",
|
||||
"prettier-plugin-jinja-template": "^2.1.0",
|
||||
@@ -1894,6 +1895,187 @@
|
||||
"url": "https://github.com/sponsors/nzakas"
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/ansi": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz",
|
||||
"integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/confirm": {
|
||||
"version": "5.1.21",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz",
|
||||
"integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@inquirer/core": "^10.3.2",
|
||||
"@inquirer/type": "^3.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/core": {
|
||||
"version": "10.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz",
|
||||
"integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@inquirer/ansi": "^1.0.2",
|
||||
"@inquirer/figures": "^1.0.15",
|
||||
"@inquirer/type": "^3.0.10",
|
||||
"cli-width": "^4.1.0",
|
||||
"mute-stream": "^2.0.0",
|
||||
"signal-exit": "^4.1.0",
|
||||
"wrap-ansi": "^6.2.0",
|
||||
"yoctocolors-cjs": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/core/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/core/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@inquirer/core/node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/core/node_modules/mute-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz",
|
||||
"integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/core/node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/core/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/core/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/core/node_modules/wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/figures": {
|
||||
"version": "1.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz",
|
||||
"integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/type": {
|
||||
"version": "3.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz",
|
||||
"integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/balanced-match": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||
@@ -1964,6 +2146,24 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@mswjs/interceptors": {
|
||||
"version": "0.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.40.0.tgz",
|
||||
"integrity": "sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@open-draft/deferred-promise": "^2.2.0",
|
||||
"@open-draft/logger": "^0.3.0",
|
||||
"@open-draft/until": "^2.0.0",
|
||||
"is-node-process": "^1.2.0",
|
||||
"outvariant": "^1.4.3",
|
||||
"strict-event-emitter": "^0.5.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "0.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
||||
@@ -2015,6 +2215,31 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@open-draft/deferred-promise": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz",
|
||||
"integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@open-draft/logger": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz",
|
||||
"integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-node-process": "^1.2.0",
|
||||
"outvariant": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@open-draft/until": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz",
|
||||
"integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@pm2/agent": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@pm2/agent/-/agent-2.1.1.tgz",
|
||||
@@ -2880,6 +3105,13 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/statuses": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz",
|
||||
"integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/unist": {
|
||||
"version": "2.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
|
||||
@@ -4410,6 +4642,16 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-width": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
|
||||
"integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
|
||||
@@ -7038,6 +7280,16 @@
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/graphql": {
|
||||
"version": "16.12.0",
|
||||
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz",
|
||||
"integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/handlebars": {
|
||||
"version": "4.7.8",
|
||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
|
||||
@@ -7159,6 +7411,13 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/headers-polyfill": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz",
|
||||
"integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/helmet": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz",
|
||||
@@ -7762,6 +8021,13 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-node-process": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz",
|
||||
"integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
@@ -9319,6 +9585,205 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/msw": {
|
||||
"version": "2.12.7",
|
||||
"resolved": "https://registry.npmjs.org/msw/-/msw-2.12.7.tgz",
|
||||
"integrity": "sha512-retd5i3xCZDVWMYjHEVuKTmhqY8lSsxujjVrZiGbbdoxxIBg5S7rCuYy/YQpfrTYIxpd/o0Kyb/3H+1udBMoYg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@inquirer/confirm": "^5.0.0",
|
||||
"@mswjs/interceptors": "^0.40.0",
|
||||
"@open-draft/deferred-promise": "^2.2.0",
|
||||
"@types/statuses": "^2.0.6",
|
||||
"cookie": "^1.0.2",
|
||||
"graphql": "^16.12.0",
|
||||
"headers-polyfill": "^4.0.2",
|
||||
"is-node-process": "^1.2.0",
|
||||
"outvariant": "^1.4.3",
|
||||
"path-to-regexp": "^6.3.0",
|
||||
"picocolors": "^1.1.1",
|
||||
"rettime": "^0.7.0",
|
||||
"statuses": "^2.0.2",
|
||||
"strict-event-emitter": "^0.5.1",
|
||||
"tough-cookie": "^6.0.0",
|
||||
"type-fest": "^5.2.0",
|
||||
"until-async": "^3.0.2",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"msw": "cli/index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/mswjs"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">= 4.8.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/msw/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/msw/node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/msw/node_modules/cookie": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
||||
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/msw/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/msw/node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/msw/node_modules/path-to-regexp": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
|
||||
"integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/msw/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/msw/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/msw/node_modules/type-fest": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.3.1.tgz",
|
||||
"integrity": "sha512-VCn+LMHbd4t6sF3wfU/+HKT63C9OoyrSIf4b+vtWHpt2U7/4InZG467YDNMFMR70DdHjAdpPWmw2lzRdg0Xqqg==",
|
||||
"dev": true,
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"dependencies": {
|
||||
"tagged-tag": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/msw/node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/msw/node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/msw/node_modules/yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/mute-stream": {
|
||||
"version": "0.0.8",
|
||||
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
|
||||
@@ -9632,6 +10097,13 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/outvariant": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz",
|
||||
"integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/own-keys": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
|
||||
@@ -10619,6 +11091,16 @@
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
@@ -10745,6 +11227,13 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/rettime": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/rettime/-/rettime-0.7.0.tgz",
|
||||
"integrity": "sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/reusify": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
||||
@@ -11610,6 +12099,13 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/strict-event-emitter": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz",
|
||||
"integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string-argv": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz",
|
||||
@@ -12201,6 +12697,19 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/tagged-tag": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz",
|
||||
"integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/tinybench": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||
@@ -12621,6 +13130,16 @@
|
||||
"@unrs/resolver-binding-win32-x64-msvc": "1.11.1"
|
||||
}
|
||||
},
|
||||
"node_modules/until-async": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz",
|
||||
"integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kettanaito"
|
||||
}
|
||||
},
|
||||
"node_modules/uri-js": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
@@ -13309,6 +13828,19 @@
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/yoctocolors-cjs": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz",
|
||||
"integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,9 @@
|
||||
},
|
||||
"#server_functions": {
|
||||
"default": "./js/server_functions.js"
|
||||
},
|
||||
"#http_fetcher": {
|
||||
"default": "./js/http_fetcher.js"
|
||||
}
|
||||
},
|
||||
"main": "js/electron.js",
|
||||
@@ -116,6 +119,7 @@
|
||||
"jsdom": "^27.4.0",
|
||||
"lint-staged": "^16.2.7",
|
||||
"markdownlint-cli2": "^0.20.0",
|
||||
"msw": "^2.12.7",
|
||||
"playwright": "^1.57.0",
|
||||
"prettier": "^3.7.4",
|
||||
"prettier-plugin-jinja-template": "^2.1.0",
|
||||
|
||||
443
tests/unit/functions/http_fetcher_spec.js
Normal file
443
tests/unit/functions/http_fetcher_spec.js
Normal file
@@ -0,0 +1,443 @@
|
||||
const { http, HttpResponse } = require("msw");
|
||||
const { setupServer } = require("msw/node");
|
||||
const HTTPFetcher = require("#http_fetcher");
|
||||
|
||||
const TEST_URL = "http://test.example.com/data";
|
||||
let server;
|
||||
let fetcher;
|
||||
|
||||
beforeAll(() => {
|
||||
server = setupServer();
|
||||
server.listen({ onUnhandledRequest: "error" });
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers();
|
||||
if (fetcher) {
|
||||
fetcher.clearTimer();
|
||||
fetcher = null;
|
||||
}
|
||||
});
|
||||
|
||||
describe("HTTPFetcher", () => {
|
||||
|
||||
describe("Basic fetch operations", () => {
|
||||
it("should emit response event on successful fetch", async () => {
|
||||
const responseData = "test data";
|
||||
server.use(
|
||||
http.get(TEST_URL, () => {
|
||||
return HttpResponse.text(responseData);
|
||||
})
|
||||
);
|
||||
|
||||
fetcher = new HTTPFetcher(TEST_URL, { reloadInterval: 60000 });
|
||||
|
||||
const responsePromise = new Promise((resolve) => {
|
||||
fetcher.on("response", (response) => {
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
|
||||
fetcher.startPeriodicFetch();
|
||||
const response = await responsePromise;
|
||||
|
||||
expect(response.ok).toBe(true);
|
||||
expect(response.status).toBe(200);
|
||||
const text = await response.text();
|
||||
expect(text).toBe(responseData);
|
||||
});
|
||||
|
||||
it("should emit error event on network failure", async () => {
|
||||
server.use(
|
||||
http.get(TEST_URL, () => {
|
||||
return HttpResponse.error();
|
||||
})
|
||||
);
|
||||
|
||||
fetcher = new HTTPFetcher(TEST_URL, { reloadInterval: 60000 });
|
||||
|
||||
const errorPromise = new Promise((resolve) => {
|
||||
fetcher.on("error", (errorInfo) => {
|
||||
resolve(errorInfo);
|
||||
});
|
||||
});
|
||||
|
||||
fetcher.startPeriodicFetch();
|
||||
const errorInfo = await errorPromise;
|
||||
|
||||
expect(errorInfo).toHaveProperty("errorType", "NETWORK_ERROR");
|
||||
expect(errorInfo).toHaveProperty("translationKey", "MODULE_ERROR_NO_CONNECTION");
|
||||
expect(errorInfo).toHaveProperty("url", TEST_URL);
|
||||
});
|
||||
|
||||
it("should emit error event on timeout", async () => {
|
||||
server.use(
|
||||
http.get(TEST_URL, async () => {
|
||||
// Simulate a slow server that never responds
|
||||
await new Promise((resolve) => setTimeout(resolve, 60000));
|
||||
return HttpResponse.text("too late");
|
||||
})
|
||||
);
|
||||
|
||||
fetcher = new HTTPFetcher(TEST_URL, { reloadInterval: 60000, timeout: 100 });
|
||||
|
||||
const errorPromise = new Promise((resolve) => {
|
||||
fetcher.on("error", (errorInfo) => {
|
||||
resolve(errorInfo);
|
||||
});
|
||||
});
|
||||
|
||||
fetcher.startPeriodicFetch();
|
||||
const errorInfo = await errorPromise;
|
||||
|
||||
expect(errorInfo.errorType).toBe("NETWORK_ERROR");
|
||||
expect(errorInfo.message).toContain("timeout");
|
||||
expect(errorInfo.message).toContain("100ms");
|
||||
});
|
||||
});
|
||||
|
||||
describe("HTTPFetcher - HTTP status code handling", () => {
|
||||
describe("401/403 errors (Auth failures)", () => {
|
||||
it("should emit error with AUTH_FAILURE for 401", async () => {
|
||||
server.use(
|
||||
http.get(TEST_URL, () => {
|
||||
return new HttpResponse(null, { status: 401 });
|
||||
})
|
||||
);
|
||||
|
||||
fetcher = new HTTPFetcher(TEST_URL, { reloadInterval: 60000 });
|
||||
|
||||
const errorPromise = new Promise((resolve) => {
|
||||
fetcher.on("error", (errorInfo) => {
|
||||
resolve(errorInfo);
|
||||
});
|
||||
});
|
||||
|
||||
fetcher.startPeriodicFetch();
|
||||
const errorInfo = await errorPromise;
|
||||
|
||||
expect(errorInfo.status).toBe(401);
|
||||
expect(errorInfo.errorType).toBe("AUTH_FAILURE");
|
||||
expect(errorInfo.translationKey).toBe("MODULE_ERROR_UNAUTHORIZED");
|
||||
});
|
||||
|
||||
it("should emit error with AUTH_FAILURE for 403", async () => {
|
||||
server.use(
|
||||
http.get(TEST_URL, () => {
|
||||
return new HttpResponse(null, { status: 403 });
|
||||
})
|
||||
);
|
||||
|
||||
fetcher = new HTTPFetcher(TEST_URL, { reloadInterval: 60000 });
|
||||
|
||||
const errorPromise = new Promise((resolve) => {
|
||||
fetcher.on("error", (errorInfo) => {
|
||||
resolve(errorInfo);
|
||||
});
|
||||
});
|
||||
|
||||
fetcher.startPeriodicFetch();
|
||||
const errorInfo = await errorPromise;
|
||||
|
||||
expect(errorInfo.status).toBe(403);
|
||||
expect(errorInfo.errorType).toBe("AUTH_FAILURE");
|
||||
});
|
||||
});
|
||||
|
||||
describe("429 errors (Rate limiting)", () => {
|
||||
it("should emit error with RATE_LIMITED for 429", async () => {
|
||||
server.use(
|
||||
http.get(TEST_URL, () => {
|
||||
return new HttpResponse(null, {
|
||||
status: 429,
|
||||
headers: { "Retry-After": "120" }
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
fetcher = new HTTPFetcher(TEST_URL, { reloadInterval: 60000 });
|
||||
|
||||
const errorPromise = new Promise((resolve) => {
|
||||
fetcher.on("error", (errorInfo) => {
|
||||
resolve(errorInfo);
|
||||
});
|
||||
});
|
||||
|
||||
fetcher.startPeriodicFetch();
|
||||
const errorInfo = await errorPromise;
|
||||
|
||||
expect(errorInfo.status).toBe(429);
|
||||
expect(errorInfo.errorType).toBe("RATE_LIMITED");
|
||||
expect(errorInfo.retryAfter).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should parse Retry-After header in seconds", async () => {
|
||||
server.use(
|
||||
http.get(TEST_URL, () => {
|
||||
return new HttpResponse(null, {
|
||||
status: 429,
|
||||
headers: { "Retry-After": "300" }
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
fetcher = new HTTPFetcher(TEST_URL, { reloadInterval: 60000 });
|
||||
|
||||
const errorPromise = new Promise((resolve) => {
|
||||
fetcher.on("error", (errorInfo) => {
|
||||
resolve(errorInfo);
|
||||
});
|
||||
});
|
||||
|
||||
fetcher.startPeriodicFetch();
|
||||
const errorInfo = await errorPromise;
|
||||
|
||||
// 300 seconds = 300000 ms
|
||||
expect(errorInfo.retryAfter).toBe(300000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("5xx errors (Server errors)", () => {
|
||||
it("should emit error with SERVER_ERROR for 500", async () => {
|
||||
server.use(
|
||||
http.get(TEST_URL, () => {
|
||||
return new HttpResponse(null, { status: 500 });
|
||||
})
|
||||
);
|
||||
|
||||
fetcher = new HTTPFetcher(TEST_URL, { reloadInterval: 60000 });
|
||||
|
||||
const errorPromise = new Promise((resolve) => {
|
||||
fetcher.on("error", (errorInfo) => {
|
||||
resolve(errorInfo);
|
||||
});
|
||||
});
|
||||
|
||||
fetcher.startPeriodicFetch();
|
||||
const errorInfo = await errorPromise;
|
||||
|
||||
expect(errorInfo.status).toBe(500);
|
||||
expect(errorInfo.errorType).toBe("SERVER_ERROR");
|
||||
});
|
||||
|
||||
it("should emit error with SERVER_ERROR for 503", async () => {
|
||||
server.use(
|
||||
http.get(TEST_URL, () => {
|
||||
return new HttpResponse(null, { status: 503 });
|
||||
})
|
||||
);
|
||||
|
||||
fetcher = new HTTPFetcher(TEST_URL, { reloadInterval: 60000 });
|
||||
|
||||
const errorPromise = new Promise((resolve) => {
|
||||
fetcher.on("error", (errorInfo) => {
|
||||
resolve(errorInfo);
|
||||
});
|
||||
});
|
||||
|
||||
fetcher.startPeriodicFetch();
|
||||
const errorInfo = await errorPromise;
|
||||
|
||||
expect(errorInfo.status).toBe(503);
|
||||
expect(errorInfo.errorType).toBe("SERVER_ERROR");
|
||||
});
|
||||
});
|
||||
|
||||
describe("4xx errors (Client errors)", () => {
|
||||
it("should emit error with CLIENT_ERROR for 404", async () => {
|
||||
server.use(
|
||||
http.get(TEST_URL, () => {
|
||||
return new HttpResponse(null, { status: 404 });
|
||||
})
|
||||
);
|
||||
|
||||
fetcher = new HTTPFetcher(TEST_URL, { reloadInterval: 60000 });
|
||||
|
||||
const errorPromise = new Promise((resolve) => {
|
||||
fetcher.on("error", (errorInfo) => {
|
||||
resolve(errorInfo);
|
||||
});
|
||||
});
|
||||
|
||||
fetcher.startPeriodicFetch();
|
||||
const errorInfo = await errorPromise;
|
||||
|
||||
expect(errorInfo.status).toBe(404);
|
||||
expect(errorInfo.errorType).toBe("CLIENT_ERROR");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("HTTPFetcher - Authentication", () => {
|
||||
it("should include Basic auth header when configured", async () => {
|
||||
let receivedHeaders = null;
|
||||
|
||||
server.use(
|
||||
http.get(TEST_URL, ({ request }) => {
|
||||
receivedHeaders = Object.fromEntries(request.headers);
|
||||
return HttpResponse.text("ok");
|
||||
})
|
||||
);
|
||||
|
||||
fetcher = new HTTPFetcher(TEST_URL, {
|
||||
reloadInterval: 60000,
|
||||
auth: {
|
||||
method: "basic",
|
||||
user: "testuser",
|
||||
pass: "testpass"
|
||||
}
|
||||
});
|
||||
|
||||
const responsePromise = new Promise((resolve) => {
|
||||
fetcher.on("response", resolve);
|
||||
});
|
||||
|
||||
fetcher.startPeriodicFetch();
|
||||
await responsePromise;
|
||||
|
||||
const expectedAuth = `Basic ${Buffer.from("testuser:testpass").toString("base64")}`;
|
||||
expect(receivedHeaders.authorization).toBe(expectedAuth);
|
||||
});
|
||||
|
||||
it("should include Bearer auth header when configured", async () => {
|
||||
let receivedHeaders = null;
|
||||
|
||||
server.use(
|
||||
http.get(TEST_URL, ({ request }) => {
|
||||
receivedHeaders = Object.fromEntries(request.headers);
|
||||
return HttpResponse.text("ok");
|
||||
})
|
||||
);
|
||||
|
||||
fetcher = new HTTPFetcher(TEST_URL, {
|
||||
reloadInterval: 60000,
|
||||
auth: {
|
||||
method: "bearer",
|
||||
pass: "my-token-123"
|
||||
}
|
||||
});
|
||||
|
||||
const responsePromise = new Promise((resolve) => {
|
||||
fetcher.on("response", resolve);
|
||||
});
|
||||
|
||||
fetcher.startPeriodicFetch();
|
||||
await responsePromise;
|
||||
|
||||
expect(receivedHeaders.authorization).toBe("Bearer my-token-123");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Custom headers", () => {
|
||||
it("should include custom headers in request", async () => {
|
||||
let receivedHeaders = null;
|
||||
|
||||
server.use(
|
||||
http.get(TEST_URL, ({ request }) => {
|
||||
receivedHeaders = Object.fromEntries(request.headers);
|
||||
return HttpResponse.text("ok");
|
||||
})
|
||||
);
|
||||
|
||||
fetcher = new HTTPFetcher(TEST_URL, {
|
||||
reloadInterval: 60000,
|
||||
headers: {
|
||||
"X-Custom-Header": "custom-value",
|
||||
Accept: "application/json"
|
||||
}
|
||||
});
|
||||
|
||||
const responsePromise = new Promise((resolve) => {
|
||||
fetcher.on("response", resolve);
|
||||
});
|
||||
|
||||
fetcher.startPeriodicFetch();
|
||||
await responsePromise;
|
||||
|
||||
expect(receivedHeaders["x-custom-header"]).toBe("custom-value");
|
||||
expect(receivedHeaders.accept).toBe("application/json");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Timer management", () => {
|
||||
it("should not set timer in test mode", async () => {
|
||||
server.use(
|
||||
http.get(TEST_URL, () => {
|
||||
return HttpResponse.text("ok");
|
||||
})
|
||||
);
|
||||
|
||||
fetcher = new HTTPFetcher(TEST_URL, { reloadInterval: 100 });
|
||||
|
||||
const responsePromise = new Promise((resolve) => {
|
||||
fetcher.on("response", resolve);
|
||||
});
|
||||
|
||||
fetcher.startPeriodicFetch();
|
||||
await responsePromise;
|
||||
|
||||
// Timer should NOT be set in test mode (mmTestMode=true)
|
||||
expect(fetcher.reloadTimer).toBeNull();
|
||||
});
|
||||
|
||||
it("should clear timer when clearTimer is called", () => {
|
||||
fetcher = new HTTPFetcher(TEST_URL, { reloadInterval: 100 });
|
||||
|
||||
// Manually set a timer to test clearing
|
||||
fetcher.reloadTimer = setTimeout(() => {}, 10000);
|
||||
expect(fetcher.reloadTimer).not.toBeNull();
|
||||
|
||||
fetcher.clearTimer();
|
||||
|
||||
expect(fetcher.reloadTimer).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetch() method", () => {
|
||||
it("should emit response event when called", async () => {
|
||||
const responseData = "direct fetch data";
|
||||
server.use(
|
||||
http.get(TEST_URL, () => {
|
||||
return HttpResponse.text(responseData);
|
||||
})
|
||||
);
|
||||
|
||||
fetcher = new HTTPFetcher(TEST_URL, { reloadInterval: 60000 });
|
||||
|
||||
const responsePromise = new Promise((resolve) => {
|
||||
fetcher.on("response", resolve);
|
||||
});
|
||||
|
||||
await fetcher.fetch();
|
||||
const response = await responsePromise;
|
||||
|
||||
expect(response.ok).toBe(true);
|
||||
const text = await response.text();
|
||||
expect(text).toBe(responseData);
|
||||
});
|
||||
|
||||
it("should emit error event on network error", async () => {
|
||||
server.use(
|
||||
http.get(TEST_URL, () => {
|
||||
return HttpResponse.error();
|
||||
})
|
||||
);
|
||||
|
||||
fetcher = new HTTPFetcher(TEST_URL, { reloadInterval: 60000 });
|
||||
|
||||
const errorPromise = new Promise((resolve) => {
|
||||
fetcher.on("error", resolve);
|
||||
});
|
||||
|
||||
await fetcher.fetch();
|
||||
const errorInfo = await errorPromise;
|
||||
|
||||
expect(errorInfo.errorType).toBe("NETWORK_ERROR");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "Ongeldige URL.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "Geen internetverbinding.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "Owerheid het misluk.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "Te veel versoeke. Probeer later weer.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Bediener fout. Probeer later weer.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "Versoek het misluk.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Gaan die logs na vir meer besonderhede.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "Geen nuus op die oomblik.",
|
||||
|
||||
@@ -34,6 +34,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "رابط غير صحيح.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "لا يوجد اتصال بالإنترنت.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "فشل التصريح.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "طلبات كثيرة جدا. إعادة المحاولة لاحقا.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "خطأ في الخادم. إعادة المحاولة لاحقا.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "فشل الطلب.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "تحقق من السجلات لمزيد من التفاصيل.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "لا توجد أخبار في الوقت الحالي.",
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "Неправилен URL адрес.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "Няма интернет връзка.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "Неуспешна авторизация.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "Твърде много заявки. Повторен опит по-късно.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Грешка в сървъра. Повторен опит по-късно.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "Заявката неуспешна.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Проверете логовете за повече подробности.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "Няма новини в момента.",
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "L'URL és mal format.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "No hi ha connexió a Internet.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "L'autorització ha fallat.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "Masses sol·licituds. Reintentant més tard.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Error del servidor. Reintentant més tard.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "La sol·licitud ha fallat.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Consulta els registres per a més detalls.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "No hi ha notícies disponibles en aquest moment.",
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "Nesprávná URL adresa.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "Není připojení k internetu.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "Autorizace selhala.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "Příliš mnoho požadavků. Zkouším znovu později.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Chyba serveru. Zkouším znovu později.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "Požadavek selhal.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Zkontrolujte protokoly pro více informací.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "Žádné zprávy.",
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "Ҫӗҫ ҫӗнӗ URL хата.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "Интернет-пулла хӗҫҫӗн.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "Авторизация хата.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "Нумай ыйту. Хыҫра тепӗр хут.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Сервер хатӗ. Хыҫра тепӗр хут.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "Ыйту хатӗ.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Тӗп лог ҫӗнтерӗ.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "Пулас ҫӗнтер ҫук.",
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "URL anghywir.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "Dim cysylltiad rhyngrwyd.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "Methiant awdurdodi.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "Gormod o geisiadau. Yn ceisio eto yn nes ymlaen.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Gwall gweinydd. Yn ceisio eto yn nes ymlaen.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "Cais wedi methu.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Gwiriwch y logiau am ragor o fanylion.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "Dim newyddion ar hyn o bryd.",
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "Forkert url.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "Ingen internetforbindelse.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "Godkendelse mislykkedes.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "For mange anmodninger. Prøver igen senere.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Serverfejl. Prøver igen senere.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "Anmodning mislykkedes.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Tjek logfiler for flere detaljer.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "Ingen nyheder i øjeblikket.",
|
||||
|
||||
@@ -37,6 +37,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "Fehlerhafte URL.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "Keine Internetverbindung.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "Autorisierung fehlgeschlagen.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "Zu viele Anfragen. Erneuter Versuch später.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Serverfehler. Erneuter Versuch später.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "Anfrage fehlgeschlagen.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Prüfe die Logdateien für weitere Details.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "Momentan keine Neuigkeiten.",
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "Λανθασμένη μορφή url.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "Δεν υπάρχει σύνδεση στο διαδίκτυο.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "Η εξουσιοδότηση απέτυχε.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "Πάρα πολλά αιτήματα. Θα ξαναπροσπαθήσω αργότερα.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Σφάλμα διακομιστή. Θα ξαναπροσπαθήσω αργότερα.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "Το αίτημα απέτυχε.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Ελέγξτε τα αρχεία καταγραφής για περισσότερες λεπτομέρειες.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "Δεν υπάρχουν ειδήσεις αυτή τη στιγμή.",
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "Malformed url.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "No internet connection.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "Authorization failed.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "Too many requests. Retrying later.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Server error. Retrying later.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "Request failed.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Check logs for more details.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "No news at the moment.",
|
||||
|
||||
@@ -37,6 +37,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "Malĝusta URL.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "Neniu interreta konekto.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "Aŭtorigo malsukcesis.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "Tro multaj petoj. Reprovo poste.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Servila eraro. Reprovo poste.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "Peto malsukcesis.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Kontrolu la protokolajn dosierojn por pli da detaloj.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "Momente neniu novaĵoj.",
|
||||
|
||||
@@ -37,6 +37,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "URL mal formado.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "No hay conexión a Internet.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "No autorizado.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "Demasiadas solicitudes. Reintentando más tarde.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Error del servidor. Reintentando más tarde.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "La solicitud falló.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Por favor, consulte los registros para obtener más información.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "No hay noticias disponibles en este momento.",
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "Ebakorrektne url.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "Interneti ühendus puudub.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "Autoriseerimine ebaõnnestus.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "Liiga palju päringuid. Proovin hiljem uuesti.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Serveri viga. Proovin hiljem uuesti.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "Päring ebaõnnestus.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Lisateabe saamiseks kontrollige logifaile.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "Hetkel ei ole uudiseid.",
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "Virheellinen url.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "Ei internet-yhteyttä.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "Valtuutus epäonnistui.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "Liikaa pyyntöjä. Yritetään myöhemmin uudelleen.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Palvelinvirhe. Yritetään myöhemmin uudelleen.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "Pyyntö epäonnistui.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Tarkista lokitiedostot saadaksesi lisätietoja.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "Ei uutisia tällä hetkellä.",
|
||||
|
||||
@@ -37,6 +37,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "URL mal formée.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "Pas de connexion Internet.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "L'autorisation à échouée.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "Trop de requêtes. Nouvelle tentative plus tard.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Erreur du serveur. Nouvelle tentative plus tard.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "La requête a échoué.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Consultez les journaux pour plus de détails.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "Aucune nouvelle pour le moment.",
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "De URL is net jildich.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "Gjin ynternetferbining.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "Autorisearje mislearre.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "Te folle fersiken. Letter opnij besykje.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Tsjinnerfout. Letter opnij besykje.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "Fersyk mislearre.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Sjoch de logs foar mear ynformaasje.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "Op it stuit gjin nijsberjochten.",
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "URL mal formado.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "Non hai conexión a Internet.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "A autorización fallou.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "Demasiadas solicitudes. Reintentando máis tarde.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Erro do servidor. Reintentando máis tarde.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "A solicitude fallou.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Verifique os rexistros para obter máis información.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "Non hai novas no momento.",
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "ખોટી URL.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "ઇન્ટરનેટ કનેક્શન નથી.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "અધિકૃત કરવું નિષ્ફળ.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "ઘણી બધી વિનંતીઓ. પછીથી પુનઃપ્રયાસ.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "સર્વર ભૂલ. પછીથી પુનઃપ્રયાસ.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "વિનંતી નિષ્ફળ.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "વધુ વિગતો માટે લોગ તપાસો.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "હાલમાં કોઈ સમાચાર નથી.",
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "כתובת אתר לא תקינה.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "אין חיבור לאינטרנט.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "הזדהות נכשלה.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "יותר מדי בקשות. מנסה שוב מאוחר יותר.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "שגיאת שרת. מנסה שוב מאוחר יותר.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "הבקשה נכשלה.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "בדוק את היומנים לפרטים נוספים.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "אין חדשות כרגע.",
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "गलत URL।",
|
||||
"MODULE_ERROR_NO_CONNECTION": "कोई इंटरनेट कनेक्शन नहीं।",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "प्राधिकरण विफल।",
|
||||
"MODULE_ERROR_RATE_LIMITED": "बहुत सारे अनुरोध। बाद में पुनः प्रयास करना।",
|
||||
"MODULE_ERROR_SERVER_ERROR": "सर्वर त्रुटि। बाद में पुनः प्रयास करना।",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "अनुरोध विफल रहा।",
|
||||
"MODULE_ERROR_UNSPECIFIED": "अधिक जानकारी के लिए लॉग जांचें।",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "इस समय कोई समाचार नहीं।",
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "Neispravan URL.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "Nema internetske veze.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "Autorizacija nije uspjela.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "Previše zahtjeva. Ponovni pokušaj kasnije.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Greška poslužitelja. Ponovni pokušaj kasnije.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "Zahtjev nije uspio.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Provjerite dnevnike za više informacija.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "Trenutno nema vijesti.",
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "Hibás URL.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "Nincs internetkapcsolat.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "Azonosítás sikertelen.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "Túl sok kérés. Újrapróbálkozás később.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Szerverhiba. Újrapróbálkozás később.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "A kérés meghiúsult.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Ellenőrizze a naplókat további részletekért.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "Jelenleg nincsenek hírek.",
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "URL tidak valid.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "Tidak ada koneksi internet.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "Gagal otentikasi.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "Terlalu banyak permintaan. Mencoba lagi nanti.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Kesalahan server. Mencoba lagi nanti.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "Permintaan gagal.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Silakan periksa log untuk informasi lebih lanjut.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "Saat ini tidak ada berita.",
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "Villa í slóð.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "Engin nettenging.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "Auðkenning mistókst.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "Of margar beiðnir. Reyni aftur síðar.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Villa í þjóni. Reyni aftur síðar.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "Beiðni mistókst.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Vinsamlegast athugaðu skráningu fyrir frekari upplýsingar.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "Engar fréttir í boði núna.",
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "URL non valido.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "Nessuna connessione a Internet.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "Autenticazione non riuscita.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "Troppe richieste. Riprovo più tardi.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Errore del server. Riprovo più tardi.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "Richiesta fallita.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Si prega di controllare i log per ulteriori dettagli.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "Al momento non ci sono notizie.",
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "不正なURLです。",
|
||||
"MODULE_ERROR_NO_CONNECTION": "インターネット接続がありません。",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "認証に失敗しました。",
|
||||
"MODULE_ERROR_RATE_LIMITED": "リクエストが多すぎます。後で再試行します。",
|
||||
"MODULE_ERROR_SERVER_ERROR": "サーバーエラー。後で再試行します。",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "リクエストが失敗しました。",
|
||||
"MODULE_ERROR_UNSPECIFIED": "詳細はログを確認してください。",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "現在ニュースはありません。",
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "잘못된 URL 형식입니다.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "인터넷이 연결되지 않았습니다.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "인증이 실패했습니다.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "요청이 너무 많습니다. 나중에 다시 시도합니다.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "서버 오류. 나중에 다시 시도합니다.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "요청 실패.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "상세 내용은 로그를 확인하세요.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "현재 뉴스가 없습니다.",
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "Netinkama URL nuoroda.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "Nėra interneto ryšio.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "Autorizacija nepavyko.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "Per daug užklausų. Bandoma vėl vėliau.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Serverio klaida. Bandoma vėl vėliau.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "Užklausa nepavyko.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Patikrinkite žurnalus, kad gautumėte daugiau informacijos.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "Šiuo metu naujienų nėra.",
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "URL tidak sah.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "Tiada sambungan internet.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "Kebenaran gagal.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "Terlalu banyak permintaan. Cuba lagi nanti.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Ralat pelayan. Cuba lagi nanti.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "Permintaan gagal.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Sila semak log untuk maklumat lanjut.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "Tiada berita buat masa ini.",
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "Ugyldig URL.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "Ingen internettforbindelse.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "Autentisering mislyktes.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "For mange forespørsler. Prøver igjen senere.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Serverfeil. Prøver igjen senere.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "Forespørselen mislyktes.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Vennligst sjekk loggene for mer informasjon.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "Ingen nyheter tilgjengelig for øyeblikket.",
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "Ongeldige url.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "Geen internet verbinding.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "Authenticatie mislukt.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "Te veel verzoeken. Later opnieuw proberen.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Serverfout. Later opnieuw proberen.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "Verzoek mislukt.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Bekijk de logs voor meer informatie.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "Geen nieuws op dit moment.",
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "Ugyldig URL.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "Ingen internettforbindelse.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "Autentisering mislyktes.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "For mange førespurnader. Prøvar igjen seinare.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Serverfeil. Prøvar igjen seinare.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "Førespurnaden feila.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Vennligst sjekk loggfilene for meir informasjon.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "Ingen nyhende tilgjengeleg no.",
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "Nieprawidłowy adres URL.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "Brak połączenia z internetem.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "Autoryzacja nie powiodła się.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "Zbyt wiele żądań. Ponowna próba później.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Błąd serwera. Ponowna próba później.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "Żądanie nie powiodło się.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Sprawdź logi, aby uzyskać więcej informacji.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "Brak wiadomości w tej chwili.",
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "URL inválido.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "Sem conexão com a Internet.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "Falha na autenticação.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "Muitas requisições. Tentando novamente mais tarde.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Erro do servidor. Tentando novamente mais tarde.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "Requisição falhou.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Verifique os logs para mais detalhes.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "Atualmente não há notícias.",
|
||||
|
||||
@@ -37,20 +37,13 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "URL inválido.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "Sem ligação à internet.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "Falha na autorização.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "Demasiados pedidos. A tentar novamente mais tarde.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Erro do servidor. A tentar novamente mais tarde.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "Pedido falhou.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Consulta os registos para mais detalhes.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "Sem notícias de momento.",
|
||||
|
||||
"UPDATE_NOTIFICATION": "Está disponível uma atualização do MagicMirror².",
|
||||
"UPDATE_NOTIFICATION_MODULE": "Atualização disponível para o módulo {MODULE_NAME}.",
|
||||
"UPDATE_INFO_SINGLE": "A instalação atual está {COMMIT_COUNT} commit atrás na ramificação {BRANCH_NAME}.",
|
||||
"UPDATE_INFO_MULTIPLE": "A instalação atual está {COMMIT_COUNT} commits atrás na ramificação {BRANCH_NAME}.",
|
||||
"UPDATE_NOTIFICATION_DONE": "Atualização concluída do módulo {MODULE_NAME}.",
|
||||
"UPDATE_NOTIFICATION_ERROR": "Erro na atualização do módulo {MODULE_NAME}.",
|
||||
"UPDATE_NOTIFICATION_NEED-RESTART": "É necessário reiniciar o MagicMirror.",
|
||||
|
||||
"MODULE_ERROR_MALFORMED_URL": "URL Inválido.",
|
||||
|
||||
"UPDATE_NOTIFICATION": "Está disponível uma atualização do MagicMirror².",
|
||||
"UPDATE_NOTIFICATION_MODULE": "Atualização disponível para o módulo {MODULE_NAME}.",
|
||||
"UPDATE_INFO_SINGLE": "A instalação atual está {COMMIT_COUNT} commit atrás na ramificação {BRANCH_NAME}.",
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "URL incorect.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "Fără conexiune la internet.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "Autorizarea a eșuat.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "Prea multe cereri. Se reîncearcă mai târziu.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Eroare de server. Se reîncearcă mai târziu.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "Cererea a eșuat.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Verificați jurnalele pentru mai multe detalii.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "Nu există știri în acest moment.",
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "Неверный URL.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "Нет интернет-соединения.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "Не удалось авторизоваться.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "Слишком много запросов. Повторная попытка позже.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Ошибка сервера. Повторная попытка позже.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "Запрос не удался.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Пожалуйста, проверьте логи для получения дополнительной информации.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "В данный момент нет новостей.",
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "Nesprávna URL adresa.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "Nie je pripojenie k internetu.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "Autorizácia zlyhala.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "Príliš veľa požiadaviek. Skúšam znovu neskôr.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Chyba servera. Skúšam znovu neskôr.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "Požiadavka zlyhala.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Skontrolujte protokoly pre viac informácií.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "Momentálne žiadne správy.",
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "Felaktig URL.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "Ingen internetanslutning.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "Autentisering misslyckades.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "För många förfrågningar. Försöker igen senare.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Serverfel. Försöker igen senare.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "Förfrågan misslyckades.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Vänligen kontrollera loggarna för mer information.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "Inga nyheter för tillfället.",
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "URL ผิดรูปแบบ",
|
||||
"MODULE_ERROR_NO_CONNECTION": "ไม่มีการเชื่อมต่ออินเทอร์เน็ต.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "การอนุญาตล้มเหลว",
|
||||
"MODULE_ERROR_RATE_LIMITED": "คำขอมากเกินไป กำลังลองใหม่ในภายหลัง",
|
||||
"MODULE_ERROR_SERVER_ERROR": "ข้อผิดพลาดเซิร์ฟเวอร์ กำลังลองใหม่ในภายหลัง",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "คำขอล้มเหลว",
|
||||
"MODULE_ERROR_UNSPECIFIED": "ตรวจสอบบันทึกสำหรับรายละเอียดเพิ่มเติม",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "ไม่มีข่าวในขณะนี้",
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "URL ghobe' yImev.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "Internet ghobe' yImev.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "ghobe' yImev.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "tlhoy rurqu' lut. vaj ratlh.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Qagh server. vaj ratlh.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "lut Qagh.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "logmeyDaq yImev.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "DaHghachmey ghobe' yImev.",
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "Hatalı URL.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "İnternet bağlantısı yok.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "Yetkilendirme başarısız.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "Çok fazla istek. Daha sonra yeniden deneniyor.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Sunucu hatası. Daha sonra yeniden deneniyor.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "İstek başarısız oldu.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Daha fazla ayrıntı için günlükleri kontrol edin.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "Şu anda haber yok.",
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "Неправильний URL.",
|
||||
"MODULE_ERROR_NO_CONNECTION": "Немає підключення до Інтернету.",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "Авторизація не вдалася.",
|
||||
"MODULE_ERROR_RATE_LIMITED": "Забагато запитів. Повторна спроба пізніше.",
|
||||
"MODULE_ERROR_SERVER_ERROR": "Помилка сервера. Повторна спроба пізніше.",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "Запит не вдався.",
|
||||
"MODULE_ERROR_UNSPECIFIED": "Перевірте журнали для отримання додаткової інформації.",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "Немає новин на даний момент.",
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "URL格式错误。",
|
||||
"MODULE_ERROR_NO_CONNECTION": "无网络连接。",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "授权失败。",
|
||||
"MODULE_ERROR_RATE_LIMITED": "请求过多。稍后重试。",
|
||||
"MODULE_ERROR_SERVER_ERROR": "服务器错误。稍后重试。",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "请求失败。",
|
||||
"MODULE_ERROR_UNSPECIFIED": "请查看日志以获取更多详细信息。",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "目前没有新闻。",
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
"MODULE_ERROR_MALFORMED_URL": "網址格式錯誤。",
|
||||
"MODULE_ERROR_NO_CONNECTION": "無網路連線。",
|
||||
"MODULE_ERROR_UNAUTHORIZED": "授權失敗。",
|
||||
"MODULE_ERROR_RATE_LIMITED": "請求過多。稍後重試。",
|
||||
"MODULE_ERROR_SERVER_ERROR": "伺服器錯誤。稍後重試。",
|
||||
"MODULE_ERROR_CLIENT_ERROR": "請求失敗。",
|
||||
"MODULE_ERROR_UNSPECIFIED": "查看日誌以了解詳情。",
|
||||
|
||||
"NEWSFEED_NO_ITEMS": "目前沒有新聞。",
|
||||
|
||||
Reference in New Issue
Block a user