@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
228 lines (196 loc) • 5.22 kB
JavaScript
//
import { assert } from "../assert.js";
import { returnZero } from "../function/returnZero.js";
import { current_time_in_seconds } from "../time/current_time_in_seconds.js";
import { Cache } from "./Cache.js";
/**
* @template R
*/
class Record {
/**
* @template R
* @param {R} value
* @param {number} time
*/
constructor(value, time) {
this.value = value;
this.time = time;
this.failed = false;
this.weight = 0;
}
}
/**
* In seconds
* @readonly
* @type {number}
*/
const DEFAULT_TIME_TO_LIVE = Infinity;
/**
* @template T
* @param {Record<T>} record
* @returns {number}
*/
function record_get_value_weight(record) {
return record.weight;
}
/**
* Asynchronous cache capable of resolving its own values by keys
* Modelled on Guava's LoadingCache concept
* @template K
* @template V
* @class
*
* @author Alex Goldring
* @copyright Company Named Limited (c) 2025
*/
export class LoadingCache {
/**
* @type {Cache<K,Record<Promise<V>>>}
*/
#internal
/**
* Time until records are marked as invalid, invalid records are not served back and trigger re-loading of the value
* In seconds
* @type {number}
*/
#timeToLive = DEFAULT_TIME_TO_LIVE;
/**
* @type {function(key:K):Promise<V>}
*/
#load
/**
*
* @type {boolean}
*/
#policyRetryFailed = true;
/**
*
* @type {function(V): number}
*/
#value_weigher;
/**
* @see {@link Cache} for more details on what each parameter means
* @param [maxWeight]
* @param [keyWeigher]
* @param [valueWeigher]
* @param [keyHashFunction]
* @param [keyEqualityFunction]
* @param [capacity]
* @param {number} [timeToLive] in seconds, default is 10 seconds
* @param {function(key:K):Promise<V>} load
* @param {boolean} [retryFailed]
*/
constructor({
maxWeight,
keyWeigher,
valueWeigher = returnZero,
keyHashFunction,
keyEqualityFunction,
capacity,
timeToLive = DEFAULT_TIME_TO_LIVE,
load,
retryFailed = true
}) {
assert.isFunction(load, 'load');
this.#internal = new Cache({
maxWeight,
keyWeigher,
valueWeigher: record_get_value_weight,
keyHashFunction,
keyEqualityFunction,
capacity,
});
this.#timeToLive = timeToLive;
this.#load = load;
this.#policyRetryFailed = retryFailed;
this.#value_weigher = valueWeigher;
}
/**
*
* @param {K} key
* @returns {boolean}
*/
invalidate(key) {
return this.#internal.remove(key);
}
/**
*
*/
clear() {
this.#internal.clear();
}
/**
*
* @param {K} key
* @param {number} timestamp
* @return {Record<Promise<V>>}
*/
#load_value(key, timestamp) {
let promise;
try {
promise = this.#load(key);
} catch (e) {
promise = Promise.reject(e);
}
const record = new Record(promise, timestamp);
this.#internal.put(key, record);
promise.then(
(value) => {
// re-score value based on actual data
record.weight = this.#value_weigher(value);
this.#internal.updateElementWeight(key);
},
() => {
// mark as failure
record.failed = true;
}
);
return record;
}
/**
* Load new value for the key (happens asynchronously)
* @param {K} key
* @returns {Promise<V>}
*/
refresh(key) {
const record = this.#load_value(key, current_time_in_seconds());
return record.value;
}
/**
* Directly insert value into the cache
* @param {K} key
* @param {V} value
*/
put(key, value) {
this.#internal.put(key, new Record(Promise.resolve(value), current_time_in_seconds()));
}
/**
*
* @param {K} key
* @returns {boolean}
*/
contains(key) {
return this.#internal.contains(key);
}
/**
*
* @param {K} key
* @return {Promise<V>}
*/
async get(key) {
const currentTime = current_time_in_seconds();
/**
*
* @type {Record<Promise<V>>}
*/
let record = this.#internal.get(key);
if (record === null
|| (record.time + this.#timeToLive) < currentTime // timeout
|| (record.failed && this.#policyRetryFailed) // load failed and we're configured to retry
) {
// record needs to be loaded
record = this.#load_value(key, currentTime);
}
return record.value;
}
}