Skip to content

Instantly share code, notes, and snippets.

@blakek
Last active January 29, 2025 18:13
Show Gist options
  • Save blakek/51a0d457d2eca1fd9c17d43abe26a447 to your computer and use it in GitHub Desktop.
Save blakek/51a0d457d2eca1fd9c17d43abe26a447 to your computer and use it in GitHub Desktop.
An LRU Cache for Zapier… Add to a Code step and fill in `keyToCheck` and `storageId`. I basically use this as an imperfect-but-workable solution for skipping duplicates.
/**
* @typedef {Object} InputData
* @property {string} keyToCheck
* @property {string} storageId
*
* @typedef {string | number | boolean | null | undefined | Record<string, JSONValue> | JSONValue[]} JSONValue
*
* @typedef {Object} StoreClient
* @property {() => Promise<void>} clear
* @property {(key: string) => Promise<void>} delete
* @property {(keys: string[]) => Promise<void>} deleteMany
* @property {(key: string) => Promise<JSONValue>} get
* @property {(keys: string[]) => Promise<Record<string, JSONValue>>} getMany
* @property {(key: string, value: JSONValue) => Promise<void>} set
* @property {(keyValues: Record<string, JSONValue>) => Promise<void>} setMany
*/
/** @type {InputData} */
// var inputData;
/**
* Keeps a list of recently used keys with a limit that expires the least recently used keys first.
*
* Storage format:
* {
* "__lru_cache__": {
* "key1": "2021-01-01T00:00:00.000Z",
* "key2": "2021-01-01T00:00:00.000Z"
* }
* }
*/
class LRUCache {
/** @type {number} - The maximum number of entries to keep in the cache */
maxEntries;
/** @type {Map<string, Date>} */
cache = new Map();
/** @type {StoreClient} */
storage;
/** @type {"__lru_cache__"} */
storagePrefix = "__lru_cache__";
/** @type {StoreClient["get"] | null} */
loadPromise = null;
constructor(storageId, { maxEntries = 250 } = {}) {
this.maxEntries = maxEntries;
this.storage = StoreClient(storageId);
// Start loading the cache from storage
this._loadCache();
}
async has(key) {
await this._ensureLoaded();
const hasKey = this.cache.has(key);
await this._touchCache(key);
return hasKey;
}
async _ensureLoaded() {
if (this.loadPromise) {
await this.loadPromise;
}
}
async _loadCache() {
// Don't allow multiple loads at once
if (this.loadPromise) {
return this.loadPromise;
}
this.loadPromise = this.storage.get(this.storagePrefix);
/** @type {Record<string, string>} */
const cache = await this.loadPromise;
if (cache) {
const entries = Object.entries(cache);
for (const [key, value] of entries) {
this.cache.set(key, new Date(value));
}
}
this.loadPromise = null;
}
async _saveCache() {
// Sort by date, limit to maxEntries, and convert to object
const entries = [...this.cache.entries()]
.sort(([, date1], [, date2]) => date2 - date1)
.slice(0, this.maxEntries)
.map(([key, date]) => [key, date.toISOString()]);
const cache = Object.fromEntries(entries);
await this.storage.set(this.storagePrefix, cache);
await this._loadCache();
}
async _touchCache(key) {
this.cache.set(key, new Date());
await this._saveCache();
}
}
const cache = new LRUCache(inputData.storageId);
const isInCache = await cache.has(inputData.keyToCheck);
return {
isInCache,
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment