Initial Commit
This commit is contained in:
95
packages/api-stack/api/api-tenant/src/storage/storage.md
Normal file
95
packages/api-stack/api/api-tenant/src/storage/storage.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# storage
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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 to `false`.
|
||||
- `tenantStorageMiddleware()` — pass-through when ALS off, wraps
|
||||
`next()` in `tenantStorage.run(tenant, ...)` when ALS on.
|
||||
- `getCurrentTenant()` / `getCurrentTenantId()` — read the threaded
|
||||
value, `undefined` when 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_hooks` import is bare so bundlers route it to the Node
|
||||
runtime; safe on Bun (which provides a compatible implementation).
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
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:
|
||||
|
||||
```ts
|
||||
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.
|
||||
Reference in New Issue
Block a user