Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/two-loops-jog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'bentocache': minor
---

Add `waitUntil` option to support grace periods in serverless environments
58 changes: 51 additions & 7 deletions docs/content/docs/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ const bento = new BentoCache({
// Store level 👇
ttl: '30m',
grace: false,
})
}
}),
},
})

bento.getOrSet({
Expand Down Expand Up @@ -104,6 +104,7 @@ Levels: `global`, `store`, `operation`
A duration to define a hard [timeout](./timeouts.md#hard-timeouts).

### `forceFresh`

Default: `false`

Levels: `operation`
Expand All @@ -117,7 +118,7 @@ Default: `undefined`

Levels: `global`, `store`, `operation`

The maximum amount of time (in milliseconds) that the in-memory lock for [stampeded protection](./stampede_protection.md) can be held. If the lock is not released before this timeout, it will be released automatically.
The maximum amount of time (in milliseconds) that the in-memory lock for [stampeded protection](./stampede_protection.md) can be held. If the lock is not released before this timeout, it will be released automatically.

This is usually not needed, but can provide an extra layer of protection against theoretical deadlocks.

Expand Down Expand Up @@ -146,9 +147,9 @@ Default: `undefined (disabled)`

Levels: `global`, `store`, `operation`

This option allows you to enable a simple circuit breaker system for the L2 Cache. If defined, the circuit breaker will open when a call to our distributed cache fails. It will stay open for `l2CircuitBreakerDuration` seconds.
This option allows you to enable a simple circuit breaker system for the L2 Cache. If defined, the circuit breaker will open when a call to our distributed cache fails. It will stay open for `l2CircuitBreakerDuration` seconds.

If you're not familiar with the circuit breaker system, to summarize it very simply: if an operation on the L2 Cache fails and the circuit breaker option is activated, then all future operations on the L2 Cache will be rejected for `l2CircuitBreakerDuration` seconds, in order to avoid overloading the L2 Cache with operations that are likely to fail.
If you're not familiar with the circuit breaker system, to summarize it very simply: if an operation on the L2 Cache fails and the circuit breaker option is activated, then all future operations on the L2 Cache will be rejected for `l2CircuitBreakerDuration` seconds, in order to avoid overloading the L2 Cache with operations that are likely to fail.

Once the `l2CircuitBreakerDuration` seconds have passed, the circuit breaker closes and operations on the L2 Cache can resume.

Expand Down Expand Up @@ -180,8 +181,8 @@ import superjson from 'superjson'
const bento = new BentoCache({
serializer: {
serialize: superjson.stringify,
deserialize: superjson.parse
}
deserialize: superjson.parse,
},
})
```

Expand All @@ -204,3 +205,46 @@ Levels: `global`
Only configurable at the BentoCache level.

See [events](./digging_deeper/events.md) for more details.

### `waitUntil`

Default: `undefined`.

Levels: `global`

Only configurable at the BentoCache level.

A function to register background tasks in serverless environments. This is essential for SWR (Stale-While-Revalidate) to work properly in serverless platforms like Vercel, Cloudflare Workers, Netlify, etc.

When BentoCache uses background revalidation (when a stale value is returned and the cache is refreshed in the background), it needs to inform the serverless platform that work is still ongoing after the response is sent. Without this, the serverless function may be terminated before the background task completes.

```ts
// title: Vercel Functions
import { waitUntil } from '@vercel/functions'

const bento = new BentoCache({
default: 'memory',
waitUntil: waitUntil,
stores: {
memory: bentostore().useL1Layer(memoryDriver({})),
},
})
```

```ts
// title: Cloudflare Workers
import { waitUntil } from 'cloudflare:workers'

const bento = new BentoCache({
default: 'memory',
waitUntil: waitUntil,
stores: {
memory: bentostore().useL1Layer(memoryDriver({})),
},
})
```

This option is only needed when:

- You're running in a serverless environment
- You're using features that trigger background revalidation (SWR with grace periods, soft timeouts, etc.)
6 changes: 6 additions & 0 deletions packages/bentocache/src/bento_cache_options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ export class BentoCacheOptions {
serializeL1: boolean = true
onFactoryError?: (error: FactoryError) => void

/**
* A function to register background tasks in serverless environments
*/
waitUntil?: (promise: Promise<unknown>) => void

constructor(options: RawBentoCacheOptions) {
this.#options = { ...this, ...options }

Expand All @@ -98,6 +103,7 @@ export class BentoCacheOptions {

this.logger = new Logger(this.#options.logger ?? noopLogger())
this.onFactoryError = this.#options.onFactoryError
this.waitUntil = this.#options.waitUntil
}

serializeL1Cache(shouldSerialize: boolean = true) {
Expand Down
4 changes: 3 additions & 1 deletion packages/bentocache/src/cache/factory_runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ export class FactoryRunner {
* And immediately return the fallback value
*/
if (options.shouldSwr(hasGracedValue)) {
this.#runFactory({ key, factory, options, lockReleaser, isBackground: true })
const promise = this.#runFactory({ key, factory, options, lockReleaser, isBackground: true })
this.#stack.options.waitUntil?.(promise)
throw new errors.E_FACTORY_SOFT_TIMEOUT(key)
}

Expand All @@ -140,6 +141,7 @@ export class FactoryRunner {
{ cache: this.#stack.name, opId: options.id, key },
`factory timed out after ${timeout?.duration}ms`,
)
this.#stack.options.waitUntil?.(runFactory)
throw new timeout!.exception(key)
},
})
Expand Down
16 changes: 16 additions & 0 deletions packages/bentocache/src/types/options/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,22 @@ export type RawBentoCacheOptions = {
* Custom serializer
*/
serializer?: CacheSerializer

/**
* A function to register background tasks in serverless environments.
* This is needed for SWR (Stale-While-Revalidate) to work properly in
* serverless platforms like Vercel, Cloudflare Workers, etc.
*
* Example with Vercel:
* ```ts
* import { waitUntil } from '@vercel/functions'
*
* const bento = new BentoCache({
* waitUntil: waitUntil
* })
* ```
*/
waitUntil?: (promise: Promise<unknown>) => void
} & Omit<RawCommonOptions, 'tags' | 'skipBusNotify' | 'skipL2Write'>

/**
Expand Down