feat(auto-refresh): centralized auto refresh System

This commit is contained in:
Bastien Wirtz
2025-08-07 20:11:22 +02:00
parent ee57fa05fb
commit 4b2087dc37
21 changed files with 310 additions and 114 deletions

View File

@@ -130,6 +130,11 @@ export default {
DarkMode,
DynamicTheme,
},
provide() {
return {
config: () => this.config,
};
},
data: function () {
return {
loaded: false,

View File

@@ -48,10 +48,10 @@ export default {
};
},
created: function () {
const checkInterval = parseInt(this.item.checkInterval, 10) || 0;
if (checkInterval > 0) {
setInterval(() => this.fetchData(), checkInterval);
}
// Set up auto-update method for the scheduler
this.autoUpdateMethod = this.fetchData;
// Initial data fetch
this.fetchData();
},
methods: {

View File

@@ -40,10 +40,10 @@ export default {
};
},
created: function () {
const updateInterval = parseInt(this.item.updateInterval, 10) || 0;
if (updateInterval > 0)
setInterval(() => this.fetchConfig(), updateInterval);
// Set up auto-update method for the scheduler
this.autoUpdateMethod = this.fetchConfig;
// Initial data fetch
this.fetchConfig();
},
methods: {

View File

@@ -29,10 +29,10 @@ export default {
error: null,
}),
created() {
const updateInterval = parseInt(this.item.updateInterval, 10) || 0;
if (updateInterval > 0) {
setInterval(() => this.fetchStat(), updateInterval);
}
// Set up auto-update method for the scheduler
this.autoUpdateMethod = this.fetchStat;
// Initial data fetch
this.fetchStat();
},
methods: {

View File

@@ -62,10 +62,10 @@ export default {
},
},
created: function () {
const updateInterval = parseInt(this.item.updateInterval, 10) || 0;
if (updateInterval > 0) {
setInterval(() => this.fetchConfig(), updateInterval);
}
// Set up auto-update method for the scheduler
this.autoUpdateMethod = this.fetchConfig;
// Initial data fetch
this.fetchConfig();
},
methods: {

View File

@@ -43,12 +43,11 @@ export default {
serverError: false,
};
},
created: function () {
const checkInterval = parseInt(this.item.checkInterval, 10) || 0;
if (checkInterval > 0) {
setInterval(() => this.fetchConfig(), checkInterval);
}
created() {
// Set up auto-update method for the scheduler
this.autoUpdateMethod = this.fetchConfig;
// Initial data fetch
this.fetchConfig();
},
methods: {

View File

@@ -44,10 +44,10 @@ export default {
};
},
created() {
const updateInterval = parseInt(this.item.updateInterval, 10) || 0;
if (updateInterval > 0) {
setInterval(() => this.fetchStatus(), updateInterval);
}
// Set up auto-update method for the scheduler
this.autoUpdateMethod = this.fetchStatus;
// Initial data fetch
this.fetchStatus();
},
methods: {

View File

@@ -39,8 +39,6 @@ export default {
retryCount: 0,
maxRetries: 3,
retryDelay: 5000,
localCheckInterval: 1000, // Default value or a fallback
pollInterval: null,
}),
computed: {
percentage: function () {
@@ -57,18 +55,16 @@ export default {
},
created() {
if (parseInt(this.item.apiVersion, 10) === 6) {
// Set the interval to the checkInterval or default to 5 minutes
this.localCheckInterval = parseInt(this.item.checkInterval, 10) || 300000;
this.loadCachedSession();
this.startStatusPolling();
// Set up auto-update method for the scheduler
this.autoUpdateMethod = this.fetchStatus;
} else {
this.fetchStatus_v5();
}
},
beforeUnmount() {
if (parseInt(this.item.apiVersion, 10) === 6) {
this.stopStatusPolling();
// Set up auto-update method for the scheduler
this.autoUpdateMethod = this.fetchStatus_v5();
}
// Initial data fetch
this.autoUpdateMethod();
},
methods: {
handleError: function (error, status) {
@@ -76,21 +72,6 @@ export default {
this.subtitle = error;
this.status = status;
},
startStatusPolling: function () {
this.fetchStatus();
if (this.localCheckInterval < 1000) {
this.localCheckInterval = 1000;
}
this.pollInterval = setInterval(
this.fetchStatus,
this.localCheckInterval,
);
},
stopStatusPolling: function () {
if (this.pollInterval) {
clearInterval(this.pollInterval);
}
},
loadCachedSession: function () {
try {
const cachedSession = localStorage.getItem(

View File

@@ -41,11 +41,10 @@ export default {
},
},
created() {
const updateInterval = parseInt(this.item.updateInterval, 10) || 0;
if (updateInterval > 0) {
setInterval(this.fetchStatus, updateInterval);
}
// Set up auto-update method for the scheduler
this.autoUpdateMethod = this.fetchStatus;
// Initial data fetch
this.fetchStatus();
},
methods: {

View File

@@ -52,10 +52,10 @@ export default {
};
},
created: function () {
const checkInterval = parseInt(this.item.checkInterval, 10) || 0;
if (checkInterval > 0) {
setInterval(() => this.fetchData(), checkInterval);
}
// Set up auto-update method for the scheduler
this.autoUpdateMethod = this.fetchData;
// Initial data fetch
this.fetchData();
},
methods: {

View File

@@ -36,12 +36,11 @@ export default {
serverError: false,
};
},
created: function () {
const checkInterval = parseInt(this.item.checkInterval, 10) || 0;
if (checkInterval > 0) {
setInterval(() => this.fetchConfig(), checkInterval);
}
created() {
// Set up auto-update method for the scheduler
this.autoUpdateMethod = this.fetchConfig;
// Initial data fetch
this.fetchConfig();
},
methods: {

View File

@@ -51,12 +51,11 @@ export default {
return this.item.legacyApi ? LEGACY_API : V3_API;
},
},
created: function () {
const checkInterval = parseInt(this.item.checkInterval, 10) || 0;
if (checkInterval > 0) {
setInterval(() => this.fetchConfig(), checkInterval);
}
created() {
// Set up auto-update method for the scheduler
this.autoUpdateMethod = this.fetchConfig;
// Initial data fetch
this.fetchConfig();
},
methods: {

View File

@@ -59,24 +59,18 @@ export default {
},
},
created() {
// Set intervals if configured so the rates and/or torrent count
// will be updated.
const rateInterval = parseInt(this.item.rateInterval, 10) || 0;
const torrentInterval = parseInt(this.item.torrentInterval, 10) || 0;
if (rateInterval > 0) {
setInterval(() => this.fetchRates(), rateInterval);
}
if (torrentInterval > 0) {
setInterval(() => this.fetchCount(), torrentInterval);
}
// Set up auto-update method for the scheduler
this.autoUpdateMethod = this.fetchAllData;
// Fetch the initial values.
this.fetchRates();
this.fetchCount();
this.fetchAllData();
},
methods: {
// Combined method for scheduler - fetches both rates and count
fetchAllData: async function () {
this.fetchRates();
this.fetchCount();
},
// Perform two calls to the XML-RPC service and fetch download
// and upload rates. Values are saved to the `ul` and `dl`
// properties.

View File

@@ -80,11 +80,10 @@ export default {
},
},
created() {
const downloadInterval = parseInt(this.item.downloadInterval, 10) || 0;
if (downloadInterval > 0) {
setInterval(() => this.fetchStatus(), downloadInterval);
}
// Set up auto-update method for the scheduler
this.autoUpdateMethod = this.fetchStatus;
// Initial data fetch
this.fetchStatus();
},
methods: {

View File

@@ -40,10 +40,10 @@ export default {
};
},
created: function () {
const updateInterval = parseInt(this.item.updateInterval, 10) || 0;
if (updateInterval > 0) {
setInterval(() => this.fetchSummary(), updateInterval);
}
// Set up auto-update method for the scheduler
this.autoUpdateMethod = this.fetchSummary;
// Initial data fetch
this.fetchSummary();
},
methods: {

View File

@@ -52,12 +52,11 @@ export default {
return this.item.legacyApi ? LEGACY_API : V3_API;
},
},
created: function () {
const checkInterval = parseInt(this.item.checkInterval, 10) || 0;
if (checkInterval > 0) {
setInterval(() => this.fetchConfig(), checkInterval);
}
created() {
// Set up auto-update method for the scheduler
this.autoUpdateMethod = this.fetchConfig;
// Initial data fetch
this.fetchConfig();
},
methods: {

View File

@@ -41,11 +41,10 @@ export default {
},
},
created() {
const checkInterval = parseInt(this.item.checkInterval, 10) || 0;
if (checkInterval > 0) {
setInterval(() => this.fetchStatus(), checkInterval);
}
// Set up auto-update method for the scheduler
this.autoUpdateMethod = this.fetchStatus;
// Initial data fetch
this.fetchStatus();
},
methods: {

View File

@@ -54,11 +54,10 @@ export default {
},
},
created() {
const checkInterval = parseInt(this.item.checkInterval, 10) || 0;
if (checkInterval > 0) {
setInterval(() => this.fetchStatus(), checkInterval);
}
// Set up auto-update method for the scheduler
this.autoUpdateMethod = this.fetchStatus;
// Initial data fetch
this.fetchStatus();
},
methods: {

View File

@@ -61,19 +61,18 @@ export default {
},
},
created() {
const rateInterval = parseInt(this.item.rateInterval, 10) || 0;
const torrentInterval = parseInt(this.item.torrentInterval, 10) || 0;
if (rateInterval > 0) {
setInterval(() => this.getRate(), rateInterval);
}
if (torrentInterval > 0) {
setInterval(() => this.fetchCount(), torrentInterval);
}
// Set up auto-update method for the scheduler
this.autoUpdateMethod = this.fetchAllData;
this.getRate();
this.fetchCount();
// Fetch initial values
this.fetchAllData();
},
methods: {
// Combined method for scheduler - fetches both rates and count
fetchAllData: async function () {
this.getRate();
this.fetchCount();
},
fetchCount: async function () {
try {
const body = await this.fetch("/api/v2/torrents/info");

View File

@@ -1,15 +1,35 @@
import updateScheduler from "@/utils/updateScheduler.js";
export default {
props: {
proxy: Object,
},
inject: {
// Inject global config from parent components
config: {
default: () => ({}),
},
},
computed: {
globalConfig() {
return this.config() || {};
},
},
created: function () {
// custom service often consume info from an API using the item link (url) as a base url,
// Custom service often consume info from an API using the item link (url) as a base url,
// but sometimes the base url is different. An optional alternative URL can be provided with the "endpoint" key.
this.endpoint = this.item.endpoint || this.item.url;
if (this.endpoint && this.endpoint.endsWith("/")) {
this.endpoint = this.endpoint.slice(0, -1);
}
// Initialize auto-update if configured
this.initAutoUpdate();
},
beforeUnmount() {
// Clean up auto-update registration
updateScheduler.unregister(this);
},
methods: {
fetch: function (path, init, json = true) {
@@ -62,5 +82,79 @@ export default {
return json ? response.json() : response.text();
});
},
initAutoUpdate: function () {
// Check if component has defined an auto-update method and interval
const interval = this.getUpdateInterval();
if (
interval > 0 &&
this.autoUpdateMethod &&
typeof this.autoUpdateMethod === "function"
) {
updateScheduler.register(this, interval, this.autoUpdateMethod);
}
},
getUpdateInterval: function () {
// Check if auto-update is explicitly disabled for this service
if (this.item.autoUpdateInterval === false) {
return 0;
}
// Use service-specific interval if defined
if (this.item.autoUpdateInterval) {
return parseInt(this.item.autoUpdateInterval, 10) || 0;
}
// Check for deprecated keys and warn users
const deprecatedKeys = [
"updateInterval",
"checkInterval",
"localCheckInterval",
"downloadInterval",
"rateInterval",
"torrentInterval",
];
for (const key of deprecatedKeys) {
if (this.item[key]) {
console.warn(
`[DEPRECATED] Service "${this.item.name || "unknown"}" uses deprecated config key "${key}". ` +
`Please use "autoUpdateInterval" instead. Support for "${key}" will be removed in a future version.`,
);
return parseInt(this.item[key], 10) || 0;
}
}
// Use global auto-update configuration
return this.getGlobalAutoUpdateInterval();
},
getGlobalAutoUpdateInterval: function () {
const globalAutoUpdate = this.globalConfig.autoUpdate;
// If auto-update is not configured globally, disable
if (!globalAutoUpdate) {
return 0;
}
// If global auto-update is explicitly disabled
if (globalAutoUpdate.enabled === false) {
return 0;
}
// If autoUpdate is just a number (simplified config)
if (typeof globalAutoUpdate === "number") {
return globalAutoUpdate;
}
// If autoUpdate is an object, use defaultInterval
if (
typeof globalAutoUpdate === "object" &&
globalAutoUpdate.defaultInterval
) {
return parseInt(globalAutoUpdate.defaultInterval, 10) || 0;
}
return 0;
},
},
};

View File

@@ -0,0 +1,131 @@
/**
* This module provides a single-timer solution for managing automatic data updates
* across all service components in Homer. Instead of each service component creating
* its own setInterval timer, all components register with this centralized scheduler.
*
*/
class UpdateScheduler {
constructor() {
this.registeredComponents = new Map();
this.globalTimer = null;
this.tickCount = 0;
this.isRunning = false;
}
register(component, intervalMs, updateMethod) {
if (!component || !updateMethod || intervalMs <= 0) {
console.warn("UpdateScheduler: Invalid registration parameters");
return;
}
const intervalSeconds = Math.floor(intervalMs / 1000);
const componentId = this.generateComponentId(component);
this.registeredComponents.set(componentId, {
component,
interval: intervalSeconds,
method: updateMethod,
lastUpdate: 0,
});
this.startGlobalTimer();
console.log(
`UpdateScheduler: Registered component with ${intervalSeconds}s interval`,
);
}
unregister(component) {
const componentId = this.generateComponentId(component);
const removed = this.registeredComponents.delete(componentId);
if (removed) {
console.log("UpdateScheduler: Unregistered component");
}
if (this.registeredComponents.size === 0) {
this.stopGlobalTimer();
}
}
generateComponentId(component) {
// Use component's unique identifier or Vue instance uid
return component._uid || component.$.uid || Symbol("component");
}
startGlobalTimer() {
if (!this.globalTimer && !this.isRunning) {
this.isRunning = true;
this.tickCount = 0;
this.globalTimer = setInterval(() => {
this.tickCount++;
this.processUpdates();
}, 1000);
console.log("UpdateScheduler: Global timer started");
}
}
stopGlobalTimer() {
if (this.globalTimer) {
clearInterval(this.globalTimer);
this.globalTimer = null;
this.isRunning = false;
this.tickCount = 0;
console.log("UpdateScheduler: Global timer stopped");
}
}
processUpdates() {
for (const [, config] of this.registeredComponents) {
try {
if (this.tickCount - config.lastUpdate >= config.interval) {
config.method.call(config.component);
config.lastUpdate = this.tickCount;
}
} catch (error) {
console.error("UpdateScheduler: Error during component update:", error);
}
}
}
pause() {
if (this.globalTimer) {
clearInterval(this.globalTimer);
this.globalTimer = null;
this.isRunning = false;
console.log("UpdateScheduler: Paused");
}
}
resume() {
if (!this.globalTimer && this.registeredComponents.size > 0) {
this.startGlobalTimer();
console.log("UpdateScheduler: Resumed");
}
}
getStatus() {
return {
isRunning: this.isRunning,
registeredCount: this.registeredComponents.size,
tickCount: this.tickCount,
};
}
}
// Create and export global singleton instance
const updateScheduler = new UpdateScheduler();
// Pause updates when tab is hidden (power saving)
if (typeof document !== "undefined") {
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
updateScheduler.pause();
} else {
updateScheduler.resume();
}
});
}
export default updateScheduler;