Skip to content

feat(cache): make cached decorator async-aware#167

Merged
jaysin586 merged 4 commits into
mainfrom
codex/async-cached
Jun 7, 2026
Merged

feat(cache): make cached decorator async-aware#167
jaysin586 merged 4 commits into
mainfrom
codex/async-cached

Conversation

@jaysin586

Copy link
Copy Markdown
Contributor

Summary

Adds async-aware behavior to the @cached decorator so concurrent async method calls share one in-flight execution and rejected promises are not cached. This makes decorator memoization match the retry-safe single-flight semantics already available through getOrSet().

Changes

✨ New features

  • Add single-flight request sharing for async @cached method calls with identical arguments.
  • Cache resolved async values while allowing rejected promises to retry on later calls.
  • Keep async decorator metadata bounded by clearing it on cache eviction, expiration, and deletion.

🧪 Testing

  • Replace the rejected-promise known limitation test with retry-safe rejection behavior.
  • Add decorator coverage for concurrent async success/failure, TTL, and nullish resolved values.
  • Add a regression for clearing async metadata after TTL expiration.

📚 Documentation

  • Document async decorator behavior in README and API docs.
  • Add a new Async Decorator docs example page and link it from docs navigation.
  • Normalize docs seo.description fields to the 110-160 character recommendation.

Commits

  • fbe3212 feat(cache): make cached decorator async-aware

Closes #162

- Share concurrent async decorator calls with single-flight behavior
- Avoid caching rejected promises so later calls can retry
- Document async decorator usage and normalize docs SEO descriptions

Closes #162
@jaysin586 jaysin586 self-assigned this Jun 6, 2026
@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4ce7c156-e1bc-43b8-8d7b-ebb0ca9e02a4

📥 Commits

Reviewing files that changed from the base of the PR and between 733b3b2 and e1da935.

📒 Files selected for processing (2)
  • docs/src/routes/docs/examples/sessions/+page.svx
  • docs/src/routes/docs/examples/sessions/+page.ts
✅ Files skipped from review due to trivial changes (2)
  • docs/src/routes/docs/examples/sessions/+page.svx
  • docs/src/routes/docs/examples/sessions/+page.ts

📝 Walkthrough

Walkthrough

The @cached decorator now detects promise-like returns, deduplicates concurrent async calls per-key (single-flight), caches resolved values (including nullish), and does not cache rejected promises by default. Tests and docs were added/updated to validate and document these behaviors.

Changes

Async-aware @cached Decorator

Layer / File(s) Summary
Core async implementation: promise detection and single-flight logic
src/cache.ts
Adds isPromiseLike runtime check; per-decorator MemoryCache<Awaited<T>>; inFlight map and asyncKeys set; wraps onEvict/onExpire/onDelete hooks to clear async metadata; on hit returns Promise.resolve for async keys; on miss joins existing in-flight promise or executes the method, caching resolved values and avoiding caching of rejections.
Async behavior test coverage
src/cache.decorator.test.ts
Adds tests for retry after rejection and subsequent caching, single-flight concurrent deduplication (Promise.all), shared rejection propagation without caching, TTL expiration and metadata cleanup, and caching of resolved undefined/null. Minor typing cleanup in circular-reference test.
New async-decorator example documentation
docs/src/routes/docs/examples/async-decorator/
New example page with SEO setup and examples for single-flight requests, rejection/retry behavior, TTL/nullish result handling, and usage guidance comparing @cached vs getOrSet(); includes a load export.
Examples index and navigation updates
docs/src/lib/docsNav.ts, docs/src/routes/docs/examples/+page.svelte
Registers the new Async Decorator example in the docs navigation and examples catalog, imports the AtSign icon, and reformats page data/markup for readability (no behavioral changes).
Main @cached decorator documentation
docs/src/routes/docs/api/cached-decorator/+page.svx
Updates SEO/OG metadata and clarifies async methods section to describe single-flight deduplication, caching of resolved values, and non-caching of rejected promises; replaces the async example with Promise.all concurrent-call and rejection try/catch examples.
SEO descriptions and README
README.md, numerous docs/src/routes/** and docs/src/lib/docs-config.ts, docs/package.json
Updates README Method Decorator paragraph and many docs pages' SEO/description strings for consistent wording; bumps docs-kit dependency and applies minor formatting/whitespace fixes.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant CachedDecorator
  participant InFlightMap
  participant MemoryCache
  participant UnderlyingMethod
  Caller->>CachedDecorator: call(key)
  CachedDecorator->>MemoryCache: lookup(key)
  alt hit and key is async
    MemoryCache-->>CachedDecorator: value
    CachedDecorator->>Caller: Promise.resolve(value)
  else miss
    CachedDecorator->>InFlightMap: has(key)?
    alt exists
      InFlightMap-->>CachedDecorator: existing promise
      CachedDecorator->>Caller: existing promise
    else not exists
      CachedDecorator->>UnderlyingMethod: invoke()
      UnderlyingMethod-->>CachedDecorator: Promise
      CachedDecorator->>InFlightMap: store promise
      CachedDecorator->>Caller: promise
      UnderlyingMethod->>CachedDecorator: resolve(value) / reject(error)
      alt resolved
        CachedDecorator->>MemoryCache: store resolved value
        CachedDecorator->>CachedDecorator: mark asyncKey
        CachedDecorator->>InFlightMap: remove(key)
        CachedDecorator->>Caller: resolved value
      else rejected
        CachedDecorator->>InFlightMap: remove(key)
        CachedDecorator->>Caller: propagate rejection (not cached)
      end
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

enhancement, documentation

Poem

One call, two callers — they share a single flight,
Resolved results rest safe, while failures take flight.
Tests and docs sing the new async decree,
Cache nullish with TTL, let rejections be free. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: making the @cached decorator async-aware, which is the primary objective of this PR.
Description check ✅ Passed The description is well-related to the changeset, detailing async-aware behavior, single-flight semantics, rejected promise handling, testing updates, and documentation changes.
Linked Issues check ✅ Passed The PR addresses all acceptance criteria from #162: async calls share one in-flight execution, rejected promises are not cached, sync behavior is preserved, and test coverage includes success, rejection, concurrency, TTL, and nullish results.
Out of Scope Changes check ✅ Passed All changes align with PR objectives: core async decorator implementation in src/cache.ts, comprehensive test additions, and documentation updates across README, API docs, and example pages are all in scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/async-cached

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

161-162: 💤 Low value

Redundant type alias.

PromiseLikeValue<T> is identical to the built-in PromiseLike<T>. Consider using PromiseLike<T> directly in isPromiseLike to reduce indirection.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cache.ts` around lines 161 - 162, Remove the redundant type alias
PromiseLikeValue<T> and use the built-in PromiseLike<T> directly in the
isPromiseLike utility: replace references to PromiseLikeValue<T> with
PromiseLike<T>, delete the PromiseLikeValue declaration, and update the
isPromiseLike function signature/usage to reference PromiseLike so there is no
unnecessary indirection.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/cache.ts`:
- Around line 161-162: Remove the redundant type alias PromiseLikeValue<T> and
use the built-in PromiseLike<T> directly in the isPromiseLike utility: replace
references to PromiseLikeValue<T> with PromiseLike<T>, delete the
PromiseLikeValue declaration, and update the isPromiseLike function
signature/usage to reference PromiseLike so there is no unnecessary indirection.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 511aa0f2-7540-4224-a9e6-07d098672fe5

📥 Commits

Reviewing files that changed from the base of the PR and between b2b089e and fbe3212.

📒 Files selected for processing (18)
  • README.md
  • docs/src/lib/docsNav.ts
  • docs/src/routes/docs/api/cached-decorator/+page.svx
  • docs/src/routes/docs/examples/+page.svelte
  • docs/src/routes/docs/examples/api-caching/+page.svx
  • docs/src/routes/docs/examples/async-decorator/+page.svx
  • docs/src/routes/docs/examples/async-decorator/+page.ts
  • docs/src/routes/docs/examples/async-fetching/+page.svx
  • docs/src/routes/docs/examples/computed-values/+page.svx
  • docs/src/routes/docs/examples/configuration/+page.svx
  • docs/src/routes/docs/examples/database-caching/+page.svx
  • docs/src/routes/docs/examples/monitoring/+page.svx
  • docs/src/routes/docs/examples/multi-tenant/+page.svx
  • docs/src/routes/docs/examples/rate-limiting/+page.svx
  • docs/src/routes/docs/examples/service-class/+page.svx
  • docs/src/routes/docs/examples/sessions/+page.svx
  • src/cache.decorator.test.ts
  • src/cache.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/src/routes/docs/examples/sessions/`+page.svx:
- Line 3: Update the description text that currently calls
"`@humanspeak/memory-cache`" a "lightweight Redis alternative" to avoid implying
feature parity with Redis; change the phrasing to accurately describe the
library's behavior (for example: "lightweight in-memory session store" or
"simple TTL-based session cache") wherever that description appears (see the
description line and any repeated instances around lines referencing
`@humanspeak/memory-cache` in +page.svx) so the docs correctly convey it is an
in-process TTL cache, not a distributed Redis replacement.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1bcf37a3-703b-473e-8912-2a5e531b6f1a

📥 Commits

Reviewing files that changed from the base of the PR and between 51e7a03 and 733b3b2.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !pnpm-lock.yaml
📒 Files selected for processing (42)
  • docs/package.json
  • docs/src/lib/docs-config.ts
  • docs/src/routes/+page.ts
  • docs/src/routes/docs/api/cached-decorator/+page.svx
  • docs/src/routes/docs/api/cached-decorator/+page.ts
  • docs/src/routes/docs/api/memory-cache/+page.svx
  • docs/src/routes/docs/api/memory-cache/+page.ts
  • docs/src/routes/docs/examples/+page.ts
  • docs/src/routes/docs/examples/api-caching/+page.svx
  • docs/src/routes/docs/examples/api-caching/+page.ts
  • docs/src/routes/docs/examples/async-decorator/+page.svx
  • docs/src/routes/docs/examples/async-decorator/+page.ts
  • docs/src/routes/docs/examples/async-fetching/+page.svx
  • docs/src/routes/docs/examples/async-fetching/+page.ts
  • docs/src/routes/docs/examples/computed-values/+page.svx
  • docs/src/routes/docs/examples/computed-values/+page.ts
  • docs/src/routes/docs/examples/configuration/+page.svx
  • docs/src/routes/docs/examples/configuration/+page.ts
  • docs/src/routes/docs/examples/database-caching/+page.svx
  • docs/src/routes/docs/examples/database-caching/+page.ts
  • docs/src/routes/docs/examples/monitoring/+page.svx
  • docs/src/routes/docs/examples/monitoring/+page.ts
  • docs/src/routes/docs/examples/multi-tenant/+page.svx
  • docs/src/routes/docs/examples/multi-tenant/+page.ts
  • docs/src/routes/docs/examples/rate-limiting/+page.svx
  • docs/src/routes/docs/examples/rate-limiting/+page.ts
  • docs/src/routes/docs/examples/service-class/+page.svx
  • docs/src/routes/docs/examples/service-class/+page.ts
  • docs/src/routes/docs/examples/sessions/+page.svx
  • docs/src/routes/docs/examples/sessions/+page.ts
  • docs/src/routes/docs/getting-started/+page.svx
  • docs/src/routes/docs/getting-started/+page.ts
  • docs/src/routes/examples/+page.svelte
  • docs/src/routes/examples/+page.ts
  • docs/src/routes/examples/basic-cache/+page.svelte
  • docs/src/routes/examples/basic-cache/+page.ts
  • docs/src/routes/examples/cache-statistics/+page.svelte
  • docs/src/routes/examples/cache-statistics/+page.ts
  • docs/src/routes/examples/lru-eviction/+page.svelte
  • docs/src/routes/examples/lru-eviction/+page.ts
  • docs/src/routes/examples/ttl-expiration/+page.svelte
  • docs/src/routes/examples/ttl-expiration/+page.ts
✅ Files skipped from review due to trivial changes (32)
  • docs/src/routes/docs/getting-started/+page.ts
  • docs/src/routes/docs/examples/monitoring/+page.ts
  • docs/src/routes/docs/examples/sessions/+page.ts
  • docs/src/routes/examples/lru-eviction/+page.ts
  • docs/src/routes/examples/+page.ts
  • docs/src/routes/docs/examples/database-caching/+page.ts
  • docs/src/routes/docs/api/memory-cache/+page.ts
  • docs/package.json
  • docs/src/routes/+page.ts
  • docs/src/routes/examples/basic-cache/+page.ts
  • docs/src/routes/docs/examples/+page.ts
  • docs/src/routes/docs/examples/rate-limiting/+page.ts
  • docs/src/routes/examples/ttl-expiration/+page.ts
  • docs/src/routes/docs/examples/api-caching/+page.ts
  • docs/src/routes/docs/examples/computed-values/+page.ts
  • docs/src/routes/docs/examples/configuration/+page.ts
  • docs/src/routes/docs/examples/async-fetching/+page.ts
  • docs/src/routes/docs/api/cached-decorator/+page.ts
  • docs/src/routes/docs/examples/multi-tenant/+page.ts
  • docs/src/routes/docs/getting-started/+page.svx
  • docs/src/routes/examples/cache-statistics/+page.ts
  • docs/src/routes/docs/examples/service-class/+page.ts
  • docs/src/lib/docs-config.ts
  • docs/src/routes/docs/examples/service-class/+page.svx
  • docs/src/routes/docs/examples/api-caching/+page.svx
  • docs/src/routes/docs/examples/async-decorator/+page.ts
  • docs/src/routes/docs/examples/multi-tenant/+page.svx
  • docs/src/routes/docs/api/memory-cache/+page.svx
  • docs/src/routes/docs/examples/computed-values/+page.svx
  • docs/src/routes/docs/examples/monitoring/+page.svx
  • docs/src/routes/docs/examples/async-fetching/+page.svx
  • docs/src/routes/docs/api/cached-decorator/+page.svx
🚧 Files skipped from review as they are similar to previous changes (3)
  • docs/src/routes/docs/examples/async-decorator/+page.svx
  • docs/src/routes/docs/examples/configuration/+page.svx
  • docs/src/routes/docs/examples/database-caching/+page.svx

Comment thread docs/src/routes/docs/examples/sessions/+page.svx Outdated
@jaysin586 jaysin586 merged commit 9668657 into main Jun 7, 2026
6 checks passed
@jaysin586

Copy link
Copy Markdown
Contributor Author

@coderabbitai

Responding to your review comments:

  • Redundant PromiseLike alias: Fixed in 51e7a03 by removing PromiseLikeValue<T> and using the built-in PromiseLike<T> directly in isPromiseLike.
  • Misleading Redis alternative wording: Fixed in e1da935 by removing the Redis replacement claim and describing the session example as simple per-process in-memory session storage.
  • Docstring coverage warning: Leaving this as-is for this PR. It is a broad project-level coverage warning rather than a regression introduced by the async decorator change.

@jaysin586 jaysin586 deployed to production June 7, 2026 14:35 — with GitHub Actions Active
@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@jaysin586 Thanks for the update!

  • PromiseLike alias (51e7a03): Confirmed — using the built-in PromiseLike<T> directly is cleaner.
  • Redis wording (e1da935): Confirmed — the revised session description is accurate now.
  • Docstring coverage: Understood, agreed it's not a regression from this change.

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.

Make @cached async-aware and avoid caching rejected promises by default

1 participant