Skip to content

Commit 2893f46

Browse files
committed
fix: optimize cookie value comparison using FNV-1a hash
- Add FNV-1a hash function for fast object comparison - Use hash-based comparison to avoid unnecessary cookie updates - Add tests for repeated identical object values and large objects
1 parent a6ea431 commit 2893f46

File tree

2 files changed

+54
-4
lines changed

2 files changed

+54
-4
lines changed

src/cookies.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,23 @@ import { InvalidCookieSignature } from './error'
88
import type { Context } from './context'
99
import type { Prettify } from './types'
1010

11+
// FNV-1a hash for fast object comparison
12+
const hashObject = (obj: unknown): number => {
13+
const FNV_OFFSET_BASIS = 2166136261
14+
const FNV_PRIME = 16777619
15+
16+
let hash = FNV_OFFSET_BASIS
17+
const str = JSON.stringify(obj)
18+
const len = str.length
19+
20+
for (let i = 0; i < len; i++) {
21+
hash ^= str.charCodeAt(i)
22+
hash = Math.imul(hash, FNV_PRIME)
23+
}
24+
25+
return hash >>> 0
26+
}
27+
1128
export interface CookieOptions {
1229
/**
1330
* Specifies the value for the {@link https://tools.ietf.org/html/rfc6265#section-5.2.3|Domain Set-Cookie attribute}. By default, no
@@ -125,6 +142,8 @@ export type ElysiaCookie = Prettify<
125142
type Updater<T> = T | ((value: T) => T)
126143

127144
export class Cookie<T> implements ElysiaCookie {
145+
private valueHash?: number
146+
128147
constructor(
129148
private name: string,
130149
private jar: Record<string, ElysiaCookie>,
@@ -170,10 +189,18 @@ export class Cookie<T> implements ElysiaCookie {
170189
value !== null
171190
) {
172191
try {
173-
if (JSON.stringify(current) === JSON.stringify(value)) return
174-
} catch {
175-
// If stringify fails, proceed with setting the value
176-
}
192+
const newHash = hashObject(value)
193+
194+
if (this.valueHash !== undefined && this.valueHash !== newHash) {
195+
this.valueHash = newHash
196+
} else {
197+
if (JSON.stringify(current) === JSON.stringify(value)) {
198+
this.valueHash = newHash
199+
return
200+
}
201+
this.valueHash = newHash
202+
}
203+
} catch {}
177204
}
178205

179206
// Only create entry in jar if value actually changed

test/cookie/unchanged.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,4 +106,27 @@ describe('Cookie - Unchanged Values', () => {
106106

107107
expect(response.headers.getAll('set-cookie').length).toBeGreaterThan(0)
108108
})
109+
110+
it('should handle repeated identical object values', async () => {
111+
const app = new Elysia().post('/', ({ cookie: { data } }) => {
112+
data.value = { id: 123, name: 'test' }
113+
data.value = { id: 123, name: 'test' }
114+
return 'ok'
115+
})
116+
117+
const res = await app.handle(new Request('http://localhost/', { method: 'POST' }))
118+
expect(res.headers.getAll('set-cookie').length).toBe(1)
119+
})
120+
121+
it('should handle large object values efficiently', async () => {
122+
const large = { users: Array.from({ length: 100 }, (_, i) => ({ id: i, name: `User ${i}` })) }
123+
const app = new Elysia().post('/', ({ cookie: { data } }) => {
124+
data.value = large
125+
data.value = JSON.parse(JSON.stringify(large))
126+
return 'ok'
127+
})
128+
129+
const res = await app.handle(new Request('http://localhost/', { method: 'POST' }))
130+
expect(res.headers.getAll('set-cookie').length).toBe(1)
131+
})
109132
})

0 commit comments

Comments
 (0)