Skip to content

Commit fd3bb5b

Browse files
committed
feat(express): add key generator
should close #9
1 parent 3aa3b67 commit fd3bb5b

3 files changed

Lines changed: 122 additions & 67 deletions

File tree

README.md

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@
2121
</p>
2222

2323
Stalier is cache strategy middleware controled by your frontend by using `x-stalier-cache-control` header.
24-
It is an advanced middleware that can be used to cache your backend responses with support for stale-while-revalidate strategy.
25-
Stalier will act as a proxy to your routes and cache the response if front-end asks for it.
24+
This means that instead of your backend sending `Cache-Control` header to your browser for your browser to cache the returned data, your frontend will send the header `X-Stalier-Cache-Control` to your backend for it to cache the returned data from your source of truth.
25+
It is an advanced middleware that with support for stale-while-revalidate strategy.
26+
Stalier will act as a proxy for your routes and cache the response if front-end asks for it.
2627
Since it's embedded in your backend, it's much more efficient than using a separate proxy.
27-
It implements part of RFC7234 and RFC5861 but on the backend. It does not use `cache-control` header to not allow the browser to interfere with it.
28-
If you want both your browser to cache the response the response and if not present cache on the backend, you can use `x-stalier-cache-control` and `cache-control` headers at the same time.
28+
It implements part of RFC7234 and RFC5861 but on the backend. It does not use `cache-control` since the cache is controlled by the frontend.
29+
If you want both your browser and backend to cache the responses, you can use `x-stalier-cache-control` for requests and `cache-control` for responses at the same time.
2930

3031
## INSTALL
3132

@@ -58,6 +59,34 @@ const app = express();
5859
app.use(stalier({ cachePrefix: 'test', cacheClient: redisCache }));
5960
```
6061

62+
## Stalier Options
63+
64+
```typescript
65+
type StalierMiddlewareOptions = {
66+
/**
67+
* name of the upstream application
68+
*/
69+
appName: string;
70+
/**
71+
* client to use for caching
72+
* should have an async `get` method and `set` method
73+
*/
74+
cacheClient: CacheClient;
75+
/**
76+
* function to generate a cache key per request
77+
* Use a custom one to handle per user caching
78+
* @default `<appName>-<HTTP Verb>-<path>`
79+
*/
80+
cacheKeyGen?: (req: Request) => string;
81+
/**
82+
* logger to use for logging
83+
* should have a log, warn and error method that takes a message parameter
84+
* @default `console`
85+
*/
86+
logger?: Logger;
87+
};
88+
```
89+
6190
## Header `x-stalier-cache-control` params
6291

6392
Stalier is using the `x-stalier-cache-control` header to control the cache behaviour.

src/express.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ describe('stalier-express', () => {
1717

1818
beforeAll(() => {
1919
app = express();
20-
app.use(stalier({ cachePrefix: 'test', cacheClient: fakeCache }));
20+
app.use(stalier({ appName: 'test', cacheClient: fakeCache }));
2121
app.get('/string', async (req, res) => {
2222
await new Promise(resolve => setTimeout(resolve, Math.floor(Math.random() * 100)));
2323
res.send('hello');

src/express.ts

Lines changed: 88 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { RequestHandler } from 'express';
1+
import type { RequestHandler, Request } from 'express';
22
import type { OutgoingHttpHeaders } from 'http';
33
import { withStaleWhileRevalidate } from './stalier';
44
import { StalierOptions } from './stalier.types';
@@ -8,71 +8,97 @@ export const STALIER_HEADER_KEY = 'X-Stalier-Cache-Control';
88
const MATCH_HEADER = /s-maxage=([0-9]+)(\s*,\s*(stale-while-revalidate=([0-9]+)))?/;
99

1010
export type StalierMiddlewareOptions = {
11-
cachePrefix: string;
11+
/**
12+
* name of the upstream application
13+
*/
14+
appName: string;
15+
/**
16+
* client to use for caching
17+
*/
1218
cacheClient: StalierOptions['cacheClient'];
19+
/**
20+
* function to generate a cache key per request
21+
* Use a custom one to handle per user caching
22+
* @default `<appName>-<HTTP Verb>-<path>`
23+
*/
24+
cacheKeyGen?: (req: Request) => string;
25+
/**
26+
* logger to use for logging
27+
* @default `console`
28+
*/
1329
logger?: StalierOptions['logger'];
1430
};
1531

16-
export const stalier: (options: StalierMiddlewareOptions) => RequestHandler =
17-
({ cachePrefix, cacheClient, logger = console }) =>
18-
(req, res, next) => {
19-
const options = req.get(STALIER_HEADER_KEY);
20-
if (options) {
21-
const matched = options.match(MATCH_HEADER);
22-
if (matched) {
23-
const maxAge = parseInt(matched[1]);
24-
const staleWhileRevalidate = matched[4] ? parseInt(matched[4]) : 0;
25-
const send = res.send.bind(res);
26-
const freshResult = new Promise<{ data: Buffer | string; statusCode: number; headers: OutgoingHttpHeaders }>(
27-
(resolve, reject) => {
28-
res.send = function (data) {
29-
if (this.statusCode >= 200 && this.statusCode <= 300) {
30-
resolve({ data, statusCode: this.statusCode, headers: this.getHeaders() });
31-
} else {
32-
reject({ data, statusCode: this.statusCode, headers: this.getHeaders() });
33-
}
34-
return this;
35-
};
36-
},
37-
);
38-
const result = withStaleWhileRevalidate(
39-
() => {
40-
next();
41-
return freshResult;
42-
},
43-
{
44-
maxAge,
45-
staleWhileRevalidate,
46-
cacheKey: `${cachePrefix}-${req.method}-${req.originalUrl}`,
47-
cacheClient,
48-
logger,
49-
},
50-
);
51-
return result
52-
.then(({ data, status }) => {
53-
res.status(data.statusCode);
54-
if (data.headers) {
55-
res.set(data.headers);
56-
}
57-
res.set('X-Cache-Status', status);
58-
send(data.data);
59-
})
60-
.catch(reason => {
61-
if (reason.data && reason.statusCode) {
62-
res.status(reason.statusCode);
63-
if (reason.headers) {
64-
res.set(reason.headers);
65-
}
66-
res.set('X-Cache-Status', 'NO_CACHE');
67-
send(reason.data);
32+
/**
33+
* default cache key generator
34+
*/
35+
const defaultKeyGenerator = (name: string) => (req: Request) => {
36+
return `${name}-${req.method}-${req.originalUrl}`;
37+
};
38+
39+
/**
40+
* middleware to cache responses
41+
* @param options - options for stalier
42+
*/
43+
export const stalier: (options: StalierMiddlewareOptions) => RequestHandler = options => (req, res, next) => {
44+
const { cacheClient, cacheKeyGen = defaultKeyGenerator(options.appName), logger = console } = options;
45+
const cacheControl = req.get(STALIER_HEADER_KEY);
46+
if (cacheControl) {
47+
const matched = cacheControl.match(MATCH_HEADER);
48+
if (matched) {
49+
const maxAge = parseInt(matched[1]);
50+
const staleWhileRevalidate = matched[4] ? parseInt(matched[4]) : 0;
51+
const send = res.send.bind(res);
52+
const freshResult = new Promise<{ data: Buffer | string; statusCode: number; headers: OutgoingHttpHeaders }>(
53+
(resolve, reject) => {
54+
res.send = function (data) {
55+
if (this.statusCode >= 200 && this.statusCode <= 300) {
56+
resolve({ data, statusCode: this.statusCode, headers: this.getHeaders() });
6857
} else {
69-
res.status(500);
70-
res.set('X-Cache-Status', 'NO_CACHE');
71-
send(JSON.stringify({ error: 'Unexpected error' }));
58+
reject({ data, statusCode: this.statusCode, headers: this.getHeaders() });
7259
}
73-
});
74-
}
60+
return this;
61+
};
62+
},
63+
);
64+
const result = withStaleWhileRevalidate(
65+
() => {
66+
next();
67+
return freshResult;
68+
},
69+
{
70+
maxAge,
71+
staleWhileRevalidate,
72+
cacheKey: cacheKeyGen(req),
73+
cacheClient,
74+
logger,
75+
},
76+
);
77+
return result
78+
.then(({ data, status }) => {
79+
res.status(data.statusCode);
80+
if (data.headers) {
81+
res.set(data.headers);
82+
}
83+
res.set('X-Cache-Status', status);
84+
send(data.data);
85+
})
86+
.catch(reason => {
87+
if (reason.data && reason.statusCode) {
88+
res.status(reason.statusCode);
89+
if (reason.headers) {
90+
res.set(reason.headers);
91+
}
92+
res.set('X-Cache-Status', 'NO_CACHE');
93+
send(reason.data);
94+
} else {
95+
res.status(500);
96+
res.set('X-Cache-Status', 'NO_CACHE');
97+
send(JSON.stringify({ error: 'unexpected error while processing cache' }));
98+
}
99+
});
75100
}
76-
res.set('X-Cache-Status', 'NO_CACHE');
77-
next();
78-
};
101+
}
102+
res.set('X-Cache-Status', 'NO_CACHE');
103+
next();
104+
};

0 commit comments

Comments
 (0)