Skip to content

perf(cachekit): an encrypted set() on Node.js awaits no promise around the synchronous encrypt (LAB-12935) - #319

Merged
27Bslash6 merged 6 commits into
mainfrom
lab-12935-encrypted-set-sync
Nov 7, 2026
Merged

27Bslash6 merged 6 commits into
mainfrom
lab-12935-encrypted-set-sync

Conversation

@27Bslash6

@27Bslash6 27Bslash6 commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

On Node.js an encrypted set() no longer awaits the encryption manager around the synchronous NAPI encrypt. In bench:ir, the new secure-set-100b drops from 71,719 to 70,007 instructions per op (−2.39%) and secure-set-get from 490,893 to 488,909 (−0.40%). napi-encrypted reads +0.05%, from a comment-only change. Every A/A spread is 0.00%.

Closes LAB-12935

Change

  • EncryptionManagerCore.encryptNow() (package-internal). This mirrors decryptNow() from #318. Once the keys are derived, a binding that returns a plain value encrypts in the call: it returns the ciphertext, or throws the same NonceExhaustedError or EncryptionError that encrypt() rejects with. Before the keys are derived, after dispose(), and on Workers (WebCrypto encrypts asynchronously) it returns a Promise, so those paths behave as before. The public encrypt() is byte-identical.
  • setEntry awaits the encrypt only when it is a thenable. This is the one call site; it uses the shared isThenable, not instanceof Promise, for the reason #318 gives.
  • Nonce reservation is unchanged. Both bindings reserve the nonce inside their own encrypt call, before their first await, and encryptNow() adds no await before that call.

encrypt() keeps its own copy of the error classifier instead of sharing one with encryptNow(), as decrypt() does: in #318, routing decrypt() through a shared helper changed how V8 inlined napi-encrypted and cost it 5.6% more instructions. A parity test holds the copies equal.

Unchanged: the AAD, the keys, every stored byte, the nonce-exhaustion classification, and the set's failure handling (counted, then thrown with degradation off and absorbed with one rate-limited warning with it on).

Sibling paths

path this PR
set() encrypt, Node.js changed
wrap() miss and SWR refresh write changed: they write through the same setEntry
set() encrypt, Workers not applicable: WebCrypto encrypts asynchronously, so the set still awaits it
encrypted L1 hit and L2 read already changed in #318
plaintext set() unchanged: no encrypt; plaintext-set-64b reads −0.02% and plaintext-set-1kb +0.00%
cachekit-py, cachekit-rs no equivalent cost: a coroutine or future that returns without suspending does not yield

Tests

  • cache.encryption-l1.test.ts: on Node.js, once the keys are derived, an encrypted set() settles in as many microtask turns as a plaintext set(), and stores ciphertext in L1 and L2. With bindings that return Promises, as Workers' do, the set takes more turns and stores the same. With the global Promise replaced by a subclass, a fresh cache's first set(), which encrypts through the async encrypt(), stores ciphertext that a second cache decodes.
  • manager-core.test.ts: encryptNow() returns a Promise before key derivation, a plain value after it, throws NonceExhaustedError in the call on nonce exhaustion, rejects after dispose(), and returns a binding's Promise with its rejection mapped as encrypt() maps it. A parity test runs one failing binding, throwing and rejecting, through decrypt() and decryptNow() and through encrypt() and encryptNow(), and asserts the same error class, message and cause: an Error, nonce exhaustion under both spellings, and a thrown value that is not an Error.
  • utils/thenable.test.ts tests isThenable over a table: a native Promise, a Promise subclass, a plain thenable, an object whose then is not a function, a Uint8Array, a { value } wrapper, null and undefined.
  • encryption.protocol.test.ts: the aad_hex vectors and the v0x03 layout rows now also check the AAD encryptNow() hands the binding.
  • The tests that failed or spied on the cache's encrypt now intercept encryptNow, which the cache calls. The write-failure suite's encrypt step now throws in the call, as the NAPI encrypt does, and one test rejects a Promise, as the Workers bindings do.

Mutants: putting the await back unconditionally in setEntry fails the settle test; checking instanceof Promise instead of a thenable fails the replaced-Promise test; making encryptNow() always go through encrypt() fails eleven tests; dropping the mapping on its Promise branch fails five; dropping one spelling from encryptionFailed()'s nonce check, or changing decryptionFailed()'s fallback message, fails the parity test.

Local runs: on Node.js 24.22.1, vitest run passed 2,885 tests (1 skipped), test:protocol 1,004 and test:workers 307. The full suite also passed on Node.js 22.23.4 and 26.12.1. eslint, type-check, type-check:tests, prettier and check:workers-bundle are clean.

Benchmarks

bench:ir gains secure-set-100b: an encrypted cache.set() with L1 on, 100 B of msgpack, keys derived in setup. It takes the default 2,000 warm-up ops: under --trace-opt, no compile lands in its window after 2,000 (two did after 1,000). Both builds ran 8 workloads with 3 repeats, from trees built as the CI gate builds them, with the same workload file, on the same machine (Node.js 24.22.1, valgrind 3.22.0). The branch side is this PR's head.

workload main this branch delta A/A (branch)
secure-set-100b (new) 71,719 70,007 −2.39% 0.00%
secure-set-get (L1 off) 490,893 488,909 −0.40% 0.00%
napi-encrypted 41,508 41,527 +0.05% 0.00%
secure-get-l1-hit 48,874 48,869 −0.01% 0.00%
secure-wrap-l1-hit-100b 56,929 56,927 −0.00% 0.00%
plaintext-set-64b 42,365 42,357 −0.02% 0.00%
plaintext-set-1kb 220,736 220,746 +0.00% 0.00%
get-l1-hit 2,114 2,113 −0.05% 0.00%

The A/A spread on main is 0.00% for all eight. An encrypted set saves about 1,700 instructions (1,712); secure-set-get saves about 2,000, which includes the set.

napi-encrypted awaits encrypt() and decrypt(), whose code this PR does not change. It read 41,507 (−0.00%) at an earlier commit of this branch; the +19 instructions came with comment-only edits to manager-core.ts, and swapping that commit's manager-core.ts back into the head build restores 41,507. That is code layout, the effect CONTRIBUTING's "Measure it" practice describes, and well inside the workload's 4% gate.

secure-set-100b has no ir-seconds.json row yet, so the shard split counts it as the median row.

@kodus-27b

kodus-27b Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Providing Context (Files & MCPs)

Add these hints in your PR description (or a comment) to unlock deeper checks:

  • Ticket / Acceptance Criteria: `Refs: ABC-123` (Linear/Jira/Asana/ClickUp/Trello) or a direct ticket link.
  • Bugfix Validation: a Sentry/Datadog/Bugsnag event link (or paste the stack trace/error message).
  • Endpoint Risk: mention the route (e.g., `POST /api/payments`) or controller/action name.
  • Attach a repo file as context: use an explicit marker like `@file:docs/guide.mdx#L10-L50` (replace with your real path).
  • API Contract Docs: include `@file:openapi.yaml` or `@file:swagger.json` when changing routes/schemas.
  • Definition of Done / Standards: include `@file:DOD.md` or `@file:CONTRIBUTING.md` if your repo has them.
  • Design System Source of Truth: include `@file:ui/index.ts` (replace with your DS entrypoint path).
  • Feature Flags: include the flag key/name and `@file:flags.ts` / `@file:config.json` (and optionally the PostHog flag name).
  • Edge/CDN Rules: link the Cloudflare rule/zone or describe the intended redirect/header behavior.
  • Attach an MCP tool output: use `@mcp<provider|tool>` (replace with an installed MCP provider + tool, e.g., `@mcp<sentry|events.search>`).
Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ✅

Access your configuration settings here.

Kody Code Review — 1 suggested fix.
Paste the prompt below to your agent and all review fixed at once!

🛠️ Open Agent Prompt
A code review identified the following issues in this pull request.
Each section describes what was found and includes a reference implementation where available.

Files involved:
- packages/cachekit/src/encryption/manager-core.test.ts:469

---

### [1/1] packages/cachekit/src/encryption/manager-core.test.ts:469
Issue identified during code review:
Unsafe type assertion in packages/cachekit/src/encryption/manager-core.test.ts: `return error as Error;` casts the caught value to `Error` without any runtime check, which violates the team rule 'Avoid unsafe type assertions'. When the code under test throws a non-Error value such as a string or plain object, the cast lets it through, and test assertions on `.message` or `instanceof Error` receive `undefined` or fail for the wrong reason. Fix: guard with `if (error instanceof Error) return error;` and otherwise wrap it, for example `return new Error(String(error));`.

---

Review each issue in context, use the reference implementations as guidance, and apply fixes that are consistent with the surrounding codebase.

@coderabbitai

coderabbitai Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

Explain Change →

No actionable comments were generated in the recent review.

Summary by CodeRabbit

  • Performance

    • Encrypted cache writes on Node.js can now complete synchronously once keys are ready, while asynchronous encryption remains supported and awaited.
    • Added a benchmark covering encrypted 100 B cache writes on Node.js with L1 caching enabled.
  • Documentation

    • Clarified that encrypted reads and writes on Node.js use synchronous native operations after key derivation, while Cloudflare Workers use asynchronous WebCrypto operations that reads and writes await.

Walkthrough

The encryption manager and cache write path now support synchronous or asynchronous encryption. Tests cover results, error handling and ciphertext storage. The instruction-count benchmark and documentation include encrypted set() behaviour.

Changes

Encrypted cache writes

Layer / File(s) Summary
Encryption manager contract and implementation
packages/cachekit/src/encryption/manager-core.ts, packages/cachekit/src/encryption/manager-core.test.ts, packages/cachekit/test/protocol/encryption.protocol.test.ts
EncryptionManagerCore.encryptNow returns a direct result when possible and a promise otherwise. It maps nonce exhaustion and other failures. Tests cover return types, error mapping and protocol behaviour.
Cache write integration
packages/cachekit/src/cache-core.ts, packages/cachekit/src/utils/thenable.test.ts, packages/cachekit/src/cache.encryption-l1.test.ts, packages/cachekit/src/cache.l2-write-failure.test.ts, packages/cachekit/src/cache.test.ts
CacheImpl.setEntry calls encryptNow and awaits thenable results. Tests cover encrypted writes, ciphertext storage and encryption failures.
Encrypted set benchmark and documentation
packages/cachekit/bench/ir-workload.mjs, CONTRIBUTING.md, packages/cachekit/README.md
The instruction-count workload list and benchmark include secure-set-100b. The performance note describes synchronous Node.js encryption and asynchronous Cloudflare Workers encryption.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Refactor

Sequence Diagram(s)

sequenceDiagram
  participant CacheImpl
  participant EncryptionManagerCore
  participant EncryptionBinding
  participant MemoryBackend
  CacheImpl->>EncryptionManagerCore: encryptNow(data, cacheKey)
  EncryptionManagerCore->>EncryptionBinding: encrypt data
  EncryptionBinding-->>EncryptionManagerCore: ciphertext or Promise
  EncryptionManagerCore-->>CacheImpl: ciphertext or Promise
  CacheImpl->>MemoryBackend: store ciphertext after encryption resolves
Loading

Possibly related PRs

Merge Risk: Minimal · up to a0b2a

No issue requiring a fix before merge was established. Encrypted writes retain their failure handling while avoiding the encryption await on the synchronous Node.js path.

Pre-merge checks: Passed 5

Check name Status Explanation
Title check Passed The title identifies the main change: avoiding an await around synchronous encryption for encrypted Node.js set() calls.
Description check Passed The description explains the encryption change, its tests, and the benchmark results.
Docstring Coverage Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentat…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 3 functions across 9 files.

Finishing Touches

Generate docstrings

  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)

  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

Recent review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between c0c1533 and a0b2a09.

Files selected for processing: 6

  • packages/cachekit/bench/ir-workload.mjs
  • packages/cachekit/src/cache.encryption-l1.test.ts
  • packages/cachekit/src/encryption/manager-core.test.ts
  • packages/cachekit/src/encryption/manager-core.ts
  • packages/cachekit/src/utils/thenable.test.ts
  • packages/cachekit/test/protocol/encryption.protocol.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

@codecov

codecov Bot commented Nov 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

coderabbitai[bot]
coderabbitai Bot previously approved these changes Nov 7, 2026
@kodus-27b

kodus-27b Bot commented Nov 7, 2026

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Providing Context (Files & MCPs)

Add these hints in your PR description (or a comment) to unlock deeper checks:

  • Ticket / Acceptance Criteria: `Refs: ABC-123` (Linear/Jira/Asana/ClickUp/Trello) or a direct ticket link.
  • Bugfix Validation: a Sentry/Datadog/Bugsnag event link (or paste the stack trace/error message).
  • Endpoint Risk: mention the route (e.g., `POST /api/payments`) or controller/action name.
  • Attach a repo file as context: use an explicit marker like `@file:docs/guide.mdx#L10-L50` (replace with your real path).
  • API Contract Docs: include `@file:openapi.yaml` or `@file:swagger.json` when changing routes/schemas.
  • Definition of Done / Standards: include `@file:DOD.md` or `@file:CONTRIBUTING.md` if your repo has them.
  • Design System Source of Truth: include `@file:ui/index.ts` (replace with your DS entrypoint path).
  • Feature Flags: include the flag key/name and `@file:flags.ts` / `@file:config.json` (and optionally the PostHog flag name).
  • Edge/CDN Rules: link the Cloudflare rule/zone or describe the intended redirect/header behavior.
  • Attach an MCP tool output: use `@mcp<provider|tool>` (replace with an installed MCP provider + tool, e.g., `@mcp<sentry|events.search>`).
Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ✅

Access your configuration settings here.

kodus-27b[bot]
kodus-27b Bot previously approved these changes Nov 7, 2026
@kodus-27b

kodus-27b Bot commented Nov 7, 2026

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the `@kody start-review` command at the root of your PR.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Providing Context (Files & MCPs)

Add these hints in your PR description (or a comment) to unlock deeper checks:

  • Ticket / Acceptance Criteria: `Refs: ABC-123` (Linear/Jira/Asana/ClickUp/Trello) or a direct ticket link.
  • Bugfix Validation: a Sentry/Datadog/Bugsnag event link (or paste the stack trace/error message).
  • Endpoint Risk: mention the route (e.g., `POST /api/payments`) or controller/action name.
  • Attach a repo file as context: use an explicit marker like `@file:docs/guide.mdx#L10-L50` (replace with your real path).
  • API Contract Docs: include `@file:openapi.yaml` or `@file:swagger.json` when changing routes/schemas.
  • Definition of Done / Standards: include `@file:DOD.md` or `@file:CONTRIBUTING.md` if your repo has them.
  • Design System Source of Truth: include `@file:ui/index.ts` (replace with your DS entrypoint path).
  • Feature Flags: include the flag key/name and `@file:flags.ts` / `@file:config.json` (and optionally the PostHog flag name).
  • Edge/CDN Rules: link the Cloudflare rule/zone or describe the intended redirect/header behavior.
  • Attach an MCP tool output: use `@mcp<provider|tool>` (replace with an installed MCP provider + tool, e.g., `@mcp<sentry|events.search>`).
Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug ✅
Performance ✅
Security ✅
Business Logic ✅

Access your configuration settings here.

@27Bslash6

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions

github-actions Bot commented Nov 7, 2026

Copy link
Copy Markdown

Instruction counts: PASS

Head a0b2a09 against base e28cd6e4081929f0c1429716fc7caefcf43e3e92, Node v24.22.2, valgrind-3.26.0. Instructions per op on the main thread (bench:ir --compare); median of the repeats, A/A = (max − min) / median. A workload fails above +2.00% and warns above +1.00% (its own threshold: plaintext-set-1mb at +10.00%, napi-encrypted at +4.00%, warning at half); an A/A over 0.40% reads INCONCLUSIVE.

workload base Ir/op head Ir/op delta A/A base / head verdict
secure-set-100b 73,947 72,275 -2.26% 0.00% / 0.00% improved
All 41 workloads: 1 improved, 40 ok
workload base Ir/op head Ir/op delta A/A base / head verdict
l1-get-hit 1,203 1,203 0.00% 0.00% / 0.00% ok
l1-set-at-capacity-1k 6,374 6,374 0.00% 0.00% / 0.00% ok
l1-set-at-capacity-10k 4,156 4,156 0.00% 0.00% / 0.00% ok
l1-set-at-capacity-100k 4,353 4,353 0.00% 0.00% / 0.00% ok
plaintext-set-1kb 219,745 219,755 +0.00% 0.00% / 0.00% ok
plaintext-set-10kb 1,849,111 1,849,138 +0.00% 0.00% / 0.00% ok
plaintext-set-100kb 18,086,878 18,086,829 -0.00% 0.01% / 0.00% ok
plaintext-set-64b 43,155 43,150 -0.01% 0.00% / 0.00% ok
plaintext-set-64b-metrics 48,390 48,340 -0.10% 0.02% / 0.00% ok
plaintext-set-1kb-metrics 224,428 224,359 -0.03% 0.02% / 0.01% ok
plaintext-set-1mb 178,105,637 178,088,112 -0.01% 0.01% / 0.00% ok
plaintext-set-containers 1,400,423 1,400,469 +0.00% 0.00% / 0.00% ok
keygen 12,356 12,356 0.00% 0.00% / 0.00% ok
keygen-1kb 276,268 276,271 +0.00% 0.00% / 0.00% ok
keygen-wasm 15,931 15,931 0.00% 0.00% / 0.00% ok
keygen-wasm-1kb 315,758 315,761 +0.00% 0.00% / 0.00% ok
serializer-roundtrip 379,774 379,774 0.00% 0.00% / 0.00% ok
napi-envelope 45,512 45,512 0.00% 0.00% / 0.00% ok
napi-envelope-40b 16,070 16,070 0.00% 0.00% / 0.00% ok
napi-envelope-64kb 498,934 498,936 +0.00% 0.00% / 0.00% ok
wasm-envelope 58,979 58,979 0.00% 0.00% / 0.00% ok
wasm-envelope-40b 14,513 14,513 0.00% 0.00% / 0.00% ok
wasm-envelope-64kb 904,572 904,599 +0.00% 0.00% / 0.00% ok
napi-encrypted 44,410 44,414 +0.01% 0.00% / 0.00% ok
napi-encrypted-64kb 850,188 849,903 -0.03% 0.00% / 0.00% ok
wasm-encrypted 324,823 324,820 -0.00% 0.00% / 0.00% ok
wasm-encrypted-64kb 32,108,179 32,108,174 -0.00% 0.00% / 0.00% ok
secure-set-get 488,889 486,993 -0.39% 0.00% / 0.00% ok
secure-set-100b 73,947 72,275 -2.26% 0.00% / 0.00% improved
get-l1-hit 2,113 2,112 -0.05% 0.00% / 0.00% ok
secure-get-l1-hit 50,856 50,842 -0.03% 0.00% / 0.00% ok
wrap-l1-hit 17,021 17,021 0.00% 0.00% / 0.00% ok
wrap-l1-hit-metrics 16,977 16,978 +0.01% 0.00% / 0.00% ok
get-l2-hit-metrics 236,678 236,654 -0.01% 0.01% / 0.00% ok
get-l2-hit-l1-metrics 243,326 243,330 +0.00% 0.00% / 0.02% ok
secure-wrap-l1-hit-100b 58,710 58,705 -0.01% 0.00% / 0.00% ok
secure-wrap-l1-hit-10kb 1,518,138 1,518,234 +0.01% 0.00% / 0.00% ok
secure-wrap-l1-hit-1mb 239,142,625 238,769,113 -0.16% 0.05% / 0.08% ok
wrap-l1-hit-cachekitio 17,046 17,046 0.00% 0.00% / 0.00% ok
wrap-l1-hit-int-cachekitio 2,363 2,363 0.00% 0.00% / 0.00% ok
wrap-l1-hit-int 2,352 2,351 -0.04% 0.00% / 0.00% ok

Run log

@27Bslash6

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@27Bslash6
27Bslash6 merged commit ad9afd6 into main Nov 7, 2026
26 checks passed
@27Bslash6
27Bslash6 deleted the lab-12935-encrypted-set-sync branch November 7, 2026 13:18
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.

1 participant