Skip to content

Commit fda2093

Browse files
authored
Merge pull request #2 from MakerXStudio/add-clear-cache
feat: add clear cache
2 parents 5cbe4c1 + 3a29de5 commit fda2093

10 files changed

Lines changed: 255 additions & 1 deletion

.env.sample

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
BUCKET_NAME=
2+
AWS_REGION=
3+
ACCESS_KEY=
4+
SECRET=

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,5 @@ coverage
4040
# Website & Code docs generation
4141
code-docs/
4242
out/
43+
44+
.env

jest.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import type { Config } from '@jest/types'
2+
import dotenv from 'dotenv'
3+
dotenv.config()
24

35
const config: Config.InitialOptions = {
46
preset: 'ts-jest',

package-lock.json

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

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"@typescript-eslint/eslint-plugin": "^6.15.0",
6060
"@typescript-eslint/parser": "^6.15.0",
6161
"conventional-changelog-conventionalcommits": "^7.0.2",
62+
"dotenv": "^16.4.5",
6263
"eslint": "^8.56.0",
6364
"eslint-config-prettier": "^9.1.0",
6465
"eslint-plugin-node": "^11.1.0",

src/cache.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,10 @@ export interface ObjectCache {
5757
* @param mimeType Optional mime type of the data; default = `application/json` or `application/octet-stream` depending on if the data is binary or JSON.
5858
*/
5959
putBinary(cacheKey: string, data: Uint8Array, mimeType?: string): Promise<void>
60+
61+
/**
62+
* Clear the cache value for the given cache key
63+
* @param cacheKey A unique key that identifies the cached value
64+
*/
65+
clearCache(cacheKey: string): void
6066
}

src/fileSystemObjectCache.spec.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, beforeEach, test } from '@jest/globals'
22
import path from 'node:path'
33
import * as fs from 'node:fs/promises'
4+
import * as fsSync from 'node:fs'
45
import { FileSystemObjectCache } from './fileSystemObjectCache'
56

67
const testDir = path.join(__dirname, '..', '.cache')
@@ -30,6 +31,37 @@ describe('FileSystemObjectCache', () => {
3031
`)
3132
})
3233

34+
test('Clear json file cache', async () => {
35+
const cache = new FileSystemObjectCache(testDir, true)
36+
37+
await cache.put('test', { test: 1 })
38+
39+
const data = await fs.readFile(path.join(testDir, 'test.json'), { encoding: 'utf-8' })
40+
expect(data).toMatchInlineSnapshot(`
41+
"{
42+
"test": 1
43+
}"
44+
`)
45+
46+
await cache.clearCache('test')
47+
const fileExist = fsSync.existsSync(path.join(testDir, 'test.json'))
48+
expect(fileExist).toBe(false)
49+
})
50+
51+
test('Clear binary cache', async () => {
52+
const cache = new FileSystemObjectCache(testDir, true)
53+
const fileData = Uint8Array.from(atob('test'), (c) => c.charCodeAt(0))
54+
55+
await cache.put('test', fileData)
56+
57+
const data = await fs.readFile(path.join(testDir, 'test'))
58+
expect(data.compare(fileData)).toBe(0)
59+
60+
await cache.clearCache('test')
61+
const fileExist = fsSync.existsSync(path.join(testDir, 'test'))
62+
expect(fileExist).toBe(false)
63+
})
64+
3365
test("Get an object from cache if it didn't exist", async () => {
3466
const cache = new FileSystemObjectCache(testDir, true)
3567

src/fileSystemObjectCache.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,17 @@ export class FileSystemObjectCache implements ObjectCache {
2323
}
2424
}
2525

26+
/**
27+
* Clear the cache value for te given cache key
28+
* @param cacheKey A unique key that identifies the cached value
29+
*/
30+
async clearCache(cacheKey: string): Promise<void> {
31+
if (fsSync.existsSync(path.join(this.cacheDirectory, cacheKey))) await fs.unlink(path.join(this.cacheDirectory, cacheKey))
32+
33+
if (fsSync.existsSync(path.join(this.cacheDirectory, `${cacheKey}.json`)))
34+
await fs.unlink(path.join(this.cacheDirectory, `${cacheKey}.json`))
35+
}
36+
2637
/** Adds the given value to the cache for the given cache key
2738
* @param cacheKey A unique key that identifies the cached value
2839
* @param data The data to cache

src/s3ObjectCache.spec.ts

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import { GetObjectCommand, HeadObjectCommand, S3Client } from '@aws-sdk/client-s3'
2+
import { describe, test } from '@jest/globals'
3+
import { S3ObjectCache } from './s3ObjectCache'
4+
5+
const maybe = process.env.BUCKET_NAME && process.env.AWS_REGION && process.env.ACCESS_KEY && process.env.SECRET ? describe : describe.skip
6+
7+
// eslint-disable-next-line @typescript-eslint/no-var-requires
8+
const zlib = require('zlib')
9+
10+
maybe('S3ObjectCache', () => {
11+
const s3Client = new S3Client({
12+
region: process.env.AWS_REGION,
13+
credentials: {
14+
accessKeyId: process.env.ACCESS_KEY!,
15+
secretAccessKey: process.env.SECRET!,
16+
},
17+
})
18+
const bucketName = process.env.BUCKET_NAME
19+
20+
const checkFileExist = async (key: string) => {
21+
const bucketAndKey = {
22+
Bucket: bucketName,
23+
Key: key,
24+
}
25+
26+
const fileExist = await s3Client
27+
.send(new HeadObjectCommand(bucketAndKey))
28+
.then(() => true)
29+
.catch(() => false)
30+
31+
return fileExist
32+
}
33+
34+
test('Puts an object in cache as json', async () => {
35+
const cache = new S3ObjectCache(s3Client, bucketName!)
36+
await cache.put('test', { test: 1 })
37+
38+
const bucketAndKey = {
39+
Bucket: bucketName,
40+
Key: 'test.json.gz',
41+
}
42+
43+
const data = await s3Client
44+
.send(new GetObjectCommand(bucketAndKey))
45+
.then(async (x) => ({ Body: await x.Body!.transformToByteArray(), LastModified: x.LastModified, ContentType: x.ContentType }))
46+
.catch(() => undefined)
47+
48+
const json = data ? zlib.gunzipSync(data.Body!).toString('utf-8') : undefined
49+
expect(json).toMatchInlineSnapshot(`
50+
"{
51+
"test": 1
52+
}"
53+
`)
54+
})
55+
56+
test('Clear json file cache', async () => {
57+
const cache = new S3ObjectCache(s3Client, bucketName!)
58+
const key = 'test.json.gz'
59+
60+
await cache.put('test', { test: 1 })
61+
let fileExist = await checkFileExist(key)
62+
expect(fileExist).toBe(true)
63+
64+
await cache.clearCache('test')
65+
fileExist = await checkFileExist(key)
66+
expect(fileExist).toBe(false)
67+
})
68+
69+
test('Clear binary cache', async () => {
70+
const cache = new S3ObjectCache(s3Client, bucketName!)
71+
const fileData = Uint8Array.from(atob('test'), (c) => c.charCodeAt(0))
72+
const key = 'test.gz'
73+
74+
await cache.put('test', fileData)
75+
let fileExist = await checkFileExist(key)
76+
expect(fileExist).toBe(true)
77+
78+
await cache.clearCache('test')
79+
fileExist = await checkFileExist(key)
80+
expect(fileExist).toBe(false)
81+
})
82+
83+
test("Get an object from cache if it didn't exist", async () => {
84+
const cache = new S3ObjectCache(s3Client, bucketName!)
85+
86+
const cached = await cache.getAndCache<{ test: number }>('test', async (_) => ({
87+
test: 1,
88+
}))
89+
90+
expect(cached.test).toBe(1)
91+
})
92+
93+
test('Get an object from cache if it did exist', async () => {
94+
const cache = new S3ObjectCache(s3Client, bucketName!)
95+
await cache.put('test', { test: 1 })
96+
97+
const cached = await cache.getAndCache<{ test: number }>('test', async (_) => ({
98+
test: 2,
99+
}))
100+
101+
expect(cached.test).toBe(1)
102+
})
103+
104+
test('Get an object from cache if it is not yet stale', async () => {
105+
const cache = new S3ObjectCache(s3Client, bucketName!)
106+
await cache.put('test', { test: 1 })
107+
await new Promise((resolve) => setTimeout(resolve, 100))
108+
109+
const cached = await cache.getAndCache<{ test: number }>(
110+
'test',
111+
async (_) => ({
112+
test: 2,
113+
}),
114+
{ staleAfterSeconds: 2 },
115+
)
116+
117+
expect(cached.test).toBe(1)
118+
})
119+
120+
test('Get a fresh object if it is stale', async () => {
121+
const cache = new S3ObjectCache(s3Client, bucketName!)
122+
await cache.put('test', { test: 1 })
123+
await new Promise((resolve) => setTimeout(resolve, 1000))
124+
125+
const cached = await cache.getAndCache<{ test: number }>(
126+
'test',
127+
async (_) => ({
128+
test: 2,
129+
}),
130+
{ staleAfterSeconds: 0 },
131+
)
132+
133+
expect(cached.test).toBe(2)
134+
})
135+
136+
test('Get an object from cache if it is stale but there is an error and returnStaleOnError is set', async () => {
137+
const cache = new S3ObjectCache(s3Client, bucketName!)
138+
await cache.put('test', { test: 1 })
139+
await new Promise((resolve) => setTimeout(resolve, 100))
140+
141+
const cached = await cache.getAndCache<{ test: number }>(
142+
'test',
143+
async (_) => {
144+
throw new Error('error')
145+
},
146+
{
147+
staleAfterSeconds: 0,
148+
returnStaleResultOnError: true,
149+
},
150+
)
151+
152+
expect(cached.test).toBe(1)
153+
})
154+
})

src/s3ObjectCache.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3'
1+
import { DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3'
22
import { BinaryCacheOptions, BinaryWithMetadata, CacheOptions, ObjectCache } from './cache'
33
import * as path from 'node:path'
44

@@ -22,6 +22,35 @@ export class S3ObjectCache implements ObjectCache {
2222
this.keyPrefix = keyPrefix
2323
}
2424

25+
/**
26+
* Clear the cache value for te given cache key
27+
* @param cacheKey A unique key that identifies the cached value
28+
*/
29+
async clearCache(cacheKey: string): Promise<void> {
30+
const deleteFile = async (key: string) => {
31+
const bucketAndKey = {
32+
Bucket: this.bucket,
33+
Key: key,
34+
}
35+
36+
const fileExist = await this.s3Client
37+
.send(new HeadObjectCommand(bucketAndKey))
38+
.then(() => true)
39+
.catch(() => false)
40+
41+
if (fileExist) {
42+
await this.s3Client.send(
43+
new DeleteObjectCommand({
44+
...bucketAndKey,
45+
}),
46+
)
47+
}
48+
}
49+
50+
await deleteFile(this.keyPrefix ? path.join(this.keyPrefix, `${cacheKey}.gz`) : `${cacheKey}.gz`)
51+
await deleteFile(this.keyPrefix ? path.join(this.keyPrefix, `${cacheKey}.json.gz`) : `${cacheKey}.json.gz`)
52+
}
53+
2554
/** Adds the given value to the cache for the given cache key
2655
* @param cacheKey A unique key that identifies the cached value
2756
* @param data The data to cache

0 commit comments

Comments
 (0)