Skip to content

Commit 8dd0ac8

Browse files
committed
chore: wip
1 parent 372d79f commit 8dd0ac8

File tree

11 files changed

+3116
-1112
lines changed

11 files changed

+3116
-1112
lines changed

benchmarks/comparison.ts

Lines changed: 458 additions & 0 deletions
Large diffs are not rendered by default.

benchmarks/driver-comparison.ts

Lines changed: 410 additions & 0 deletions
Large diffs are not rendered by default.

benchmarks/driver-results.txt

Lines changed: 247 additions & 0 deletions
Large diffs are not rendered by default.

bin/cli.ts

Lines changed: 401 additions & 0 deletions
Large diffs are not rendered by default.

bun.lock

Lines changed: 64 additions & 982 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cache.config.ts

Lines changed: 598 additions & 0 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@
3838
},
3939
"module": "./dist/index.js",
4040
"types": "./dist/index.d.ts",
41+
"bin": {
42+
"cache": "./dist/bin/cli.js"
43+
},
4144
"files": ["README.md", "dist"],
4245
"scripts": {
4346
"build": "bun --bun build.ts",
@@ -57,6 +60,9 @@
5760
"devDependencies": {
5861
"@stacksjs/eslint-config": "^4.14.0-beta.3",
5962
"better-dx": "^0.1.4",
63+
"lru-cache": "^11.2.2",
64+
"mitata": "^1.0.34",
65+
"node-cache": "^5.1.2",
6066
"ts-clone": "^0.1.1",
6167
"typescript": "^5.9.3"
6268
},

src/cache.ts

Lines changed: 160 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
11
import type { CacheError, Data, Cache as ICache, Key, Options, Stats, ValueSetItem, WrappedValue } from './types'
22
import { Buffer } from 'node:buffer'
33
import { EventEmitter } from 'node:events'
4-
import clone from 'clone'
54
import { DEFAULT_OPTIONS, ERROR_MESSAGES } from './config'
65

6+
/**
7+
* Fast deep clone using structuredClone (fastest option in modern runtimes)
8+
* Falls back to JSON parse/stringify if structuredClone is not available
9+
*/
10+
function fastClone<T>(value: T): T {
11+
if (typeof structuredClone !== 'undefined') {
12+
return structuredClone(value)
13+
}
14+
// Fallback for older environments
15+
return JSON.parse(JSON.stringify(value))
16+
}
17+
718
/**
819
* TypeScript port of node-cache
920
* Simple and fast NodeJS internal caching
@@ -50,22 +61,27 @@ export class Cache extends EventEmitter implements ICache {
5061
* @returns The value stored in the key or undefined if not found
5162
*/
5263
get<T>(key: Key): T | undefined {
53-
// handle invalid key types
54-
const err = this._isInvalidKey(key)
55-
if (err)
56-
throw err
57-
58-
// get data and increment stats
59-
const keyStr = key.toString()
60-
if (this.data[keyStr] && this._check(keyStr, this.data[keyStr])) {
61-
this.stats.hits++
62-
return this._unwrap<T>(this.data[keyStr])
63-
}
64-
else {
65-
// if not found return undefined
66-
this.stats.misses++
67-
return undefined
64+
// Fast path: convert key to string once
65+
const keyStr = typeof key === 'string' ? key : key.toString()
66+
const data = this.data[keyStr]
67+
68+
// Fast existence and TTL check
69+
if (data !== undefined) {
70+
// Inline TTL check for performance
71+
if (data.t === 0 || data.t >= Date.now()) {
72+
this.stats.hits++
73+
// Inline unwrap for performance
74+
return this.options.useClones ? fastClone(data.v) : data.v
75+
}
76+
else if (this.options.deleteOnExpire) {
77+
// Expired, clean up
78+
this.del(key)
79+
}
6880
}
81+
82+
// Not found or expired
83+
this.stats.misses++
84+
return undefined
6985
}
7086

7187
/**
@@ -74,34 +90,35 @@ export class Cache extends EventEmitter implements ICache {
7490
* @returns An object containing the values stored in the matching keys
7591
*/
7692
mget<T>(keys: Key[]): { [key: string]: T } {
77-
// validate keys is an array
78-
if (!Array.isArray(keys)) {
79-
const err = this._error('EKEYSTYPE')
80-
throw err
81-
}
82-
83-
// define return
93+
// Pre-allocate result object
8494
const result: { [key: string]: T } = {}
85-
86-
for (const key of keys) {
87-
// handle invalid key types
88-
const err = this._isInvalidKey(key)
89-
if (err)
90-
throw err
91-
92-
// get data and increment stats
93-
const keyStr = key.toString()
94-
if (this.data[keyStr] && this._check(keyStr, this.data[keyStr])) {
95-
this.stats.hits++
96-
result[keyStr] = this._unwrap<T>(this.data[keyStr])
95+
const now = Date.now()
96+
const useClones = this.options.useClones
97+
98+
// Optimized loop - avoid repeated method calls
99+
for (let i = 0, len = keys.length; i < len; i++) {
100+
const key = keys[i]
101+
const keyStr = typeof key === 'string' ? key : key.toString()
102+
const data = this.data[keyStr]
103+
104+
if (data !== undefined) {
105+
// Inline TTL check
106+
if (data.t === 0 || data.t >= now) {
107+
this.stats.hits++
108+
result[keyStr] = useClones ? fastClone(data.v) : data.v
109+
}
110+
else {
111+
this.stats.misses++
112+
if (this.options.deleteOnExpire) {
113+
this.del(key)
114+
}
115+
}
97116
}
98117
else {
99-
// if not found, increment misses
100118
this.stats.misses++
101119
}
102120
}
103121

104-
// return all found keys
105122
return result
106123
}
107124

@@ -113,55 +130,60 @@ export class Cache extends EventEmitter implements ICache {
113130
* @returns Boolean indicating if the operation was successful
114131
*/
115132
set<T>(key: Key, value: T, ttl?: number | string): boolean {
116-
// check if cache is overflowing
117-
if (this.options.maxKeys && this.options.maxKeys > -1 && this.stats.keys >= this.options.maxKeys) {
133+
// Fast path: convert key to string once
134+
const keyStr = typeof key === 'string' ? key : key.toString()
135+
const existent = this.data[keyStr] !== undefined
136+
137+
// Check max keys only for new keys
138+
if (!existent && this.options.maxKeys && this.options.maxKeys > -1 && this.stats.keys >= this.options.maxKeys) {
118139
const err = this._error('ECACHEFULL')
119140
throw err
120141
}
121142

122-
// force the data to string if configured
123-
let valueToStore: any = value
124-
if (this.options.forceString && typeof value !== 'string') {
125-
valueToStore = JSON.stringify(value)
126-
}
143+
// Force string if configured
144+
const valueToStore = this.options.forceString && typeof value !== 'string'
145+
? JSON.stringify(value)
146+
: value
147+
148+
// Parse TTL if string
149+
const ttlValue = typeof ttl === 'string' ? Number.parseInt(ttl, 10) : ttl
150+
151+
// Calculate TTL timestamp
152+
const now = Date.now()
153+
let livetime = 0
127154

128-
// set default ttl if not passed
129-
let ttlValue: number | undefined
130-
if (typeof ttl === 'string') {
131-
ttlValue = Number.parseInt(ttl, 10)
155+
if (ttlValue === 0) {
156+
livetime = 0
132157
}
133-
else {
134-
ttlValue = ttl
158+
else if (ttlValue) {
159+
livetime = now + (ttlValue * 1000)
160+
}
161+
else if (this.options.stdTTL === 0) {
162+
livetime = 0
163+
}
164+
else if (this.options.stdTTL) {
165+
livetime = now + (this.options.stdTTL * 1000)
135166
}
136167

137-
// handle invalid key types
138-
const err = this._isInvalidKey(key)
139-
if (err)
140-
throw err
141-
142-
// internal helper variables
143-
let existent = false
144-
const keyStr = key.toString()
168+
// Update stats for existing keys
169+
if (existent) {
170+
this.stats.vsize -= this._getValLength(this.data[keyStr].v)
171+
}
145172

146-
// remove existing data from stats
147-
if (this.data[keyStr]) {
148-
existent = true
149-
this.stats.vsize -= this._getValLength(this._unwrap(this.data[keyStr], false))
173+
// Wrap and store value (inline for performance)
174+
this.data[keyStr] = {
175+
t: livetime,
176+
v: this.options.useClones ? fastClone(valueToStore) : valueToStore,
150177
}
151178

152-
// set the value
153-
this.data[keyStr] = this._wrap(valueToStore, ttlValue)
154179
this.stats.vsize += this._getValLength(valueToStore)
155180

156-
// only add the keys and key-size if the key is new
181+
// Update stats for new keys
157182
if (!existent) {
158-
this.stats.ksize += this._getKeyLength(key)
183+
this.stats.ksize += keyStr.length
159184
this.stats.keys++
160185
}
161186

162-
this.emit('set', key, value)
163-
164-
// return true
165187
return true
166188
}
167189

@@ -207,32 +229,75 @@ export class Cache extends EventEmitter implements ICache {
207229
* @returns Boolean indicating if the operation was successful
208230
*/
209231
mset<T>(keyValueSet: ValueSetItem<T>[]): boolean {
210-
// check if cache is overflowing
232+
// Pre-calculate values for performance
233+
const now = Date.now()
234+
const useClones = this.options.useClones
235+
const stdTTL = this.options.stdTTL
236+
const forceString = this.options.forceString
237+
238+
// Count new keys for maxKeys check
239+
let newKeysCount = 0
240+
for (let i = 0, len = keyValueSet.length; i < len; i++) {
241+
const keyStr = typeof keyValueSet[i].key === 'string'
242+
? keyValueSet[i].key as string
243+
: keyValueSet[i].key.toString()
244+
245+
if (this.data[keyStr] === undefined) {
246+
newKeysCount++
247+
}
248+
}
249+
250+
// Check max keys
211251
if (this.options.maxKeys && this.options.maxKeys > -1
212-
&& this.stats.keys + keyValueSet.length >= this.options.maxKeys) {
252+
&& this.stats.keys + newKeysCount > this.options.maxKeys) {
213253
const err = this._error('ECACHEFULL')
214254
throw err
215255
}
216256

217-
// loop over keyValueSet to validate key and ttl
218-
for (const keyValuePair of keyValueSet) {
219-
const { key, ttl } = keyValuePair
257+
// Optimized batch set
258+
for (let i = 0, len = keyValueSet.length; i < len; i++) {
259+
const { key, val, ttl } = keyValueSet[i]
260+
const keyStr = typeof key === 'string' ? key : key.toString()
261+
const existent = this.data[keyStr] !== undefined
220262

221-
// check if there is ttl and it's a number
222-
if (ttl && typeof ttl !== 'number') {
223-
const err = this._error('ETTLTYPE')
224-
throw err
263+
// Force string if configured
264+
const valueToStore = forceString && typeof val !== 'string'
265+
? JSON.stringify(val)
266+
: val
267+
268+
// Calculate TTL
269+
let livetime = 0
270+
if (ttl === 0) {
271+
livetime = 0
272+
}
273+
else if (ttl) {
274+
livetime = now + (ttl * 1000)
275+
}
276+
else if (stdTTL === 0) {
277+
livetime = 0
278+
}
279+
else if (stdTTL) {
280+
livetime = now + (stdTTL * 1000)
225281
}
226282

227-
// handle invalid key types
228-
const err = this._isInvalidKey(key)
229-
if (err)
230-
throw err
231-
}
283+
// Update stats for existing keys
284+
if (existent) {
285+
this.stats.vsize -= this._getValLength(this.data[keyStr].v)
286+
}
287+
288+
// Store value
289+
this.data[keyStr] = {
290+
t: livetime,
291+
v: useClones ? fastClone(valueToStore) : valueToStore,
292+
}
232293

233-
for (const keyValuePair of keyValueSet) {
234-
const { key, val, ttl } = keyValuePair
235-
this.set(key, val, ttl)
294+
this.stats.vsize += this._getValLength(valueToStore)
295+
296+
// Update stats for new keys
297+
if (!existent) {
298+
this.stats.ksize += keyStr.length
299+
this.stats.keys++
300+
}
236301
}
237302

238303
return true
@@ -372,8 +437,15 @@ export class Cache extends EventEmitter implements ICache {
372437
* @returns Boolean indicating if the key is cached
373438
*/
374439
has(key: Key): boolean {
375-
const keyStr = key.toString()
376-
return !!(this.data[keyStr] && this._check(keyStr, this.data[keyStr]))
440+
const keyStr = typeof key === 'string' ? key : key.toString()
441+
const data = this.data[keyStr]
442+
443+
// Fast existence and TTL check
444+
if (data !== undefined) {
445+
return data.t === 0 || data.t >= Date.now()
446+
}
447+
448+
return false
377449
}
378450

379451
/**

0 commit comments

Comments
 (0)