99 lines
2.5 KiB
JavaScript
99 lines
2.5 KiB
JavaScript
import {adjustedInterval, Logger} from '@ztimson/utils';
|
|
|
|
const cacheOptions = {
|
|
ttl: null,
|
|
reload: null,
|
|
retry: 3,
|
|
url: null,
|
|
}
|
|
|
|
export class CacheService {
|
|
#fetchLoop;
|
|
|
|
cached = null;
|
|
lastUpdate = null;
|
|
logger;
|
|
name;
|
|
options;
|
|
pending;
|
|
|
|
constructor(name, options) {
|
|
this.name = name;
|
|
this.options = {
|
|
...cacheOptions,
|
|
ttl: options.reload,
|
|
...options
|
|
};
|
|
this.logger = new Logger(name);
|
|
if(this.options.reload) setTimeout(() => this.startLoop(), 1000);
|
|
}
|
|
|
|
async #fetchWithRetry() {
|
|
let lastError;
|
|
for(let attempt = 1; attempt <= this.options.retry; attempt++) {
|
|
try {
|
|
return await this.fetch();
|
|
} catch(err) {
|
|
lastError = err;
|
|
if(attempt < this.options.retry)
|
|
this.logger.warn(`Retrying (${attempt + 1}/${this.options.retry}): ${err.message || err.toString()}`);
|
|
}
|
|
}
|
|
throw lastError;
|
|
}
|
|
|
|
async fetch() {
|
|
if(!this.options.url) return Promise.reject('Fetch is not configured');
|
|
const resp = await fetch(this.options.url);
|
|
if(!resp.ok) throw new Error(`${resp.status} ${resp.statusText}`);
|
|
return resp.json();
|
|
}
|
|
|
|
startLoop(speed = this.options.reload) {
|
|
this.stopLoop();
|
|
this.#fetchLoop = adjustedInterval(() => this.get(), speed);
|
|
}
|
|
|
|
stopLoop() {
|
|
if(!this.#fetchLoop) return;
|
|
clearInterval(this.#fetchLoop);
|
|
this.#fetchLoop = null;
|
|
}
|
|
|
|
update(catchErr = true) {
|
|
if(this.pending) return this.pending;
|
|
this.logger.info('Fetching latest');
|
|
this.pending = this.#fetchWithRetry().then(data => {
|
|
if(data?.err) throw new Error(data.err?.stack || data.err?.message || data.err);
|
|
if(data?.error) throw new Error(data.error?.stack || data.error?.message || data.error);
|
|
this.lastUpdate = new Date();
|
|
this.cached = data || null;
|
|
this.logger.debug('Finished updating');
|
|
return {timestamp: this.lastUpdate, data: this.cached};
|
|
}).catch(err => {
|
|
if(catchErr) this.logger.error(`Failed: ${typeof err == 'object' ? (err.stackTrace || err.message) : err}`)
|
|
else throw err;
|
|
}).finally(() => this.pending = null);
|
|
return this.pending;
|
|
}
|
|
|
|
async get(wait = false) {
|
|
if(!this.cached || !this.lastUpdate || this.lastUpdate?.getTime() + this.options.ttl <= Date.now()) {
|
|
if(wait) await this.update();
|
|
else this.update();
|
|
}
|
|
return Promise.resolve({timestamp: this.lastUpdate || null, data: this.cached || null});
|
|
}
|
|
}
|
|
|
|
export class AuroraService extends CacheService {
|
|
constructor() {
|
|
super('Aurora', {
|
|
reload: 60_000 * 15,
|
|
url: 'https://services.swpc.noaa.gov/json/ovation_aurora_latest.json',
|
|
});
|
|
}
|
|
}
|
|
|
|
export const Aurora = new AuroraService();
|