Skip to content

feat: add per-request store API (getRequestStore)#608

Open
JamesbbBriz wants to merge 1 commit intocloudflare:mainfrom
JamesbbBriz:feat/request-store
Open

feat: add per-request store API (getRequestStore)#608
JamesbbBriz wants to merge 1 commit intocloudflare:mainfrom
JamesbbBriz:feat/request-store

Conversation

@JamesbbBriz
Copy link

Summary

Add getRequestStore() — a per-request key-value store backed by vinext's existing unified AsyncLocalStorage scope. Values are isolated per request and automatically garbage-collected when the request completes.

Primary use case: per-request database clients on Cloudflare Workers, where global singletons cause alternating request failures (#537).

API

import { getRequestStore } from "vinext/request-store";

export function getPrisma(connectionString: string): PrismaClient {
  const store = getRequestStore();
  let prisma = store.get("prisma") as PrismaClient | undefined;
  if (!prisma) {
    const pool = new Pool({ connectionString });
    prisma = new PrismaClient({ adapter: new PrismaPg(pool) });
    store.set("prisma", prisma);
  }
  return prisma;
  // Request ends → Map GC'd → connection released
}

Similar pattern to SvelteKit event.locals, Hono c.set/get, Remix context.

Scope

Works in all code paths:

Code path Supported Notes
App Router route handlers Already wrapped by RSC entry
App Router server components/actions Already wrapped by RSC entry
Pages Router API routes (dev) Wrapped in this PR (api-handler.ts)
Pages Router API routes (prod) Wrapped in this PR (pages-server-entry.ts)
Middleware Inherits ExecutionContext into unified scope
Outside request scope (tests, top-level) ⚠️ Returns fresh empty Map each call — safe but non-persistent

Changes

File Change
shims/unified-request-context.ts Add userStore: Map<string, unknown> to UnifiedRequestContext + default in createRequestContext()
shims/request-store.ts NewgetRequestStore() public API
server/api-handler.ts Wrap Pages Router API handler in runWithRequestContext (dev server)
entries/pages-server-entry.ts Wrap Pages Router API handler in _runWithUnifiedCtx (prod/Workers)
package.json Add "./request-store" export

Motivation

We're running a production SaaS on vinext + Cloudflare Workers (Free plan) with Prisma v7 + Hyperdrive + R2 + NextAuth. We hit the exact alternating-failure pattern from #537 and initially worked around it with a 50ms TTL heuristic. This PR provides the proper framework-level solution.

vinext already has the UnifiedRequestContext + AsyncLocalStorage infrastructure. This PR exposes it to users via a clean public API — 73 lines across 5 files, zero breaking changes.

Test plan

  • Verified in production with Prisma v7 + Hyperdrive (zero alternating failures across 500+ requests)
  • Outside-scope fallback returns fresh Map each call (prevents cross-call data leakage)
  • Happy to add unit tests — let me know the preferred test location

Closes #537

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 625d36e5e7

ℹ️ 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".

Comment on lines +46 to +47
if (!isInsideUnifiedScope()) {
return new Map();

Choose a reason for hiding this comment

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

P2 Badge Keep middleware request store stable within a request

getRequestStore() returns a brand-new Map whenever isInsideUnifiedScope() is false, which means callers outside a unified scope never see stable per-request state. In Pages Router middleware, execution is wrapped with runWithExecutionContext but not runWithRequestContext, so this branch is always taken there; two getRequestStore() calls in the same middleware invocation won't share data, and values set in middleware cannot be read later in that request path, which breaks the advertised per-request middleware behavior.

Useful? React with 👍 / 👎.

Add getRequestStore() — a per-request key-value store backed by
vinext's existing AsyncLocalStorage infrastructure.

Resolution order:
1. Unified scope (App Router, Pages Router API routes) → userStore
2. Fallback ALS (middleware with only ExecutionContext) → stable Map
3. Outside any scope (tests, top-level) → fresh empty Map

Also wraps Pages Router API route handlers in unified request context
so getRequestStore() works consistently across all code paths.

Changes:
- shims/unified-request-context.ts: add userStore field
- shims/request-store.ts: new module with 3-tier resolution
- server/api-handler.ts: wrap Pages Router API handler (dev)
- entries/pages-server-entry.ts: wrap Pages Router API handler (prod)
- package.json: add vinext/request-store export
@JamesbbBriz
Copy link
Author

/bigbonk review this PR

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.

Alternating request failures (success, fail, success...) when using Drizzle + Postgres lazily evaluated in vinext / Cloudflare Workers

1 participant