@cap-js-community/event-queue
Version:
An event queue that enables secure transactional processing of asynchronous and periodic events, featuring instant event processing with Redis Pub/Sub and load distribution across all application instances.
149 lines (141 loc) • 4.62 kB
JavaScript
"use strict";
const DEFAULT_SEPARATOR = "##";
const DEFAULT_EXPIRATION_GAP = 60 * 1000; // 60 seconds
class LazyCache {
constructor({ separator = DEFAULT_SEPARATOR } = {}) {
this.__data = Object.create(null);
this.__separator = separator;
}
_separator() {
return this.__separator;
}
_data() {
return this.__data;
}
async _dataSettled() {
return await Object.entries(this.__data).reduce(async (result, [key, value]) => {
(await result)[key] = await value;
return result;
}, Promise.resolve({}));
}
_key(keyOrKeys) {
return Array.isArray(keyOrKeys) ? keyOrKeys.join(this.__separator) : keyOrKeys;
}
has(keyOrKeys) {
return Object.prototype.hasOwnProperty.call(this.__data, this._key(keyOrKeys));
}
get(keyOrKeys) {
return this.__data[this._key(keyOrKeys)];
}
set(keyOrKeys, value) {
this.__data[this._key(keyOrKeys)] = value;
return this;
}
setCb(keyOrKeys, callback) {
const resultOrPromise = callback();
return this.set(
keyOrKeys,
resultOrPromise instanceof Promise
? resultOrPromise.catch((err) => {
this.delete(keyOrKeys);
return Promise.reject(err);
})
: resultOrPromise
);
}
getSetCb(keyOrKeys, callback) {
const key = this._key(keyOrKeys);
if (!this.has(key)) {
this.setCb(key, callback);
}
return this.get(key);
}
count() {
return Object.keys(this.__data).length;
}
delete(keyOrKeys) {
Reflect.deleteProperty(this.__data, this._key(keyOrKeys));
return this;
}
clear() {
this.__data = Object.create(null);
}
}
class ExpiringLazyCache extends LazyCache {
constructor({ separator = DEFAULT_SEPARATOR, expirationGap = DEFAULT_EXPIRATION_GAP } = {}) {
super({ separator });
this.__expirationGap = expirationGap;
}
_expiringGap() {
return this.__expirationGap;
}
_isValid(expirationTime, currentTime = Date.now()) {
return expirationTime && currentTime <= expirationTime;
}
has(keyOrKeys, currentTime = Date.now()) {
if (!super.has(keyOrKeys)) {
return false;
}
const [expirationTime] = super.get(keyOrKeys) ?? [];
return this._isValid(expirationTime, currentTime);
}
get(keyOrKeys, currentTime = Date.now()) {
const [expirationTime, value] = super.get(keyOrKeys) ?? [];
return this._isValid(expirationTime, currentTime) ? value : undefined;
}
// NOTE the expiration gap is substracted here, because we want to expire a
// little earlier than necessary.
// NOTE if the expiration is _less_ than the gap, the value is never valid,
// we still need to call set, because we want getSetCb to always return
// when the callback is used.
set(keyOrKeys, expiration, value, currentTime = Date.now()) {
return super.set(keyOrKeys, [currentTime + expiration - this.__expirationGap, value]);
}
static _extract(result, extractor) {
if (!extractor) {
return result;
}
const { expiration, expiry, value, result: extractedResult } = extractor(result);
return [expiration ?? expiry, value ?? extractedResult];
}
// NOTE callback can either return a pair [expiration, value] or use an extractor to extract the right values from
// the callback's result
setCb(keyOrKeys, callback, { currentTime = Date.now(), extractor } = {}) {
const resultOrPromise = callback();
if (!(resultOrPromise instanceof Promise)) {
const [expiration, value] = ExpiringLazyCache._extract(resultOrPromise, extractor);
return this.set(keyOrKeys, expiration, value, currentTime);
}
return this.set(
keyOrKeys,
Infinity,
resultOrPromise
.catch((err) => {
this.delete(keyOrKeys);
return Promise.reject(err);
})
.then((result) => {
const [expiration, value] = ExpiringLazyCache._extract(result, extractor);
this.set(keyOrKeys, expiration, value, currentTime);
return value;
})
);
}
// NOTE callback can either return a pair [expiration, value] or use an extractor to extract the right values from
// the callback's result
getSetCb(keyOrKeys, callback, { currentTime = Date.now(), extractor } = {}) {
const key = this._key(keyOrKeys);
if (!this.has(key, currentTime) || !super.has(key)) {
this.setCb(key, callback, { currentTime, extractor });
const [, value] = super.get(key);
return value;
}
return this.get(key, currentTime);
}
}
module.exports = {
DEFAULT_EXPIRATION_GAP,
DEFAULT_SEPARATOR,
LazyCache,
ExpiringLazyCache,
};