Skip to content

Commit aeb0976

Browse files
committed
major refactoring
Implemented cache policies
1 parent 6f998e1 commit aeb0976

File tree

5 files changed

+474
-196
lines changed

5 files changed

+474
-196
lines changed

README.md

Lines changed: 166 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,47 @@
44
![Language](https://img.shields.io/github/languages/top/grischaerbe/cacheables)
55
![Build](https://img.shields.io/github/workflow/status/grischaerbe/cacheables/Node.js%20Package)
66

7-
A simple in-memory cache with automatic or manual cache invalidation and elegant syntax written in Typescript.
7+
A simple in-memory cache with support of different cache policies and elegant syntax written in Typescript.
88

9-
- Elegant syntax: Wrap existing API calls in the `cacheable` method, assign a cache key and optionally a maxAge to save some of those precious API calls.
10-
- Written in Typescript.
11-
- Integrated Logs: Check on the timing of your API calls.
9+
- Elegant syntax: **Wrap existing API calls** to save some of those precious API calls.
10+
- **Fully typed results**. No type casting required.
11+
- Supports different **cache policies**.
12+
- Written in **Typescript**.
13+
- **Integrated Logs**: Check on the timing of your API calls.
1214
- Helper function to build cache keys.
1315
- Works in the browser and Node.js.
14-
- No dependencies.
16+
- **No dependencies**.
1517
- Extensively tested.
1618

19+
```ts
20+
// without caching
21+
fetch('https://some-url.com/api')
22+
23+
// with caching
24+
cache.cacheable(() => fetch('https://some-url.com/api'), 'key')
25+
```
26+
27+
* [Installation](#installation)
28+
* [Quickstart](#quickstart)
29+
* [Usage](#usage)
30+
* [API](#api)
31+
* [new Cacheables(options?): Cacheables](#new-cacheablesoptions-cacheables)
32+
* [cache.cacheable(resource, key, options?): Promise<T>](#cachecacheableresource-key-options-promiset)
33+
* [cache.delete(key: string): void](#cachedeletekey-string-void)
34+
* [cache.clear(): void](#cacheclear-void)
35+
* [cache.keys(): string[]](#cachekeys-string)
36+
* [cache.isCached(key: string): boolean](#cacheiscachedkey-string-boolean)
37+
* [Cacheables.key(...args: (string | number)[]): string](#cacheableskeyargs-string--number-string)
38+
* [Cache Policies](#cache-policies)
39+
* [Cache Only](#cache-only)
40+
* [Network Only](#network-only)
41+
* [Network Only – Non Concurrent](#network-only--non-concurrent)
42+
* [Max Age](#max-age)
43+
* [Stale While Revalidate](#stale-while-revalidate)
44+
* [Cache Policy Composition](#cache-policy-composition)
45+
* [In Progress](#in-progress)
46+
* [License](#license)
47+
1748
## Installation
1849

1950
```bash
@@ -38,39 +69,35 @@ const cache = new Cacheables({
3869
log: true
3970
})
4071

41-
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
42-
43-
// Use the method `cacheable` to both set and get from the cache
44-
// Here we set up a method `getWeatherData` that returns a Promise
45-
// resolving our weather data. Depending on the maxAge `cacheable`
46-
// returns the remotely fetched resource or the cache value.
47-
// It's best practice to generate a single
48-
// source of truth to our cached remote resource.
49-
export const getWeatherData = () => cache.cacheable(() => fetch(apiUrl), 'weather', 5e3)
72+
// Wrap the existing API call `fetch(apiUrl)` and assign a cache
73+
// key `weather` to it. This example uses the cache policy 'max-age'
74+
// which invalidates the cache after a certain time.
75+
// The method returns a fully typed Promise just like `fetch(apiUrl)`
76+
// would but with the benefit of caching the result.
77+
const getWeatherData = () => cache.cacheable(() => fetch(apiUrl), 'weather', {
78+
cachePolicy: 'max-age',
79+
maxAge: 5e3
80+
})
5081

5182
// Fetch some fresh weather data and store it in our cache.
5283
const weatherData = await getWeatherData()
5384

54-
// 3 seconds later
55-
await wait (3e3)
85+
/** 3 seconds later **/
5686

57-
// In this case the cached weather data is returned as the
58-
// maxAge of 5 seconds probably did not yet expire.
59-
// Please be aware that the timestamp that maxAge is checked
60-
// against is set **before** the resource is fetched.
87+
// The cached weather data is returned as the
88+
// maxAge of 5 seconds did not yet expire.
6189
const cachedWeatherData = await getWeatherData()
6290

63-
// Another 3 seconds later
64-
await wait (3e3)
91+
/** Another 3 seconds later **/
6592

66-
// Now that the maxAge of cacheable `weather` is
67-
// expired, the resource will be refetched and stored in our cache.
93+
// Now that the maxAge is expired, the resource
94+
// will be fetched and stored in our cache.
6895
const freshWeatherData = await getWeatherData()
6996
```
7097

7198
`cacheable` serves both as the getter and setter. This method will return a cached resource if available or use the provided argument `resource` to fill the cache and return a value.
7299

73-
> Be aware that there is no exclusive getter as the Promise provided by the first argument to `cacheable` is used to infer the return type of the cached resource.
100+
> Be aware that there is no exclusive cache getter (like `cache.get('key)`). This is by design as the Promise provided by the first argument to `cacheable` is used to infer the return type of the cached resource.
74101
75102
## API
76103

@@ -85,8 +112,8 @@ const freshWeatherData = await getWeatherData()
85112
```ts
86113
interface CacheOptions {
87114
enabled?: boolean // Enable/disable the cache, can be set anytime, default: true.
88-
log?: boolean // Log hits and misses to the cache, default: false.
89-
logTiming?: boolean // Log the timing of cache hits/misses and returns, default: false.
115+
log?: boolean // Log hits to the cache, default: false.
116+
logTiming?: boolean // Log the timing, default: false.
90117
}
91118
```
92119

@@ -100,32 +127,45 @@ const cache = new Cacheables({
100127
})
101128
```
102129

103-
### `cache.cacheable(resource, key, maxAge?): Promise<T>`
130+
### `cache.cacheable(resource, key, options?): Promise<T>`
104131

105-
- If a resource exists in the cache (determined by the presence of a value with key `key`) `cacheable` returns the cached resource.
106-
- If there's no resource in the cache, the provided argument `resource` will be used to store a value with key `key` and the value is returned.
132+
- If a resource exists in the cache (determined by the presence of a value with key `key`) `cacheable` decides on returning a cache based on the provided cache policy.
133+
- If there's no resource in the cache, the provided `resource` will be called and used to store a cache value with key `key` and the value is returned.
107134

108135
#### Arguments
109136

110137
##### - `resource: () => Promise<T>`
111138

112-
A function that returns a `Promise<T>`.
139+
A function that returns a `Promise<T>`.
113140

114141
##### - `key: string`
115142

116-
A key to store the cache at.
143+
A key to store the cache at.
144+
See [Cacheables.key()](#cacheableskeyargs-string--number-string) for a safe and easy way to generate unique keys.
145+
146+
##### - `options?: CacheableOptions` (optional)
117147

118-
##### - `maxAge?: number` (optional)
148+
An object defining the cache policy and possibly other options in the future.
149+
The default cache policy is `cache-only`.
150+
See [Cache Policies](#cache-policies).
119151

120-
A maxAge in milliseconds after which the cache will be treated as invalid.
152+
```ts
153+
type CacheableOptions = {
154+
cachePolicy: 'cache-only' | 'network-only-non-concurrent' | 'network-only' | 'max-age' | 'stale-while-revalidate' // See cache policies for details
155+
maxAge?: number // Required if cache policy is `max-age`
156+
}
157+
```
121158
122159
#### Example
123160
124161
```ts
125-
const apiResponse = await cache.cacheable(
162+
const cachedApiResponse = await cache.cacheable(
126163
() => fetch('https://github.com/'),
127164
'key',
128-
60e3
165+
{
166+
cachePolicy: 'max-age',
167+
maxAge: 10e3
168+
}
129169
)
130170
```
131171

@@ -135,7 +175,7 @@ const apiResponse = await cache.cacheable(
135175

136176
##### - `key: string`
137177

138-
Delete a cache for a certain key. This will preserve the hit/miss counts for a cache.
178+
Delete a cache for a certain key.
139179

140180
#### Example
141181

@@ -145,46 +185,124 @@ cache.delete('key')
145185

146186
### `cache.clear(): void`
147187

148-
Delete all cached resources. This will preserve the hit/miss counts for a cache.
188+
Delete all cached resources.
149189

150190
### `cache.keys(): string[]`
151191

152192
Returns all the cache keys
153193

154-
### `cache.isCached(key: string, maxAge?: number): boolean`
194+
### `cache.isCached(key: string): boolean`
155195

156196
#### Arguments
157197

158198
##### - `key: string`
159199

160-
Returns whether a cacheable is present and valid (i.e., did not time out).
161-
162-
##### - `maxAge?: number` (optional)
163-
164-
A maxAge in milliseconds after which the cache will be treated as invalid.
200+
Returns whether a cacheable is present for a certain key.
165201

166202
#### Example
167203

168204
```ts
169-
const aIsCached = cache.isCached('a', 100)
205+
const aIsCached = cache.isCached('a')
170206
```
171207

172208
### `Cacheables.key(...args: (string | number)[]): string`
173209

174-
A static helper function to easily build a key for a cache.
210+
A static helper function to easily build safe and consistent cache keys.
175211

176212
#### Example
177213

178214
```ts
179-
const user = Cacheables.key('user', id)
215+
const id = '5d3c5be6-2da4-11ec-8d3d-0242ac130003'
216+
console.log(Cacheables.key('user', id))
217+
// 'user:5d3c5be6-2da4-11ec-8d3d-0242ac130003'
218+
```
219+
220+
## Cache Policies
221+
222+
*Cacheables* comes with multiple cache policies.
223+
Each policy has different behaviour when it comes to preheating the cache (i.e. the first time it is requested) and balancing network requests.
224+
225+
| Cache Policy | Behaviour |
226+
|-------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
227+
| `cache-only` (default) | All requests should return a value from the cache. |
228+
| `network-only` | All requests should be handled by the network.<br>Simultaneous requests trigger simultaneous network requests. |
229+
| `network-only-non-concurrent` | All requests should be handled by the network but no concurrent network requests are allowed.<br>All requests made in the timeframe of a network request are resolved once that is finished. |
230+
| `max-age` | All requests should be checked against max-age.<br>If max-age is expired, a network request is triggered.<br>All requests made in the timeframe of a network request are resolved once that is finished. |
231+
| `stale-while-revalidate` | All requests immediately return a cached value.<br>If no network request is running, a network request is triggered, 'silently' updating the cache in the background.<br>After the network request finished, subsequent requests will receive the updated cached value. |
232+
233+
### Cache Only (default)
234+
235+
The default and simplest cache policy. If there is a cache, return it.
236+
If there is no cache yet, all calls will be resolved by the first network request (i.e. non-concurrent).
237+
238+
##### Example
239+
```ts
240+
cache.cacheable(() => fetch(url), 'a', { cachePolicy: 'cache-only' })
241+
```
242+
243+
### Network Only
244+
245+
The opposite of `cache-only`.
246+
Simultaneous requests trigger simultaneous network requests.
247+
248+
##### Example
249+
```ts
250+
cache.cacheable(() => fetch(url), 'a', { cachePolicy: 'network-only' })
251+
```
252+
253+
### Network Only – Non Concurrent
254+
255+
A version of `network-only` but only one network request is running at any point in time.
256+
All requests should be handled by the network but no concurrent network requests are allowed. All requests made in the timeframe of a network request are resolved once that is finished.
257+
258+
##### Example
259+
```ts
260+
cache.cacheable(() => fetch(url), 'a', { cachePolicy: 'network-only-non-concurrent' })
261+
```
262+
263+
### Max Age
264+
265+
The cache policy `max-age` defines after what time a cached value is treated as invalid.
266+
All requests should be checked against max-age. If max-age is expired, a network request is triggered. All requests made in the timeframe of a network request are resolved once that is finished.
267+
268+
##### Example
269+
```ts
270+
// Trigger a network request if the cached value is older than 1 second.
271+
cache.cacheable(() => fetch(url), 'a', {
272+
cachePolicy: 'max-age',
273+
maxAge: 1000
274+
})
275+
```
276+
277+
### Stale While Revalidate
278+
279+
The cache policy `stale-while-revalidate` will return a cached value immediately and – if there is no network request already running – trigger a network request to 'silently' update the cache in the background.
280+
281+
##### Example
282+
```ts
283+
// If there is a cache, return it but 'silently' update the cache.
284+
cache.cacheable(() => fetch(url), 'a', { cachePolicy: 'stale-while-revalidate'})
285+
```
286+
287+
### Cache Policy Composition
288+
A single cacheable can be requested with different cache policies at any time.
289+
290+
#### Example
291+
```ts
292+
// If there is a cache, return it.
293+
cache.cacheable(() => fetch(url), 'a', { cachePolicy: 'cache-only' })
294+
295+
// If there is a cache, return it but 'silently' update the cache.
296+
cache.cacheable(() => fetch(url), 'a', { cachePolicy: 'stale-while-revalidate' })
180297
```
181298

182299
## In Progress
183300

184301
PRs welcome
185302

186-
- [ ] Cache invalidation callback
303+
- [ ] ~~Cache invalidation callback~~
187304
- [ ] Adapters to store cache not only in memory
305+
- [X] Cache policies
188306
- [X] Tests
189307

190308
## License

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "cacheables",
3-
"version": "1.0.0",
3+
"version": "1.1.0",
44
"description": "A simple in-memory cache written in Typescript with automatic cache invalidation and an elegant syntax.",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

0 commit comments

Comments
 (0)