From 5f86cb724ffe719b51ab9af474798176d0fd2fbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Nikoli=C4=87?= Date: Mon, 20 Apr 2026 15:34:25 +0200 Subject: [PATCH] feat: support adaptive `grace` and `graceBackoff` via factory context --- .../cache/cache_entry/cache_entry_options.ts | 27 +++++++++++- .../bentocache/src/cache/factory_runner.ts | 2 + packages/bentocache/src/types/helpers.ts | 8 +++- .../tests/cache/cache_entry_options.spec.ts | 17 ++++++++ .../tests/cache/factory_context.spec.ts | 41 +++++++++++++++++++ packages/bentocache/tests/typings.spec.ts | 8 ++++ 6 files changed, 100 insertions(+), 3 deletions(-) diff --git a/packages/bentocache/src/cache/cache_entry/cache_entry_options.ts b/packages/bentocache/src/cache/cache_entry/cache_entry_options.ts index eb5aa2c6..8bad5d46 100644 --- a/packages/bentocache/src/cache/cache_entry/cache_entry_options.ts +++ b/packages/bentocache/src/cache/cache_entry/cache_entry_options.ts @@ -31,8 +31,8 @@ export function createCacheEntryOptions( ) { const options = { ...defaults, ...newOptions } - const grace = resolveGrace(options) - const graceBackoff = resolveTtl(options.graceBackoff, null) ?? 0 + let grace = resolveGrace(options) + let graceBackoff = resolveTtl(options.graceBackoff, null) ?? 0 let logicalTtl = resolveTtl(options.ttl) let physicalTtl = grace > 0 ? grace : logicalTtl @@ -132,6 +132,29 @@ export function createCacheEntryOptions( return self }, + /** + * Set a new grace period + */ + setGrace(newGrace: false | Duration) { + options.grace = newGrace + grace = resolveGrace(options) + self.grace = grace + physicalTtl = self.isGraceEnabled() ? grace : logicalTtl + + return self + }, + + /** + * Set a new grace backoff duration + */ + setGraceBackoff(newGraceBackoff: Duration) { + options.graceBackoff = newGraceBackoff + graceBackoff = resolveTtl(options.graceBackoff, null) ?? 0 + self.graceBackoff = graceBackoff + + return self + }, + /** * Compute the logical TTL timestamp from now */ diff --git a/packages/bentocache/src/cache/factory_runner.ts b/packages/bentocache/src/cache/factory_runner.ts index 5a731134..bb033a2f 100644 --- a/packages/bentocache/src/cache/factory_runner.ts +++ b/packages/bentocache/src/cache/factory_runner.ts @@ -74,6 +74,8 @@ export class FactoryRunner { setTags: (tags) => params.options.tags.push(...tags), setOptions: (options) => { if (options.ttl) params.options.setLogicalTtl(options.ttl) + if ('grace' in options) params.options.setGrace(options.grace ?? false) + if ('graceBackoff' in options) params.options.setGraceBackoff(options.graceBackoff ?? null) params.options.skipBusNotify = options.skipBusNotify ?? false params.options.skipL2Write = options.skipL2Write ?? false }, diff --git a/packages/bentocache/src/types/helpers.ts b/packages/bentocache/src/types/helpers.ts index 89713d9c..51676093 100644 --- a/packages/bentocache/src/types/helpers.ts +++ b/packages/bentocache/src/types/helpers.ts @@ -24,7 +24,13 @@ export type GetSetFactoryContext = { /** * Set the options for the current factory */ - setOptions: (options: { ttl?: Duration; skipBusNotify?: boolean; skipL2Write?: boolean }) => void + setOptions: (options: { + ttl?: Duration + grace?: false | Duration + graceBackoff?: Duration + skipBusNotify?: boolean + skipL2Write?: boolean + }) => void /** * Set the tags for the current factory diff --git a/packages/bentocache/tests/cache/cache_entry_options.spec.ts b/packages/bentocache/tests/cache/cache_entry_options.spec.ts index 0272f870..137bc692 100644 --- a/packages/bentocache/tests/cache/cache_entry_options.spec.ts +++ b/packages/bentocache/tests/cache/cache_entry_options.spec.ts @@ -115,4 +115,21 @@ test.group('Cache Entry Options', () => { assert.equal(options.getLogicalTtl(), ms.parse('5m')) assert.equal(options.getPhysicalTtl(), ms.parse('5m')) }) + + test('setGrace should re-compute physical ttl', ({ assert }) => { + const options = createCacheEntryOptions({ ttl: '10m' }) + + options.setGrace('30m') + assert.equal(options.getPhysicalTtl(), ms.parse('30m')) + + options.setGrace(false) + assert.equal(options.getPhysicalTtl(), ms.parse('10m')) + }) + + test('setGraceBackoff should update grace backoff value', ({ assert }) => { + const options = createCacheEntryOptions({ graceBackoff: '10s' }) + + options.setGraceBackoff('25s') + assert.equal(options.graceBackoff, ms.parse('25s')) + }) }) diff --git a/packages/bentocache/tests/cache/factory_context.spec.ts b/packages/bentocache/tests/cache/factory_context.spec.ts index d2daa327..18d1d0fe 100644 --- a/packages/bentocache/tests/cache/factory_context.spec.ts +++ b/packages/bentocache/tests/cache/factory_context.spec.ts @@ -120,4 +120,45 @@ test.group('Factory Context', () => { assert.deepEqual(r1, 'bar') }) + + test('can set grace and graceBackoff with adaptive options', async ({ assert }) => { + const { cache } = new CacheFactory().withMemoryL1().merge({ timeout: '2s' }).create() + + const r1 = await cache.getOrSet({ + key: 'key1', + factory: (ctx) => { + ctx.setOptions({ ttl: 10, grace: '6h' }) + return { foo: 'bar' } + }, + }) + + await sleep(50) + + const r2 = await cache.getOrSet({ + key: 'key1', + factory: (ctx) => { + ctx.setOptions({ graceBackoff: '0.5s', grace: '6h' }) + throw new Error('factory error') + }, + }) + + let factoryCalledDuringBackoff = false + const r3 = await cache.getOrSet({ + key: 'key1', + factory: () => { + factoryCalledDuringBackoff = true + throw new Error('should not be called') + }, + }) + + await sleep(800) + + const r4 = await cache.getOrSet({ key: 'key1', factory: () => ({ foo: 'baz' }) }) + + assert.deepEqual(r1, { foo: 'bar' }) + assert.deepEqual(r2, { foo: 'bar' }) + assert.deepEqual(r3, { foo: 'bar' }) + assert.deepEqual(r4, { foo: 'baz' }) + assert.isFalse(factoryCalledDuringBackoff) + }) }) diff --git a/packages/bentocache/tests/typings.spec.ts b/packages/bentocache/tests/typings.spec.ts index 0b12cc52..e01429ec 100644 --- a/packages/bentocache/tests/typings.spec.ts +++ b/packages/bentocache/tests/typings.spec.ts @@ -94,11 +94,19 @@ test.group('Typings', () => { ttl: 1000, factory: () => 34, }) + const r5 = await cache.getOrSet({ + key: 'key', + factory: (ctx) => { + ctx.setOptions({ ttl: '1m', grace: '2m', graceBackoff: '10s' }) + return 99 + }, + }) expectTypeOf(r1).toEqualTypeOf() expectTypeOf(r2).toEqualTypeOf() expectTypeOf(r3).toEqualTypeOf() expectTypeOf(r4).toEqualTypeOf() + expectTypeOf(r5).toEqualTypeOf() }) test('getOrSet() typings on bento', async ({ expectTypeOf }) => {