Skip to content

Commit 7afdb10

Browse files
committed
feat:a add fail and skip methods to factory ctx
1 parent cc6dc49 commit 7afdb10

File tree

7 files changed

+152
-15
lines changed

7 files changed

+152
-15
lines changed

.changeset/curvy-poets-attack.md

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
'bentocache': minor
3+
---
4+
5+
Add two new functions in the factory callback context:
6+
7+
```ts
8+
cache.getOrSet({
9+
key: 'foo',
10+
factory: ({ skip, fail }) => {
11+
const item = await getFromDb()
12+
if (!item) {
13+
return skip()
14+
}
15+
16+
if (item.isInvalid) {
17+
return fail('Item is invalid')
18+
}
19+
20+
return item
21+
}
22+
})
23+
```
24+
25+
## Skip
26+
27+
Returning `skip` in a factory will not cache the value, and `getOrSet` will returns `undefined` even if there is a stale item in cache.
28+
It will force the key to be recalculated on the next call.
29+
30+
## Fail
31+
32+
Returning `fail` in a factory will not cache the value and will throw an error. If there is a stale item in cache, it will be used.

packages/bentocache/src/cache/factory_runner.ts

+7
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type { CacheEntryOptions } from './cache_entry/cache_entry_options.js'
1313
export class FactoryRunner {
1414
#locks: Locks
1515
#stack: CacheStack
16+
#skipSymbol = Symbol('bentocache.skip')
1617

1718
constructor(stack: CacheStack, locks: Locks) {
1819
this.#stack = stack
@@ -29,8 +30,14 @@ export class FactoryRunner {
2930
try {
3031
const result = await factory({
3132
setTtl: (ttl) => options.setLogicalTtl(ttl),
33+
skip: () => this.#skipSymbol as any as undefined,
34+
fail: (message) => {
35+
throw new Error(message ?? 'Factory failed')
36+
},
3237
})
3338

39+
if (result === this.#skipSymbol) return
40+
3441
await this.#stack.set(key, result, options)
3542
return result
3643
} catch (error) {

packages/bentocache/src/types/helpers.ts

+16-4
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,30 @@ export type Duration = number | string | null
1212
*/
1313
export type Factory<T = any> = T | (() => T) | Promise<T> | (() => Promise<T>)
1414

15-
export type GetSetFactoryOptions = {
15+
export type GetSetFactoryContext = {
1616
/**
17-
* Set dynamically the TTL
18-
* See Adaptive caching documentation for more information
17+
* Dynamically set the TTL
18+
* @see https://bentocache.dev/docs/adaptive-caching
1919
*/
2020
setTtl: (ttl: Duration) => void
21+
22+
/**
23+
* Make the factory fail with a custom error.
24+
* Nothing will be cached and if a graced value is available, it will be returned
25+
*/
26+
fail: (message?: string) => void
27+
28+
/**
29+
* Make the factory do not cache anything. **If a graced value is available,
30+
* it will not be used**
31+
*/
32+
skip: () => undefined
2133
}
2234

2335
/**
2436
* GetOrSet Factory
2537
*/
26-
export type GetSetFactory<T = any> = (options: GetSetFactoryOptions) => T | Promise<T>
38+
export type GetSetFactory<T = any> = (options: GetSetFactoryContext) => T | Promise<T>
2739

2840
/**
2941
* Logger interface
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { test } from '@japa/runner'
2+
import { sleep } from '@julr/utils/misc'
3+
4+
import { sequentialFactory } from '../helpers/index.js'
5+
import { CacheFactory } from '../../factories/cache_factory.js'
6+
7+
test.group('Fail', () => {
8+
test('should use graced value', async ({ assert }) => {
9+
const { cache } = new CacheFactory()
10+
.merge({ grace: '2m', timeout: '2s' })
11+
.withL1L2Config()
12+
.create()
13+
14+
const factory = sequentialFactory([() => 'foo', ({ fail }) => fail('error')])
15+
16+
const r1 = await cache.getOrSet({ ttl: 100, key: 'foo', factory })
17+
await sleep(100)
18+
const r2 = await cache.getOrSet({ key: 'foo', factory })
19+
20+
assert.deepEqual(factory.callsCount(), 2)
21+
assert.deepEqual(r1, 'foo')
22+
assert.deepEqual(r2, 'foo')
23+
})
24+
25+
test('throw error if no graced value', async ({ assert }) => {
26+
const { cache } = new CacheFactory().withL1L2Config().create()
27+
const factory = sequentialFactory([({ fail }) => fail()])
28+
29+
await assert.rejects(() => cache.getOrSet({ key: 'foo', factory }))
30+
})
31+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { test } from '@japa/runner'
2+
import { sleep } from '@julr/utils/misc'
3+
4+
import { sequentialFactory } from '../helpers/index.js'
5+
import { CacheFactory } from '../../factories/cache_factory.js'
6+
7+
test.group('Skip caching', () => {
8+
test('do not cache if skip is returned', async ({ assert }) => {
9+
const { cache } = new CacheFactory().withL1L2Config().create()
10+
11+
const factory = sequentialFactory([({ skip }) => skip(), () => 'bar'])
12+
13+
const r1 = await cache.getOrSet({ key: 'foo', factory })
14+
const r2 = await cache.getOrSet({ key: 'foo', factory })
15+
16+
assert.deepEqual(factory.callsCount(), 2)
17+
assert.deepEqual(r1, undefined)
18+
assert.deepEqual(r2, 'bar')
19+
})
20+
21+
test('do not use graced value if skip is returned', async ({ assert }) => {
22+
const { cache } = new CacheFactory()
23+
.merge({ timeout: '2s', grace: '2m' })
24+
.withL1L2Config()
25+
.create()
26+
27+
const factory = sequentialFactory([() => 'bar', ({ skip }) => skip()])
28+
29+
const r1 = await cache.getOrSet({ key: 'foo', factory, ttl: 10 })
30+
await sleep(50)
31+
const r2 = await cache.getOrSet({ key: 'foo', factory })
32+
33+
assert.deepEqual(factory.callsCount(), 2)
34+
assert.deepEqual(r1, 'bar')
35+
assert.deepEqual(r2, undefined)
36+
})
37+
})

packages/bentocache/tests/drivers/dynamodb.spec.ts

+13-11
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,19 @@ const dynamoClient = new DynamoDBClient({
1414
* Create the table for storing the cache
1515
*/
1616
async function createTable() {
17-
await dynamoClient.send(
18-
new CreateTableCommand({
19-
TableName: 'cache',
20-
KeySchema: [{ AttributeName: 'key', KeyType: 'HASH' }],
21-
AttributeDefinitions: [{ AttributeName: 'key', AttributeType: 'S' }],
22-
ProvisionedThroughput: {
23-
ReadCapacityUnits: 4,
24-
WriteCapacityUnits: 4,
25-
},
26-
}),
27-
)
17+
await dynamoClient
18+
.send(
19+
new CreateTableCommand({
20+
TableName: 'cache',
21+
KeySchema: [{ AttributeName: 'key', KeyType: 'HASH' }],
22+
AttributeDefinitions: [{ AttributeName: 'key', AttributeType: 'S' }],
23+
ProvisionedThroughput: {
24+
ReadCapacityUnits: 4,
25+
WriteCapacityUnits: 4,
26+
},
27+
}),
28+
)
29+
.catch(() => {})
2830
}
2931

3032
/**

packages/bentocache/tests/helpers/index.ts

+16
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { pino } from 'pino'
22
import pinoLoki from 'pino-loki'
33
import { sleep } from '@julr/utils/misc'
44

5+
import type { GetSetFactory, GetSetFactoryContext } from '../../src/types/helpers.js'
6+
57
export const BASE_URL = new URL('./tmp/', import.meta.url)
68
export const REDIS_CREDENTIALS = { host: 'localhost', port: 6379 }
79

@@ -51,3 +53,17 @@ export const traceLogger = (pretty = true) => {
5153
}),
5254
)
5355
}
56+
57+
export function sequentialFactory(callbacks: GetSetFactory[]) {
58+
let index = 0
59+
60+
const sequence = (options: GetSetFactoryContext) => {
61+
const cb = callbacks[index]
62+
index++
63+
64+
return cb(options)
65+
}
66+
67+
sequence.callsCount = () => index
68+
return sequence
69+
}

0 commit comments

Comments
 (0)