🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@andrewshell/cacheism

Package Overview
Dependencies
Maintainers
1
Versions
3
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@andrewshell/cacheism - npm Package Compare versions

Comparing version
2.0.0
to
3.0.0
+332
dist/index.cjs
Object.defineProperty(exports, '__esModule', { value: true });
//#region rolldown:runtime
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
key = keys[i];
if (!__hasOwnProp.call(to, key) && key !== except) {
__defProp(to, key, {
get: ((k) => from[k]).bind(null, key),
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
});
}
}
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
value: mod,
enumerable: true
}) : target, mod));
//#endregion
let node_crypto = require("node:crypto");
let node_fs = require("node:fs");
node_fs = __toESM(node_fs);
let node_path = require("node:path");
node_path = __toESM(node_path);
let node_fs_promises = require("node:fs/promises");
//#region src/etag.ts
/*!
* etag
* Copyright(c) 2014-2016 Douglas Christopher Wilson
* MIT Licensed
*/
/**
* Generate an entity tag.
*/
function entitytag(entity) {
if (entity.length === 0) return "\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"";
const hashObj = (0, node_crypto.createHash)("sha1");
if (typeof entity === "string") hashObj.update(entity, "utf8");
else hashObj.update(entity);
const hash = hashObj.digest("base64").substring(0, 27);
return "\"" + (typeof entity === "string" ? Buffer.byteLength(entity, "utf8") : entity.length).toString(16) + "-" + hash + "\"";
}
/**
* Determine if object is a Stats object.
*/
function isstats(obj) {
if (typeof node_fs.Stats === "function" && obj instanceof node_fs.Stats) return true;
return obj != null && typeof obj === "object" && "ctime" in obj && Object.prototype.toString.call(obj.ctime) === "[object Date]" && "mtime" in obj && Object.prototype.toString.call(obj.mtime) === "[object Date]" && "ino" in obj && typeof obj.ino === "number" && "size" in obj && typeof obj.size === "number";
}
/**
* Generate a tag for a stat.
*/
function stattag(stat) {
const mtime = stat.mtime.getTime().toString(16);
return "\"" + stat.size.toString(16) + "-" + mtime + "\"";
}
/**
* Create a simple ETag.
*/
function etag(entity, options) {
if (entity == null) throw new TypeError("argument entity is required");
const isStats = isstats(entity);
const weak = options && typeof options.weak === "boolean" ? options.weak : isStats;
if (!isStats && typeof entity !== "string" && !Buffer.isBuffer(entity)) throw new TypeError("argument entity must be string, Buffer, or fs.Stats");
const tag = isStats ? stattag(entity) : entitytag(entity);
return weak ? "W/" + tag : tag;
}
//#endregion
//#region src/common.ts
let Status = /* @__PURE__ */ function(Status$1) {
Status$1[Status$1["onlyFresh"] = 0] = "onlyFresh";
Status$1[Status$1["cacheOnFail"] = 1] = "cacheOnFail";
Status$1[Status$1["preferCache"] = 2] = "preferCache";
Status$1[Status$1["onlyCache"] = 3] = "onlyCache";
return Status$1;
}({});
Object.freeze(Status);
let Type = /* @__PURE__ */ function(Type$1) {
Type$1["hit"] = "Hit";
Type$1["miss"] = "Miss";
return Type$1;
}({});
Object.freeze(Type);
var Hit = class Hit {
version = 3;
cacheName;
cached = false;
created;
data;
error = null;
errorTime = null;
consecutiveErrors = 0;
etag;
isHit = true;
isMiss = false;
constructor(name, data, existingEtag) {
this.cacheName = name;
this.created = /* @__PURE__ */ new Date();
this.data = data;
this.etag = existingEtag ?? etag(JSON.stringify(data));
}
withError(error, consecutiveErrors) {
const hit = new Hit(this.cacheName, this.data, this.etag);
hit.cached = this.cached;
hit.created = this.created;
hit.error = error;
hit.errorTime = /* @__PURE__ */ new Date();
hit.consecutiveErrors = consecutiveErrors;
return hit;
}
};
var Miss = class {
version = 3;
cacheName;
cached = false;
created;
data = null;
error;
errorTime;
consecutiveErrors;
etag = null;
isHit = false;
isMiss = true;
constructor(name, error, consecutiveErrors) {
this.cacheName = name;
this.created = /* @__PURE__ */ new Date();
this.error = error;
this.errorTime = new Date(this.created);
this.consecutiveErrors = consecutiveErrors;
}
};
var Data = class Data {
version;
type;
cacheName;
created;
data;
error;
errorTime;
consecutiveErrors;
etag;
constructor(version, type, name, created, data, error, errorTime, consecutiveErrors, etag$1) {
this.version = version;
this.type = type;
this.cacheName = name;
this.created = created;
this.data = data;
this.error = error;
this.errorTime = errorTime;
this.consecutiveErrors = consecutiveErrors;
this.etag = etag$1;
}
response() {
if (this.version !== 3) throw new Error(`Unknown cache version number: ${String(this.version)}`);
let response;
if (this.type === Type.hit) {
response = new Hit(this.cacheName, this.data, this.etag ?? void 0);
response.cached = true;
response.created = this.created;
response.consecutiveErrors = this.consecutiveErrors;
} else {
response = new Miss(this.cacheName, this.error ?? "", this.consecutiveErrors);
response.cached = false;
response.created = this.created;
response.errorTime = this.errorTime ?? /* @__PURE__ */ new Date();
}
return response;
}
stringify() {
return JSON.stringify({
version: this.version,
type: this.type,
cacheName: this.cacheName,
created: this.created,
data: this.data,
error: this.error,
errorTime: this.errorTime,
consecutiveErrors: this.consecutiveErrors,
etag: this.etag
}, null, 2);
}
static fromResponse(response) {
return new Data(response.version, response.isHit ? Type.hit : Type.miss, response.cacheName, response.created, response.data, response.error, response.errorTime, response.consecutiveErrors, response.etag);
}
static parse(value) {
const parsed = JSON.parse(value);
if (parsed.version === 2) return new Data(3, Type.hit, parsed.cacheName, new Date(parsed.created), parsed.data, null, null, 0, parsed.etag);
if (parsed.version === 3) return new Data(3, parsed.type, parsed.cacheName, new Date(parsed.created), parsed.data, parsed.error, parsed.errorTime === null ? null : new Date(parsed.errorTime), parsed.consecutiveErrors, parsed.etag);
throw new Error(`Unknown cache version number: ${String(parsed.version)}`);
}
};
//#endregion
//#region src/store-filesystem.ts
async function mkdir(dirpath) {
await node_fs.default.promises.mkdir(dirpath, { recursive: true });
}
function createFilesystemStore(config) {
return {
async get(cacheName) {
const data = await (0, node_fs_promises.readFile)(node_path.default.resolve(config.datadir, `${cacheName}.json`), "utf8");
return Data.parse(data);
},
async set(data) {
const filename = node_path.default.resolve(config.datadir, `${data.cacheName}.json`);
await mkdir(node_path.default.dirname(filename));
await (0, node_fs_promises.writeFile)(filename, data.stringify(), "utf8");
},
async isset(cacheName) {
const filename = node_path.default.resolve(config.datadir, `${cacheName}.json`);
try {
await node_fs.default.promises.access(filename);
return true;
} catch {
return false;
}
},
async unset(cacheName) {
await (0, node_fs_promises.rm)(node_path.default.resolve(config.datadir, `${cacheName}.json`), { force: true });
}
};
}
//#endregion
//#region src/store-memory.ts
function createMemoryStore(_config) {
const storeMemory = {
data: {},
get(cacheName) {
return Promise.resolve(Data.parse(storeMemory.data[cacheName]));
},
set(data) {
storeMemory.data[data.cacheName] = data.stringify();
return Promise.resolve();
},
isset(cacheName) {
return Promise.resolve(cacheName in storeMemory.data);
},
unset(cacheName) {
delete storeMemory.data[cacheName];
return Promise.resolve();
}
};
return storeMemory;
}
//#endregion
//#region src/index.ts
function sanitize(string) {
return string.replaceAll(/[^a-z0-9]+/gi, "-");
}
var Cacheism = class {
store;
status = Status;
type = Type;
constructor(store) {
this.store = store;
}
async go(cacheDomain, cachePath, status, callback) {
if (typeof cacheDomain !== "string") throw new TypeError("cacheDomain must be a string");
if (typeof cachePath !== "string") throw new TypeError("cachePath must be a string");
if (typeof status !== "number" || status < Status.onlyFresh || status > Status.onlyCache) throw new TypeError("status must be a valid Status value (0-3)");
if (typeof callback !== "function") throw new TypeError("callback must be a function");
const name = this.cacheName(cacheDomain, cachePath);
let existing = new Miss(name, "Missing cache", 0);
const hasCache = await this.store.isset(name);
if (hasCache) existing = (await this.store.get(name)).response();
let response;
try {
if (status >= Status.preferCache && hasCache && existing.isHit) response = existing;
else if (status === Status.onlyCache) throw new Error("Missing cache");
else {
const result = await callback(existing);
if (result instanceof Hit) response = result;
else response = new Hit(name, result);
}
} catch (err) {
const errorMessage = err instanceof Error ? err.toString() : String(err);
if (status >= Status.cacheOnFail && hasCache && existing.isHit) response = existing.withError(errorMessage, existing.consecutiveErrors + 1);
else response = new Miss(name, errorMessage, existing.consecutiveErrors + 1);
}
await this.store.set(Data.fromResponse(response));
Object.freeze(response);
return response;
}
cacheName(cacheDomain, cachePath) {
return `${sanitize(cacheDomain)}/${sanitize(cachePath)}`;
}
setStore(store) {
this.store = store;
}
hit(name, data, etag$1) {
return new Hit(name, data, etag$1);
}
miss(name, error) {
return new Miss(name, error, 0);
}
static Hit = Hit;
static Miss = Miss;
static Data = Data;
static Status = Status;
static Type = Type;
static store = {
filesystem: createFilesystemStore,
memory: createMemoryStore
};
};
var src_default = Cacheism;
//#endregion
exports.Cacheism = Cacheism;
exports.Data = Data;
exports.Hit = Hit;
exports.Miss = Miss;
exports.Status = Status;
exports.Type = Type;
exports.createFilesystemStore = createFilesystemStore;
exports.createMemoryStore = createMemoryStore;
exports.default = src_default;
exports.etag = etag;
//# sourceMappingURL=index.cjs.map
{"version":3,"file":"index.cjs","names":["Stats","generateEtag","etag","response: Hit | Miss","fs","path","storeMemory: MemoryStore","existing: Hit | Miss","response: Hit | Miss","etag"],"sources":["../src/etag.ts","../src/common.ts","../src/store-filesystem.ts","../src/store-memory.ts","../src/index.ts"],"sourcesContent":["/*!\n * etag\n * Copyright(c) 2014-2016 Douglas Christopher Wilson\n * MIT Licensed\n */\n\nimport { createHash } from 'node:crypto';\nimport { Stats } from 'node:fs';\n\nexport interface EtagOptions {\n weak?: boolean;\n}\n\n/**\n * Generate an entity tag.\n */\nfunction entitytag(entity: string | Buffer): string {\n if (entity.length === 0) {\n // fast-path empty\n return '\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"';\n }\n\n // compute hash of entity\n const hashObj = createHash('sha1');\n if (typeof entity === 'string') {\n hashObj.update(entity, 'utf8');\n } else {\n hashObj.update(entity);\n }\n const hash = hashObj.digest('base64').substring(0, 27);\n\n // compute length of entity\n const len = typeof entity === 'string'\n ? Buffer.byteLength(entity, 'utf8')\n : entity.length;\n\n return '\"' + len.toString(16) + '-' + hash + '\"';\n}\n\n/**\n * Determine if object is a Stats object.\n */\nfunction isstats(obj: unknown): obj is Stats {\n // genuine fs.Stats\n if (typeof Stats === 'function' && obj instanceof Stats) {\n return true;\n }\n\n // quack quack\n return obj != null && typeof obj === 'object' &&\n 'ctime' in obj && Object.prototype.toString.call((obj as Stats).ctime) === '[object Date]' &&\n 'mtime' in obj && Object.prototype.toString.call((obj as Stats).mtime) === '[object Date]' &&\n 'ino' in obj && typeof (obj as Stats).ino === 'number' &&\n 'size' in obj && typeof (obj as Stats).size === 'number';\n}\n\n/**\n * Generate a tag for a stat.\n */\nfunction stattag(stat: Stats): string {\n const mtime = stat.mtime.getTime().toString(16);\n const size = stat.size.toString(16);\n\n return '\"' + size + '-' + mtime + '\"';\n}\n\n/**\n * Create a simple ETag.\n */\nexport function etag(entity: string | Buffer | Stats | null | undefined, options?: EtagOptions): string {\n if (entity == null) {\n throw new TypeError('argument entity is required');\n }\n\n // support fs.Stats object\n const isStats = isstats(entity);\n const weak = options && typeof options.weak === 'boolean'\n ? options.weak\n : isStats;\n\n // validate argument\n if (!isStats && typeof entity !== 'string' && !Buffer.isBuffer(entity)) {\n throw new TypeError('argument entity must be string, Buffer, or fs.Stats');\n }\n\n // generate entity tag\n const tag = isStats\n ? stattag(entity)\n : entitytag(entity);\n\n return weak\n ? 'W/' + tag\n : tag;\n}\n\nexport default etag;\n","import { etag as generateEtag } from './etag.js';\n\nexport enum Status {\n onlyFresh = 0,\n cacheOnFail = 1,\n preferCache = 2,\n onlyCache = 3,\n}\nObject.freeze(Status);\n\nexport enum Type {\n hit = 'Hit',\n miss = 'Miss',\n}\nObject.freeze(Type);\n\nexport class Hit {\n readonly version = 3;\n readonly cacheName: string;\n cached = false;\n created: Date;\n data: unknown;\n error: string | null = null;\n errorTime: Date | null = null;\n consecutiveErrors = 0;\n etag: string;\n readonly isHit = true;\n readonly isMiss = false;\n\n constructor(name: string, data: unknown, existingEtag?: string) {\n this.cacheName = name;\n this.created = new Date();\n this.data = data;\n this.etag = existingEtag ?? generateEtag(JSON.stringify(data));\n }\n\n withError(error: string, consecutiveErrors: number): Hit {\n const hit = new Hit(this.cacheName, this.data, this.etag);\n hit.cached = this.cached;\n hit.created = this.created;\n hit.error = error;\n hit.errorTime = new Date();\n hit.consecutiveErrors = consecutiveErrors;\n return hit;\n }\n}\n\nexport class Miss {\n readonly version = 3;\n readonly cacheName: string;\n cached = false;\n created: Date;\n readonly data = null;\n error: string;\n errorTime: Date;\n consecutiveErrors: number;\n readonly etag = null;\n readonly isHit = false;\n readonly isMiss = true;\n\n constructor(name: string, error: string, consecutiveErrors: number) {\n this.cacheName = name;\n this.created = new Date();\n this.error = error;\n this.errorTime = new Date(this.created);\n this.consecutiveErrors = consecutiveErrors;\n }\n}\n\ninterface ParsedData {\n version: number;\n type: string;\n cacheName: string;\n created: string;\n data: unknown;\n error: string | null;\n errorTime: string | null;\n consecutiveErrors: number;\n etag: string | null;\n}\n\nexport class Data {\n version: number;\n type: Type;\n cacheName: string;\n created: Date;\n data: unknown;\n error: string | null;\n errorTime: Date | null;\n consecutiveErrors: number;\n etag: string | null;\n\n constructor(\n version: number,\n type: Type,\n name: string,\n created: Date,\n data: unknown,\n error: string | null,\n errorTime: Date | null,\n consecutiveErrors: number,\n etag: string | null\n ) {\n this.version = version;\n this.type = type;\n this.cacheName = name;\n this.created = created;\n this.data = data;\n this.error = error;\n this.errorTime = errorTime;\n this.consecutiveErrors = consecutiveErrors;\n this.etag = etag;\n }\n\n response(): Hit | Miss {\n if (this.version !== 3) {\n throw new Error(`Unknown cache version number: ${String(this.version)}`);\n }\n\n let response: Hit | Miss;\n\n if (this.type === Type.hit) {\n response = new Hit(this.cacheName, this.data, this.etag ?? undefined);\n response.cached = true;\n response.created = this.created;\n response.consecutiveErrors = this.consecutiveErrors;\n } else {\n response = new Miss(this.cacheName, this.error ?? '', this.consecutiveErrors);\n response.cached = false;\n response.created = this.created;\n response.errorTime = this.errorTime ?? new Date();\n }\n\n return response;\n }\n\n stringify(): string {\n return JSON.stringify({\n version: this.version,\n type: this.type,\n cacheName: this.cacheName,\n created: this.created,\n data: this.data,\n error: this.error,\n errorTime: this.errorTime,\n consecutiveErrors: this.consecutiveErrors,\n etag: this.etag,\n }, null, 2);\n }\n\n static fromResponse(response: Hit | Miss): Data {\n return new Data(\n response.version,\n response.isHit ? Type.hit : Type.miss,\n response.cacheName,\n response.created,\n response.data,\n response.error,\n response.errorTime,\n response.consecutiveErrors,\n response.etag\n );\n }\n\n static parse(value: string): Data {\n const parsed = JSON.parse(value) as ParsedData;\n\n if (parsed.version === 2) {\n return new Data(\n 3,\n Type.hit,\n parsed.cacheName,\n new Date(parsed.created),\n parsed.data,\n null,\n null,\n 0,\n parsed.etag\n );\n }\n\n if (parsed.version === 3) {\n return new Data(\n 3,\n parsed.type as Type,\n parsed.cacheName,\n new Date(parsed.created),\n parsed.data,\n parsed.error,\n parsed.errorTime === null ? null : new Date(parsed.errorTime),\n parsed.consecutiveErrors,\n parsed.etag\n );\n }\n\n throw new Error(`Unknown cache version number: ${String(parsed.version)}`);\n }\n}\n","import path from 'node:path';\nimport fs from 'node:fs';\nimport { readFile, writeFile, rm } from 'node:fs/promises';\nimport { Data } from './common.js';\nimport type { Store } from './store-memory.js';\n\nexport interface FilesystemStoreConfig {\n datadir: string;\n}\n\nasync function mkdir(dirpath: string): Promise<void> {\n await fs.promises.mkdir(dirpath, { recursive: true });\n}\n\nexport function createFilesystemStore(config: FilesystemStoreConfig): Store {\n return {\n async get(cacheName: string): Promise<Data> {\n const filename = path.resolve(config.datadir, `${cacheName}.json`);\n const data = await readFile(filename, 'utf8');\n return Data.parse(data);\n },\n\n async set(data: Data): Promise<void> {\n const filename = path.resolve(config.datadir, `${data.cacheName}.json`);\n await mkdir(path.dirname(filename));\n await writeFile(filename, data.stringify(), 'utf8');\n },\n\n async isset(cacheName: string): Promise<boolean> {\n const filename = path.resolve(config.datadir, `${cacheName}.json`);\n try {\n await fs.promises.access(filename);\n return true;\n } catch {\n return false;\n }\n },\n\n async unset(cacheName: string): Promise<void> {\n const filename = path.resolve(config.datadir, `${cacheName}.json`);\n await rm(filename, { force: true });\n }\n };\n}\n\nexport default createFilesystemStore;\n","import { Data } from './common.js';\n\nexport interface Store {\n get(cacheName: string): Promise<Data>;\n set(data: Data): Promise<void>;\n isset(cacheName: string): Promise<boolean>;\n unset(cacheName: string): Promise<void>;\n}\n\nexport interface MemoryStore extends Store {\n data: Record<string, string>;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type -- Future expansion\nexport interface MemoryStoreConfig {}\n\nexport function createMemoryStore(_config?: MemoryStoreConfig): MemoryStore {\n const storeMemory: MemoryStore = {\n data: {},\n\n get(cacheName: string): Promise<Data> {\n return Promise.resolve(Data.parse(storeMemory.data[cacheName]));\n },\n\n set(data: Data): Promise<void> {\n storeMemory.data[data.cacheName] = data.stringify();\n return Promise.resolve();\n },\n\n isset(cacheName: string): Promise<boolean> {\n return Promise.resolve(cacheName in storeMemory.data);\n },\n\n unset(cacheName: string): Promise<void> {\n // eslint-disable-next-line @typescript-eslint/no-dynamic-delete\n delete storeMemory.data[cacheName];\n return Promise.resolve();\n }\n };\n\n return storeMemory;\n}\n\nexport default createMemoryStore;\n","import { Hit, Miss, Data, Status, Type } from './common.js';\nimport { etag } from './etag.js';\nimport { createFilesystemStore } from './store-filesystem.js';\nimport { createMemoryStore, type Store } from './store-memory.js';\n\nfunction sanitize(string: string): string {\n return string.replaceAll(/[^a-z0-9]+/gi, '-');\n}\n\nexport class Cacheism {\n store: Store;\n status = Status;\n type = Type;\n\n constructor(store: Store) {\n this.store = store;\n }\n\n async go(\n cacheDomain: string,\n cachePath: string,\n status: Status,\n callback: (existing: Hit | Miss) => Promise<unknown>\n ): Promise<Hit | Miss> {\n // Input validation\n if (typeof cacheDomain !== 'string') {\n throw new TypeError('cacheDomain must be a string');\n }\n if (typeof cachePath !== 'string') {\n throw new TypeError('cachePath must be a string');\n }\n if (typeof status !== 'number' || status < Status.onlyFresh || status > Status.onlyCache) {\n throw new TypeError('status must be a valid Status value (0-3)');\n }\n if (typeof callback !== 'function') {\n throw new TypeError('callback must be a function');\n }\n\n const name = this.cacheName(cacheDomain, cachePath);\n let existing: Hit | Miss = new Miss(name, 'Missing cache', 0);\n const hasCache = await this.store.isset(name);\n\n if (hasCache) {\n existing = (await this.store.get(name)).response();\n }\n\n let response: Hit | Miss;\n\n try {\n if (status >= Status.preferCache && hasCache && existing.isHit) {\n response = existing;\n } else if (status === Status.onlyCache) {\n throw new Error('Missing cache');\n } else {\n const result = await callback(existing);\n if (result instanceof Hit) {\n response = result;\n } else {\n response = new Hit(name, result);\n }\n }\n } catch (err) {\n const errorMessage = err instanceof Error ? err.toString() : String(err);\n\n if (status >= Status.cacheOnFail && hasCache && existing.isHit) {\n response = existing.withError(errorMessage, existing.consecutiveErrors + 1);\n } else {\n response = new Miss(name, errorMessage, existing.consecutiveErrors + 1);\n }\n }\n\n await this.store.set(Data.fromResponse(response));\n Object.freeze(response);\n return response;\n }\n\n cacheName(cacheDomain: string, cachePath: string): string {\n return `${sanitize(cacheDomain)}/${sanitize(cachePath)}`;\n }\n\n setStore(store: Store): void {\n this.store = store;\n }\n\n hit(name: string, data: unknown, etag?: string): Hit {\n return new Hit(name, data, etag);\n }\n\n miss(name: string, error: string): Miss {\n return new Miss(name, error, 0);\n }\n\n // Static members for backward compatibility\n static Hit = Hit;\n static Miss = Miss;\n static Data = Data;\n static Status = Status;\n static Type = Type;\n\n static store = {\n filesystem: createFilesystemStore,\n memory: createMemoryStore,\n };\n}\n\n// Named exports for ESM users\nexport { Hit, Miss, Data, Status, Type };\nexport { etag };\nexport { createFilesystemStore, createMemoryStore };\nexport type { Store } from './store-memory.js';\nexport type { FilesystemStoreConfig } from './store-filesystem.js';\nexport type { MemoryStoreConfig, MemoryStore } from './store-memory.js';\n\n// Default export for backward compatibility\nexport default Cacheism;\n"],"mappings":"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"}
import { Stats } from "node:fs";
//#region src/common.d.ts
declare enum Status {
onlyFresh = 0,
cacheOnFail = 1,
preferCache = 2,
onlyCache = 3,
}
declare enum Type {
hit = "Hit",
miss = "Miss",
}
declare class Hit {
readonly version = 3;
readonly cacheName: string;
cached: boolean;
created: Date;
data: unknown;
error: string | null;
errorTime: Date | null;
consecutiveErrors: number;
etag: string;
readonly isHit = true;
readonly isMiss = false;
constructor(name: string, data: unknown, existingEtag?: string);
withError(error: string, consecutiveErrors: number): Hit;
}
declare class Miss {
readonly version = 3;
readonly cacheName: string;
cached: boolean;
created: Date;
readonly data: null;
error: string;
errorTime: Date;
consecutiveErrors: number;
readonly etag: null;
readonly isHit = false;
readonly isMiss = true;
constructor(name: string, error: string, consecutiveErrors: number);
}
declare class Data {
version: number;
type: Type;
cacheName: string;
created: Date;
data: unknown;
error: string | null;
errorTime: Date | null;
consecutiveErrors: number;
etag: string | null;
constructor(version: number, type: Type, name: string, created: Date, data: unknown, error: string | null, errorTime: Date | null, consecutiveErrors: number, etag: string | null);
response(): Hit | Miss;
stringify(): string;
static fromResponse(response: Hit | Miss): Data;
static parse(value: string): Data;
}
//#endregion
//#region src/etag.d.ts
interface EtagOptions {
weak?: boolean;
}
/**
* Create a simple ETag.
*/
declare function etag(entity: string | Buffer | Stats | null | undefined, options?: EtagOptions): string;
//#endregion
//#region src/store-memory.d.ts
interface Store {
get(cacheName: string): Promise<Data>;
set(data: Data): Promise<void>;
isset(cacheName: string): Promise<boolean>;
unset(cacheName: string): Promise<void>;
}
interface MemoryStore extends Store {
data: Record<string, string>;
}
interface MemoryStoreConfig {}
declare function createMemoryStore(_config?: MemoryStoreConfig): MemoryStore;
//#endregion
//#region src/store-filesystem.d.ts
interface FilesystemStoreConfig {
datadir: string;
}
declare function createFilesystemStore(config: FilesystemStoreConfig): Store;
//#endregion
//#region src/index.d.ts
declare class Cacheism {
store: Store;
status: typeof Status;
type: typeof Type;
constructor(store: Store);
go(cacheDomain: string, cachePath: string, status: Status, callback: (existing: Hit | Miss) => Promise<unknown>): Promise<Hit | Miss>;
cacheName(cacheDomain: string, cachePath: string): string;
setStore(store: Store): void;
hit(name: string, data: unknown, etag?: string): Hit;
miss(name: string, error: string): Miss;
static Hit: typeof Hit;
static Miss: typeof Miss;
static Data: typeof Data;
static Status: typeof Status;
static Type: typeof Type;
static store: {
filesystem: typeof createFilesystemStore;
memory: typeof createMemoryStore;
};
}
//#endregion
export { Cacheism, Cacheism as default, Data, type FilesystemStoreConfig, Hit, type MemoryStore, type MemoryStoreConfig, Miss, Status, type Store, Type, createFilesystemStore, createMemoryStore, etag };
//# sourceMappingURL=index.d.cts.map
import { Stats } from "node:fs";
//#region src/common.d.ts
declare enum Status {
onlyFresh = 0,
cacheOnFail = 1,
preferCache = 2,
onlyCache = 3,
}
declare enum Type {
hit = "Hit",
miss = "Miss",
}
declare class Hit {
readonly version = 3;
readonly cacheName: string;
cached: boolean;
created: Date;
data: unknown;
error: string | null;
errorTime: Date | null;
consecutiveErrors: number;
etag: string;
readonly isHit = true;
readonly isMiss = false;
constructor(name: string, data: unknown, existingEtag?: string);
withError(error: string, consecutiveErrors: number): Hit;
}
declare class Miss {
readonly version = 3;
readonly cacheName: string;
cached: boolean;
created: Date;
readonly data: null;
error: string;
errorTime: Date;
consecutiveErrors: number;
readonly etag: null;
readonly isHit = false;
readonly isMiss = true;
constructor(name: string, error: string, consecutiveErrors: number);
}
declare class Data {
version: number;
type: Type;
cacheName: string;
created: Date;
data: unknown;
error: string | null;
errorTime: Date | null;
consecutiveErrors: number;
etag: string | null;
constructor(version: number, type: Type, name: string, created: Date, data: unknown, error: string | null, errorTime: Date | null, consecutiveErrors: number, etag: string | null);
response(): Hit | Miss;
stringify(): string;
static fromResponse(response: Hit | Miss): Data;
static parse(value: string): Data;
}
//#endregion
//#region src/etag.d.ts
interface EtagOptions {
weak?: boolean;
}
/**
* Create a simple ETag.
*/
declare function etag(entity: string | Buffer | Stats | null | undefined, options?: EtagOptions): string;
//#endregion
//#region src/store-memory.d.ts
interface Store {
get(cacheName: string): Promise<Data>;
set(data: Data): Promise<void>;
isset(cacheName: string): Promise<boolean>;
unset(cacheName: string): Promise<void>;
}
interface MemoryStore extends Store {
data: Record<string, string>;
}
interface MemoryStoreConfig {}
declare function createMemoryStore(_config?: MemoryStoreConfig): MemoryStore;
//#endregion
//#region src/store-filesystem.d.ts
interface FilesystemStoreConfig {
datadir: string;
}
declare function createFilesystemStore(config: FilesystemStoreConfig): Store;
//#endregion
//#region src/index.d.ts
declare class Cacheism {
store: Store;
status: typeof Status;
type: typeof Type;
constructor(store: Store);
go(cacheDomain: string, cachePath: string, status: Status, callback: (existing: Hit | Miss) => Promise<unknown>): Promise<Hit | Miss>;
cacheName(cacheDomain: string, cachePath: string): string;
setStore(store: Store): void;
hit(name: string, data: unknown, etag?: string): Hit;
miss(name: string, error: string): Miss;
static Hit: typeof Hit;
static Miss: typeof Miss;
static Data: typeof Data;
static Status: typeof Status;
static Type: typeof Type;
static store: {
filesystem: typeof createFilesystemStore;
memory: typeof createMemoryStore;
};
}
//#endregion
export { Cacheism, Cacheism as default, Data, type FilesystemStoreConfig, Hit, type MemoryStore, type MemoryStoreConfig, Miss, Status, type Store, Type, createFilesystemStore, createMemoryStore, etag };
//# sourceMappingURL=index.d.mts.map
import { createHash } from "node:crypto";
import fs, { Stats } from "node:fs";
import path from "node:path";
import { readFile, rm, writeFile } from "node:fs/promises";
//#region src/etag.ts
/*!
* etag
* Copyright(c) 2014-2016 Douglas Christopher Wilson
* MIT Licensed
*/
/**
* Generate an entity tag.
*/
function entitytag(entity) {
if (entity.length === 0) return "\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"";
const hashObj = createHash("sha1");
if (typeof entity === "string") hashObj.update(entity, "utf8");
else hashObj.update(entity);
const hash = hashObj.digest("base64").substring(0, 27);
return "\"" + (typeof entity === "string" ? Buffer.byteLength(entity, "utf8") : entity.length).toString(16) + "-" + hash + "\"";
}
/**
* Determine if object is a Stats object.
*/
function isstats(obj) {
if (typeof Stats === "function" && obj instanceof Stats) return true;
return obj != null && typeof obj === "object" && "ctime" in obj && Object.prototype.toString.call(obj.ctime) === "[object Date]" && "mtime" in obj && Object.prototype.toString.call(obj.mtime) === "[object Date]" && "ino" in obj && typeof obj.ino === "number" && "size" in obj && typeof obj.size === "number";
}
/**
* Generate a tag for a stat.
*/
function stattag(stat) {
const mtime = stat.mtime.getTime().toString(16);
return "\"" + stat.size.toString(16) + "-" + mtime + "\"";
}
/**
* Create a simple ETag.
*/
function etag(entity, options) {
if (entity == null) throw new TypeError("argument entity is required");
const isStats = isstats(entity);
const weak = options && typeof options.weak === "boolean" ? options.weak : isStats;
if (!isStats && typeof entity !== "string" && !Buffer.isBuffer(entity)) throw new TypeError("argument entity must be string, Buffer, or fs.Stats");
const tag = isStats ? stattag(entity) : entitytag(entity);
return weak ? "W/" + tag : tag;
}
//#endregion
//#region src/common.ts
let Status = /* @__PURE__ */ function(Status$1) {
Status$1[Status$1["onlyFresh"] = 0] = "onlyFresh";
Status$1[Status$1["cacheOnFail"] = 1] = "cacheOnFail";
Status$1[Status$1["preferCache"] = 2] = "preferCache";
Status$1[Status$1["onlyCache"] = 3] = "onlyCache";
return Status$1;
}({});
Object.freeze(Status);
let Type = /* @__PURE__ */ function(Type$1) {
Type$1["hit"] = "Hit";
Type$1["miss"] = "Miss";
return Type$1;
}({});
Object.freeze(Type);
var Hit = class Hit {
version = 3;
cacheName;
cached = false;
created;
data;
error = null;
errorTime = null;
consecutiveErrors = 0;
etag;
isHit = true;
isMiss = false;
constructor(name, data, existingEtag) {
this.cacheName = name;
this.created = /* @__PURE__ */ new Date();
this.data = data;
this.etag = existingEtag ?? etag(JSON.stringify(data));
}
withError(error, consecutiveErrors) {
const hit = new Hit(this.cacheName, this.data, this.etag);
hit.cached = this.cached;
hit.created = this.created;
hit.error = error;
hit.errorTime = /* @__PURE__ */ new Date();
hit.consecutiveErrors = consecutiveErrors;
return hit;
}
};
var Miss = class {
version = 3;
cacheName;
cached = false;
created;
data = null;
error;
errorTime;
consecutiveErrors;
etag = null;
isHit = false;
isMiss = true;
constructor(name, error, consecutiveErrors) {
this.cacheName = name;
this.created = /* @__PURE__ */ new Date();
this.error = error;
this.errorTime = new Date(this.created);
this.consecutiveErrors = consecutiveErrors;
}
};
var Data = class Data {
version;
type;
cacheName;
created;
data;
error;
errorTime;
consecutiveErrors;
etag;
constructor(version, type, name, created, data, error, errorTime, consecutiveErrors, etag$1) {
this.version = version;
this.type = type;
this.cacheName = name;
this.created = created;
this.data = data;
this.error = error;
this.errorTime = errorTime;
this.consecutiveErrors = consecutiveErrors;
this.etag = etag$1;
}
response() {
if (this.version !== 3) throw new Error(`Unknown cache version number: ${String(this.version)}`);
let response;
if (this.type === Type.hit) {
response = new Hit(this.cacheName, this.data, this.etag ?? void 0);
response.cached = true;
response.created = this.created;
response.consecutiveErrors = this.consecutiveErrors;
} else {
response = new Miss(this.cacheName, this.error ?? "", this.consecutiveErrors);
response.cached = false;
response.created = this.created;
response.errorTime = this.errorTime ?? /* @__PURE__ */ new Date();
}
return response;
}
stringify() {
return JSON.stringify({
version: this.version,
type: this.type,
cacheName: this.cacheName,
created: this.created,
data: this.data,
error: this.error,
errorTime: this.errorTime,
consecutiveErrors: this.consecutiveErrors,
etag: this.etag
}, null, 2);
}
static fromResponse(response) {
return new Data(response.version, response.isHit ? Type.hit : Type.miss, response.cacheName, response.created, response.data, response.error, response.errorTime, response.consecutiveErrors, response.etag);
}
static parse(value) {
const parsed = JSON.parse(value);
if (parsed.version === 2) return new Data(3, Type.hit, parsed.cacheName, new Date(parsed.created), parsed.data, null, null, 0, parsed.etag);
if (parsed.version === 3) return new Data(3, parsed.type, parsed.cacheName, new Date(parsed.created), parsed.data, parsed.error, parsed.errorTime === null ? null : new Date(parsed.errorTime), parsed.consecutiveErrors, parsed.etag);
throw new Error(`Unknown cache version number: ${String(parsed.version)}`);
}
};
//#endregion
//#region src/store-filesystem.ts
async function mkdir(dirpath) {
await fs.promises.mkdir(dirpath, { recursive: true });
}
function createFilesystemStore(config) {
return {
async get(cacheName) {
const data = await readFile(path.resolve(config.datadir, `${cacheName}.json`), "utf8");
return Data.parse(data);
},
async set(data) {
const filename = path.resolve(config.datadir, `${data.cacheName}.json`);
await mkdir(path.dirname(filename));
await writeFile(filename, data.stringify(), "utf8");
},
async isset(cacheName) {
const filename = path.resolve(config.datadir, `${cacheName}.json`);
try {
await fs.promises.access(filename);
return true;
} catch {
return false;
}
},
async unset(cacheName) {
await rm(path.resolve(config.datadir, `${cacheName}.json`), { force: true });
}
};
}
//#endregion
//#region src/store-memory.ts
function createMemoryStore(_config) {
const storeMemory = {
data: {},
get(cacheName) {
return Promise.resolve(Data.parse(storeMemory.data[cacheName]));
},
set(data) {
storeMemory.data[data.cacheName] = data.stringify();
return Promise.resolve();
},
isset(cacheName) {
return Promise.resolve(cacheName in storeMemory.data);
},
unset(cacheName) {
delete storeMemory.data[cacheName];
return Promise.resolve();
}
};
return storeMemory;
}
//#endregion
//#region src/index.ts
function sanitize(string) {
return string.replaceAll(/[^a-z0-9]+/gi, "-");
}
var Cacheism = class {
store;
status = Status;
type = Type;
constructor(store) {
this.store = store;
}
async go(cacheDomain, cachePath, status, callback) {
if (typeof cacheDomain !== "string") throw new TypeError("cacheDomain must be a string");
if (typeof cachePath !== "string") throw new TypeError("cachePath must be a string");
if (typeof status !== "number" || status < Status.onlyFresh || status > Status.onlyCache) throw new TypeError("status must be a valid Status value (0-3)");
if (typeof callback !== "function") throw new TypeError("callback must be a function");
const name = this.cacheName(cacheDomain, cachePath);
let existing = new Miss(name, "Missing cache", 0);
const hasCache = await this.store.isset(name);
if (hasCache) existing = (await this.store.get(name)).response();
let response;
try {
if (status >= Status.preferCache && hasCache && existing.isHit) response = existing;
else if (status === Status.onlyCache) throw new Error("Missing cache");
else {
const result = await callback(existing);
if (result instanceof Hit) response = result;
else response = new Hit(name, result);
}
} catch (err) {
const errorMessage = err instanceof Error ? err.toString() : String(err);
if (status >= Status.cacheOnFail && hasCache && existing.isHit) response = existing.withError(errorMessage, existing.consecutiveErrors + 1);
else response = new Miss(name, errorMessage, existing.consecutiveErrors + 1);
}
await this.store.set(Data.fromResponse(response));
Object.freeze(response);
return response;
}
cacheName(cacheDomain, cachePath) {
return `${sanitize(cacheDomain)}/${sanitize(cachePath)}`;
}
setStore(store) {
this.store = store;
}
hit(name, data, etag$1) {
return new Hit(name, data, etag$1);
}
miss(name, error) {
return new Miss(name, error, 0);
}
static Hit = Hit;
static Miss = Miss;
static Data = Data;
static Status = Status;
static Type = Type;
static store = {
filesystem: createFilesystemStore,
memory: createMemoryStore
};
};
var src_default = Cacheism;
//#endregion
export { Cacheism, Data, Hit, Miss, Status, Type, createFilesystemStore, createMemoryStore, src_default as default, etag };
//# sourceMappingURL=index.mjs.map
{"version":3,"file":"index.mjs","names":["generateEtag","etag","response: Hit | Miss","storeMemory: MemoryStore","existing: Hit | Miss","response: Hit | Miss","etag"],"sources":["../src/etag.ts","../src/common.ts","../src/store-filesystem.ts","../src/store-memory.ts","../src/index.ts"],"sourcesContent":["/*!\n * etag\n * Copyright(c) 2014-2016 Douglas Christopher Wilson\n * MIT Licensed\n */\n\nimport { createHash } from 'node:crypto';\nimport { Stats } from 'node:fs';\n\nexport interface EtagOptions {\n weak?: boolean;\n}\n\n/**\n * Generate an entity tag.\n */\nfunction entitytag(entity: string | Buffer): string {\n if (entity.length === 0) {\n // fast-path empty\n return '\"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk\"';\n }\n\n // compute hash of entity\n const hashObj = createHash('sha1');\n if (typeof entity === 'string') {\n hashObj.update(entity, 'utf8');\n } else {\n hashObj.update(entity);\n }\n const hash = hashObj.digest('base64').substring(0, 27);\n\n // compute length of entity\n const len = typeof entity === 'string'\n ? Buffer.byteLength(entity, 'utf8')\n : entity.length;\n\n return '\"' + len.toString(16) + '-' + hash + '\"';\n}\n\n/**\n * Determine if object is a Stats object.\n */\nfunction isstats(obj: unknown): obj is Stats {\n // genuine fs.Stats\n if (typeof Stats === 'function' && obj instanceof Stats) {\n return true;\n }\n\n // quack quack\n return obj != null && typeof obj === 'object' &&\n 'ctime' in obj && Object.prototype.toString.call((obj as Stats).ctime) === '[object Date]' &&\n 'mtime' in obj && Object.prototype.toString.call((obj as Stats).mtime) === '[object Date]' &&\n 'ino' in obj && typeof (obj as Stats).ino === 'number' &&\n 'size' in obj && typeof (obj as Stats).size === 'number';\n}\n\n/**\n * Generate a tag for a stat.\n */\nfunction stattag(stat: Stats): string {\n const mtime = stat.mtime.getTime().toString(16);\n const size = stat.size.toString(16);\n\n return '\"' + size + '-' + mtime + '\"';\n}\n\n/**\n * Create a simple ETag.\n */\nexport function etag(entity: string | Buffer | Stats | null | undefined, options?: EtagOptions): string {\n if (entity == null) {\n throw new TypeError('argument entity is required');\n }\n\n // support fs.Stats object\n const isStats = isstats(entity);\n const weak = options && typeof options.weak === 'boolean'\n ? options.weak\n : isStats;\n\n // validate argument\n if (!isStats && typeof entity !== 'string' && !Buffer.isBuffer(entity)) {\n throw new TypeError('argument entity must be string, Buffer, or fs.Stats');\n }\n\n // generate entity tag\n const tag = isStats\n ? stattag(entity)\n : entitytag(entity);\n\n return weak\n ? 'W/' + tag\n : tag;\n}\n\nexport default etag;\n","import { etag as generateEtag } from './etag.js';\n\nexport enum Status {\n onlyFresh = 0,\n cacheOnFail = 1,\n preferCache = 2,\n onlyCache = 3,\n}\nObject.freeze(Status);\n\nexport enum Type {\n hit = 'Hit',\n miss = 'Miss',\n}\nObject.freeze(Type);\n\nexport class Hit {\n readonly version = 3;\n readonly cacheName: string;\n cached = false;\n created: Date;\n data: unknown;\n error: string | null = null;\n errorTime: Date | null = null;\n consecutiveErrors = 0;\n etag: string;\n readonly isHit = true;\n readonly isMiss = false;\n\n constructor(name: string, data: unknown, existingEtag?: string) {\n this.cacheName = name;\n this.created = new Date();\n this.data = data;\n this.etag = existingEtag ?? generateEtag(JSON.stringify(data));\n }\n\n withError(error: string, consecutiveErrors: number): Hit {\n const hit = new Hit(this.cacheName, this.data, this.etag);\n hit.cached = this.cached;\n hit.created = this.created;\n hit.error = error;\n hit.errorTime = new Date();\n hit.consecutiveErrors = consecutiveErrors;\n return hit;\n }\n}\n\nexport class Miss {\n readonly version = 3;\n readonly cacheName: string;\n cached = false;\n created: Date;\n readonly data = null;\n error: string;\n errorTime: Date;\n consecutiveErrors: number;\n readonly etag = null;\n readonly isHit = false;\n readonly isMiss = true;\n\n constructor(name: string, error: string, consecutiveErrors: number) {\n this.cacheName = name;\n this.created = new Date();\n this.error = error;\n this.errorTime = new Date(this.created);\n this.consecutiveErrors = consecutiveErrors;\n }\n}\n\ninterface ParsedData {\n version: number;\n type: string;\n cacheName: string;\n created: string;\n data: unknown;\n error: string | null;\n errorTime: string | null;\n consecutiveErrors: number;\n etag: string | null;\n}\n\nexport class Data {\n version: number;\n type: Type;\n cacheName: string;\n created: Date;\n data: unknown;\n error: string | null;\n errorTime: Date | null;\n consecutiveErrors: number;\n etag: string | null;\n\n constructor(\n version: number,\n type: Type,\n name: string,\n created: Date,\n data: unknown,\n error: string | null,\n errorTime: Date | null,\n consecutiveErrors: number,\n etag: string | null\n ) {\n this.version = version;\n this.type = type;\n this.cacheName = name;\n this.created = created;\n this.data = data;\n this.error = error;\n this.errorTime = errorTime;\n this.consecutiveErrors = consecutiveErrors;\n this.etag = etag;\n }\n\n response(): Hit | Miss {\n if (this.version !== 3) {\n throw new Error(`Unknown cache version number: ${String(this.version)}`);\n }\n\n let response: Hit | Miss;\n\n if (this.type === Type.hit) {\n response = new Hit(this.cacheName, this.data, this.etag ?? undefined);\n response.cached = true;\n response.created = this.created;\n response.consecutiveErrors = this.consecutiveErrors;\n } else {\n response = new Miss(this.cacheName, this.error ?? '', this.consecutiveErrors);\n response.cached = false;\n response.created = this.created;\n response.errorTime = this.errorTime ?? new Date();\n }\n\n return response;\n }\n\n stringify(): string {\n return JSON.stringify({\n version: this.version,\n type: this.type,\n cacheName: this.cacheName,\n created: this.created,\n data: this.data,\n error: this.error,\n errorTime: this.errorTime,\n consecutiveErrors: this.consecutiveErrors,\n etag: this.etag,\n }, null, 2);\n }\n\n static fromResponse(response: Hit | Miss): Data {\n return new Data(\n response.version,\n response.isHit ? Type.hit : Type.miss,\n response.cacheName,\n response.created,\n response.data,\n response.error,\n response.errorTime,\n response.consecutiveErrors,\n response.etag\n );\n }\n\n static parse(value: string): Data {\n const parsed = JSON.parse(value) as ParsedData;\n\n if (parsed.version === 2) {\n return new Data(\n 3,\n Type.hit,\n parsed.cacheName,\n new Date(parsed.created),\n parsed.data,\n null,\n null,\n 0,\n parsed.etag\n );\n }\n\n if (parsed.version === 3) {\n return new Data(\n 3,\n parsed.type as Type,\n parsed.cacheName,\n new Date(parsed.created),\n parsed.data,\n parsed.error,\n parsed.errorTime === null ? null : new Date(parsed.errorTime),\n parsed.consecutiveErrors,\n parsed.etag\n );\n }\n\n throw new Error(`Unknown cache version number: ${String(parsed.version)}`);\n }\n}\n","import path from 'node:path';\nimport fs from 'node:fs';\nimport { readFile, writeFile, rm } from 'node:fs/promises';\nimport { Data } from './common.js';\nimport type { Store } from './store-memory.js';\n\nexport interface FilesystemStoreConfig {\n datadir: string;\n}\n\nasync function mkdir(dirpath: string): Promise<void> {\n await fs.promises.mkdir(dirpath, { recursive: true });\n}\n\nexport function createFilesystemStore(config: FilesystemStoreConfig): Store {\n return {\n async get(cacheName: string): Promise<Data> {\n const filename = path.resolve(config.datadir, `${cacheName}.json`);\n const data = await readFile(filename, 'utf8');\n return Data.parse(data);\n },\n\n async set(data: Data): Promise<void> {\n const filename = path.resolve(config.datadir, `${data.cacheName}.json`);\n await mkdir(path.dirname(filename));\n await writeFile(filename, data.stringify(), 'utf8');\n },\n\n async isset(cacheName: string): Promise<boolean> {\n const filename = path.resolve(config.datadir, `${cacheName}.json`);\n try {\n await fs.promises.access(filename);\n return true;\n } catch {\n return false;\n }\n },\n\n async unset(cacheName: string): Promise<void> {\n const filename = path.resolve(config.datadir, `${cacheName}.json`);\n await rm(filename, { force: true });\n }\n };\n}\n\nexport default createFilesystemStore;\n","import { Data } from './common.js';\n\nexport interface Store {\n get(cacheName: string): Promise<Data>;\n set(data: Data): Promise<void>;\n isset(cacheName: string): Promise<boolean>;\n unset(cacheName: string): Promise<void>;\n}\n\nexport interface MemoryStore extends Store {\n data: Record<string, string>;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type -- Future expansion\nexport interface MemoryStoreConfig {}\n\nexport function createMemoryStore(_config?: MemoryStoreConfig): MemoryStore {\n const storeMemory: MemoryStore = {\n data: {},\n\n get(cacheName: string): Promise<Data> {\n return Promise.resolve(Data.parse(storeMemory.data[cacheName]));\n },\n\n set(data: Data): Promise<void> {\n storeMemory.data[data.cacheName] = data.stringify();\n return Promise.resolve();\n },\n\n isset(cacheName: string): Promise<boolean> {\n return Promise.resolve(cacheName in storeMemory.data);\n },\n\n unset(cacheName: string): Promise<void> {\n // eslint-disable-next-line @typescript-eslint/no-dynamic-delete\n delete storeMemory.data[cacheName];\n return Promise.resolve();\n }\n };\n\n return storeMemory;\n}\n\nexport default createMemoryStore;\n","import { Hit, Miss, Data, Status, Type } from './common.js';\nimport { etag } from './etag.js';\nimport { createFilesystemStore } from './store-filesystem.js';\nimport { createMemoryStore, type Store } from './store-memory.js';\n\nfunction sanitize(string: string): string {\n return string.replaceAll(/[^a-z0-9]+/gi, '-');\n}\n\nexport class Cacheism {\n store: Store;\n status = Status;\n type = Type;\n\n constructor(store: Store) {\n this.store = store;\n }\n\n async go(\n cacheDomain: string,\n cachePath: string,\n status: Status,\n callback: (existing: Hit | Miss) => Promise<unknown>\n ): Promise<Hit | Miss> {\n // Input validation\n if (typeof cacheDomain !== 'string') {\n throw new TypeError('cacheDomain must be a string');\n }\n if (typeof cachePath !== 'string') {\n throw new TypeError('cachePath must be a string');\n }\n if (typeof status !== 'number' || status < Status.onlyFresh || status > Status.onlyCache) {\n throw new TypeError('status must be a valid Status value (0-3)');\n }\n if (typeof callback !== 'function') {\n throw new TypeError('callback must be a function');\n }\n\n const name = this.cacheName(cacheDomain, cachePath);\n let existing: Hit | Miss = new Miss(name, 'Missing cache', 0);\n const hasCache = await this.store.isset(name);\n\n if (hasCache) {\n existing = (await this.store.get(name)).response();\n }\n\n let response: Hit | Miss;\n\n try {\n if (status >= Status.preferCache && hasCache && existing.isHit) {\n response = existing;\n } else if (status === Status.onlyCache) {\n throw new Error('Missing cache');\n } else {\n const result = await callback(existing);\n if (result instanceof Hit) {\n response = result;\n } else {\n response = new Hit(name, result);\n }\n }\n } catch (err) {\n const errorMessage = err instanceof Error ? err.toString() : String(err);\n\n if (status >= Status.cacheOnFail && hasCache && existing.isHit) {\n response = existing.withError(errorMessage, existing.consecutiveErrors + 1);\n } else {\n response = new Miss(name, errorMessage, existing.consecutiveErrors + 1);\n }\n }\n\n await this.store.set(Data.fromResponse(response));\n Object.freeze(response);\n return response;\n }\n\n cacheName(cacheDomain: string, cachePath: string): string {\n return `${sanitize(cacheDomain)}/${sanitize(cachePath)}`;\n }\n\n setStore(store: Store): void {\n this.store = store;\n }\n\n hit(name: string, data: unknown, etag?: string): Hit {\n return new Hit(name, data, etag);\n }\n\n miss(name: string, error: string): Miss {\n return new Miss(name, error, 0);\n }\n\n // Static members for backward compatibility\n static Hit = Hit;\n static Miss = Miss;\n static Data = Data;\n static Status = Status;\n static Type = Type;\n\n static store = {\n filesystem: createFilesystemStore,\n memory: createMemoryStore,\n };\n}\n\n// Named exports for ESM users\nexport { Hit, Miss, Data, Status, Type };\nexport { etag };\nexport { createFilesystemStore, createMemoryStore };\nexport type { Store } from './store-memory.js';\nexport type { FilesystemStoreConfig } from './store-filesystem.js';\nexport type { MemoryStoreConfig, MemoryStore } from './store-memory.js';\n\n// Default export for backward compatibility\nexport default Cacheism;\n"],"mappings":"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"}
+45
-9
{
"name": "@andrewshell/cacheism",
"version": "2.0.0",
"version": "3.0.0",
"description": "Simple caching library",
"main": "lib/cacheism.js",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.mts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
}
},
"files": [
"dist"
],
"packageManager": "pnpm@9.15.1",
"engines": {
"node": ">=20.0.0"
},
"scripts": {
"test": "mocha"
"build": "tsdown",
"test": "pnpm build && c8 node --test test/*.test.cjs",
"coverage": "c8 report --reporter=text-lcov",
"typecheck": "tsc --noEmit",
"lint": "eslint src/ test/",
"lint:fix": "eslint src/ test/ --fix",
"prepublishOnly": "pnpm build",
"prepare": "husky"
},

@@ -24,9 +53,16 @@ "repository": {

"devDependencies": {
"expect.js": "^0.3.1",
"mocha": "^10.2.0",
"mockdate": "^3.0.5"
},
"dependencies": {
"etag": "^1.8.1"
"@commitlint/cli": "^20.2.0",
"@commitlint/config-conventional": "^20.2.0",
"@eslint/js": "^9.39.1",
"@types/node": "^25.0.0",
"c8": "^10.1.3",
"eslint": "^9.39.1",
"husky": "^9.1.7",
"mockdate": "^3.0.5",
"safe-buffer": "^5.2.1",
"seedrandom": "^3.0.5",
"tsdown": "^0.17.2",
"typescript": "^5.9.3",
"typescript-eslint": "^8.49.0"
}
}
-36
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
name: Node.js CI
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x, 16.x, 18.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- name: Archive npm failure logs
uses: actions/upload-artifact@v3
if: failure()
with:
name: npm-logs
path: ~/.npm/_logs
- run: npm test
const common = require('./common');
function _sanitize(string) {
return string.replaceAll(new RegExp('[^a-z0-9]+', 'ig'), '-');
}
function Cacheism(store) {
this.store = store;
this.status = common.Status;
this.type = common.Type;
}
Cacheism.prototype.go = async function (cacheDomain, cachePath, status, callback) {
let response, name = this.cacheName(cacheDomain, cachePath);
let existing = new common.Miss(name, new Error('Missing cache'), 0);
let hasCache = await this.store.isset(name);
if (hasCache) {
existing = (await this.store.get(name)).response();
}
try {
if (status >= this.status.preferCache && hasCache && existing.isHit) {
response = existing;
} else if (status === this.status.onlyCache) {
throw new Error('Missing cache');
} else {
response = await callback(existing);
if (!(response instanceof common.Hit)) {
response = new common.Hit(name, response);
}
}
} catch (err) {
if (status >= this.status.cacheOnFail && hasCache) {
response = existing;
response.error = err.toString();
response.errorTime = new Date();
response.consecutiveErrors++;
} else {
response = new common.Miss(
name,
err.toString(),
existing.consecutiveErrors + 1
);
}
}
await this.store.set(common.Data.fromResponse(response));
Object.freeze(response);
return response;
}
Cacheism.prototype.cacheName = function (cacheDomain, cachePath) {
return `${_sanitize(cacheDomain)}/${_sanitize(cachePath)}`;
}
Cacheism.prototype.setStore = function (store) {
this.store = store;
}
Cacheism.prototype.hit = function (name, data, etag) {
return new common.Hit(name, data, etag);
}
Cacheism.prototype.miss = function (name, error) {
return new common.Miss(name, error);
}
Cacheism.Hit = common.Hit;
Cacheism.Miss = common.Miss;
Cacheism.Data = common.Data;
Cacheism.Status = common.Status;
Cacheism.Type = common.Type;
Cacheism.store = {
filesystem: require('./store-filesystem'),
memory: require('./store-memory'),
};
module.exports = Cacheism;
const generateEtag = require('etag');
class Status {}
Status.onlyFresh = 0;
Status.cacheOnFail = 1;
Status.preferCache = 2;
Status.onlyCache = 3;
class Type {}
Type.hit = 'Hit';
Type.miss = 'Miss';
Object.freeze(Status);
class Hit {
constructor(name, data, etag) {
this.version = 3;
this.cacheName = name;
this.cached = false;
this.created = new Date();
this.data = data;
this.error = null;
this.errorTime = null;
this.consecutiveErrors = 0;
this.etag = null == etag ? generateEtag(JSON.stringify(data)) : etag;
this.isHit = true;
this.isMiss = false;
}
}
class Miss {
constructor(name, error, consecutiveErrors) {
this.version = 3;
this.cacheName = name;
this.cached = false;
this.created = new Date();
this.data = null;
this.error = error;
this.errorTime = new Date(this.created);
this.consecutiveErrors = consecutiveErrors;
this.etag = null;
this.isHit = false;
this.isMiss = true;
}
}
class Data {
constructor(version, type, name, created, data, error, errorTime, consecutiveErrors, etag) {
this.version = version;
this.type = type;
this.cacheName = name;
this.created = created;
this.data = data;
this.error = error;
this.errorTime = errorTime;
this.consecutiveErrors = consecutiveErrors;
this.etag = etag;
}
response() {
if (3 !== this.version) {
throw new Error(`Unknown cache version number: ${this.version}`);
}
let response;
if (this.type === Type.hit) {
response = new Hit(this.cacheName, this.data, this.etag);
response.cached = true;
response.created = this.created;
response.consecutiveErrors = this.consecutiveErrors;
} else {
response = new Miss(this.cacheName, this.error, this.consecutiveErrors);
response.cached = false;
response.created = this.created;
response.errorTime = this.errorTime;
}
return response;
}
stringify() {
return JSON.stringify({
version: this.version,
type: this.type,
cacheName: this.cacheName,
created: this.created,
data: this.data,
error: this.error,
errorTime: this.errorTime,
consecutiveErrors: this.consecutiveErrors,
etag: this.etag,
}, null, 2);
}
static fromResponse(response) {
return new Data(
response.version,
response.isHit ? Type.hit : Type.miss,
response.cacheName,
response.created,
response.data,
response.error,
response.errorTime,
response.consecutiveErrors,
response.etag
);
}
static parse(value) {
const parsed = JSON.parse(value);
if (2 === parsed.version) {
return new Data(
3,
Type.hit,
parsed.cacheName,
new Date(parsed.created),
parsed.data,
null,
null,
0,
parsed.etag
);
}
if (3 === parsed.version) {
return new Data(
3,
parsed.type,
parsed.cacheName,
new Date(parsed.created),
parsed.data,
parsed.error,
null === parsed.errorTime ? null : new Date(parsed.errorTime),
parsed.consecutiveErrors,
parsed.etag
);
}
throw new Error(`Unknown cache version number: ${parsed.version}`);
}
}
module.exports = { Hit, Miss, Data, Status, Type };
/**
* String.prototype.replaceAll() polyfill
* https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/
* @author Chris Ferdinandi
* @license MIT
*/
if (!String.prototype.replaceAll) {
String.prototype.replaceAll = function(str, newStr){
// If a regex pattern
if (Object.prototype.toString.call(str).toLowerCase() === '[object regexp]') {
return this.replace(str, newStr);
}
// If a string
return this.replace(new RegExp(str, 'g'), newStr);
};
}
const common = require('./common');
const storeFilesystem = { };
const path = require('path');
const fs = require('fs');
const fsPromises = require('fs/promises');
module.exports = function (config) {
function _mkdir(dirpath) {
if (false === fs.existsSync(dirpath)) {
fs.mkdirSync(dirpath, { recursive: true });
}
}
_mkdir(config.datadir);
storeFilesystem.get = async (cacheName) => {
const filename = path.resolve(config.datadir, `${cacheName}.json`);
const data = await fsPromises.readFile(filename, 'utf8');
return common.Data.parse(data);
}
storeFilesystem.set = async (data) => {
const filename = path.resolve(config.datadir, `${data.cacheName}.json`);
_mkdir(path.dirname(filename));
await fsPromises.writeFile(filename, data.stringify(), 'utf8');
}
storeFilesystem.isset = async (cacheName) => {
const filename = path.resolve(config.datadir, `${cacheName}.json`);
return fs.existsSync(filename);
}
storeFilesystem.unset = async (cacheName) => {
const filename = path.resolve(config.datadir, `${cacheName}.json`);
await fsPromises.rm(filename, { force: true });
}
return storeFilesystem;
}
const common = require('./common');
module.exports = function (config) {
const storeMemory = { data: {} };
storeMemory.get = async (cacheName) => {
return common.Data.parse(storeMemory.data[cacheName]);
}
storeMemory.set = async (data) => {
storeMemory.data[data.cacheName] = data.stringify();
}
storeMemory.isset = async (cacheName) => {
return null != storeMemory.data[cacheName];
}
storeMemory.unset = async (cacheName) => {
delete storeMemory.data[cacheName];
}
return storeMemory;
}
const expect = require('expect.js');
const mockdate = require('mockdate');
const fs = require('fs');
const path = require('path');
const datadir = path.resolve(__dirname, './cache-filesystem');
const Cacheism = require('../lib/cacheism');
const cache = new Cacheism(Cacheism.store.filesystem({ datadir }));
const helpers = require('./helpers');
describe('filesystem', function() {
beforeEach(function() {
// runs before each test in this block
if (fs.existsSync(datadir)) {
fs.rmSync(datadir, { recursive: true, force: true });
}
});
after(function() {
// runs once after the last test in this block
if (fs.existsSync(datadir)) {
fs.rmSync(datadir, { recursive: true, force: true });
}
});
it('should export as a function', function() {
expect(cache.go).to.be.a('function');
});
describe('when status=onlyFresh', async function () {
describe('and no existing cache', async function () {
it('should return a Hit (live value) on success', async function () {
const c = await cache.go('-internal', 'cache', Cacheism.Status.onlyFresh, async () => {
return 'live';
});
helpers.expectCacheHit(c, false, 'live');
helpers.expectCacheNoErrors(c);
expect(await cache.store.isset('-internal/cache')).to.be(true);
const d = await cache.store.get('-internal/cache');
helpers.expectDataHit(d, 'live', c.etag);
helpers.expectDataNoErrors(d);
});
it('should return a Miss on error', async function () {
const c = await cache.go('-internal', 'cache', Cacheism.Status.onlyFresh, async () => {
throw Error('cache error');
});
helpers.expectCacheMiss(c, false, null);
helpers.expectCacheErrors(c, 'cache error', 1);
expect(c.created).to.eql(c.errorTime);
expect(await cache.store.isset('-internal/cache')).to.be(true);
const d = await cache.store.get('-internal/cache');
helpers.expectDataMiss(d, null);
helpers.expectDataErrors(d, 'Error: cache error', 1);
expect(d.created).to.eql(d.errorTime);
});
});
describe('and having existing cache', async function () {
it('should return a Hit (live value) on success', async function () {
mockdate.set('2000-11-22');
await cache.store.set(Cacheism.Data.fromResponse(
new Cacheism.Hit('-internal/cache', 'cached')
));
mockdate.reset();
const c = await cache.go('-internal', 'cache', Cacheism.Status.onlyFresh, async () => {
return 'live';
});
helpers.expectCacheHit(c, false, 'live');
helpers.expectCacheNoErrors(c);
expect(await cache.store.isset('-internal/cache')).to.be(true);
const d = await cache.store.get('-internal/cache');
helpers.expectDataHit(d, 'live', c.etag);
helpers.expectDataNoErrors(d);
});
it('should return a Miss on error', async function () {
mockdate.set('2000-11-22');
await cache.store.set(Cacheism.Data.fromResponse(
new Cacheism.Hit('-internal/cache', 'cached')
));
mockdate.reset();
const c = await cache.go('-internal', 'cache', Cacheism.Status.onlyFresh, async () => {
throw Error('cache error');
});
helpers.expectCacheMiss(c, false, null);
helpers.expectCacheErrors(c, 'cache error', 1);
expect(c.created).to.eql(c.errorTime);
expect(await cache.store.isset('-internal/cache')).to.be(true);
const d = await cache.store.get('-internal/cache');
helpers.expectDataMiss(d, null);
helpers.expectDataErrors(d, 'Error: cache error', 1);
expect(d.created).to.eql(d.errorTime);
});
});
});
describe('when status=cacheOnFail', function () {
describe('and no existing cache', async function () {
it('should return a Hit (live value) on success', async function () {
const c = await cache.go('-internal', 'cache', Cacheism.Status.cacheOnFail, async () => {
return 'live';
});
helpers.expectCacheHit(c, false, 'live');
helpers.expectCacheNoErrors(c);
expect(await cache.store.isset('-internal/cache')).to.be(true);
const d = await cache.store.get('-internal/cache');
helpers.expectDataHit(d, 'live', c.etag);
helpers.expectDataNoErrors(d);
});
it('should return a Miss on error', async function () {
const c = await cache.go('-internal', 'cache', Cacheism.Status.cacheOnFail, async () => {
throw Error('cache error');
});
helpers.expectCacheMiss(c, false, null);
helpers.expectCacheErrors(c, 'cache error', 1);
expect(c.created).to.eql(c.errorTime);
expect(await cache.store.isset('-internal/cache')).to.be(true);
const d = await cache.store.get('-internal/cache');
helpers.expectDataMiss(d, null, c.etag);
helpers.expectDataErrors(d, 'Error: cache error', 1);
expect(d.created).to.eql(d.errorTime);
});
});
describe('and having existing cache', async function () {
it('should return a Hit (live value) on success', async function () {
mockdate.set('2000-11-22');
await cache.store.set(Cacheism.Data.fromResponse(
new Cacheism.Hit('-internal/cache', 'cached')
));
mockdate.reset();
const c = await cache.go('-internal', 'cache', Cacheism.Status.cacheOnFail, async () => {
return 'live';
});
helpers.expectCacheHit(c, false, 'live');
helpers.expectCacheNoErrors(c);
expect(await cache.store.isset('-internal/cache')).to.be(true);
const d = await cache.store.get('-internal/cache');
helpers.expectDataHit(d, 'live', c.etag);
helpers.expectDataNoErrors(d);
});
it('should return a Hit (cached value) on error', async function () {
mockdate.set('2000-11-22');
await cache.store.set(Cacheism.Data.fromResponse(
new Cacheism.Hit('-internal/cache', 'cached')
));
mockdate.reset();
const c = await cache.go('-internal', 'cache', Cacheism.Status.cacheOnFail, async () => {
throw Error('cache error');
});
helpers.expectCacheHit(c, true, 'cached');
helpers.expectCacheErrors(c, 'cache error', 1);
expect(c.created).not.to.eql(c.errorTime);
expect(await cache.store.isset('-internal/cache')).to.be(true);
const d = await cache.store.get('-internal/cache');
helpers.expectDataHit(d, 'cached', c.etag);
helpers.expectDataErrors(d, 'Error: cache error', 1);
expect(d.created).not.to.eql(d.errorTime);
});
});
});
describe('when status=preferCache', function () {
describe('and no existing cache', async function () {
it('should return a Hit (live value) on success', async function () {
const c = await cache.go('-internal', 'cache', Cacheism.Status.preferCache, async () => {
return 'live';
});
helpers.expectCacheHit(c, false, 'live');
helpers.expectCacheNoErrors(c);
expect(await cache.store.isset('-internal/cache')).to.be(true);
const d = await cache.store.get('-internal/cache');
helpers.expectDataHit(d, 'live', c.etag);
helpers.expectDataNoErrors(d);
});
it('should return a Miss on error', async function () {
const c = await cache.go('-internal', 'cache', Cacheism.Status.preferCache, async () => {
throw Error('cache error');
});
helpers.expectCacheMiss(c, false, null);
helpers.expectCacheErrors(c, 'cache error', 1);
expect(c.created).to.eql(c.errorTime);
expect(await cache.store.isset('-internal/cache')).to.be(true);
const d = await cache.store.get('-internal/cache');
helpers.expectDataMiss(d, null, c.etag);
helpers.expectDataErrors(d, 'Error: cache error', 1);
expect(d.created).to.eql(d.errorTime);
});
});
describe('and having existing cache', async function () {
it('should return a Hit (cached value) on success', async function () {
mockdate.set('2000-11-22');
await cache.store.set(Cacheism.Data.fromResponse(
new Cacheism.Hit('-internal/cache', 'cached')
));
mockdate.reset();
const c = await cache.go('-internal', 'cache', Cacheism.Status.preferCache, async () => {
return 'live';
});
helpers.expectCacheHit(c, true, 'cached');
helpers.expectCacheNoErrors(c);
expect(await cache.store.isset('-internal/cache')).to.be(true);
const d = await cache.store.get('-internal/cache');
helpers.expectDataHit(d, 'cached', c.etag);
helpers.expectDataNoErrors(d);
});
it('should return a Hit (cached value) on error', async function () {
mockdate.set('2000-11-22');
await cache.store.set(Cacheism.Data.fromResponse(
new Cacheism.Hit('-internal/cache', 'cached')
));
mockdate.reset();
const c = await cache.go('-internal', 'cache', Cacheism.Status.preferCache, async () => {
throw Error('cache error');
});
helpers.expectCacheHit(c, true, 'cached');
helpers.expectCacheNoErrors(c);
expect(await cache.store.isset('-internal/cache')).to.be(true);
const d = await cache.store.get('-internal/cache');
helpers.expectDataHit(d, 'cached', c.etag);
helpers.expectDataNoErrors(d);
});
});
});
describe('when status=onlyCache', function () {
describe('and no existing cache', async function () {
it('should return a Miss on success', async function () {
const c = await cache.go('-internal', 'cache', Cacheism.Status.onlyCache, async () => {
return 'live';
});
helpers.expectCacheMiss(c, false, null);
helpers.expectCacheErrors(c, 'Missing cache', 1);
expect(c.created).to.eql(c.errorTime);
expect(await cache.store.isset('-internal/cache')).to.be(true);
const d = await cache.store.get('-internal/cache');
helpers.expectDataMiss(d, null, null);
helpers.expectDataErrors(d, 'Error: Missing cache', 1);
expect(d.created).to.eql(d.errorTime);
});
it('should return a Miss on error', async function () {
const c = await cache.go('-internal', 'cache', Cacheism.Status.onlyCache, async () => {
throw Error('cache error');
});
helpers.expectCacheMiss(c, false, null);
helpers.expectCacheErrors(c, 'Missing cache', 1);
expect(c.created).to.eql(c.errorTime);
expect(await cache.store.isset('-internal/cache')).to.be(true);
const d = await cache.store.get('-internal/cache');
helpers.expectDataMiss(d, null, null);
helpers.expectDataErrors(d, 'Error: Missing cache', 1);
expect(d.created).to.eql(d.errorTime);
});
});
describe('and having existing cache', async function () {
it('should return a Hit (cached value) on success', async function () {
mockdate.set('2000-11-22');
await cache.store.set(Cacheism.Data.fromResponse(
new Cacheism.Hit('-internal/cache', 'cached')
));
mockdate.reset();
const c = await cache.go('-internal', 'cache', Cacheism.Status.onlyCache, async () => {
return 'live';
});
helpers.expectCacheHit(c, true, 'cached');
helpers.expectCacheNoErrors(c);
expect(await cache.store.isset('-internal/cache')).to.be(true);
const d = await cache.store.get('-internal/cache');
helpers.expectDataHit(d, 'cached', c.etag);
helpers.expectDataNoErrors(d);
});
it('should return a Hit (cached value) on error', async function () {
mockdate.set('2000-11-22');
await cache.store.set(Cacheism.Data.fromResponse(
new Cacheism.Hit('-internal/cache', 'cached')
));
mockdate.reset();
const c = await cache.go('-internal', 'cache', Cacheism.Status.onlyCache, async () => {
return 'live';
});
helpers.expectCacheHit(c, true, 'cached');
helpers.expectCacheNoErrors(c);
expect(await cache.store.isset('-internal/cache')).to.be(true);
const d = await cache.store.get('-internal/cache');
helpers.expectDataHit(d, 'cached', c.etag);
helpers.expectDataNoErrors(d);
});
});
});
});
const expect = require('expect.js');
const mockdate = require('mockdate');
const Cacheism = require('../lib/cacheism');
function expectCacheHit(c, cached, data) {
expect(c).to.be.a(Cacheism.Hit);
expect(c).to.have.property('version', 3);
expect(c).to.have.property('cacheName', '-internal/cache');
expect(c).to.have.property('cached', cached);
expect(c).to.have.property('created');
expect(c.created).to.be.a(Date);
expect(c).to.have.property('data', data);
expect(c).to.have.property('etag');
}
function expectCacheMiss(c, cached, data) {
expect(c).to.be.a(Cacheism.Miss);
expect(c).to.have.property('version', 3);
expect(c).to.have.property('cacheName', '-internal/cache');
expect(c).to.have.property('cached', cached);
expect(c).to.have.property('created');
expect(c.created).to.be.a(Date);
expect(c).to.have.property('data', data);
expect(c).to.have.property('etag', null);
}
function expectCacheNoErrors(c) {
expect(c).to.have.property('error', null);
expect(c).to.have.property('errorTime', null);
expect(c).to.have.property('consecutiveErrors', 0);
}
function expectCacheErrors(c, error, errors) {
expect(c).to.have.property('error', error);
expect(c).to.have.property('errorTime');
expect(c.errorTime).to.be.a(Date);
expect(c).to.have.property('consecutiveErrors', errors);
}
function expectDataHit(d, data, etag) {
expect(d).to.be.a(Cacheism.Data);
expect(d).to.have.property('version', 3);
expect(d).to.have.property('type', Cacheism.Type.hit);
expect(d).to.have.property('created');
expect(d.created).to.be.a(Date);
expect(d).to.have.property('data', data);
expect(d).to.have.property('etag', etag);
}
function expectDataMiss(d, data, etag) {
expect(d).to.be.a(Cacheism.Data);
expect(d).to.have.property('version', 3);
expect(d).to.have.property('type', Cacheism.Type.miss);
expect(d).to.have.property('created');
expect(d.created).to.be.a(Date);
expect(d).to.have.property('data', data);
expect(d).to.have.property('etag', etag);
}
function expectDataNoErrors(d) {
expect(d).to.have.property('error', null);
expect(d).to.have.property('errorTime', null);
expect(d).to.have.property('consecutiveErrors', 0);
}
function expectDataErrors(d, error, errors) {
expect(d).to.have.property('error', error);
expect(d).to.have.property('errorTime');
expect(d.errorTime).to.be.a(Date);
expect(d).to.have.property('consecutiveErrors', errors);
}
module.exports = {
expectCacheHit,
expectCacheMiss,
expectCacheNoErrors,
expectCacheErrors,
expectDataHit,
expectDataMiss,
expectDataNoErrors,
expectDataErrors,
};
const expect = require('expect.js');
const mockdate = require('mockdate');
const Cacheism = require('../lib/cacheism');
const cache = new Cacheism(Cacheism.store.memory());
const helpers = require('./helpers');
describe.only('memory', function() {
beforeEach(function() {
// runs before each test in this block
cache.store.data = {};
});
it('should export as a function', function() {
expect(cache.go).to.be.a('function');
});
describe('when status=onlyFresh', async function () {
describe('and no existing cache', async function () {
it('should return a Hit (live value) on success', async function () {
const c = await cache.go('-internal', 'cache', Cacheism.Status.onlyFresh, async () => {
return 'live';
});
helpers.expectCacheHit(c, false, 'live');
helpers.expectCacheNoErrors(c);
expect(await cache.store.isset('-internal/cache')).to.be(true);
const d = await cache.store.get('-internal/cache');
helpers.expectDataHit(d, 'live', c.etag);
helpers.expectDataNoErrors(d);
});
it('should return a Miss on error', async function () {
let c, d, e;
for (e = 1; e < 3; e++) {
c = await cache.go('-internal', 'cache', Cacheism.Status.onlyFresh, async () => {
throw Error('cache error');
});
helpers.expectCacheMiss(c, false, null);
helpers.expectCacheErrors(c, 'Error: cache error', e);
expect(await cache.store.isset('-internal/cache')).to.be(true);
d = await cache.store.get('-internal/cache');
helpers.expectDataMiss(d, null);
helpers.expectDataErrors(d, 'Error: cache error', e);
}
});
});
describe('and having existing cache', async function () {
it('should return a Hit (live value) on success', async function () {
mockdate.set('2000-11-22');
await cache.store.set(Cacheism.Data.fromResponse(
new Cacheism.Hit('-internal/cache', 'cached')
));
mockdate.reset();
const c = await cache.go('-internal', 'cache', Cacheism.Status.onlyFresh, async () => {
return 'live';
});
helpers.expectCacheHit(c, false, 'live');
helpers.expectCacheNoErrors(c);
expect(await cache.store.isset('-internal/cache')).to.be(true);
const d = await cache.store.get('-internal/cache');
helpers.expectDataHit(d, 'live', c.etag);
helpers.expectDataNoErrors(d);
});
it('should return a Miss on error', async function () {
mockdate.set('2000-11-22');
await cache.store.set(Cacheism.Data.fromResponse(
new Cacheism.Hit('-internal/cache', 'cached')
));
mockdate.reset();
let c, d, e;
for (e = 1; e < 3; e++) {
c = await cache.go('-internal', 'cache', Cacheism.Status.onlyFresh, async () => {
throw Error('cache error');
});
helpers.expectCacheMiss(c, false, null);
helpers.expectCacheErrors(c, 'Error: cache error', e);
expect(await cache.store.isset('-internal/cache')).to.be(true);
d = await cache.store.get('-internal/cache');
helpers.expectDataMiss(d, null);
helpers.expectDataErrors(d, 'Error: cache error', e);
}
});
});
});
describe('when status=cacheOnFail', function () {
describe('and no existing cache', async function () {
it('should return a Hit (live value) on success', async function () {
const c = await cache.go('-internal', 'cache', Cacheism.Status.cacheOnFail, async () => {
return 'live';
});
helpers.expectCacheHit(c, false, 'live');
helpers.expectCacheNoErrors(c);
expect(await cache.store.isset('-internal/cache')).to.be(true);
const d = await cache.store.get('-internal/cache');
helpers.expectDataHit(d, 'live', c.etag);
helpers.expectDataNoErrors(d);
});
it('should return a Miss on error', async function () {
let c, d, e;
for (e = 1; e < 3; e++) {
c = await cache.go('-internal', 'cache', Cacheism.Status.cacheOnFail, async () => {
throw Error('cache error');
});
helpers.expectCacheMiss(c, false, null);
helpers.expectCacheErrors(c, 'Error: cache error', e);
expect(await cache.store.isset('-internal/cache')).to.be(true);
d = await cache.store.get('-internal/cache');
helpers.expectDataMiss(d, null, c.etag);
helpers.expectDataErrors(d, 'Error: cache error', e);
}
});
});
describe('and having existing cache', async function () {
it('should return a Hit (live value) on success', async function () {
mockdate.set('2000-11-22');
await cache.store.set(Cacheism.Data.fromResponse(
new Cacheism.Hit('-internal/cache', 'cached')
));
mockdate.reset();
const c = await cache.go('-internal', 'cache', Cacheism.Status.cacheOnFail, async () => {
return 'live';
});
helpers.expectCacheHit(c, false, 'live');
helpers.expectCacheNoErrors(c);
expect(await cache.store.isset('-internal/cache')).to.be(true);
const d = await cache.store.get('-internal/cache');
helpers.expectDataHit(d, 'live', c.etag);
helpers.expectDataNoErrors(d);
});
it('should return a Hit (cached value) on error', async function () {
mockdate.set('2000-11-22');
await cache.store.set(Cacheism.Data.fromResponse(
new Cacheism.Hit('-internal/cache', 'cached')
));
mockdate.reset();
let c, d, e;
for (e = 1; e < 3; e++) {
c = await cache.go('-internal', 'cache', Cacheism.Status.cacheOnFail, async () => {
throw Error('cache error');
});
helpers.expectCacheHit(c, true, 'cached');
helpers.expectCacheErrors(c, 'Error: cache error', e);
expect(await cache.store.isset('-internal/cache')).to.be(true);
d = await cache.store.get('-internal/cache');
helpers.expectDataHit(d, 'cached', c.etag);
helpers.expectDataErrors(d, 'Error: cache error', e);
}
});
});
});
describe('when status=preferCache', function () {
describe('and no existing cache', async function () {
it('should return a Hit (live value) on success', async function () {
const c = await cache.go('-internal', 'cache', Cacheism.Status.preferCache, async () => {
return 'live';
});
helpers.expectCacheHit(c, false, 'live');
helpers.expectCacheNoErrors(c);
expect(await cache.store.isset('-internal/cache')).to.be(true);
const d = await cache.store.get('-internal/cache');
helpers.expectDataHit(d, 'live', c.etag);
helpers.expectDataNoErrors(d);
});
it('should return a Miss on error', async function () {
let c, d, e;
for (e = 1; e < 3; e++) {
c = await cache.go('-internal', 'cache', Cacheism.Status.preferCache, async () => {
throw Error('cache error');
});
helpers.expectCacheMiss(c, false, null);
helpers.expectCacheErrors(c, 'Error: cache error', e);
expect(await cache.store.isset('-internal/cache')).to.be(true);
d = await cache.store.get('-internal/cache');
helpers.expectDataMiss(d, null, c.etag);
helpers.expectDataErrors(d, 'Error: cache error', e);
}
});
});
describe('and having existing cache', async function () {
it('should return a Hit (cached value) on success', async function () {
mockdate.set('2000-11-22');
await cache.store.set(Cacheism.Data.fromResponse(
new Cacheism.Hit('-internal/cache', 'cached')
));
mockdate.reset();
const c = await cache.go('-internal', 'cache', Cacheism.Status.preferCache, async () => {
return 'live';
});
helpers.expectCacheHit(c, true, 'cached');
helpers.expectCacheNoErrors(c);
expect(await cache.store.isset('-internal/cache')).to.be(true);
const d = await cache.store.get('-internal/cache');
helpers.expectDataHit(d, 'cached', c.etag);
helpers.expectDataNoErrors(d);
});
it('should return a Hit (cached value) on error', async function () {
mockdate.set('2000-11-22');
await cache.store.set(Cacheism.Data.fromResponse(
new Cacheism.Hit('-internal/cache', 'cached')
));
mockdate.reset();
let c, d, e;
for (e = 1; e < 3; e++) {
c = await cache.go('-internal', 'cache', Cacheism.Status.preferCache, async () => {
throw Error('cache error');
});
helpers.expectCacheHit(c, true, 'cached');
helpers.expectCacheNoErrors(c);
expect(await cache.store.isset('-internal/cache')).to.be(true);
d = await cache.store.get('-internal/cache');
helpers.expectDataHit(d, 'cached', c.etag);
helpers.expectDataNoErrors(d);
}
});
});
});
describe('when status=onlyCache', function () {
describe('and no existing cache', async function () {
it('should return a Miss on success', async function () {
const c = await cache.go('-internal', 'cache', Cacheism.Status.onlyCache, async () => {
return 'live';
});
helpers.expectCacheMiss(c, false, null);
helpers.expectCacheErrors(c, 'Error: Missing cache', 1);
expect(await cache.store.isset('-internal/cache')).to.be(true);
const d = await cache.store.get('-internal/cache');
helpers.expectDataMiss(d, null, null);
helpers.expectDataErrors(d, 'Error: Missing cache', 1);
});
it('should return a Miss on error', async function () {
let c, d, e;
for (e = 1; e < 3; e++) {
c = await cache.go('-internal', 'cache', Cacheism.Status.onlyCache, async () => {
throw Error('cache error');
});
helpers.expectCacheMiss(c, false, null);
helpers.expectCacheErrors(c, 'Error: Missing cache', e);
expect(await cache.store.isset('-internal/cache')).to.be(true);
d = await cache.store.get('-internal/cache');
helpers.expectDataMiss(d, null, null);
helpers.expectDataErrors(d, 'Error: Missing cache', e);
}
});
});
describe('and having existing cache', async function () {
it('should return a Hit (cached value) on success', async function () {
mockdate.set('2000-11-22');
await cache.store.set(Cacheism.Data.fromResponse(
new Cacheism.Hit('-internal/cache', 'cached')
));
mockdate.reset();
const c = await cache.go('-internal', 'cache', Cacheism.Status.onlyCache, async () => {
return 'live';
});
helpers.expectCacheHit(c, true, 'cached');
helpers.expectCacheNoErrors(c);
expect(await cache.store.isset('-internal/cache')).to.be(true);
const d = await cache.store.get('-internal/cache');
helpers.expectDataHit(d, 'cached', c.etag);
helpers.expectDataNoErrors(d);
});
it('should return a Hit (cached value) on error', async function () {
mockdate.set('2000-11-22');
await cache.store.set(Cacheism.Data.fromResponse(
new Cacheism.Hit('-internal/cache', 'cached')
));
mockdate.reset();
const c = await cache.go('-internal', 'cache', Cacheism.Status.onlyCache, async () => {
return 'live';
});
helpers.expectCacheHit(c, true, 'cached');
helpers.expectCacheNoErrors(c);
expect(await cache.store.isset('-internal/cache')).to.be(true);
const d = await cache.store.get('-internal/cache');
helpers.expectDataHit(d, 'cached', c.etag);
helpers.expectDataNoErrors(d);
});
});
});
});