Skip to content

Commit 001db67

Browse files
committed
Initial public version
1 parent 19e0d7f commit 001db67

File tree

9 files changed

+2506
-1
lines changed

9 files changed

+2506
-1
lines changed

README.md

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,110 @@
1-
# cacheous
1+
# cacheism
22
Simple caching library
3+
4+
## Overview
5+
6+
The goal of cacheism is to wrap an async function with caching logic where we
7+
can easily specify under what circumstances we want to return the cache or fetch
8+
the live data.
9+
10+
Your callback will get passed to it a Hit if there is an existing cache stored
11+
or a Miss if there is no existing cache.
12+
13+
```js
14+
const Cacheism = require('@andrewshell/cacheism');
15+
16+
const datadir = __dirname + '/data';
17+
const cache = new Cacheism(Cacheism.store.filesystem({ datadir }));
18+
19+
async function run() {
20+
let result = await cache.go('-internal', 'hoopla', Cacheism.Status.cacheOnFail, async (existing) => {
21+
if (Math.random() < 0.5) {
22+
throw Error('Death');
23+
}
24+
return { message: 'Hoopla!' };
25+
});
26+
27+
if (result.isHit) {
28+
console.dir(result.data);
29+
}
30+
31+
if (result.error) {
32+
console.error(result.error);
33+
}
34+
}
35+
36+
run().catch(err => console.error(err));
37+
```
38+
39+
## Statuses
40+
41+
### Only Fresh
42+
43+
The onlyFresh status is for times where we never want to use the cache, but we
44+
want to fetch the fresh data and store it in the cache for other requests.
45+
46+
### Cache on Fail
47+
48+
The cacheOnFail status is for times where we want to try to fetch fresh data,
49+
but if an error is thrown, use the cache if present.
50+
51+
### Prefer Cache
52+
53+
The preferCache status is for times where we want to use the cache if available
54+
and only fetch fresh if the cache is not available.
55+
56+
### Only Cache
57+
58+
The onlyCache status if for times where we don't want to attempt to fetch fresh
59+
data and only return the cache if present.
60+
61+
## Results
62+
63+
The cache.go function will always return either a Hit or a Miss object.
64+
65+
### Hit
66+
67+
A hit is returned when we have good data. The `cached` param will be `true` if
68+
the data was fetched from cache versus fresh data.
69+
70+
```
71+
Hit {
72+
version: 2,
73+
cacheName: '-internal/hoopla',
74+
cached: true,
75+
created: 2022-04-22T21:05:14.094Z,
76+
data: { message: 'Hoopla!' },
77+
error: Error: Death
78+
at /Users/andrewshell/code/personal/test-cacheism/index.js:8:15
79+
at Cacheism.go (/Users/andrewshell/code/personal/cacheism/lib/cacheism.js:29:30)
80+
at async run (/Users/andrewshell/code/personal/test-cacheism/index.js:7:13),
81+
etag: '"15-QcHvuZdyxCmLJ4zoYIPsP6pkNoM"',
82+
isHit: true,
83+
isMiss: false
84+
}
85+
```
86+
87+
In the case of Cache on Fail, the error param may be set which is the error
88+
thrown while fetching fresh data.
89+
90+
### Miss
91+
92+
A miss is returned when we don't have good data. For instance, if there wasn't
93+
cached data and an error was thrown while fetching fresh data. You'll also get a
94+
miss if you fetch with the onlyCache status and there isn't a cache.
95+
96+
```
97+
Miss {
98+
version: 2,
99+
cacheName: '-internal/hoopla',
100+
cached: false,
101+
created: 2022-04-22T21:30:56.275Z,
102+
data: null,
103+
error: Error: Missing cache
104+
at Cacheism.go (/Users/andrewshell/code/personal/cacheism/lib/cacheism.js:27:19)
105+
at async run (/Users/andrewshell/code/personal/test-cacheism/index.js:7:18),
106+
etag: null,
107+
isHit: false,
108+
isMiss: true
109+
}
110+
```

lib/cacheism.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
const common = require('./common');
2+
3+
function _sanitize(string) {
4+
return string.replaceAll(new RegExp('[^a-z0-9]+', 'ig'), '-');
5+
}
6+
7+
function Cacheism(store) {
8+
this.store = store;
9+
this.status = common.Status;
10+
}
11+
12+
Cacheism.prototype.go = async function (cacheDomain, cachePath, status, callback) {
13+
let response, name = this.cacheName(cacheDomain, cachePath);
14+
15+
try {
16+
17+
let existing = new common.Miss(name, new Error('Missing cache'));
18+
let hasCache = await this.store.isset(name);
19+
20+
if (hasCache) {
21+
existing = await this.store.get(name);
22+
}
23+
24+
if (status >= this.status.preferCache && hasCache) {
25+
response = existing;
26+
} else if (status === this.status.onlyCache) {
27+
throw new Error('Missing cache');
28+
} else {
29+
response = await callback(existing);
30+
if (!(response instanceof common.Hit)) {
31+
response = new common.Hit(name, response);
32+
}
33+
await this.store.set(response);
34+
}
35+
36+
} catch (err) {
37+
38+
if (status >= this.status.cacheOnFail && await this.store.isset(name)) {
39+
response = await this.store.get(name);
40+
response.error = err;
41+
} else {
42+
response = new common.Miss(name, err);
43+
}
44+
45+
}
46+
47+
Object.freeze(response);
48+
return response;
49+
}
50+
51+
Cacheism.prototype.cacheName = function (cacheDomain, cachePath) {
52+
return `${_sanitize(cacheDomain)}/${_sanitize(cachePath)}`;
53+
}
54+
55+
Cacheism.prototype.setStore = function (store) {
56+
this.store = store;
57+
}
58+
59+
Cacheism.prototype.hit = function (name, data, etag) {
60+
return new common.Hit(name, data, etag);
61+
}
62+
63+
Cacheism.prototype.miss = function (name, error) {
64+
return new common.Miss(name, error);
65+
}
66+
67+
Cacheism.Hit = common.Hit;
68+
Cacheism.Miss = common.Miss;
69+
Cacheism.Data = common.Data;
70+
Cacheism.Status = common.Status;
71+
72+
Cacheism.store = {
73+
filesystem: require('./store-filesystem'),
74+
memory: require('./store-memory'),
75+
};
76+
77+
module.exports = Cacheism;

lib/common.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
const generateEtag = require('etag');
2+
3+
class Status {}
4+
5+
Status.onlyFresh = 0;
6+
Status.cacheOnFail = 1;
7+
Status.preferCache = 2;
8+
Status.onlyCache = 3;
9+
10+
Object.freeze(Status);
11+
12+
class Hit {
13+
constructor(name, data, etag) {
14+
this.version = 2;
15+
this.cacheName = name;
16+
this.cached = false;
17+
this.created = new Date();
18+
this.data = data;
19+
this.error = null;
20+
this.etag = null == etag ? generateEtag(JSON.stringify(data)) : etag;
21+
this.isHit = true;
22+
this.isMiss = false;
23+
}
24+
}
25+
26+
class Miss {
27+
constructor(name, error) {
28+
this.version = 2;
29+
this.cacheName = name;
30+
this.cached = false;
31+
this.created = new Date();
32+
this.data = null;
33+
this.error = error;
34+
this.etag = null;
35+
this.isHit = false;
36+
this.isMiss = true;
37+
}
38+
}
39+
40+
class Data {
41+
constructor(version, name, created, data, etag) {
42+
this.version = version;
43+
this.cacheName = name;
44+
this.created = created;
45+
this.data = data;
46+
this.etag = etag;
47+
}
48+
49+
hit() {
50+
if (2 !== this.version) {
51+
throw new Error(`Unknown cache version number: ${this.version}`);
52+
}
53+
54+
const hit = new Hit(this.cacheName, this.data, this.etag);
55+
hit.cached = true;
56+
hit.created = this.created;
57+
58+
return hit;
59+
}
60+
61+
stringify() {
62+
return JSON.stringify({
63+
version: this.version,
64+
cacheName: this.cacheName,
65+
created: this.created,
66+
data: this.data,
67+
etag: this.etag,
68+
}, null, 2);
69+
}
70+
71+
static fromHit(hit) {
72+
return new Data(hit.version, hit.cacheName, hit.created, hit.data, hit.etag);
73+
}
74+
75+
static parse(value) {
76+
const parsed = JSON.parse(value);
77+
return new Data(parsed.version, parsed.cacheName, new Date(parsed.created), parsed.data, parsed.etag);
78+
}
79+
}
80+
81+
module.exports = { Hit, Miss, Data, Status };

lib/store-filesystem.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
const common = require('./common');
2+
const storeFilesystem = { };
3+
const path = require('path');
4+
const fs = require('fs');
5+
const fsPromises = require('fs/promises');
6+
7+
module.exports = function (config) {
8+
function _mkdir(dirpath) {
9+
if (false === fs.existsSync(dirpath)) {
10+
fs.mkdirSync(dirpath, { recursive: true });
11+
}
12+
}
13+
14+
_mkdir(config.datadir);
15+
16+
storeFilesystem.get = async (cacheName) => {
17+
const filename = path.resolve(config.datadir, `${cacheName}.json`);
18+
const data = await fsPromises.readFile(filename, 'utf8');
19+
return common.Data.parse(data).hit();
20+
}
21+
22+
storeFilesystem.set = async (hit) => {
23+
const filename = path.resolve(config.datadir, `${hit.cacheName}.json`);
24+
_mkdir(path.dirname(filename));
25+
await fsPromises.writeFile(filename, common.Data.fromHit(hit).stringify(), 'utf8');
26+
}
27+
28+
storeFilesystem.isset = async (cacheName) => {
29+
const filename = path.resolve(config.datadir, `${cacheName}.json`);
30+
return fs.existsSync(filename);
31+
}
32+
33+
storeFilesystem.unset = async (cacheName) => {
34+
const filename = path.resolve(config.datadir, `${cacheName}.json`);
35+
await fsPromises.rm(filename, { force: true });
36+
}
37+
38+
return storeFilesystem;
39+
}

lib/store-memory.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const common = require('./common');
2+
3+
module.exports = function (config) {
4+
const storeMemory = { data: {} };
5+
6+
storeMemory.get = async (cacheName) => {
7+
return common.Data.parse(storeMemory.data[cacheName]).hit();
8+
}
9+
10+
storeMemory.set = async (hit) => {
11+
storeMemory.data[hit.cacheName] = common.Data.fromHit(hit).stringify();
12+
}
13+
14+
storeMemory.isset = async (cacheName) => {
15+
return null != storeMemory.data[cacheName];
16+
}
17+
18+
storeMemory.unset = async (cacheName) => {
19+
delete storeMemory.data[cacheName];
20+
}
21+
22+
return storeMemory;
23+
}

0 commit comments

Comments
 (0)