UNPKG

@hazae41/glacier

Version:

Yet another React data (re)fetching library

446 lines (443 loc) 16.5 kB
import { Mutex } from '@hazae41/mutex'; import { Option, Some } from '@hazae41/option'; import { SuperEventTarget } from '@hazae41/plume'; import { Result } from '@hazae41/result'; import { equals } from '../../libs/equals/index.mjs'; import { Time } from '../../libs/time/time.mjs'; import { Data } from '../fetched/data.mjs'; import { Fail } from '../fetched/fail.mjs'; import { Fetched } from '../fetched/fetched.mjs'; import { RealState, FailState, DataState, FakeState } from '../types/state.mjs'; var _a, _b, _c, _d, _e; class AsyncStorageError extends Error { #class = _a; name = this.#class.name; constructor() { super(`Storage is asynchronous`); } } _a = AsyncStorageError; class TimeoutError extends Error { #class = _b; name = this.#class.name; constructor() { super(`Timed out`); } } _b = TimeoutError; class CooldownError extends Error { #class = _c; name = this.#class.name; constructor() { super(`Cooled down`); } } _c = CooldownError; class MissingKeyError extends Error { #class = _d; name = this.#class.name; constructor() { super(`Missing a key`); } } _d = MissingKeyError; class MissingFetcherError extends Error { #class = _e; name = this.#class.name; constructor() { super(`Missing a fetcher`); } } _e = MissingFetcherError; class Core { onState = new SuperEventTarget(); onAborter = new SuperEventTarget(); mutexes = new Map(); unstoreds = new Map(); storeds = new Map(); promises = new Map(); aborters = new Map(); timeouts = new Map(); counters = new Map(); optimizers = new Map(); #mounted = true; constructor() { new FinalizationRegistry(() => { this.clean(); }).register(this, undefined); } clean() { for (const timeout of this.timeouts.values()) clearTimeout(timeout); this.#mounted = false; } getAborterSync(cacheKey) { return this.aborters.get(cacheKey); } getStateSync(cacheKey) { return this.unstoreds.get(cacheKey); } getOrCreateMutex(cacheKey) { let mutex = this.mutexes.get(cacheKey); if (mutex != null) return mutex; mutex = new Mutex(undefined); this.mutexes.set(cacheKey, mutex); return mutex; } async runOrReplace(cacheKey, aborter, callback) { const previous = this.promises.get(cacheKey); if (previous != null) this.aborters.get(cacheKey).abort(); try { const promise = callback(); this.promises.set(cacheKey, promise); this.aborters.set(cacheKey, aborter); await this.onAborter.emit("*", cacheKey); await this.onAborter.emit(cacheKey, cacheKey); return await promise; } finally { /** * Avoid cleaning if it has been replaced */ if (this.aborters.get(cacheKey) === aborter) { this.aborters.delete(cacheKey); this.promises.delete(cacheKey); await this.onAborter.emit("*", cacheKey); await this.onAborter.emit(cacheKey, cacheKey); } } } async runOrJoin(cacheKey, aborter, callback) { const previous = this.promises.get(cacheKey); if (previous != null) return await previous; try { const promise = callback(); this.promises.set(cacheKey, promise); this.aborters.set(cacheKey, aborter); await this.onAborter.emit("*", cacheKey); await this.onAborter.emit(cacheKey, cacheKey); return await promise; } finally { /** * Avoid cleaning if it has been replaced */ if (this.aborters.get(cacheKey) === aborter) { this.aborters.delete(cacheKey); this.promises.delete(cacheKey); await this.onAborter.emit("*", cacheKey); await this.onAborter.emit(cacheKey, cacheKey); } } } async #getOrThrow(cacheKey, settings) { if (this.unstoreds.has(cacheKey)) return this.unstoreds.get(cacheKey); if (this.storeds.has(cacheKey)) { const stored = this.storeds.get(cacheKey); const unstored = await this.unstoreOrThrow(stored, settings); this.unstoreds.set(cacheKey, unstored); await this.onState.emit("*", cacheKey); await this.onState.emit(cacheKey, cacheKey); return unstored; } const stored = await Result.runAndWrap(async () => { return settings.storage?.getOrThrow?.(cacheKey); }).then(r => r?.getOrNull()); const unstored = await this.unstoreOrThrow(stored, settings); this.storeds.set(cacheKey, stored); this.unstoreds.set(cacheKey, unstored); await this.onState.emit("*", cacheKey); await this.onState.emit(cacheKey, cacheKey); return unstored; } async getOrThrow(cacheKey, settings) { return await this.getOrCreateMutex(cacheKey).runOrWait(() => this.#getOrThrow(cacheKey, settings)); } async tryGet(cacheKey, settings) { return await Result.runAndDoubleWrap(() => this.getOrThrow(cacheKey, settings)); } async storeOrThrow(state, settings) { const { key } = settings; if (state.real == null) return undefined; const { time, cooldown, expiration } = state.real.current; const data = state.real.data; const error = state.real.error; return { key, version: 3, data, error, time, cooldown, expiration }; } async unstoreOrThrow(stored, settings) { if (stored == null) return new RealState(undefined); if (stored.version == null) { const { time, cooldown, expiration } = stored; const times = { time, cooldown, expiration }; const data = Option.wrap(stored.data).mapSync(x => new Data(x, times)); const error = Option.wrap(stored.error).mapSync(x => new Fail(x, times)); if (error.isSome()) return new RealState(new FailState(error.get(), data.getOrNull())); if (data.isSome()) return new RealState(new DataState(data.get())); return new RealState(undefined); } if (stored.version === 2 || stored.version === 3) { const data = Option.wrap(stored.data).mapSync(x => Data.from(x)); const error = Option.wrap(stored.error).mapSync(x => Fail.from(x)); if (error.isSome()) return new RealState(new FailState(error.get(), data.getOrNull())); if (data.isSome()) return new RealState(new DataState(data.get())); return new RealState(undefined); } return new RealState(undefined); } /** * Set full state and store it in storage * @param cacheKey * @param setter * @param settings * @returns */ async setOrThrow(cacheKey, setter, settings) { return await this.getOrCreateMutex(cacheKey).runOrWait(async () => { const previous = await this.#getOrThrow(cacheKey, settings); const current = await Promise.resolve(setter(previous)); if (current === previous) return previous; const stored = await this.storeOrThrow(current, settings); this.storeds.set(cacheKey, stored); this.unstoreds.set(cacheKey, current); await this.onState.emit("*", cacheKey); await this.onState.emit(cacheKey, cacheKey); await Promise.resolve(settings.storage?.setOrThrow?.(cacheKey, stored)); await settings.indexer?.({ current, previous }); return current; }); } async trySet(cacheKey, setter, settings) { return await Result.runAndDoubleWrap(() => this.setOrThrow(cacheKey, setter, settings)); } #mergeRealStateWithFetched(previous, fetched) { if (fetched == null) return new RealState(undefined); if (fetched.isData()) return new RealState(new DataState(fetched)); return new RealState(new FailState(fetched, previous.real?.data)); } #mergeFakeStateWithFetched(previous, fetched) { if (fetched == null) return new FakeState(undefined, previous.real); if (fetched.isData()) return new FakeState(new DataState(fetched), previous.real); return new FakeState(new FailState(fetched, previous.data), previous.real); } /** * Set real state, compare times, compare data/error, and then reoptimize * @param cacheKey * @param setter * @param settings * @returns */ async updateOrThrow(cacheKey, setter, settings) { return await this.setOrThrow(cacheKey, async (previous) => { const updated = await Promise.resolve(setter(previous)); if (updated === previous) return previous; let next = new RealState(updated.real); if (next.real && previous.real && Time.isBefore(next.real?.current.time, previous.real.current.time)) return previous; const normalized = await this.#normalizeOrThrow(next.real?.current, settings); next = this.#mergeRealStateWithFetched(next, normalized); if (next.real?.current.isData() && previous.real?.current.isData() && equals(next.real.current.get(), previous.real.current.get())) next = new RealState(new DataState(new Data(previous.real.current.get(), next.real.current))); if (next.real?.current.isFail() && previous.real?.current.isFail() && equals(next.real.current.getErr(), previous.real.current.getErr())) next = new RealState(new FailState(new Fail(previous.real.current.getErr(), next.real.current), previous.real.data)); return await this.#reoptimizeOrThrow(cacheKey, next); }, settings); } /** * Merge real state with returned Some(fetched), if None do nothing * @param cacheKey * @param previous * @param fetched * @param settings * @returns */ async mutateOrThrow(cacheKey, mutator, settings) { return await this.updateOrThrow(cacheKey, async (previous) => { const mutate = await Promise.resolve(mutator(previous)); if (mutate.isNone()) return previous; const init = mutate.get(); if (init == null) return this.#mergeRealStateWithFetched(previous, undefined); const fetched = Fetched.from(init); return this.#mergeRealStateWithFetched(previous, fetched); }, settings); } /** * Merge real state with given fetched * @param cacheKey * @param fetched * @param settings * @returns */ async replaceOrThrow(cacheKey, fetched, settings) { return await this.mutateOrThrow(cacheKey, () => new Some(fetched), settings); } /** * Set real state to undefined * @param cacheKey * @param settings * @returns */ async deleteOrThrow(cacheKey, settings) { return await this.replaceOrThrow(cacheKey, undefined, settings); } /** * Erase and reapply all optimizations * @param state * @param optimizers * @returns */ async #reoptimizeOrThrow(cacheKey, state) { let reoptimized = new RealState(state.real); const optimizers = this.optimizers.get(cacheKey); if (optimizers == null) return reoptimized; for (const optimizer of optimizers.values()) { const optimized = await optimizer(reoptimized); if (optimized.isNone()) continue; const fetched = Option.wrap(optimized.get()).mapSync(Fetched.from).getOrNull(); reoptimized = this.#mergeFakeStateWithFetched(reoptimized, fetched); } return reoptimized; } async reoptimizeOrThrow(cacheKey, settings) { return await this.setOrThrow(cacheKey, async (previous) => { return await this.#reoptimizeOrThrow(cacheKey, previous); }, settings); } async optimizeOrThrow(cacheKey, uuid, optimizer, settings) { return await this.setOrThrow(cacheKey, async (previous) => { let optimizers = this.optimizers.get(cacheKey); if (optimizers == null) { optimizers = new Map(); this.optimizers.set(cacheKey, optimizers); } if (optimizers.has(uuid)) { optimizers.delete(uuid); previous = await this.#reoptimizeOrThrow(cacheKey, previous); } optimizers.set(uuid, optimizer); const optimized = await Promise.resolve(optimizer(previous)); if (optimized.isNone()) return previous; const fetched = Option.wrap(optimized.get()).mapSync(Fetched.from).getOrNull(); return this.#mergeFakeStateWithFetched(previous, fetched); }, settings); } async deoptimize(cacheKey, uuid) { const optimizers = this.optimizers.get(cacheKey); if (optimizers == null) return; optimizers.delete(uuid); } /** * Transform children into refs and normalize them * @param data * @param settings * @returns */ async #normalizeOrThrow(fetched, settings) { if (settings.normalizer == null) return fetched; return await settings.normalizer(fetched, { shallow: false }); } /** * Transform children into refs but do not normalize them * @param data * @param settings * @returns */ async prenormalizeOrThrow(fetched, settings) { if (settings.normalizer == null) return fetched; return await settings.normalizer(fetched, { shallow: true }); } /** * Assume cacheKey changed and reindex it * @param cacheKey * @param settings */ async reindexOrThrow(cacheKey, settings) { const current = await this.getOrThrow(cacheKey, settings); await settings.indexer?.({ current }); } async increment(cacheKey, settings) { const counter = this.counters.get(cacheKey); const timeout = this.timeouts.get(cacheKey); this.counters.set(cacheKey, (counter || 0) + 1); if (timeout != null) { clearTimeout(timeout); this.timeouts.delete(cacheKey); } return; } async decrementOrThrow(cacheKey, settings) { const counter = this.counters.get(cacheKey); /** * Already deleted */ if (counter == null) return; /** * Not deletable */ if (counter > 1) { this.counters.set(cacheKey, counter - 1); return; } /** * Counter can't go under 1 */ this.counters.delete(cacheKey); const state = await this.getOrThrow(cacheKey, settings); const expiration = state.real?.current.expiration; if (expiration == null) return; if (Date.now() > expiration) { await this.deleteOrThrow(cacheKey, settings); return; } const deleteOrThrow = async () => { /** * This should not happen but check anyway */ if (!this.#mounted) return; const counter = this.counters.get(cacheKey); /** * No longer deletable */ if (counter != null) return; await this.deleteOrThrow(cacheKey, settings); }; const onTimeout = () => { deleteOrThrow().catch(console.warn); }; const delay = expiration - Date.now(); if (delay > (2 ** 31)) return; const timeout = setTimeout(onTimeout, delay); this.timeouts.set(cacheKey, timeout); } } const core = new Core(); export { AsyncStorageError, CooldownError, Core, MissingFetcherError, MissingKeyError, TimeoutError, core }; //# sourceMappingURL=core.mjs.map