feat: add per-request store API (getRequestStore)#607
feat: add per-request store API (getRequestStore)#607JamesbbBriz wants to merge 3 commits intocloudflare:mainfrom
Conversation
Add a user-accessible per-request key-value store backed by vinext's existing AsyncLocalStorage-based unified request context. Primary use case: per-request database clients on Cloudflare Workers. Global DB singletons cause alternating request failures (cloudflare#537) because Workers isolates reuse across requests but I/O contexts are per-request. getRequestStore() gives each request its own Map that's automatically garbage-collected when the request completes. API: import { getRequestStore } from 'vinext/request-store'; const store = getRequestStore(); store.set('prisma', new PrismaClient({ adapter })); store.get('prisma'); // same instance within this request Similar pattern to SvelteKit locals, Hono c.set/get, Remix context. Closes cloudflare#537
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f7cdddfc4a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| // Fallback store for calls outside a request scope (dev server top-level, tests). | ||
| // Ephemeral — not persisted across calls, prevents crashes but data won't survive. | ||
| const _fallbackStore = new Map<string, unknown>(); |
There was a problem hiding this comment.
Return a fresh fallback store outside request scope
getRequestStore() is documented as non-persistent outside unified scope, but _fallbackStore is a module-global Map, so every out-of-scope call shares and retains the same data. In practice this can leak request-specific objects (for example DB clients keyed by dynamic values) across unrelated calls/tests and grow unbounded over process lifetime, which defeats the isolation guarantees this API is meant to provide when context propagation is missing.
Useful? React with 👍 / 👎.
Address review feedback: _fallbackStore was a module-global Map shared across all out-of-scope calls, which could leak request-specific data (like DB clients) across unrelated calls and grow unbounded. Now returns a new Map() each time when outside unified scope.
Local verificationTested with our production app (Prisma v7 + Hyperdrive + Cloudflare Workers Free plan):
Happy to add unit tests if needed — let me know the preferred test framework/location. |
Pages Router API routes (pages/api/*) were the only code path missing a runWithRequestContext scope — both dev server (api-handler.ts) and prod/Workers (pages-server-entry.ts) now wrap the handler call. This ensures getRequestStore() and other per-request APIs work consistently across all code paths (App Router already had this).
|
Superseded by a cleaner PR — squashed commits and addressed all review feedback. |
Summary
Adds a user-accessible per-request key-value store backed by vinext's existing
AsyncLocalStorage-based unified request context.Primary use case: per-request database clients on Cloudflare Workers. Global DB singletons cause alternating request failures (#537) because Workers isolates reuse across requests but I/O contexts are per-request.
getRequestStore()gives each request its ownMapthat's automatically garbage-collected when the request completes — no manual cleanup, no TTL hacks.API
Motivation
We're running a production SaaS (OptiTalent) on vinext + Cloudflare Workers with Prisma v7 + Hyperdrive + R2. We hit the exact alternating-failure pattern described in #537 and initially worked around it with a 50ms TTL heuristic. It works, but it's fragile — the correct solution is framework-level per-request scoping.
vinext already has the infrastructure for this via
UnifiedRequestContext+AsyncLocalStorage. This PR simply exposes it to users via a clean public API, similar to:event.localsc.set()/c.get()contextScope & Limitations
getRequestStore()works in any code path wrapped byrunWithRequestContext()(the unified ALS scope):app/api/*/route.ts)pages/api/*)985e6ec(both dev + prod)middleware.ts)runWithExecutionContext, inherits into unified scopeChanges
packages/vinext/src/shims/unified-request-context.tsuserStore: Map<string, unknown>toUnifiedRequestContextpackages/vinext/src/shims/request-store.tsgetRequestStore()public APIpackages/vinext/package.json"./request-store"exportTest plan
getRequestStore()returns isolated Maps across concurrentrunWithRequestContext()callsgetRequestStore()returns fallback Map outside request scopeCloses #537