Skip to content

Commit f1c7c51

Browse files
committed
feat: add serialize option to memory driver
1 parent 67017c2 commit f1c7c51

File tree

12 files changed

+126
-29
lines changed

12 files changed

+126
-29
lines changed

.changeset/kind-forks-repeat.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'bentocache': minor
3+
---
4+
5+
Added a `serialize: false` to the memory driver.
6+
7+
It means that, the data stored in the memory cache will not be serialized/parsed using `JSON.stringify` and `JSON.parse`. This allows for a much faster throughput but at the expense of:
8+
- not being able to limit the size of the stored data, because we can't really know the size of an unserialized object
9+
- Having inconsistent return between the L1 and L2 cache. The data stored in the L2 Cache will always be serialized because it passes over the network. Therefore, depending on whether the data is retrieved from the L1 and L2, we can have data that does not have the same form. For example, a Date instance will become a string if retrieved from the L2, but will remain a Date instance if retrieved from the L1. So, you should put extra care when using this feature with an additional L2 cache.
10+

docs/content/docs/cache_drivers.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,19 @@ const bento = new BentoCache({
111111
| `maxSize` | The maximum size of the cache **in bytes**. | N/A |
112112
| `maxItems` | The maximum number of entries that the cache can contain. Note that fewer items may be stored if you are also using `maxSize` and the cache is full. | N/A |
113113
| `maxEntrySize` | The maximum size of a single entry in bytes. | N/A |
114+
| `serialize` | If the data stored in the memory cache should be serialized/parsed using `JSON.stringify` and `JSON.parse`. | `true` |
114115

115116
`maxSize` and `maxEntrySize` accept human-readable strings. We use [bytes](https://www.npmjs.com/package/bytes) under the hood so make sure to ensure the format is correct. A `number` can also be passed to these options.
116117

118+
### `serialize` option
119+
120+
By default, the data stored in the memory cache will always be serialized using `JSON.stringify` and `JSON.parse`.
121+
You can disable this feature by setting `serialize` to `false`. This allows for a much faster throughput but at the expense of:
122+
- not being able to limit the size of the stored data, because we can't really know the size of an unserialized object. So if `maxSize` or `maxEntrySize` is set, it throws an error, but you still can use `maxItems` option.
123+
- **Having inconsistent return between the L1 and L2 cache**. The data stored in the L2 Cache will always be serialized because it passes over the network. Therefore, depending on whether the data is retrieved from the L1 and L2, we can have data that does not have the same form. For example, a Date instance will become a string if retrieved from the L2, but will remain a Date instance if retrieved from the L1. So, **you should put extra care when using this feature with an additional L2 cache**.
124+
125+
We recommend never storing anything that is not serializable in the memory cache when an L2 cache is used ( `Date`, `Map`, classes instances, functions etc.. ). However, if you are only using the memory cache, it is safe to store anything you.
126+
117127
## DynamoDB
118128

119129
DynamoDB is also supported by bentocache. You will need to install `@aws-sdk/client-dynamodb` to use this driver.

packages/bentocache/factories/cache_factory.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import { MemoryTransport } from '@boringnode/bus/transports/memory'
66
import { Cache } from '../src/cache/cache.js'
77
import { RedisDriver } from '../src/drivers/redis.js'
88
import { MemoryDriver } from '../src/drivers/memory.js'
9-
import type { CacheStackDrivers } from '../src/types/main.js'
109
import { CacheStack } from '../src/cache/stack/cache_stack.js'
1110
import { BentoCacheOptions } from '../src/bento_cache_options.js'
11+
import type { CacheStackDrivers, MemoryConfig } from '../src/types/main.js'
1212
import type { RawBentoCacheOptions } from '../src/types/options/options.js'
1313

1414
/**
@@ -17,6 +17,7 @@ import type { RawBentoCacheOptions } from '../src/types/options/options.js'
1717
*/
1818
export class CacheFactory {
1919
#parameters: Partial<RawBentoCacheOptions & CacheStackDrivers> = {}
20+
#l1Options: MemoryConfig = {}
2021
enabledL1L2Config: boolean = false
2122

2223
#cleanupCache(cache: Cache) {
@@ -40,7 +41,7 @@ export class CacheFactory {
4041
emitter: this.#parameters.emitter,
4142
lockTimeout: this.#parameters.lockTimeout,
4243
serializer: this.#parameters.serializer,
43-
})
44+
}).serializeL1Cache(this.#l1Options.serialize ?? true)
4445

4546
const stack = new CacheStack('primary', options, {
4647
l1Driver: this.#parameters.l1Driver,
@@ -63,8 +64,9 @@ export class CacheFactory {
6364
/**
6465
* Adds a Memory L1 driver to the cache stack
6566
*/
66-
withMemoryL1() {
67-
this.#parameters.l1Driver = new MemoryDriver({ maxSize: 100_000, prefix: 'test' })
67+
withMemoryL1(options: MemoryConfig = {}) {
68+
this.#l1Options = options
69+
this.#parameters.l1Driver = new MemoryDriver({ prefix: 'test', ...options })
6870
return this
6971
}
7072

packages/bentocache/src/bento_cache.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@ export class BentoCache<KnownCaches extends Record<string, BentoStore>> implemen
5959

6060
#createProvider(cacheName: string, store: BentoStore): CacheProvider {
6161
const entry = store.entry
62-
const driverItemOptions = this.#options.cloneWith(entry.options)
62+
const driverItemOptions = this.#options
63+
.cloneWith(entry.options)
64+
.serializeL1Cache(entry.l1?.options.serialize ?? true)
65+
6366
const cacheStack = new CacheStack(cacheName, driverItemOptions, {
6467
l1Driver: entry.l1?.factory({ prefix: driverItemOptions.prefix, ...entry.l1.options }),
6568
l2Driver: entry.l2?.factory({ prefix: driverItemOptions.prefix, ...entry.l2.options }),

packages/bentocache/src/bento_cache_options.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ export class BentoCacheOptions {
7272
*/
7373
lockTimeout?: Duration = null
7474

75+
/**
76+
* If the L1 cache should be serialized
77+
*/
78+
serializeL1: boolean = true
79+
7580
constructor(options: RawBentoCacheOptions) {
7681
this.#options = lodash.merge({}, this, options)
7782

@@ -86,9 +91,15 @@ export class BentoCacheOptions {
8691

8792
this.emitter = this.#options.emitter!
8893
this.serializer = this.#options.serializer ?? defaultSerializer
94+
8995
this.logger = this.#options.logger!.child({ pkg: 'bentocache' })
9096
}
9197

98+
serializeL1Cache(shouldSerialize: boolean = true) {
99+
this.serializeL1 = shouldSerialize
100+
return this
101+
}
102+
92103
cloneWith(options: RawBentoCacheOptions) {
93104
const newOptions = lodash.merge({}, this.#options, options)
94105
return new BentoCacheOptions(newOptions)

packages/bentocache/src/cache/cache_entry/cache_entry.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ export class CacheEntry {
2121
*/
2222
#logicalExpiration: number
2323

24-
#serializer: CacheSerializer
24+
#serializer?: CacheSerializer
2525

26-
constructor(key: string, item: Record<string, any>, serializer: CacheSerializer) {
26+
constructor(key: string, item: Record<string, any>, serializer?: CacheSerializer) {
2727
this.#key = key
2828
this.#value = item.value
2929
this.#logicalExpiration = item.logicalExpiration
@@ -46,8 +46,10 @@ export class CacheEntry {
4646
return Date.now() >= this.#logicalExpiration
4747
}
4848

49-
static fromDriver(key: string, item: string, serializer: CacheSerializer) {
50-
return new CacheEntry(key, serializer.deserialize(item), serializer)
49+
static fromDriver(key: string, item: string | Record<string, any>, serializer?: CacheSerializer) {
50+
if (!serializer && typeof item !== 'string') return new CacheEntry(key, item, serializer)
51+
52+
return new CacheEntry(key, serializer!.deserialize(item) ?? item, serializer)
5153
}
5254

5355
applyBackoff(duration: number) {
@@ -61,9 +63,9 @@ export class CacheEntry {
6163
}
6264

6365
serialize() {
64-
return this.#serializer.serialize({
65-
value: this.#value,
66-
logicalExpiration: this.#logicalExpiration,
67-
})
66+
const raw = { value: this.#value, logicalExpiration: this.#logicalExpiration }
67+
68+
if (this.#serializer) return this.#serializer.serialize(raw)
69+
return raw
6870
}
6971
}

packages/bentocache/src/cache/facades/local_cache.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import type { Logger, L1CacheDriver, CacheSerializer } from '../../types/main.js
99
export class LocalCache {
1010
#driver: L1CacheDriver
1111
#logger: Logger
12-
#serializer: CacheSerializer
12+
#serializer: CacheSerializer | undefined
1313

14-
constructor(driver: L1CacheDriver, logger: Logger, serializer: CacheSerializer) {
14+
constructor(driver: L1CacheDriver, logger: Logger, serializer: CacheSerializer | undefined) {
1515
this.#driver = driver
1616
this.#serializer = serializer
1717
this.#logger = logger.child({ context: 'bentocache.localCache' })
@@ -41,7 +41,7 @@ export class LocalCache {
4141
/**
4242
* Set a new item in the local cache
4343
*/
44-
set(key: string, value: string, options: CacheEntryOptions) {
44+
set(key: string, value: any, options: CacheEntryOptions) {
4545
/**
4646
* If grace period is disabled and Physical TTL is 0 or less, we can just delete the item.
4747
*/
@@ -78,7 +78,7 @@ export class LocalCache {
7878
if (value === undefined) return
7979

8080
const newEntry = CacheEntry.fromDriver(key, value, this.#serializer).expire().serialize()
81-
return this.#driver.set(key, newEntry, this.#driver.getRemainingTtl(key))
81+
return this.#driver.set(key, newEntry as any, this.#driver.getRemainingTtl(key))
8282
}
8383

8484
/**

packages/bentocache/src/cache/stack/cache_stack.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@ export class CacheStack extends BaseDriver {
3939
this.logger = options.logger.child({ cache: this.name })
4040

4141
if (drivers.l1Driver)
42-
this.l1 = new LocalCache(drivers.l1Driver, this.logger, this.options.serializer)
42+
this.l1 = new LocalCache(
43+
drivers.l1Driver,
44+
this.logger,
45+
this.options.serializeL1 ? this.options.serializer : undefined,
46+
)
4347
if (drivers.l2Driver)
4448
this.l2 = new RemoteCache(drivers.l2Driver, this.logger, this.options.serializer, !!this.l1)
4549

@@ -99,14 +103,6 @@ export class CacheStack extends BaseDriver {
99103
return this.emitter.emit(event.name, event.data)
100104
}
101105

102-
serialize(value: any) {
103-
return this.options.serializer.serialize(value)
104-
}
105-
106-
deserialize(value: string) {
107-
return this.options.serializer.deserialize(value)
108-
}
109-
110106
/**
111107
* Write a value in the cache stack
112108
* - Set value in local cache
@@ -117,12 +113,13 @@ export class CacheStack extends BaseDriver {
117113
async set(key: string, value: any, options: CacheEntryOptions) {
118114
if (is.undefined(value)) throw new UndefinedValueError(key)
119115

120-
const item = this.serialize({
116+
const rawItem = {
121117
value,
122118
logicalExpiration: options.logicalTtlFromNow(),
123-
})
119+
}
120+
const item = this.options.serializer.serialize(rawItem)
124121

125-
this.l1?.set(key, item, options)
122+
this.l1?.set(key, this.options.serializeL1 ? item : rawItem, options)
126123
await this.l2?.set(key, item, options)
127124
await this.publish({ type: CacheBusMessageType.Set, keys: [key] })
128125

packages/bentocache/src/drivers/memory.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { LRUCache } from 'lru-cache'
22
import { bytes } from '@julr/utils/string/bytes'
3+
import { InvalidArgumentsException } from '@poppinss/utils'
34

45
import { BaseDriver } from './base_driver.js'
56
import type {
@@ -34,6 +35,12 @@ export class MemoryDriver extends BaseDriver implements L1CacheDriver {
3435
return
3536
}
3637

38+
if (config.serialize === false && (config.maxEntrySize || config.maxSize)) {
39+
throw new InvalidArgumentsException(
40+
'Cannot use maxSize or maxEntrySize when serialize is set to `false`',
41+
)
42+
}
43+
3744
this.#cache = new LRUCache({
3845
max: config.maxItems ?? 1000,
3946
maxEntrySize: config.maxEntrySize ? bytes.parse(config.maxEntrySize) : undefined,

packages/bentocache/src/types/options/drivers_options.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,21 @@ export type MemoryConfig = {
7878
* it will NOT be stored
7979
*/
8080
maxEntrySize?: Bytes
81+
82+
/**
83+
* Should the entries be serialized before storing
84+
* them in the cache.
85+
*
86+
* Note that, if unset, you cannot use maxSize or maxEntrySize
87+
* since the size of deserialized objects cannot be calculated.
88+
*
89+
* **Also make sure to read the below documentation. This option
90+
* can cause issues if not used correctly.**
91+
*
92+
* @see http://bentocache.dev/docs/cache-drivers#serialize-option
93+
* @default true
94+
*/
95+
serialize?: boolean
8196
} & DriverCommonOptions
8297

8398
/**

0 commit comments

Comments
 (0)