Skip to content

Commit 65cdb59

Browse files
committed
major refactoring
No setTimeout anymore, now based on Date.now()
1 parent f9678e0 commit 65cdb59

File tree

5 files changed

+201
-91
lines changed

5 files changed

+201
-91
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,20 @@ Delete all cached resources. This will preserve the hit/miss counts for a cache.
140140

141141
Returns all the cache keys
142142

143+
### `cache.isCached(key: string): boolean`
144+
145+
#### Arguments
146+
147+
##### - `key: string`
148+
149+
Returns whether a cacheable is present and valid (i.e., did not time out).
150+
151+
#### Example
152+
153+
```ts
154+
const aIsCached = cache.isCached('a')
155+
```
156+
143157
### `Cacheables.key(...args: (string | number)[]): string`
144158

145159
A static helper function to easily build a key for a cache.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "cacheables",
3-
"version": "0.2.2",
3+
"version": "0.3.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",
@@ -21,7 +21,9 @@
2121
"browser",
2222
"cache",
2323
"in-memory",
24-
"typescript"
24+
"typescript",
25+
"cacheable",
26+
"cacheables"
2527
],
2628
"author": "Grischa Erbe <grischa.erbe@googlemail.com>",
2729
"license": "MIT",

src/index.ts

Lines changed: 96 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
//region Types
12
export interface CacheOptions {
23
/**
34
* Enables caching
@@ -12,7 +13,9 @@ export interface CacheOptions {
1213
*/
1314
logTiming?: boolean
1415
}
16+
//endregion
1517

18+
//region Cacheables
1619
/**
1720
* Provides a simple in-memory cache with automatic or manual invalidation.
1821
*/
@@ -27,58 +30,44 @@ export class Cacheables {
2730
this.logTiming = options?.logTiming ?? false
2831
}
2932

30-
private cache: Record<
31-
string,
32-
{
33-
timer?: ReturnType<typeof setTimeout>
34-
value: any
35-
misses: number
36-
hits: number
37-
}
38-
> = {}
39-
40-
private clearValue(key: string): void {
41-
if (this.cache[key] && this.cache[key]?.value) {
42-
delete this.cache[key]?.value
43-
if (this.log) Logger.logInvalidatingCache(key)
44-
}
45-
}
46-
47-
private clearTimeout(key: string): void {
48-
if (this.cache[key] && this.cache[key]?.timer) {
49-
clearTimeout(this.cache[key]?.timer as ReturnType<typeof setTimeout>)
50-
}
51-
}
33+
private cacheables: Record<string, Cacheable<any>> = {}
5234

5335
/**
54-
* Build a key by providing strings or numbers
36+
* Builds a key with the provided strings or numbers.
5537
* @param args
5638
*/
5739
public static key(...args: (string | number)[]): string {
5840
return args.join(':')
5941
}
6042

43+
/**
44+
* Deletes a cacheable.
45+
* @param key
46+
*/
6147
public delete(key: string): void {
62-
this.clearTimeout(key)
63-
this.clearValue(key)
48+
delete this.cacheables[key]
6449
}
6550

51+
/**
52+
* Clears the cache by deleting all cacheables.
53+
*/
6654
public clear(): void {
67-
Object.keys(this.cache).forEach(this.delete.bind(this))
55+
this.cacheables = {}
6856
}
6957

7058
/**
71-
* Returns whether a value is present for a certain key
59+
* Returns whether a cacheable is present and valid (i.e., did not time out).
7260
*/
7361
public isCached(key: string): boolean {
74-
return !!this.cache[key]?.value
62+
const cacheable = this.cacheables[key]
63+
return !(!cacheable || cacheable.timedOut)
7564
}
7665

7766
/**
7867
* Returns all the cache keys
7968
*/
8069
public keys(): string[] {
81-
return Object.keys(this.cache)
70+
return Object.keys(this.cacheables)
8271
}
8372

8473
/**
@@ -111,9 +100,14 @@ export class Cacheables {
111100
return resource()
112101
}
113102

114-
if (this.log) Logger.startLogTime(key)
103+
// Persist log settings as this could be a race condition
104+
const { logTiming, log } = this
105+
if (logTiming) Logger.logTime(key)
106+
115107
const result = await this.#cacheable(resource, key, timeout)
116-
if (this.log) Logger.stopLogTime(key)
108+
109+
if (logTiming) Logger.logTimeEnd(key)
110+
if (log) Logger.logStats(key, this.cacheables[key])
117111

118112
return result
119113
}
@@ -123,78 +117,96 @@ export class Cacheables {
123117
key: string,
124118
timeout?: number,
125119
): Promise<T> {
126-
const storedResource = this.cache[key]
127-
128-
if (storedResource === undefined) {
129-
const value = await resource()
130-
this.cache[key] = {
131-
value,
132-
hits: 0,
133-
misses: 1,
134-
timer: timeout
135-
? setTimeout(() => {
136-
this.clearValue(key)
137-
}, timeout)
138-
: undefined,
139-
}
140-
if (this.log) Logger.logNewCacheable(key)
141-
return value
142-
}
120+
const cacheable = this.cacheables[key] as Cacheable<T> | undefined
143121

144-
const hasRetrievedValue = storedResource.value !== undefined
145-
if (hasRetrievedValue) {
146-
storedResource.hits += 1
147-
if (this.log) Logger.logCacheHit(key, storedResource.hits)
148-
return storedResource.value as T
149-
} else {
150-
const value = await resource()
151-
this.clearTimeout(key)
152-
if (timeout) {
153-
storedResource.timer = setTimeout(() => {
154-
this.clearValue(key)
155-
}, timeout)
156-
}
157-
storedResource.value = value
158-
storedResource.misses += 1
159-
if (this.log) Logger.logCacheMiss(key, storedResource.misses)
160-
return value
122+
if (!cacheable) {
123+
this.cacheables[key] = await Cacheable.create(resource, timeout)
124+
return this.cacheables[key]?.value
161125
}
126+
127+
return await cacheable.touch(resource, timeout)
162128
}
163129
}
130+
//endregion
164131

165-
class Logger {
166-
static startLogTime(key: string): void {
167-
// eslint-disable-next-line no-console
168-
console.time(key)
132+
//region Cacheable
133+
/**
134+
* Helper class, can only be instantiated by calling its static
135+
* function `create`.
136+
*/
137+
class Cacheable<T> {
138+
private timingOutAt: number | undefined
139+
public hits = 0
140+
public misses = 1
141+
public value: T
142+
143+
/**
144+
* Cacheable Factory function. The only way to get a cacheable.
145+
* @param resource
146+
* @param timeout
147+
*/
148+
static async create<T>(resource: () => Promise<T>, timeout?: number) {
149+
const timesOutAt = timeout !== undefined ? Date.now() + timeout : undefined
150+
const value = await resource()
151+
return new Cacheable(value, timesOutAt)
169152
}
170153

171-
static stopLogTime(key: string): void {
172-
// eslint-disable-next-line no-console
173-
console.timeEnd(key)
154+
private constructor(value: T, timesOutAt?: number) {
155+
this.value = value
156+
this.timingOutAt = timesOutAt
174157
}
175158

176-
static logDisabled(): void {
177-
// eslint-disable-next-line no-console
178-
console.log('CACHE: Caching disabled')
159+
get timedOut(): boolean {
160+
if (this.timingOutAt === undefined) return false
161+
return this.timingOutAt < Date.now()
179162
}
180163

181-
static logCacheHit(key: string, hits: number): void {
182-
// eslint-disable-next-line no-console
183-
console.log(`CACHE HIT: "${key}" found, hits: ${hits}.`)
164+
/**
165+
* Get and set the value of the Cacheable.
166+
* Some tricky race are conditions going on here,
167+
* but this should behave as expected
168+
* @param resource
169+
* @param timeout
170+
*/
171+
async touch(resource: () => Promise<T>, timeout?: number): Promise<T> {
172+
if (!this.timedOut) {
173+
this.hits += 1
174+
this.timingOutAt =
175+
timeout !== undefined ? Date.now() + timeout : undefined
176+
return this.value
177+
}
178+
this.timingOutAt = timeout !== undefined ? Date.now() + timeout : undefined
179+
this.value = await resource()
180+
this.misses += 1
181+
return this.value
184182
}
183+
}
184+
//endregion
185185

186-
static logCacheMiss(key: string, misses: number): void {
186+
//region Logger
187+
/**
188+
* Logger class with static logging functions.
189+
*/
190+
class Logger {
191+
static logTime(key: string): void {
187192
// eslint-disable-next-line no-console
188-
console.log(`CACHE MISS: "${key}" has no value, misses: ${misses}.`)
193+
console.time(key)
189194
}
190195

191-
static logNewCacheable(key: string): void {
196+
static logTimeEnd(key: string): void {
192197
// eslint-disable-next-line no-console
193-
console.log(`CACHE MISS: "${key}" not in cache yet, caching.`)
198+
console.timeEnd(key)
194199
}
195200

196-
static logInvalidatingCache(key: string): void {
201+
static logDisabled(): void {
197202
// eslint-disable-next-line no-console
198-
console.log(`CACHE INVALIDATED: "${key}" invalidated.`)
203+
console.log('CACHE: Caching disabled')
204+
}
205+
206+
static logStats(key: string, cacheable: Cacheable<any> | undefined) {
207+
if (!cacheable) return
208+
const { hits, misses } = cacheable
209+
console.log(`Cacheable "${key}": hits: ${hits}, misses: ${misses}`)
199210
}
200211
}
212+
//endregion

0 commit comments

Comments
 (0)