Skip to content

Conversation

@YasserGomma
Copy link
Contributor

@YasserGomma YasserGomma commented Nov 23, 2025

Summary

This PR optimizes cookie value comparison by implementing FNV-1a hashing to avoid unnecessary cookie updates when object values haven't changed. This reduces redundant Set-Cookie headers and improves performance for applications that frequently set cookie values.

Problem

Previously, when setting cookie values (especially objects), the implementation would perform expensive JSON.stringify() comparisons on every set operation, even when the value hadn't changed. This could lead to:

  • Unnecessary Set-Cookie headers being sent
  • Performance overhead from repeated string serialization
  • Potential issues with cookie size limits when dealing with large objects

Solution

Implemented FNV-1a (Fowler–Noll–Vo) hash-based comparison for object values:

  1. Hash Function: Added a fast FNV-1a hash implementation that generates a 32-bit hash from the JSON stringified value
  2. Hash Caching: Store the hash of the current value to enable fast comparison on subsequent sets
  3. Optimized Comparison: Use hash comparison first, falling back to full JSON comparison only when hashes match (to handle potential collisions)
  4. Performance Optimization: Cache JSON.stringify(value) result to avoid duplicate serialization calls

Changes

  • Added hashString() function implementing FNV-1a algorithm
  • Added valueHash private property to Cookie class for caching
  • Modified value setter to use hash-based comparison for objects
  • Optimized to cache JSON.stringify result to avoid duplicate calls
  • Improved code comments explaining the hash comparison logic flow and JSON.stringify behavior
  • Enhanced test coverage with more realistic scenarios and descriptive test names
  • Added test for multiple assignments of the same object in a single request

Benefits

  • Performance: Hash comparison is O(1) for comparison (after O(n) hash computation), avoiding expensive string comparisons in most cases
  • Efficiency: Prevents unnecessary cookie updates when values are unchanged
  • Optimization: Caches JSON.stringify result to avoid duplicate serialization
  • Scalability: Better performance with large object values
  • Backward Compatible: No breaking changes, maintains existing behavior
  • Code Quality: Improved documentation and test coverage

Testing

Added comprehensive test cases:

  • should not send set-cookie header when setting same object value as incoming cookie: Verifies that setting the same object value as an incoming cookie doesn't trigger a Set-Cookie header
  • should not send set-cookie header for large unchanged object values: Tests performance with large objects (100+ items) to ensure hash comparison works correctly
  • should optimize multiple assignments of same object in single request: Ensures multiple assignments of the same value in one request only produce one Set-Cookie header

All existing tests continue to pass, ensuring backward compatibility.

Technical Details

The FNV-1a hash algorithm was chosen for its:

  • Fast computation speed
  • Good distribution properties
  • Simplicity and low memory overhead
  • 32-bit output suitable for JavaScript number type

Hash collisions are handled by falling back to full JSON string comparison when hashes match, ensuring correctness while maintaining performance benefits in the common case.

The implementation includes:

  • Clear comments explaining when hash comparison is used vs deep comparison
  • Note about JSON.stringify key order behavior (objects with same keys in different order are treated as different)
  • Proper handling of first-time value sets (when hash is undefined)
  • Efficient hash caching to avoid recomputation
  • Cached JSON.stringify result to prevent duplicate serialization

Summary by CodeRabbit

  • Performance
    • Improved cookie handling to detect unchanged object values and skip redundant set-cookie headers, reducing bandwidth and improving response times for repeated or large cookie updates.
    • Added safe hash caching to optimize repeated assignments while ensuring the cache is invalidated when cookies are updated directly or via update/set.
  • Tests
    • Added tests verifying unchanged-value skips, large-value optimization, repeated-assignment behavior, and cache invalidation.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 23, 2025

Walkthrough

Adds an FNV-1a string hash utility and a private valueHash?: number cache to Cookie<T> in src/cookies.ts; the value setter uses the hash to short-circuit deep stringified comparisons for object values, falls back to JSON.stringify (try/catch) when needed, and invalidates the cache when the jar is mutated. Five tests were added to validate unchanged-value behavior.

Changes

Cohort / File(s) Change Summary
Core Implementation
src/cookies.ts
Add hashString (FNV-1a) utility; introduce private valueHash?: number on Cookie; update value setter to early-return for strict equality, compute JSON.stringify and hash for objects, compare with cached valueHash to avoid unnecessary jar writes, fall back to stringified deep-compare when hashes are inconclusive, update/clear valueHash appropriately, and ensure cache invalidation when the jar is mutated directly.
Test Coverage
test/cookie/unchanged.test.ts
Add five tests under "Cookie - Unchanged Values" verifying no set-cookie header for unchanged object values (including large objects), optimization across multiple assignments within a request, and that update()/set() paths invalidate the hash cache.

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant Cookie as Cookie.value setter
    participant Hash as hashString (FNV-1a)
    participant JSON as JSON.stringify

    Caller->>Cookie: set value
    activate Cookie

    alt strict equality
        Cookie->>Cookie: short-circuit return (no jar write)
    else object value
        Cookie->>JSON: JSON.stringify(new) => valueStr
        JSON-->>Cookie: valueStr

        Cookie->>Hash: hashString(valueStr) => newHash
        Hash-->>Cookie: newHash

        alt valueHash defined & newHash != valueHash
            Cookie->>Cookie: update valueHash\nproceed to set jar
        else valueHash defined & newHash == valueHash
            Cookie->>JSON: JSON.stringify(current) => currentStr
            JSON-->>Cookie: currentStr (or throw)
            alt currentStr == valueStr
                Cookie->>Cookie: update valueHash (if needed)\nreturn early (no jar write)
            else
                Cookie->>Cookie: update valueHash\nproceed to set jar
            end
        else valueHash undefined
            Cookie->>Cookie: set valueHash\nproceed to set jar
        end
    end

    deactivate Cookie
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Verify correctness and edge cases of the FNV-1a implementation (integer overflow/bit masking).
  • Confirm all setter branches correctly update/clear valueHash and do not cause extra jar entries.
  • Check try/catch scope around JSON.stringify to ensure errors are handled without hiding other failures.
  • Review added tests for coverage of large objects and cache invalidation paths.

Possibly related PRs

Poem

🐇
I nibble at strings with a tiny hash,
hop past repeats in a tidy dash,
when JSON matches what was before,
I fold my ears and skip the chore,
fewer crumbs left on the cachey path.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: Optimize cookie value comparison using FNV-1a hash' accurately describes the main change: implementing FNV-1a hash-based optimization for cookie value comparison to avoid unnecessary Set-Cookie updates.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
test/cookie/unchanged.test.ts (1)

110-131: New tests don’t meaningfully exercise the hash-based optimization

Both tests assert that repeated assignments of the same cookie (data) yield exactly one set-cookie header. Given that serializeCookie iterates over a map of cookie names, there will only ever be one header per cookie key per response, regardless of how many times .value is assigned in a single handler.

So these tests will still pass even if the hash-based early-return logic is completely removed or broken, as long as we set data at least once.

If your goal is to guard the “unchanged object = no Set-Cookie” behavior, consider adding a test that:

  • Sends an incoming data cookie with a JSON object value, and
  • Reassigns an object with the same serialized shape, and
  • Asserts that the response has zero set-cookie headers.

The existing tests are fine as smoke tests, but they don’t protect the new optimization path.

src/cookies.ts (1)

11-26: Hash-based equality is correct but gives limited benefit and introduces extra JSON.stringify calls

From a correctness standpoint the new logic looks sound:

  • You only early-return when JSON.stringify(current) === JSON.stringify(value), so you won’t skip setting a changed cookie.
  • Hash collisions are handled by doing a stringify comparison when hashes match; in the collision case you’ll still treat the value as changed.

However, there are a couple of non-trivial performance/design issues:

  1. Extra serialization in common object cases

    In the object branch you always call:

    • hashObject(value) → internally does JSON.stringify(value), and
    • In the else block, JSON.stringify(current) and JSON.stringify(value) again.

    So:

    • When valueHash is undefined (first object comparison) you now serialize value twice and current once (3 stringifications) vs 2 before.
    • When valueHash is defined and newHash === valueHash (unchanged object by hash), you again serialize value twice and current once (3 stringifications), even though this is exactly the “unchanged” path you are trying to optimize.

    The only case that benefits is when valueHash is defined and newHash !== valueHash (changed value): you pay 1 stringify (for value) instead of 2. That’s good, but narrower than intended and partially offset by the regressions above.

    You can keep the collision-safety while avoiding the redundant work with something like:

    if (
    	typeof current === 'object' &&
    	current !== null &&
    	typeof value === 'object' &&
    	value !== null
    ) {
    	try {
    		const newHash = hashObject(value)
    
    		// Only fall back to deep compare when the hash matches a previous one
    		if (this.valueHash !== undefined && this.valueHash === newHash) {
    			if (JSON.stringify(current) === JSON.stringify(value)) {
    				// unchanged
    				return
    			}
    		}
    
    		this.valueHash = newHash
    	} catch {}
    }

    This still:

    • Uses the hash as a quick “probably changed” filter.
    • Only pays the JSON.stringify(current/value) deep-equality cost when hashes match.
    • Avoids the redundant this.valueHash assignments and the unnecessary stringify path when valueHash is undefined.
  2. Hash cache is per Cookie instance, not per cookie key

    Because createCookieJar returns a new Cookie instance on each property access, valueHash only persists for the lifetime of that instance (essentially within a single handler invocation for that destructured cookie). There’s no cross-request caching; each request reconstructs a new Cookie with a fresh valueHash.

    That means the optimization only helps when you assign to the same cookie’s .value multiple times in the same request. If the intent was to speed up “incoming cookie object is unchanged this request” checks across many requests, you likely need to store the hash alongside the cookie value in jar/initial instead of on the transient Cookie instance.

These aren’t correctness bugs, but they do undercut the performance win you’re aiming for. If you’re comfortable with the narrower optimization scope, I’d at least recommend simplifying the branch as above to remove the extra JSON.stringify calls.

Also applies to: 145-145, 177-209

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a6ea431 and 2893f46.

📒 Files selected for processing (2)
  • src/cookies.ts (3 hunks)
  • test/cookie/unchanged.test.ts (1 hunks)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/cookies.ts (2)

184-207: Optimize: JSON.stringify called twice on the same value when hashes match.

When hashes match (line 195 condition false → line 200), JSON.stringify(value) is invoked twice:

  1. Inside hashObject(value) on line 192
  2. In the comparison on line 200

This redundant serialization reduces the optimization benefit, especially for large objects.

Refactor to cache the stringified value:

-		// For objects, use hash-based comparison for performance
 		if (
 			typeof current === 'object' &&
 			current !== null &&
 			typeof value === 'object' &&
 			value !== null
 		) {
 			try {
-				const newHash = hashObject(value)
+				const valueStr = JSON.stringify(value)
+				const newHash = hashString(valueStr)
 
 				// If hash differs from cached hash, value definitely changed
 				if (this.valueHash !== undefined && this.valueHash !== newHash) {
 					this.valueHash = newHash
 				}
 				// First set (valueHash undefined) OR hashes match: do deep comparison
 				else {
-					if (JSON.stringify(current) === JSON.stringify(value)) {
+					if (JSON.stringify(current) === valueStr) {
 						this.valueHash = newHash
 						return // Values are identical, skip update
 					}
 					this.valueHash = newHash
 				}
 			} catch {}
 		}

Where hashString is a refactored helper:

const hashString = (str: string): number => {
	const FNV_OFFSET_BASIS = 2166136261
	const FNV_PRIME = 16777619

	let hash = FNV_OFFSET_BASIS
	const len = str.length

	for (let i = 0; i < len; i++) {
		hash ^= str.charCodeAt(i)
		hash = Math.imul(hash, FNV_PRIME)
	}

	return hash >>> 0
}

206-206: Consider logging errors in the catch block.

The empty catch block silently suppresses errors from hashObject() or JSON.stringify(). While acceptable as a fallback (allowing the setter to proceed with the update), it may hide issues during development.

Consider adding minimal logging:

-			} catch {}
+			} catch (err) {
+				// Fallback to always updating on hashing/comparison errors
+				// (e.g., circular references, BigInt values)
+				if (process.env.NODE_ENV === 'development') {
+					console.warn('Cookie value comparison error:', err)
+				}
+			}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2893f46 and f8c7ca0.

📒 Files selected for processing (2)
  • src/cookies.ts (3 hunks)
  • test/cookie/unchanged.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/cookie/unchanged.test.ts
🔇 Additional comments (1)
src/cookies.ts (1)

145-145: LGTM!

The private valueHash field appropriately caches the hash for comparison on subsequent sets.

src/cookies.ts Outdated
Comment on lines 11 to 26
// FNV-1a hash for fast object comparison
const hashObject = (obj: unknown): number => {
const FNV_OFFSET_BASIS = 2166136261
const FNV_PRIME = 16777619

let hash = FNV_OFFSET_BASIS
const str = JSON.stringify(obj)
const len = str.length

for (let i = 0; i < len; i++) {
hash ^= str.charCodeAt(i)
hash = Math.imul(hash, FNV_PRIME)
}

return hash >>> 0
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

JSON.stringify key ordering may defeat optimization for semantically equal objects.

The hashObject function uses JSON.stringify to serialize objects before hashing. Since JSON.stringify preserves property insertion order (not sorted), two semantically identical objects with different key orders will produce different hashes. For example:

  • {a: 1, b: 2} → hash X
  • {b: 2, a: 1} → hash Y (different)

This causes unnecessary Set-Cookie headers even when objects are semantically equal, defeating the stated PR objective of reducing redundant headers.

Consider one of these solutions:

Solution 1: Sort keys before stringifying

-const hashObject = (obj: unknown): number => {
+const hashObject = (obj: unknown): number => {
 	const FNV_OFFSET_BASIS = 2166136261
 	const FNV_PRIME = 16777619
 
 	let hash = FNV_OFFSET_BASIS
-	const str = JSON.stringify(obj)
+	const str = JSON.stringify(obj, Object.keys(obj).sort())
 	const len = str.length
 
 	for (let i = 0; i < len; i++) {
 		hash ^= str.charCodeAt(i)
 		hash = Math.imul(hash, FNV_PRIME)
 	}
 
 	return hash >>> 0
 }

Solution 2: Use a stable serialization library

Alternatively, consider using a library like fast-json-stable-stringify for deterministic serialization regardless of key order.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/cookies.ts around lines 11 to 26, the hashObject function uses
JSON.stringify which preserves property insertion order so semantically equal
objects with different key orders produce different hashes; change to a
deterministic serialization before hashing—either (preferable) import a stable
serializer like fast-json-stable-stringify and use it instead of JSON.stringify,
or implement a small utility that recursively sorts object keys (arrays
preserved) and then JSON.stringify the sorted result; ensure the chosen method
handles primitives, arrays, nested objects, and null, and then pass that stable
string into the existing FNV-1a hashing loop.

- Add FNV-1a hash function for fast object comparison
- Use hash-based comparison to avoid unnecessary cookie updates
- Cache JSON.stringify result to avoid duplicate serialization
- Improve code comments explaining hash comparison logic
- Add comprehensive tests for repeated identical values and large objects
- Rename hashObject to hashString for clarity
@YasserGomma YasserGomma force-pushed the fix/cookie-hash-optimization branch from 25448c0 to 5eb7a8b Compare November 23, 2025 23:34
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/cookies.ts (1)

183-185: Key ordering makes semantically equal objects appear different.

As noted in previous reviews, JSON.stringify preserves property insertion order, so {a: 1, b: 2} and {b: 2, a: 1} produce different hashes and are treated as different values. This causes unnecessary Set-Cookie headers for semantically equal objects, which contradicts the PR's optimization goal.

Consider using a stable serialization approach (sorting keys before stringify or using a library like fast-json-stable-stringify) as suggested in the previous review.

🧹 Nitpick comments (1)
src/cookies.ts (1)

198-208: Consider restructuring for clarity (optional).

The current if-else structure correctly handles hash comparison and collision detection, but the logic flow could be more explicit about the three distinct cases: (1) hash differs → value changed, (2) no cached hash → first set, (3) hashes match → potential collision.

Example restructuring for clarity:

 try {
 	// Cache stringified value to avoid duplicate stringify calls
 	const valueStr = JSON.stringify(value)
 	const newHash = hashString(valueStr)
 
-	// If hash differs from cached hash, value definitely changed
-	if (this.valueHash !== undefined && this.valueHash !== newHash) {
-		this.valueHash = newHash
-	}
-	// First set (valueHash undefined) OR hashes match: do deep comparison
-	else {
-		if (JSON.stringify(current) === valueStr) {
-			this.valueHash = newHash
-			return // Values are identical, skip update
-		}
-		this.valueHash = newHash
-	}
+	// Fast path: hash differs, value definitely changed
+	if (this.valueHash !== undefined && this.valueHash !== newHash) {
+		this.valueHash = newHash
+		// Fall through to update
+	}
+	// Slow path: first set or potential hash collision, do deep comparison
+	else if (JSON.stringify(current) === valueStr) {
+		this.valueHash = newHash
+		return // Values are identical, skip update
+	} else {
+		this.valueHash = newHash
+		// Fall through to update
+	}
 } catch {}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 25448c0 and 5eb7a8b.

📒 Files selected for processing (2)
  • src/cookies.ts (3 hunks)
  • test/cookie/unchanged.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/cookie/unchanged.test.ts
🔇 Additional comments (1)
src/cookies.ts (1)

11-25: LGTM: FNV-1a implementation is correct.

The hash function correctly implements the FNV-1a algorithm with proper constants, uses Math.imul for 32-bit multiplication, and converts to unsigned 32-bit integer.

type Updater<T> = T | ((value: T) => T)

export class Cookie<T> implements ElysiaCookie {
private valueHash?: number
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Stale valueHash when jar is updated via set(), update(), or remove() methods.

The valueHash cache can become stale when the jar is updated directly through the cookie setter (bypassing the value setter). This happens when calling set(), update(), or remove() methods, leading to incorrect change detection.

Example:

cookie.value = {a: 1}            // valueHash = hash({a: 1})
cookie.set({value: {b: 2}})      // jar updated, but valueHash still = hash({a: 1})
cookie.value = {b: 2}            // Compares against stale hash, treats as changed

Apply this diff to invalidate the cache when the jar is updated directly:

 set cookie(jar: ElysiaCookie) {
 	if (!(this.name in this.jar)) this.jar[this.name] = this.initial
 	this.jar[this.name] = jar
+	this.valueHash = undefined  // Invalidate cache when jar is updated
 }

Also applies to: 156-160, 168-170

🤖 Prompt for AI Agents
In src/cookies.ts around lines 144 (and also 156-160, 168-170), the cached
private field valueHash can become stale when the internal jar is modified
directly (via set(), update(), remove(), or when the cookie setter updates the
jar), causing incorrect change detection; fix by invalidating the cache whenever
the jar is mutated — i.e., after any direct jar update inside set(), update(),
remove(), and in the cookie setter that assigns into the jar, assign valueHash =
undefined (or otherwise clear the cache) so subsequent reads compute a fresh
hash.

- Invalidate hash cache when cookie jar is modified directly
- Add tests for cache invalidation with update() and set() methods
- Ensures hash cache stays consistent when value is changed through alternative methods
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/cookies.ts (1)

185-211: Key ordering in JSON.stringify defeats optimization for semantically equal objects.

The hash-based comparison uses JSON.stringify which preserves property insertion order. Two semantically identical objects with different key orders will produce different hashes and be treated as different values, causing unnecessary Set-Cookie headers:

{a: 1, b: 2}  // hash X
{b: 2, a: 1}  // hash Y (different)

While the comments on lines 186-187 acknowledge this limitation, it contradicts the PR objective of reducing redundant headers.

Consider implementing stable serialization by sorting keys before hashing. For a lightweight solution, recursively sort object keys before calling JSON.stringify:

const stableStringify = (obj: unknown): string => {
  if (obj === null || typeof obj !== 'object') return JSON.stringify(obj)
  if (Array.isArray(obj)) return JSON.stringify(obj.map(stableStringify))
  const sorted = Object.keys(obj).sort().reduce((acc, key) => {
    acc[key] = (obj as any)[key]
    return acc
  }, {} as any)
  return JSON.stringify(sorted)
}

Alternatively, use a library like fast-json-stable-stringify for production-ready stable serialization.

🧹 Nitpick comments (1)
src/cookies.ts (1)

211-211: Add comment explaining empty catch block.

The empty catch block silently swallows JSON.stringify errors (e.g., circular references). While the fallback behavior (updating the jar) is safe, a brief comment would clarify the intent.

-			} catch {}
+			} catch {
+				// Stringify failed (e.g., circular ref); fall through to update jar
+			}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5eb7a8b and dffde51.

📒 Files selected for processing (2)
  • src/cookies.ts (4 hunks)
  • test/cookie/unchanged.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/cookie/unchanged.test.ts
🔇 Additional comments (3)
src/cookies.ts (3)

11-25: LGTM - FNV-1a implementation looks correct.

The hash function follows the standard FNV-1a algorithm with correct constants and operations. The use of Math.imul ensures proper 32-bit integer multiplication, and the unsigned right shift ensures a positive 32-bit result.


144-144: LGTM - cache field properly declared.

The private optional field is appropriate for caching the hash value.


160-161: LGTM - cache invalidation properly implemented.

The hash cache is correctly invalidated when the jar is modified directly. Since set(), update(), and remove() all eventually call this setter (via the setCookie setter), the cache is properly invalidated for all mutation paths.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/cookies.ts (1)

186-193: Key ordering defeats optimization for semantically equal objects.

The comment on lines 186-187 acknowledges that JSON.stringify preserves insertion order, so {a: 1, b: 2} and {b: 2, a: 1} are treated as different values even though they're semantically identical. This defeats the stated PR objective of reducing redundant Set-Cookie headers when values are "unchanged."

As noted in the previous review, consider either:

  1. Sorting keys before stringifying, or
  2. Using a stable serialization library like fast-json-stable-stringify

However, if the current behavior is intentional to keep the implementation simple and performant, the existing comment adequately documents the limitation.

🧹 Nitpick comments (1)
src/cookies.ts (1)

211-211: Consider logging or documenting the empty catch block.

The empty catch at line 211 silently ignores JSON.stringify errors (e.g., circular references, BigInt without custom serializer). When serialization fails, the code falls through to update the jar anyway, potentially sending unnecessary Set-Cookie headers for non-serializable objects.

Consider adding a comment explaining this behavior, or optionally log the error for debugging:

-			} catch {}
+			} catch (err) {
+				// JSON.stringify failed (circular ref, BigInt, etc.)
+				// Fall through to update jar anyway
+			}

Alternatively, if you want to log for debugging:

-			} catch {}
+			} catch {
+				// Serialization failed; treat as changed value
+			}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dffde51 and 50e49c1.

📒 Files selected for processing (2)
  • src/cookies.ts (4 hunks)
  • test/cookie/unchanged.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/cookie/unchanged.test.ts
🔇 Additional comments (4)
src/cookies.ts (4)

11-25: LGTM! Correct FNV-1a implementation.

The hash function correctly implements the FNV-1a algorithm with proper constants and bit operations. The use of Math.imul ensures 32-bit integer multiplication, and the unsigned conversion via >>> 0 is appropriate.


144-144: LGTM! Appropriate cache field.

The private optional field correctly supports the hash caching strategy, with undefined indicating no cached hash.


156-162: LGTM! Cache invalidation correctly implemented.

The cache is properly invalidated when the jar is modified directly. Since set(), update(), and remove() all delegate through the setCookie setter (which calls this cookie setter), the invalidation applies consistently across all mutation paths.


178-217: Overall optimization approach is sound.

The hash-based comparison correctly implements the fast-path optimization:

  • When hashes differ, skips the expensive JSON.stringify(current) call
  • When hashes match or this is the first set, performs full comparison to guard against collisions
  • Caches the stringified new value (valueStr) to avoid duplicate serialization
  • Correctly handles primitives, null, objects, and arrays
  • Only updates the jar when values actually differ

The performance benefit for large objects (100+ properties) is legitimate, and the collision handling ensures correctness.

@SaltyAom SaltyAom merged commit 66a16a6 into elysiajs:main Nov 26, 2025
1 of 2 checks passed
@akim-bow
Copy link
Contributor

akim-bow commented Nov 26, 2025

@SaltyAom what do you think about this change?
I personally think that this isn't ideal approach for cookie handling in web frameworks.
#1567 There I left explanation of my POV regarding cookie caching.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants