3.2 KiB
storage
(no submodules)
Purpose
The storage module ships the opt-in AsyncLocalStorage thread for
the resolved tenant. defaults the thread OFF — the canonical
access path is c.get("tenant") — and exposes withTenantStorage(true)
plus the matching tenantStorageMiddleware() for applications that
legitimately need to read the tenant from deep async code without
threading the Hono Context through every function signature.
This eliminates the upstream dual-access-path footgun where
c.get("tenant") and getCurrentTenant() could disagree. With ALS
off, getCurrentTenant() always returns undefined, so any code
that "accidentally" relied on the thread surfaces immediately rather
than silently picking up stale state from a previous request.
Features
- Opt-in via
withTenantStorage(true)— defaults tofalse. tenantStorageMiddleware()— pass-through when ALS off, wrapsnext()intenantStorage.run(tenant, ...)when ALS on.getCurrentTenant()/getCurrentTenantId()— read the threaded value,undefinedwhen off or outside a scope.runWithTenant(tenant, fn)— explicit scope helper for background jobs spawned outside a request lifecycle.isTenantStorageEnabled()— inspect the toggle (primarily for tests).node:async_hooksimport is bare so bundlers route it to the Node runtime; safe on Bun (which provides a compatible implementation).
Usage: Basic
import {
createTenantMiddleware,
headerResolver,
tenantStorageMiddleware,
withTenantStorage,
getCurrentTenant,
} from "@suaveplan/api-tenant";
withTenantStorage(true);
app.use(
"*",
createTenantMiddleware({
resolver: headerResolver("x-tenant-id", lookup),
})
);
app.use("*", tenantStorageMiddleware());
async function deepCall(): Promise<string> {
return getCurrentTenant()?.id ?? "anonymous";
}
Usage: Advanced
Background job that spawns from a queue worker and needs the tenant context inside an inner async boundary:
import { runWithTenant, withTenantStorage } from "@suaveplan/api-tenant";
withTenantStorage(true);
async function processJob(job: { tenant: Tenant; payload: unknown }) {
return runWithTenant(job.tenant, async () => {
await runUserDefinedHandler(job.payload);
});
}
API Reference
withTenantStorage(enabled: boolean): void— toggle.tenantStorageMiddleware(): MiddlewareHandler.getCurrentTenant(): Tenant | undefined.getCurrentTenantId(): string | undefined.runWithTenant<T>(tenant: Tenant, fn: () => T): T.isTenantStorageEnabled(): boolean.
Implementation Notes
The toggle is a module-level boolean rather than a per-instance flag
because AsyncLocalStorage itself is a process-wide construct — two
parallel enabled=true regions would conflict regardless of how the
flag is stored. The middleware short-circuits in three places (ALS
disabled, no tenant resolved, or non-Hono caller) so the cost when
disabled is two cheap reads plus the call to next(). The
runWithTenant helper is the one piece that runs fn synchronously
when ALS is off, keeping the call signature stable across both modes.