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();