@hazae41/glacier
Version:
Yet another React data (re)fetching library
454 lines (450 loc) • 16.9 kB
JavaScript
'use strict';
var mutex = require('@hazae41/mutex');
var option = require('@hazae41/option');
var plume = require('@hazae41/plume');
var result = require('@hazae41/result');
var index = require('../../libs/equals/index.cjs');
var time = require('../../libs/time/time.cjs');
var data = require('../fetched/data.cjs');
var fail = require('../fetched/fail.cjs');
var fetched = require('../fetched/fetched.cjs');
var state = require('../types/state.cjs');
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 plume.SuperEventTarget();
onAborter = new plume.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$1 = this.mutexes.get(cacheKey);
if (mutex$1 != null)
return mutex$1;
mutex$1 = new mutex.Mutex(undefined);
this.mutexes.set(cacheKey, mutex$1);
return mutex$1;
}
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.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.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 state.RealState(undefined);
if (stored.version == null) {
const { time, cooldown, expiration } = stored;
const times = { time, cooldown, expiration };
const data$1 = option.Option.wrap(stored.data).mapSync(x => new data.Data(x, times));
const error = option.Option.wrap(stored.error).mapSync(x => new fail.Fail(x, times));
if (error.isSome())
return new state.RealState(new state.FailState(error.get(), data$1.getOrNull()));
if (data$1.isSome())
return new state.RealState(new state.DataState(data$1.get()));
return new state.RealState(undefined);
}
if (stored.version === 2 || stored.version === 3) {
const data$1 = option.Option.wrap(stored.data).mapSync(x => data.Data.from(x));
const error = option.Option.wrap(stored.error).mapSync(x => fail.Fail.from(x));
if (error.isSome())
return new state.RealState(new state.FailState(error.get(), data$1.getOrNull()));
if (data$1.isSome())
return new state.RealState(new state.DataState(data$1.get()));
return new state.RealState(undefined);
}
return new state.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.Result.runAndDoubleWrap(() => this.setOrThrow(cacheKey, setter, settings));
}
#mergeRealStateWithFetched(previous, fetched) {
if (fetched == null)
return new state.RealState(undefined);
if (fetched.isData())
return new state.RealState(new state.DataState(fetched));
return new state.RealState(new state.FailState(fetched, previous.real?.data));
}
#mergeFakeStateWithFetched(previous, fetched) {
if (fetched == null)
return new state.FakeState(undefined, previous.real);
if (fetched.isData())
return new state.FakeState(new state.DataState(fetched), previous.real);
return new state.FakeState(new state.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 state.RealState(updated.real);
if (next.real && previous.real && time.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() && index.equals(next.real.current.get(), previous.real.current.get()))
next = new state.RealState(new state.DataState(new data.Data(previous.real.current.get(), next.real.current)));
if (next.real?.current.isFail() && previous.real?.current.isFail() && index.equals(next.real.current.getErr(), previous.real.current.getErr()))
next = new state.RealState(new state.FailState(new fail.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$1 = fetched.Fetched.from(init);
return this.#mergeRealStateWithFetched(previous, fetched$1);
}, 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 option.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$1) {
let reoptimized = new state.RealState(state$1.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$1 = option.Option.wrap(optimized.get()).mapSync(fetched.Fetched.from).getOrNull();
reoptimized = this.#mergeFakeStateWithFetched(reoptimized, fetched$1);
}
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$1 = option.Option.wrap(optimized.get()).mapSync(fetched.Fetched.from).getOrNull();
return this.#mergeFakeStateWithFetched(previous, fetched$1);
}, 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();
exports.AsyncStorageError = AsyncStorageError;
exports.CooldownError = CooldownError;
exports.Core = Core;
exports.MissingFetcherError = MissingFetcherError;
exports.MissingKeyError = MissingKeyError;
exports.TimeoutError = TimeoutError;
exports.core = core;
//# sourceMappingURL=core.cjs.map