Skip to content
Merged
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/eleven-hats-retire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'bentocache': patch
---

Refactoring of CacheEntryOptions class. We switch to a simple function that returns an object rather than a class. Given that CacheEntryOptions is heavily used : it was instantiated for every cache operation, we gain a lot in performance.
298 changes: 137 additions & 161 deletions packages/bentocache/src/cache/cache_entry/cache_entry_options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,189 +3,165 @@ import { is } from '@julr/utils/is'

import { errors } from '../../errors.js'
import { resolveTtl } from '../../helpers.js'
import type { FactoryError } from '../../errors.js'
import type { Duration, RawCommonOptions } from '../../types/main.js'

const toId = hexoid(12)

export class CacheEntryOptions {
/**
* The options that were passed to the constructor
*/
#options: RawCommonOptions

/**
* Unique identifier that will be used when logging
* debug information.
*/
id: string

/**
* Logical TTL is when the value is considered expired
* but still can be in the cache ( Grace period )
*/
logicalTtl?: number

/**
* Physical TTL is the time when value will be automatically
* removed from the cache. This is the Grace period
* duration
*/
physicalTtl?: number

/**
* Timeouts for the cache operations
*/
timeout?: number
hardTimeout?: number

/**
* Resolved grace period options
*/
grace: number
graceBackoff: number

/**
* Max time to wait for the lock to be acquired
*/
lockTimeout?: number
onFactoryError?: (error: FactoryError) => void

constructor(options: RawCommonOptions = {}, defaults: Partial<RawCommonOptions> = {}) {
this.id = toId()

this.#options = { ...defaults, ...options }

this.grace = this.#resolveGrace()
this.graceBackoff = resolveTtl(this.#options.graceBackoff, null) ?? 0
this.logicalTtl = this.#resolveLogicalTtl()
this.physicalTtl = this.#resolvePhysicalTtl()
this.timeout = resolveTtl(this.#options.timeout, null)
this.hardTimeout = resolveTtl(this.#options.hardTimeout, null)
this.lockTimeout = resolveTtl(this.#options.lockTimeout, null)
this.onFactoryError = this.#options.onFactoryError ?? defaults.onFactoryError
}
export type CacheEntryOptions = ReturnType<typeof createCacheEntryOptions>

/**
* Resolve the grace period options
*/
#resolveGrace() {
if (this.#options.grace === false) return 0
return resolveTtl(this.#options.grace, null) ?? 0
}
/**
* Resolve the grace options
*/
function resolveGrace(options: RawCommonOptions) {
if (options.grace === false) return 0

/**
* Returns a new instance of `CacheItemOptions` with the same
* options as the current instance, but with any provided
* options overriding the current
*
* For performance reasons, if no options are provided, the
* current instance is returned
*/
cloneWith(options?: Partial<RawCommonOptions>) {
return options ? new CacheEntryOptions(options, this.#options) : this
}
return resolveTtl(options.grace, null) ?? 0
}

/**
* Resolve the logical TTL to a duration in milliseconds
*/
#resolveLogicalTtl() {
return resolveTtl(this.#options.ttl)
}
/**
* Cache Entry Options. Define how a cache operation should behave
*
* Yes, this is a fake class. Initially, this was a class, but
* since CacheEntryOptions is initialized each time a cache
* operation is performed, it was converted to this
* fake class to have way better performance.
*/
export function createCacheEntryOptions(
newOptions: RawCommonOptions = {},
defaults: Partial<RawCommonOptions> = {},
) {
const options = { ...defaults, ...newOptions }

const grace = resolveGrace(options)
const graceBackoff = resolveTtl(options.graceBackoff, null) ?? 0

let logicalTtl = resolveTtl(options.ttl)
let physicalTtl = grace > 0 ? grace : logicalTtl

const timeout = resolveTtl(options.timeout, null)
const hardTimeout = resolveTtl(options.hardTimeout, null)
const lockTimeout = resolveTtl(options.lockTimeout, null)

const self = {
/**
* Unique identifier that will be used when logging
* debug information.
*/
id: toId(),

/**
* Resolve the physical TTL to a duration in milliseconds
*
* If grace period is not enabled then the physical TTL
* is the same as the logical TTL
*/
#resolvePhysicalTtl() {
return this.isGraceEnabled ? this.grace : this.logicalTtl
}
/**
* Resolved grace period options
*/
grace,
graceBackoff,

get isGraceEnabled() {
return this.grace > 0
}
/**
* Logical TTL is when the value is considered expired
* but still can be in the cache ( Grace period )
*/
logicalTtl,

get suppressL2Errors() {
return this.#options.suppressL2Errors
}
/**
* Physical TTL is the time when value will be automatically
* removed from the cache. This is the Grace period
* duration
*/
physicalTtl,

/**
* Set a new logical TTL
*/
setLogicalTtl(ttl: Duration) {
this.#options.ttl = ttl
/**
* Timeouts for the cache operations
*/
timeout,
hardTimeout,

this.logicalTtl = this.#resolveLogicalTtl()
this.physicalTtl = this.#resolvePhysicalTtl()
/**
* Max time to wait for the lock to be acquired
*/
lockTimeout,
onFactoryError: options.onFactoryError ?? defaults.onFactoryError,
isGraceEnabled: grace > 0,
suppressL2Errors: options.suppressL2Errors,

return this
}
/**
* Returns a new instance of `CacheItemOptions` with the same
* options as the current instance, but with any provided
* options overriding the current
*
* For performance reasons, if no options are provided, the
* current instance is returned
*/
cloneWith(newOptions?: Partial<RawCommonOptions>) {
return newOptions ? createCacheEntryOptions(newOptions, options) : self
},

/**
* Compute the logical TTL timestamp from now
*/
logicalTtlFromNow() {
if (!this.logicalTtl) return undefined
return Date.now() + this.logicalTtl
}
/**
* Set a new logical TTL
*/
setLogicalTtl(newTtl: Duration) {
options.ttl = newTtl

/**
* Compute the physical TTL timestamp from now
*/
physicalTtlFromNow() {
if (!this.physicalTtl) return undefined
return Date.now() + this.physicalTtl
}
logicalTtl = resolveTtl(options.ttl)
physicalTtl = self.isGraceEnabled ? grace : logicalTtl

/**
* Compute the lock timeout we should use for the
* factory
*/
factoryTimeout(hasFallbackValue: boolean) {
if (hasFallbackValue && this.isGraceEnabled && is.number(this.timeout)) {
return {
type: 'soft',
duration: this.timeout,
exception: errors.E_FACTORY_SOFT_TIMEOUT,
}
}
return self
},

if (this.hardTimeout) {
return {
type: 'hard',
duration: this.hardTimeout,
exception: errors.E_FACTORY_HARD_TIMEOUT,
}
}
/**
* Compute the logical TTL timestamp from now
*/
logicalTtlFromNow() {
if (!logicalTtl) return

return
}
return Date.now() + logicalTtl
},

/**
* Determine if we should use the SWR strategy
*/
shouldSwr(hasFallback: boolean) {
return this.isGraceEnabled && this.timeout === 0 && hasFallback
}
/**
* Compute the physical TTL timestamp from now
*/
physicalTtlFromNow() {
if (!physicalTtl) return

/**
* Compute the maximum time we should wait for the
* lock to be acquired
*/
getApplicableLockTimeout(hasFallbackValue: boolean) {
if (this.lockTimeout) {
return this.lockTimeout
}
return Date.now() + physicalTtl
},

/**
* If we have a fallback value and grace period is enabled,
* that means we should wait at most for the soft timeout
* duration.
* Compute the lock timeout we should use for the
* factory
*/
if (hasFallbackValue && this.isGraceEnabled && typeof this.timeout === 'number') {
return this.timeout
}
factoryTimeout(hasFallbackValue: boolean) {
if (hasFallbackValue && self.isGraceEnabled && is.number(timeout)) {
return { type: 'soft', duration: timeout, exception: errors.E_FACTORY_SOFT_TIMEOUT }
}

if (hardTimeout) {
return { type: 'hard', duration: hardTimeout, exception: errors.E_FACTORY_HARD_TIMEOUT }
}
},

/**
* Determine if we should use the SWR strategy
*/
shouldSwr(hasFallback: boolean) {
return self.isGraceEnabled && timeout === 0 && hasFallback
},

/**
* Compute the maximum time we should wait for the
* lock to be acquired
*/
getApplicableLockTimeout(hasFallbackValue: boolean) {
if (lockTimeout) return lockTimeout

/**
* If we have a fallback value and grace period is enabled,
* that means we should wait at most for the soft timeout
* duration.
*/
if (hasFallbackValue && self.isGraceEnabled && typeof timeout === 'number') {
return timeout
}
},
}

return self
}
8 changes: 4 additions & 4 deletions packages/bentocache/src/cache/stack/cache_stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { RemoteCache } from '../facades/remote_cache.js'
import { BaseDriver } from '../../drivers/base_driver.js'
import { cacheEvents } from '../../events/cache_events.js'
import type { BentoCacheOptions } from '../../bento_cache_options.js'
import { CacheEntryOptions } from '../cache_entry/cache_entry_options.js'
import { createCacheEntryOptions } from '../cache_entry/cache_entry_options.js'
import {
type BusDriver,
type BusOptions,
Expand All @@ -23,7 +23,7 @@ export class CacheStack extends BaseDriver {
l1?: LocalCache
l2?: RemoteCache
bus?: Bus
defaultOptions: CacheEntryOptions
defaultOptions: ReturnType<typeof createCacheEntryOptions>
logger: Logger
#busDriver?: BusDriver
#busOptions?: BusOptions
Expand All @@ -50,7 +50,7 @@ export class CacheStack extends BaseDriver {
this.bus = bus ? bus : this.#createBus(drivers.busDriver, drivers.busOptions)
if (this.l1) this.bus?.manageCache(this.prefix, this.l1)

this.defaultOptions = new CacheEntryOptions(options)
this.defaultOptions = createCacheEntryOptions(this.options)
}

get emitter() {
Expand Down Expand Up @@ -110,7 +110,7 @@ export class CacheStack extends BaseDriver {
* - Publish a message to the bus
* - Emit a CacheWritten event
*/
async set(key: string, value: any, options: CacheEntryOptions) {
async set(key: string, value: any, options: ReturnType<typeof createCacheEntryOptions>) {
if (is.undefined(value)) throw new UndefinedValueError(key)

const rawItem = {
Expand Down
Loading