[PR #607] [CLOSED] feat: add per-request store API (getRequestStore) #708

Closed
opened 2026-05-06 13:09:41 +02:00 by BreizhHardware · 0 comments

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/607
Author: @JamesbbBriz
Created: 3/20/2026
Status: Closed

Base: mainHead: feat/per-request-store


📝 Commits (3)

  • f7cdddf feat: add per-request store API (getRequestStore)
  • 371d00d fix: return fresh Map outside request scope to prevent data leakage
  • 985e6ec fix: wrap Pages Router API routes in unified request context

📊 Changes

5 files changed (+95 additions, -5 deletions)

View changed files

📝 packages/vinext/package.json (+4 -0)
📝 packages/vinext/src/entries/pages-server-entry.ts (+6 -1)
📝 packages/vinext/src/server/api-handler.ts (+9 -2)
packages/vinext/src/shims/request-store.ts (+69 -0)
📝 packages/vinext/src/shims/unified-request-context.ts (+7 -2)

📄 Description

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 own Map that's automatically garbage-collected when the request completes — no manual cleanup, no TTL hacks.

API

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

// Per-request Prisma client (solves #537):
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;
}

// Works with Drizzle too:
export function getDb(connectionString: string) {
  const store = getRequestStore();
  let db = store.get("db");
  if (!db) {
    db = drizzle(connectionString);
    store.set("db", db);
  }
  return db;
}

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:

  • SvelteKit event.locals
  • Hono c.set() / c.get()
  • Remix context

Scope & Limitations

getRequestStore() works in any code path wrapped by runWithRequestContext() (the unified ALS scope):

Code path Supported Notes
App Router route handlers (app/api/*/route.ts) Wrapped by RSC entry
App Router server components Wrapped by RSC entry
App Router server actions Wrapped by RSC entry
Pages Router API routes (pages/api/*) Wrapped in 985e6ec (both dev + prod)
Middleware (middleware.ts) Has runWithExecutionContext, inherits into unified scope

Changes

File Change
packages/vinext/src/shims/unified-request-context.ts Add userStore: Map<string, unknown> to UnifiedRequestContext
packages/vinext/src/shims/request-store.ts NewgetRequestStore() public API
packages/vinext/package.json Add "./request-store" export

Test plan

  • Verified in production with Prisma v7 + Hyperdrive (per-request client, zero alternating failures)
  • Unit test: getRequestStore() returns isolated Maps across concurrent runWithRequestContext() calls
  • Unit test: getRequestStore() returns fallback Map outside request scope

Closes #537


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/cloudflare/vinext/pull/607 **Author:** [@JamesbbBriz](https://github.com/JamesbbBriz) **Created:** 3/20/2026 **Status:** ❌ Closed **Base:** `main` ← **Head:** `feat/per-request-store` --- ### 📝 Commits (3) - [`f7cdddf`](https://github.com/cloudflare/vinext/commit/f7cdddfc4aede3a64ffc0be1dc5c16517136ca64) feat: add per-request store API (getRequestStore) - [`371d00d`](https://github.com/cloudflare/vinext/commit/371d00d68087da118a892907660523cc154351d1) fix: return fresh Map outside request scope to prevent data leakage - [`985e6ec`](https://github.com/cloudflare/vinext/commit/985e6ec0da75e562a0772cfdcd08c72fae724f1c) fix: wrap Pages Router API routes in unified request context ### 📊 Changes **5 files changed** (+95 additions, -5 deletions) <details> <summary>View changed files</summary> 📝 `packages/vinext/package.json` (+4 -0) 📝 `packages/vinext/src/entries/pages-server-entry.ts` (+6 -1) 📝 `packages/vinext/src/server/api-handler.ts` (+9 -2) ➕ `packages/vinext/src/shims/request-store.ts` (+69 -0) 📝 `packages/vinext/src/shims/unified-request-context.ts` (+7 -2) </details> ### 📄 Description ## 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](https://github.com/cloudflare/vinext/issues/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 — no manual cleanup, no TTL hacks. ## API ```ts import { getRequestStore } from "vinext/request-store"; // Per-request Prisma client (solves #537): 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; } // Works with Drizzle too: export function getDb(connectionString: string) { const store = getRequestStore(); let db = store.get("db"); if (!db) { db = drizzle(connectionString); store.set("db", db); } return db; } ``` ## Motivation We're running a production SaaS ([OptiTalent](https://app.optitalent.cc)) 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](https://github.com/cloudflare/vinext/issues/537#issuecomment-4097268830). 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: - SvelteKit `event.locals` - Hono `c.set()` / `c.get()` - Remix `context` ## Scope & Limitations `getRequestStore()` works in any code path wrapped by `runWithRequestContext()` (the unified ALS scope): | Code path | Supported | Notes | |-----------|-----------|-------| | App Router route handlers (`app/api/*/route.ts`) | ✅ | Wrapped by RSC entry | | App Router server components | ✅ | Wrapped by RSC entry | | App Router server actions | ✅ | Wrapped by RSC entry | | Pages Router API routes (`pages/api/*`) | ✅ | Wrapped in `985e6ec` (both dev + prod) | | Middleware (`middleware.ts`) | ✅ | Has `runWithExecutionContext`, inherits into unified scope | ## Changes | File | Change | |------|--------| | `packages/vinext/src/shims/unified-request-context.ts` | Add `userStore: Map<string, unknown>` to `UnifiedRequestContext` | | `packages/vinext/src/shims/request-store.ts` | **New** — `getRequestStore()` public API | | `packages/vinext/package.json` | Add `"./request-store"` export | ## Test plan - [x] Verified in production with Prisma v7 + Hyperdrive (per-request client, zero alternating failures) - [ ] Unit test: `getRequestStore()` returns isolated Maps across concurrent `runWithRequestContext()` calls - [ ] Unit test: `getRequestStore()` returns fallback Map outside request scope Closes #537 --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
BreizhHardware 2026-05-06 13:09:41 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#708
No description provided.