@solomonai/cache
Version:
<div align="center"> <h1 align="center">@solomonai/cache</h1> <h5>Cache all the things</h5> </div>
250 lines (245 loc) • 6.59 kB
JavaScript
'use strict';
var error = require('@unkey/error');
// src/context.ts
var DefaultStatefulContext = class {
waitUntil(_p) {
}
};
var CacheError = class extends error.BaseError {
name = "CacheError";
retry = false;
tier;
key;
constructor(opts) {
super(opts);
this.name = "CacheError";
this.tier = opts.tier;
this.key = opts.key;
}
};
// src/cache.ts
function createCache(namespaces) {
return Object.entries(namespaces).reduce(
(acc, [n, c]) => {
acc[n] = {
get: (key) => c.get(n, key),
set: (key, value, opts) => c.set(n, key, value, opts),
remove: (key) => c.remove(n, key),
swr: (key, loadFromOrigin) => c.swr(n, key, loadFromOrigin)
};
return acc;
},
{}
);
}
var TieredStore = class {
ctx;
tiers;
name = "tiered";
/**
* Create a new tiered store
* Stored are checked in the order they are provided
* The first store to return a value will be used to populate all previous stores
*
*
* `stores` can accept `undefined` as members to allow you to construct the tiers dynamically
* @example
* ```ts
* new TieredStore(ctx, [
* new MemoryStore(..),
* process.env.ENABLE_X_STORE ? new XStore(..) : undefined
* ])
* ```
*/
constructor(ctx, stores) {
this.ctx = ctx;
this.tiers = stores.filter(Boolean);
}
/**
* Return the cached value
*
* The response will be `undefined` for cache misses or `null` when the key was not found in the origin
*/
async get(namespace, key) {
if (this.tiers.length === 0) {
return error.Ok(void 0);
}
for (let i = 0; i < this.tiers.length; i++) {
const res = await this.tiers[i].get(namespace, key);
if (res.err) {
return res;
}
if (typeof res.val !== "undefined") {
this.ctx.waitUntil(
Promise.all(
this.tiers.filter((_, j) => j < i).map((t) => () => t.set(namespace, key, res.val))
).catch((err) => {
return error.Err(
new CacheError({
tier: this.name,
key,
message: err.message
})
);
})
);
return error.Ok(res.val);
}
}
return error.Ok(void 0);
}
/**
* Sets the value for the given key.
*/
async set(namespace, key, value) {
return Promise.all(this.tiers.map((t) => t.set(namespace, key, value))).then(() => error.Ok()).catch(
(err) => error.Err(
new CacheError({
tier: this.name,
key,
message: err.message
})
)
);
}
/**
* Removes the key from the cache.
*/
async remove(namespace, key) {
return Promise.all(this.tiers.map((t) => t.remove(namespace, key))).then(() => error.Ok()).catch(
(err) => error.Err(
new CacheError({
tier: this.name,
key,
message: err.message
})
)
);
}
};
var SwrCache = class {
ctx;
store;
fresh;
stale;
/**
* To prevent concurrent revalidation of the same data, all revalidations are deduplicated using
* this map.
*/
revalidating = /* @__PURE__ */ new Map();
constructor(ctx, store, fresh, stale) {
this.ctx = ctx;
this.store = store;
this.fresh = fresh;
this.stale = stale;
}
/**
* Return the cached value
*
* The response will be `undefined` for cache misses or `null` when the key was not found in the origin
*/
async get(namespace, key) {
const res = await this._get(namespace, key);
if (res.err) {
return error.Err(res.err);
}
return error.Ok(res.val.value);
}
async _get(namespace, key) {
const res = await this.store.get(namespace, key);
if (res.err) {
return error.Err(res.err);
}
const now = Date.now();
if (!res.val) {
return error.Ok({ value: void 0 });
}
if (now >= res.val.staleUntil) {
this.ctx.waitUntil(this.remove(namespace, key));
return error.Ok({ value: void 0 });
}
if (now >= res.val.freshUntil) {
return error.Ok({ value: res.val.value, revalidate: true });
}
return error.Ok({ value: res.val.value });
}
/**
* Set the value
*/
async set(namespace, key, value, opts) {
const now = Date.now();
return this.store.set(namespace, key, {
value,
freshUntil: now + (opts?.fresh ?? this.fresh),
staleUntil: now + (opts?.stale ?? this.stale)
});
}
/**
* Removes the key from the cache.
*/
async remove(namespace, key) {
return this.store.remove(namespace, key);
}
async swr(namespace, key, loadFromOrigin) {
const res = await this._get(namespace, key);
if (res.err) {
return error.Err(res.err);
}
const { value, revalidate } = res.val;
if (typeof value !== "undefined") {
if (revalidate) {
this.ctx.waitUntil(
this.deduplicateLoadFromOrigin(namespace, key, loadFromOrigin).then(
(res2) => this.set(namespace, key, res2)
)
);
}
return error.Ok(value);
}
try {
const value2 = await this.deduplicateLoadFromOrigin(namespace, key, loadFromOrigin);
this.ctx.waitUntil(this.set(namespace, key, value2));
return error.Ok(value2);
} catch (err) {
return error.Err(
new CacheError({
tier: "cache",
key,
message: err.message
})
);
}
}
/**
* Deduplicating the origin load helps when the same value is requested many times at once and is
* not yet in the cache. If we don't deduplicate, we'd create a lot of unnecessary load on the db.
*/
async deduplicateLoadFromOrigin(namespace, key, loadFromOrigin) {
const revalidateKey = [namespace, key].join("::");
try {
const revalidating = this.revalidating.get(revalidateKey);
if (revalidating) {
return await revalidating;
}
const p = loadFromOrigin(key);
this.revalidating.set(revalidateKey, p);
return await p;
} finally {
this.revalidating.delete(revalidateKey);
}
}
};
// src/namespace.ts
var Namespace = class extends SwrCache {
constructor(ctx, opts) {
const tieredStore = new TieredStore(ctx, opts.stores);
super(ctx, tieredStore, opts.fresh, opts.stale);
}
};
exports.CacheError = CacheError;
exports.DefaultStatefulContext = DefaultStatefulContext;
exports.Namespace = Namespace;
exports.TieredStore = TieredStore;
exports.createCache = createCache;
//# sourceMappingURL=index.js.map
//# sourceMappingURL=index.js.map