Skip to content

RFC: Per-request store API #623

@james-elicx

Description

This issue is a request for feedback on the public API for our per-request store API. The idea was originally proposed in #608.

Internally, the store would interact with a map that's inside a request-scoped AsyncLocalStorage.

I've been doing some thinking this morning about the public API that we'd be exposing, as it would be nice have it well-defined from the get-go to avoid potentially introducing breaking changes in the future.

Store Interface

My thinking is that we would want to expose full control for interacting with the underlying store, which also providing some higher-level abstractions to reduce boilerplate.

The store itself would expose the following:

// checks the underlying map.
// does type-narrowing so that get(key) calls in if-statements have the non-undefined type.
has(key: string): boolean;
// returns value from underlying map. narrowed when combined with has(key).
get(key: string): unknown | undefined;
// inserts value to underlying map and returns the value back.
set(key: string, value: unknown): unknown;
// checks if the map already has a value and returns it, otherwise sets the value and returns it.
getOrSet(key: string, factory: () => unknown): unknown;

Methods accepting a callback may possibly need async variants as well, or the other way around with async as the default and sync as the variant, or neither if we find a nice way to make both work in one function.

Store Consumption

I had been thinking through a few different ways to approach this, and the following is what I arrived at for a nice balance of type-safety and control.

It would be great to get thoughts from others, or suggestions if you would like to see something else!

Function that returns a type-safe interface to the underlying map

import { createRequestStore } from 'vinext/server';

// safe to be called in global scope as it doesn't access the inner store
const store = createRequestStore<{ prisma: PrismaClient }>();

const getPrisma = () =>
  store.getOrSet("prisma", () => {
    const pool = new Pool();
    return new PrismaClient(pool);
  });

const getPrisma = () => {
  if (store.has("prisma")) {
    return store.get("prisma"); // type-narrowed by has(...)
  }

  const pool = new Pool();
  const client = new PrismaClient(pool);

  return store.set("prisma", client);
};

The alternative approach that was on my mind would be a Proxy to the underlying map where you pass the type stored to each function call, but that would sacrifice some of the type-safety.

Abstraction on top of the store for callbacks

Another option that would be interesting to explore is a cacheForRequest function, that takes a callback and lazily initialises the value in the request store on first access. Similar to how React's cache function works, but not limited to server components.

import { cacheForRequest } from 'vinext/server';

// return a function that lazily initializes the Prisma client on the first call
const getPrisma = cacheForRequest(() => {
  const pool = new Pool();
  return new PrismaClient(pool);
});

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions