@alxia/context-storage 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -41,35 +41,60 @@ The context holds through every `await`, timer and promise of the
41
41
  request, and never leaks into another's: twenty concurrent requests read
42
42
  twenty contexts.
43
43
 
44
+ With `@alxia/core`'s `Register` naming `base`, `contextStorage()` needs no
45
+ type argument: it reads the registered context, `AppContext`. Either way
46
+ the app that mounts it must give that context, a compile error otherwise:
47
+
48
+ ```ts
49
+ declare module '@alxia/core' {
50
+ interface Register {
51
+ context: typeof base;
52
+ }
53
+ }
54
+
55
+ export const requestContext = contextStorage(); // context(): AppContext
56
+ base.use(requestContext); // ok
57
+ alxia().use(requestContext); // compile error: Property 'db' is missing in type 'BaseContext & Empty'
58
+ ```
59
+
60
+ Requiring that context of the app is new in 0.4.0: a middleware used on an app
61
+ that does not give it, which read `undefined` at runtime, is now a compile
62
+ error ([Upgrading](https://github.com/softistx/alxia/blob/develop/packages/core/docs/upgrading.md#the-context-registered-once-register-and-defineroutes)).
63
+
44
64
  ## Reading it
45
65
 
46
66
  | | |
47
67
  | --- | --- |
48
- | `requestContext.context()` | the route's context, typed by the app the plugin was given: the request, `set`, `reply`, and what every hook before it added. Throws outside |
68
+ | `requestContext.context()` | the route's context, typed by the app the middleware was given: the request, `set`, `reply`, and what every middleware before it added. Throws outside |
49
69
  | `requestContext.tryContext()` | the same, or `undefined`: code that runs in and out of requests |
50
70
  | `getContext<Ctx>()`, `tryGetContext<Ctx>()` | untyped, as `hono/context-storage`'s: `Ctx` is yours to state |
51
- | `getRequestContext()`, `tryGetRequestContext()` | the request as global hooks see it — in a 404, an `onResponse` — with the `route` it reached and its `error`; the `try` form returns `undefined` outside a request |
71
+ | `getRequestContext()`, `tryGetRequestContext()` | the request as a middleware after it sees it — in a 404 too — with the `route` it reached (`undefined` when none) and its `error`; the `try` form returns `undefined` outside a request |
52
72
  | `runWithContext(ctx, work)` | runs `work` with a context: a job, a queue consumer, a test of a service |
53
73
 
54
- Outside a request, `getContext()` throws a `ContextStorageError` coded
55
- `OUTSIDE_REQUEST`; in a request that reached no route declared after the
56
- plugin, `NOT_ROUTED`. Declare it before the routes whose code reads it.
74
+ Give it to `use` before the routes whose code reads it, and before the
75
+ middlewares that read the context: it runs on every request the app takes,
76
+ a 404 included. `getRequestContext()` works in every middleware after it;
77
+ `getContext()` only in a request that reached a route. Outside a request,
78
+ or where the middleware did not run (a route declared before it),
79
+ `getContext()` throws a `ContextStorageError` coded `OUTSIDE_REQUEST`; in
80
+ a request that reached no route, `NOT_ROUTED`.
57
81
 
58
- Pass the plugin to `use` called: `use(contextStorage)`, uncalled, is refused by
59
- `tsc` (`TS2769`) and throws a `TypeError` at startup
60
- ([troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/context-storage/docs/troubleshooting.md#typeerror-contextstorage-is-a-factory-usecontextstorage-not-usecontextstorage)).
82
+ Pass it to `app.use` called: `use(contextStorage)`, uncalled, is refused by
83
+ `tsc` (`TS2345`), and throws where it is declared
84
+ ([troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/context-storage/docs/troubleshooting.md#use-argument-1-looks-like-a-factory-contextstorage-call-it-usecontextstorage)).
61
85
 
62
86
  ## API
63
87
 
64
88
  | export | |
65
89
  | --- | --- |
66
- | `contextStorage<App>()` | the plugin, with `context()` and `tryContext()` typed by `App` |
67
- | `ContextStoragePlugin<App>` | its type |
90
+ | `contextStorage<App>()` | the middleware, given to `app.use`, with `context()` and `tryContext()` typed by `App` — by default the app `@alxia/core`'s `Register` names, `BaseContext` when none — and required of the app that mounts it |
91
+ | `StoredContext<App>` | what `context()` returns: `ContextOf<App>`, or `BaseContext` when `App` is no app |
92
+ | `ContextStorageMiddleware<App>` | its type: a middleware with `context()` and `tryContext()` |
68
93
  | `getContext`, `tryGetContext`, `getRequestContext`, `tryGetRequestContext`, `runWithContext` | the store, untyped |
69
94
  | `ContextStorageError`, `ContextStorageErrorCode` | why there is no context |
70
95
 
71
96
  ## Documentation
72
97
 
73
- - [Guide](https://github.com/softistx/alxia/tree/develop/packages/context-storage/docs): what the plugin stores and when, reading it from a service or a logger, its typing, where it sits among hooks, what a timer or a detached callback sees, and jobs and tests with `runWithContext`.
98
+ - [Guide](https://github.com/softistx/alxia/tree/develop/packages/context-storage/docs): what the middleware stores and when, reading it from a service or a logger, its typing, where it sits among the other middlewares, what a timer or a detached callback sees, and jobs and tests with `runWithContext`.
74
99
  - [Troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/context-storage/docs/troubleshooting.md): a `ContextStorageError`, the `TypeError` of `use(contextStorage)`, or a `tsc` error, and what to do about it.
75
100
  - [Roadmap](https://github.com/softistx/alxia/blob/develop/packages/context-storage/docs/roadmap.md): what is coming, and what is not planned.
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { ContextStorageError, type ContextStorageErrorCode, type ContextStoragePlugin, contextStorage, getContext, getRequestContext, runWithContext, tryGetContext, tryGetRequestContext, } from './storage';
1
+ export { ContextStorageError, type ContextStorageErrorCode, type ContextStorageMiddleware, contextStorage, getContext, getRequestContext, runWithContext, type StoredContext, tryGetContext, tryGetRequestContext, } from './storage';
2
2
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,mBAAmB,EACnB,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EACzB,cAAc,EACd,UAAU,EACV,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,oBAAoB,GACpB,MAAM,WAAW,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,mBAAmB,EACnB,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,EAC7B,cAAc,EACd,UAAU,EACV,iBAAiB,EACjB,cAAc,EACd,KAAK,aAAa,EAClB,aAAa,EACb,oBAAoB,GACpB,MAAM,WAAW,CAAC"}
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  // src/storage.ts
2
2
  import { AsyncLocalStorage } from "node:async_hooks";
3
3
  import {
4
- alxia
4
+ defineMiddleware,
5
+ markFactory,
6
+ settle
5
7
  } from "@alxia/core";
6
8
 
7
9
  class ContextStorageError extends Error {
@@ -40,22 +42,21 @@ function contextStorage(...uncalled) {
40
42
  if (uncalled.length > 0) {
41
43
  throw new TypeError("contextStorage is a factory: use(contextStorage()), not use(contextStorage)");
42
44
  }
43
- const plugin = alxia().around((request, next) => {
45
+ const middleware = defineMiddleware(function contextStorage(ctx, next) {
46
+ const routed = ctx.route === undefined ? undefined : ctx;
44
47
  const current = storage.getStore();
45
- if (current?.request.request === request.request)
46
- return next();
47
- return storage.run({ request, ctx: undefined }, next);
48
- }).wrap((ctx, next) => {
49
- const holder = storage.getStore();
50
- if (holder !== undefined)
51
- holder.ctx = ctx;
52
- return next();
48
+ if (current !== undefined && current.request.url === ctx.url) {
49
+ current.ctx = routed ?? current.ctx;
50
+ return settle(ctx, next());
51
+ }
52
+ return storage.run({ request: ctx, ctx: routed }, () => settle(ctx, next()));
53
53
  });
54
- return Object.assign(plugin, {
54
+ return Object.assign(middleware, {
55
55
  context: () => getContext(),
56
56
  tryContext: () => tryGetContext()
57
57
  });
58
58
  }
59
+ markFactory(contextStorage);
59
60
  export {
60
61
  ContextStorageError,
61
62
  contextStorage,
@@ -66,5 +67,5 @@ export {
66
67
  tryGetRequestContext
67
68
  };
68
69
 
69
- //# debugId=F3B664A2ABF678EE64756E2164756E21
70
+ //# debugId=C6E5225783F4C54464756E2164756E21
70
71
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/storage.ts"],
4
4
  "sourcesContent": [
5
- "import { AsyncLocalStorage } from 'node:async_hooks';\nimport {\n\ttype Alxia,\n\talxia,\n\ttype BaseContext,\n\ttype ContextOf,\n\ttype Empty,\n\ttype RequestContext,\n} from '@alxia/core';\n\n/** Why there is no context to read. */\nexport type ContextStorageErrorCode =\n\t/** Called outside any request: at startup, in a job, after the response. */\n\t| 'OUTSIDE_REQUEST'\n\t/** In a request, but not in a route declared after `contextStorage()`: a 404, a hook, a route before it. */\n\t| 'NOT_ROUTED';\n\nexport class ContextStorageError extends Error {\n\toverride readonly name = 'ContextStorageError';\n\treadonly code: ContextStorageErrorCode;\n\n\tconstructor(code: ContextStorageErrorCode) {\n\t\tsuper(\n\t\t\tcode === 'OUTSIDE_REQUEST'\n\t\t\t\t? 'getContext(): called outside a request — use tryGetContext(), or runWithContext() in a job or a test'\n\t\t\t\t: 'getContext(): this request reached no route declared after contextStorage() — use it earlier, or getRequestContext()',\n\t\t);\n\t\tthis.code = code;\n\t}\n}\n\ninterface Holder {\n\treadonly request: RequestContext;\n\tctx: BaseContext | undefined;\n}\n\n/**\n * One store for the process: every `contextStorage()` writes to it, and\n * `getContext()` reads it wherever it is called — the way `hono/context-storage`\n * works, so code written against one ports to the other.\n */\nconst storage = new AsyncLocalStorage<Holder>();\n\n/**\n * The context of the route the current request reached: what its handler\n * reads — the request, `set`, `reply`, and what every hook before it added.\n * Throws a `ContextStorageError` outside a route declared after\n * `contextStorage()`.\n *\n * `Ctx` types what the hooks added; prefer the typed `context()` of the plugin\n * itself, typed by the app.\n */\nexport function getContext<Ctx extends object = Empty>(): BaseContext & Ctx {\n\tconst holder = storage.getStore();\n\tif (holder === undefined) throw new ContextStorageError('OUTSIDE_REQUEST');\n\tif (holder.ctx === undefined) throw new ContextStorageError('NOT_ROUTED');\n\treturn holder.ctx as BaseContext & Ctx;\n}\n\n/** `getContext()`, or `undefined` where it would throw: code that runs in and out of requests. */\nexport function tryGetContext<Ctx extends object = Empty>():\n\t| (BaseContext & Ctx)\n\t| undefined {\n\treturn storage.getStore()?.ctx as (BaseContext & Ctx) | undefined;\n}\n\n/**\n * The request as every global hook sees it — before routing, in a 404, in\n * an `onResponse` — with the route it reached and the error it failed with.\n */\nexport function getRequestContext(): RequestContext {\n\tconst holder = storage.getStore();\n\tif (holder === undefined) throw new ContextStorageError('OUTSIDE_REQUEST');\n\treturn holder.request;\n}\n\n/** `getRequestContext()`, or `undefined` outside a request. */\nexport function tryGetRequestContext(): RequestContext | undefined {\n\treturn storage.getStore()?.request;\n}\n\n/**\n * Runs `work` with `ctx` as the current context: a job, a queue consumer,\n * a test calling a service that reads `getContext()`.\n */\nexport function runWithContext<T>(ctx: BaseContext, work: () => T): T {\n\treturn storage.run({ request: ctx, ctx }, work);\n}\n\n/** The plugin, and its context typed by the app it follows. */\nexport type ContextStoragePlugin<App> = Alxia<Empty, Empty, '', never> & {\n\t/** `getContext()`, typed by `App`. */\n\tcontext(): ContextOf<App> extends never ? BaseContext : ContextOf<App>;\n\t/** `tryGetContext()`, typed by `App`. */\n\ttryContext():\n\t\t| (ContextOf<App> extends never ? BaseContext : ContextOf<App>)\n\t\t| undefined;\n};\n\n/**\n * The request's context, anywhere it runs, as a plugin: from the routes\n * declared after it, every function their handlers call — however deep,\n * through every `await` and timer — reads it with `getContext()`, without\n * it being passed down.\n *\n * Typed by the app it is used on: give the plugin that app's type, and its\n * `context()` returns what its routes read — the `user` a session derived, the\n * `db` decorated.\n *\n * ```ts\n * const base = alxia().decorate({ db }).use(session(auth, { required: true }));\n * export const requestContext = contextStorage<typeof base>();\n * const app = base.use(requestContext).get('/orders', ({ reply }) => reply(200, listOrders()));\n *\n * // orders.ts — no context passed\n * export const listOrders = () => {\n * const { db, user } = requestContext.context();\n * return db.orders.forUser(user.id);\n * };\n * ```\n */\nexport function contextStorage<App = undefined>(\n\t...uncalled: readonly never[]\n): ContextStoragePlugin<App> {\n\tif (uncalled.length > 0) {\n\t\t// `use(contextStorage)`: the app is handed to the factory, and what\n\t\t// follows would be declared on a plugin nobody serves.\n\t\tthrow new TypeError(\n\t\t\t'contextStorage is a factory: use(contextStorage()), not use(contextStorage)',\n\t\t);\n\t}\n\tconst plugin = alxia()\n\t\t.around((request, next) => {\n\t\t\tconst current = storage.getStore();\n\t\t\tif (current?.request.request === request.request) return next();\n\t\t\treturn storage.run({ request, ctx: undefined }, next);\n\t\t})\n\t\t.wrap((ctx, next) => {\n\t\t\tconst holder = storage.getStore();\n\t\t\tif (holder !== undefined) holder.ctx = ctx;\n\t\t\treturn next();\n\t\t});\n\treturn Object.assign(plugin, {\n\t\tcontext: () => getContext(),\n\t\ttryContext: () => tryGetContext(),\n\t}) as unknown as ContextStoragePlugin<App>;\n}\n"
5
+ "import { AsyncLocalStorage } from 'node:async_hooks';\nimport {\n\ttype BaseContext,\n\ttype ContextOf,\n\tdefineMiddleware,\n\ttype Empty,\n\ttype Middleware,\n\ttype Mounted,\n\tmarkFactory,\n\ttype RegisteredBase,\n\ttype RequestContext,\n\ttype RequiresOf,\n\tsettle,\n} from '@alxia/core';\n\n/** Why there is no context to read. */\nexport type ContextStorageErrorCode =\n\t/** Called outside any request: at startup, in a job, after the response. */\n\t| 'OUTSIDE_REQUEST'\n\t/** In a request `contextStorage()` ran on, but that reached no route: a 404, a 405. A route it did not run on is `OUTSIDE_REQUEST`. */\n\t| 'NOT_ROUTED';\n\nexport class ContextStorageError extends Error {\n\toverride readonly name = 'ContextStorageError';\n\treadonly code: ContextStorageErrorCode;\n\n\tconstructor(code: ContextStorageErrorCode) {\n\t\tsuper(\n\t\t\tcode === 'OUTSIDE_REQUEST'\n\t\t\t\t? 'getContext(): called outside a request — use tryGetContext(), or runWithContext() in a job or a test'\n\t\t\t\t: 'getContext(): this request reached no route declared after contextStorage() — use it earlier, or getRequestContext()',\n\t\t);\n\t\tthis.code = code;\n\t}\n}\n\ninterface Holder {\n\treadonly request: RequestContext;\n\tctx: BaseContext | undefined;\n}\n\n/**\n * One store for the process: every `contextStorage()` writes to it, and\n * `getContext()` reads it wherever it is called — the way `hono/context-storage`\n * works, so code written against one ports to the other.\n */\nconst storage = new AsyncLocalStorage<Holder>();\n\n/**\n * The context of the route the current request reached: what its handler\n * reads — the request, `set`, `reply`, and what every hook before it added.\n * Throws a `ContextStorageError` outside a route declared after\n * `contextStorage()`.\n *\n * `Ctx` types what the hooks added; prefer the typed `context()` of the middleware\n * itself, typed by the app.\n */\nexport function getContext<Ctx extends object = Empty>(): BaseContext & Ctx {\n\tconst holder = storage.getStore();\n\tif (holder === undefined) throw new ContextStorageError('OUTSIDE_REQUEST');\n\tif (holder.ctx === undefined) throw new ContextStorageError('NOT_ROUTED');\n\treturn holder.ctx as BaseContext & Ctx;\n}\n\n/** `getContext()`, or `undefined` where it would throw: code that runs in and out of requests. */\nexport function tryGetContext<Ctx extends object = Empty>():\n\t| (BaseContext & Ctx)\n\t| undefined {\n\treturn storage.getStore()?.ctx as (BaseContext & Ctx) | undefined;\n}\n\n/**\n * The request as a middleware sees it — in a 404 too — with the route it\n * reached and the error it failed with.\n */\nexport function getRequestContext(): RequestContext {\n\tconst holder = storage.getStore();\n\tif (holder === undefined) throw new ContextStorageError('OUTSIDE_REQUEST');\n\treturn holder.request;\n}\n\n/** `getRequestContext()`, or `undefined` outside a request. */\nexport function tryGetRequestContext(): RequestContext | undefined {\n\treturn storage.getStore()?.request;\n}\n\n/**\n * Runs `work` with `ctx` as the current context: a job, a queue consumer,\n * a test calling a service that reads `getContext()`.\n */\nexport function runWithContext<T>(ctx: BaseContext, work: () => T): T {\n\treturn storage.run({ request: ctx, ctx }, work);\n}\n\n/**\n * The middleware, and its context typed by the app it follows. It\n * requires that context of the app that uses it, beyond the base context:\n * `app.use` on an app that does not give it is a compile error.\n */\nexport type ContextStorageMiddleware<App> = Middleware<\n\tRequiresOf<StoredContext<App>, 'context'>,\n\tPromise<Response>\n> & {\n\t/** `getContext()`, typed by `App`. */\n\tcontext(): StoredContext<App>;\n\t/** `tryGetContext()`, typed by `App`. */\n\ttryContext(): StoredContext<App> | undefined;\n};\n\n/** What `context()` reads: the context of `App`, or the base context when `App` is no app. */\nexport type StoredContext<App> = [ContextOf<App>] extends [never]\n\t? BaseContext\n\t: Mounted<ContextOf<App>>;\n\n/**\n * The request's context, anywhere it runs, as a middleware: from the\n * routes declared after it, every function their handlers call — however\n * deep, through every `await` and timer — reads it with `getContext()`,\n * without it being passed down; the answer to an error too, a try/catch\n * middleware's included. Give it to `use` before the middlewares whose errors\n * your own middleware answers: what the rest throws is answered inside\n * it, as the route would.\n *\n * Typed by the app it is used on: give the plugin that app's type, and its\n * `context()` returns what its routes read — the `user` a session derived, the\n * `db` decorated. Given none, the app `Register` names in `@alxia/core`\n * (`BaseContext` when nothing is registered). Either way the app that uses\n * it must give that context: using it before is a compile error.\n *\n * ```ts\n * const base = alxia().decorate({ db }).use(session(auth, { required: true }));\n * export const requestContext = contextStorage<typeof base>();\n * const app = base.use(requestContext).get('/orders', ({ reply }) => reply(200, listOrders()));\n *\n * // orders.ts — no context passed\n * export const listOrders = () => {\n * const { db, user } = requestContext.context();\n * return db.orders.forUser(user.id);\n * };\n * ```\n */\nexport function contextStorage<App = RegisteredBase>(\n\t...uncalled: readonly never[]\n): ContextStorageMiddleware<App> {\n\tif (uncalled.length > 0) {\n\t\t// `use(contextStorage)`: the factory runs as the middleware, handed\n\t\t// each request's context, and would store none of them.\n\t\tthrow new TypeError(\n\t\t\t'contextStorage is a factory: use(contextStorage()), not use(contextStorage)',\n\t\t);\n\t}\n\tconst middleware = defineMiddleware(function contextStorage(ctx, next) {\n\t\t// A route's context; none for a request no route matches.\n\t\tconst routed = ctx.route === undefined ? undefined : ctx;\n\t\tconst current = storage.getStore();\n\t\t// A second one, on the same request: the context it reaches is the route's.\n\t\tif (current !== undefined && current.request.url === ctx.url) {\n\t\t\tcurrent.ctx = routed ?? current.ctx;\n\t\t\treturn settle(ctx, next());\n\t\t}\n\t\treturn storage.run({ request: ctx, ctx: routed }, () =>\n\t\t\tsettle(ctx, next()),\n\t\t);\n\t});\n\treturn Object.assign(middleware, {\n\t\tcontext: () => getContext(),\n\t\ttryContext: () => tryGetContext(),\n\t}) as unknown as ContextStorageMiddleware<App>;\n}\n\nmarkFactory(contextStorage);\n"
6
6
  ],
7
- "mappings": ";AAAA;AACA;AAAA;AAAA;AAAA;AAgBO,MAAM,4BAA4B,MAAM;AAAA,EAC5B,OAAO;AAAA,EAChB;AAAA,EAET,WAAW,CAAC,MAA+B;AAAA,IAC1C,MACC,SAAS,oBACN,yGACA,sHACJ;AAAA,IACA,KAAK,OAAO;AAAA;AAEd;AAYA,IAAM,UAAU,IAAI;AAWb,SAAS,UAAsC,GAAsB;AAAA,EAC3E,MAAM,SAAS,QAAQ,SAAS;AAAA,EAChC,IAAI,WAAW;AAAA,IAAW,MAAM,IAAI,oBAAoB,iBAAiB;AAAA,EACzE,IAAI,OAAO,QAAQ;AAAA,IAAW,MAAM,IAAI,oBAAoB,YAAY;AAAA,EACxE,OAAO,OAAO;AAAA;AAIR,SAAS,aAAyC,GAE5C;AAAA,EACZ,OAAO,QAAQ,SAAS,GAAG;AAAA;AAOrB,SAAS,iBAAiB,GAAmB;AAAA,EACnD,MAAM,SAAS,QAAQ,SAAS;AAAA,EAChC,IAAI,WAAW;AAAA,IAAW,MAAM,IAAI,oBAAoB,iBAAiB;AAAA,EACzE,OAAO,OAAO;AAAA;AAIR,SAAS,oBAAoB,GAA+B;AAAA,EAClE,OAAO,QAAQ,SAAS,GAAG;AAAA;AAOrB,SAAS,cAAiB,CAAC,KAAkB,MAAkB;AAAA,EACrE,OAAO,QAAQ,IAAI,EAAE,SAAS,KAAK,IAAI,GAAG,IAAI;AAAA;AAmCxC,SAAS,cAA+B,IAC3C,UACyB;AAAA,EAC5B,IAAI,SAAS,SAAS,GAAG;AAAA,IAGxB,MAAM,IAAI,UACT,6EACD;AAAA,EACD;AAAA,EACA,MAAM,SAAS,MAAM,EACnB,OAAO,CAAC,SAAS,SAAS;AAAA,IAC1B,MAAM,UAAU,QAAQ,SAAS;AAAA,IACjC,IAAI,SAAS,QAAQ,YAAY,QAAQ;AAAA,MAAS,OAAO,KAAK;AAAA,IAC9D,OAAO,QAAQ,IAAI,EAAE,SAAS,KAAK,UAAU,GAAG,IAAI;AAAA,GACpD,EACA,KAAK,CAAC,KAAK,SAAS;AAAA,IACpB,MAAM,SAAS,QAAQ,SAAS;AAAA,IAChC,IAAI,WAAW;AAAA,MAAW,OAAO,MAAM;AAAA,IACvC,OAAO,KAAK;AAAA,GACZ;AAAA,EACF,OAAO,OAAO,OAAO,QAAQ;AAAA,IAC5B,SAAS,MAAM,WAAW;AAAA,IAC1B,YAAY,MAAM,cAAc;AAAA,EACjC,CAAC;AAAA;",
8
- "debugId": "F3B664A2ABF678EE64756E2164756E21",
7
+ "mappings": ";AAAA;AACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAqBO,MAAM,4BAA4B,MAAM;AAAA,EAC5B,OAAO;AAAA,EAChB;AAAA,EAET,WAAW,CAAC,MAA+B;AAAA,IAC1C,MACC,SAAS,oBACN,yGACA,sHACJ;AAAA,IACA,KAAK,OAAO;AAAA;AAEd;AAYA,IAAM,UAAU,IAAI;AAWb,SAAS,UAAsC,GAAsB;AAAA,EAC3E,MAAM,SAAS,QAAQ,SAAS;AAAA,EAChC,IAAI,WAAW;AAAA,IAAW,MAAM,IAAI,oBAAoB,iBAAiB;AAAA,EACzE,IAAI,OAAO,QAAQ;AAAA,IAAW,MAAM,IAAI,oBAAoB,YAAY;AAAA,EACxE,OAAO,OAAO;AAAA;AAIR,SAAS,aAAyC,GAE5C;AAAA,EACZ,OAAO,QAAQ,SAAS,GAAG;AAAA;AAOrB,SAAS,iBAAiB,GAAmB;AAAA,EACnD,MAAM,SAAS,QAAQ,SAAS;AAAA,EAChC,IAAI,WAAW;AAAA,IAAW,MAAM,IAAI,oBAAoB,iBAAiB;AAAA,EACzE,OAAO,OAAO;AAAA;AAIR,SAAS,oBAAoB,GAA+B;AAAA,EAClE,OAAO,QAAQ,SAAS,GAAG;AAAA;AAOrB,SAAS,cAAiB,CAAC,KAAkB,MAAkB;AAAA,EACrE,OAAO,QAAQ,IAAI,EAAE,SAAS,KAAK,IAAI,GAAG,IAAI;AAAA;AAkDxC,SAAS,cAAoC,IAChD,UAC6B;AAAA,EAChC,IAAI,SAAS,SAAS,GAAG;AAAA,IAGxB,MAAM,IAAI,UACT,6EACD;AAAA,EACD;AAAA,EACA,MAAM,aAAa,iBAAiB,SAAS,cAAc,CAAC,KAAK,MAAM;AAAA,IAEtE,MAAM,SAAS,IAAI,UAAU,YAAY,YAAY;AAAA,IACrD,MAAM,UAAU,QAAQ,SAAS;AAAA,IAEjC,IAAI,YAAY,aAAa,QAAQ,QAAQ,QAAQ,IAAI,KAAK;AAAA,MAC7D,QAAQ,MAAM,UAAU,QAAQ;AAAA,MAChC,OAAO,OAAO,KAAK,KAAK,CAAC;AAAA,IAC1B;AAAA,IACA,OAAO,QAAQ,IAAI,EAAE,SAAS,KAAK,KAAK,OAAO,GAAG,MACjD,OAAO,KAAK,KAAK,CAAC,CACnB;AAAA,GACA;AAAA,EACD,OAAO,OAAO,OAAO,YAAY;AAAA,IAChC,SAAS,MAAM,WAAW;AAAA,IAC1B,YAAY,MAAM,cAAc;AAAA,EACjC,CAAC;AAAA;AAGF,YAAY,cAAc;",
8
+ "debugId": "C6E5225783F4C54464756E2164756E21",
9
9
  "names": []
10
10
  }
package/dist/storage.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { type Alxia, type BaseContext, type ContextOf, type Empty, type RequestContext } from '@alxia/core';
1
+ import { type BaseContext, type ContextOf, type Empty, type Middleware, type Mounted, type RegisteredBase, type RequestContext, type RequiresOf } from '@alxia/core';
2
2
  /** Why there is no context to read. */
3
3
  export type ContextStorageErrorCode =
4
4
  /** Called outside any request: at startup, in a job, after the response. */
5
5
  'OUTSIDE_REQUEST'
6
- /** In a request, but not in a route declared after `contextStorage()`: a 404, a hook, a route before it. */
6
+ /** In a request `contextStorage()` ran on, but that reached no route: a 404, a 405. A route it did not run on is `OUTSIDE_REQUEST`. */
7
7
  | 'NOT_ROUTED';
8
8
  export declare class ContextStorageError extends Error {
9
9
  readonly name = "ContextStorageError";
@@ -16,15 +16,15 @@ export declare class ContextStorageError extends Error {
16
16
  * Throws a `ContextStorageError` outside a route declared after
17
17
  * `contextStorage()`.
18
18
  *
19
- * `Ctx` types what the hooks added; prefer the typed `context()` of the plugin
19
+ * `Ctx` types what the hooks added; prefer the typed `context()` of the middleware
20
20
  * itself, typed by the app.
21
21
  */
22
22
  export declare function getContext<Ctx extends object = Empty>(): BaseContext & Ctx;
23
23
  /** `getContext()`, or `undefined` where it would throw: code that runs in and out of requests. */
24
24
  export declare function tryGetContext<Ctx extends object = Empty>(): (BaseContext & Ctx) | undefined;
25
25
  /**
26
- * The request as every global hook sees it — before routing, in a 404, in
27
- * an `onResponse` — with the route it reached and the error it failed with.
26
+ * The request as a middleware sees it — in a 404 too — with the route it
27
+ * reached and the error it failed with.
28
28
  */
29
29
  export declare function getRequestContext(): RequestContext;
30
30
  /** `getRequestContext()`, or `undefined` outside a request. */
@@ -34,22 +34,33 @@ export declare function tryGetRequestContext(): RequestContext | undefined;
34
34
  * a test calling a service that reads `getContext()`.
35
35
  */
36
36
  export declare function runWithContext<T>(ctx: BaseContext, work: () => T): T;
37
- /** The plugin, and its context typed by the app it follows. */
38
- export type ContextStoragePlugin<App> = Alxia<Empty, Empty, '', never> & {
37
+ /**
38
+ * The middleware, and its context typed by the app it follows. It
39
+ * requires that context of the app that uses it, beyond the base context:
40
+ * `app.use` on an app that does not give it is a compile error.
41
+ */
42
+ export type ContextStorageMiddleware<App> = Middleware<RequiresOf<StoredContext<App>, 'context'>, Promise<Response>> & {
39
43
  /** `getContext()`, typed by `App`. */
40
- context(): ContextOf<App> extends never ? BaseContext : ContextOf<App>;
44
+ context(): StoredContext<App>;
41
45
  /** `tryGetContext()`, typed by `App`. */
42
- tryContext(): (ContextOf<App> extends never ? BaseContext : ContextOf<App>) | undefined;
46
+ tryContext(): StoredContext<App> | undefined;
43
47
  };
48
+ /** What `context()` reads: the context of `App`, or the base context when `App` is no app. */
49
+ export type StoredContext<App> = [ContextOf<App>] extends [never] ? BaseContext : Mounted<ContextOf<App>>;
44
50
  /**
45
- * The request's context, anywhere it runs, as a plugin: from the routes
46
- * declared after it, every function their handlers call — however deep,
47
- * through every `await` and timer — reads it with `getContext()`, without
48
- * it being passed down.
51
+ * The request's context, anywhere it runs, as a middleware: from the
52
+ * routes declared after it, every function their handlers call — however
53
+ * deep, through every `await` and timer — reads it with `getContext()`,
54
+ * without it being passed down; the answer to an error too, a try/catch
55
+ * middleware's included. Give it to `use` before the middlewares whose errors
56
+ * your own middleware answers: what the rest throws is answered inside
57
+ * it, as the route would.
49
58
  *
50
59
  * Typed by the app it is used on: give the plugin that app's type, and its
51
60
  * `context()` returns what its routes read — the `user` a session derived, the
52
- * `db` decorated.
61
+ * `db` decorated. Given none, the app `Register` names in `@alxia/core`
62
+ * (`BaseContext` when nothing is registered). Either way the app that uses
63
+ * it must give that context: using it before is a compile error.
53
64
  *
54
65
  * ```ts
55
66
  * const base = alxia().decorate({ db }).use(session(auth, { required: true }));
@@ -63,5 +74,5 @@ export type ContextStoragePlugin<App> = Alxia<Empty, Empty, '', never> & {
63
74
  * };
64
75
  * ```
65
76
  */
66
- export declare function contextStorage<App = undefined>(...uncalled: readonly never[]): ContextStoragePlugin<App>;
77
+ export declare function contextStorage<App = RegisteredBase>(...uncalled: readonly never[]): ContextStorageMiddleware<App>;
67
78
  //# sourceMappingURL=storage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AACA,OAAO,EACN,KAAK,KAAK,EAEV,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,KAAK,KAAK,EACV,KAAK,cAAc,EACnB,MAAM,aAAa,CAAC;AAErB,uCAAuC;AACvC,MAAM,MAAM,uBAAuB;AAClC,4EAA4E;AAC1E,iBAAiB;AACnB,4GAA4G;GAC1G,YAAY,CAAC;AAEhB,qBAAa,mBAAoB,SAAQ,KAAK;IAC7C,SAAkB,IAAI,yBAAyB;IAC/C,QAAQ,CAAC,IAAI,EAAE,uBAAuB,CAAC;gBAE3B,IAAI,EAAE,uBAAuB;CAQzC;AAcD;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,GAAG,SAAS,MAAM,GAAG,KAAK,KAAK,WAAW,GAAG,GAAG,CAK1E;AAED,kGAAkG;AAClG,wBAAgB,aAAa,CAAC,GAAG,SAAS,MAAM,GAAG,KAAK,KACrD,CAAC,WAAW,GAAG,GAAG,CAAC,GACnB,SAAS,CAEX;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,cAAc,CAIlD;AAED,+DAA+D;AAC/D,wBAAgB,oBAAoB,IAAI,cAAc,GAAG,SAAS,CAEjE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,CAEpE;AAED,+DAA+D;AAC/D,MAAM,MAAM,oBAAoB,CAAC,GAAG,IAAI,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,CAAC,GAAG;IACxE,sCAAsC;IACtC,OAAO,IAAI,SAAS,CAAC,GAAG,CAAC,SAAS,KAAK,GAAG,WAAW,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;IACvE,yCAAyC;IACzC,UAAU,IACP,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,KAAK,GAAG,WAAW,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,GAC7D,SAAS,CAAC;CACb,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,cAAc,CAAC,GAAG,GAAG,SAAS,EAC7C,GAAG,QAAQ,EAAE,SAAS,KAAK,EAAE,GAC3B,oBAAoB,CAAC,GAAG,CAAC,CAuB3B"}
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AACA,OAAO,EACN,KAAK,WAAW,EAChB,KAAK,SAAS,EAEd,KAAK,KAAK,EACV,KAAK,UAAU,EACf,KAAK,OAAO,EAEZ,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,UAAU,EAEf,MAAM,aAAa,CAAC;AAErB,uCAAuC;AACvC,MAAM,MAAM,uBAAuB;AAClC,4EAA4E;AAC1E,iBAAiB;AACnB,uIAAuI;GACrI,YAAY,CAAC;AAEhB,qBAAa,mBAAoB,SAAQ,KAAK;IAC7C,SAAkB,IAAI,yBAAyB;IAC/C,QAAQ,CAAC,IAAI,EAAE,uBAAuB,CAAC;gBAE3B,IAAI,EAAE,uBAAuB;CAQzC;AAcD;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,GAAG,SAAS,MAAM,GAAG,KAAK,KAAK,WAAW,GAAG,GAAG,CAK1E;AAED,kGAAkG;AAClG,wBAAgB,aAAa,CAAC,GAAG,SAAS,MAAM,GAAG,KAAK,KACrD,CAAC,WAAW,GAAG,GAAG,CAAC,GACnB,SAAS,CAEX;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,cAAc,CAIlD;AAED,+DAA+D;AAC/D,wBAAgB,oBAAoB,IAAI,cAAc,GAAG,SAAS,CAEjE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,CAEpE;AAED;;;;GAIG;AACH,MAAM,MAAM,wBAAwB,CAAC,GAAG,IAAI,UAAU,CACrD,UAAU,CAAC,aAAa,CAAC,GAAG,CAAC,EAAE,SAAS,CAAC,EACzC,OAAO,CAAC,QAAQ,CAAC,CACjB,GAAG;IACH,sCAAsC;IACtC,OAAO,IAAI,aAAa,CAAC,GAAG,CAAC,CAAC;IAC9B,yCAAyC;IACzC,UAAU,IAAI,aAAa,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;CAC7C,CAAC;AAEF,8FAA8F;AAC9F,MAAM,MAAM,aAAa,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,GAC9D,WAAW,GACX,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;AAE3B;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,cAAc,CAAC,GAAG,GAAG,cAAc,EAClD,GAAG,QAAQ,EAAE,SAAS,KAAK,EAAE,GAC3B,wBAAwB,CAAC,GAAG,CAAC,CAyB/B"}
package/docs/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # @alxia/context-storage documentation
2
2
 
3
3
  The [package README](../README.md) is the short version. This folder is
4
- the long one: what the plugin stores and when, how to read it from a
4
+ the long one: what the middleware stores and when, how to read it from a
5
5
  service, a repository or a logger, how its type follows the app, where it
6
- sits among an app's hooks, and what a timer or a detached callback sees.
6
+ sits among an app's middlewares, and what a timer or a detached callback sees.
7
7
 
8
8
  | Page | Read it when |
9
9
  | --- | --- |
10
- | [Guide](guide.md) | reading the request's context from code the handler calls, typing it, placing the plugin among other hooks, running a job or a test with a context, or wondering what a timer sees |
11
- | [Troubleshooting](troubleshooting.md) | `getContext()` threw a `ContextStorageError`, a service read the wrong context or none, `use(contextStorage)` threw a `TypeError`, or `tsc` refused the plugin or its type |
10
+ | [Guide](guide.md) | reading the request's context from code the handler calls, typing it, placing the middleware among the others, running a job or a test with a context, or wondering what a timer sees |
11
+ | [Troubleshooting](troubleshooting.md) | `getContext()` threw a `ContextStorageError`, a service read the wrong context or none, `use(contextStorage)` threw a `TypeError`, or `tsc` refused the middleware or its type |
12
12
  | [Roadmap](roadmap.md) | wondering what is coming, and what is not planned |
package/docs/guide.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Guide
2
2
 
3
3
  This page covers what `contextStorage()` stores and when, how code outside
4
- a handler reads it, how its type follows the app, where the plugin sits
5
- among an app's hooks, and what `AsyncLocalStorage` does and does not carry.
4
+ a handler reads it, how its type follows the app, where the middleware sits
5
+ among an app's middlewares, and what `AsyncLocalStorage` does and does not carry.
6
6
 
7
7
  ```ts
8
8
  import { alxia } from '@alxia/core';
@@ -32,14 +32,22 @@ context of the request that called it, and never another's.
32
32
  ## The signature
33
33
 
34
34
  ```ts
35
- // `uncalled` takes nothing: it makes `use(contextStorage)` a compile error
36
- function contextStorage<App = undefined>(...uncalled: readonly never[]): ContextStoragePlugin<App>;
37
-
38
- type ContextStoragePlugin<App> = Alxia<Empty, Empty, '', never> & {
39
- context(): ContextOf<App> extends never ? BaseContext : ContextOf<App>;
40
- tryContext(): (ContextOf<App> extends never ? BaseContext : ContextOf<App>) | undefined;
35
+ // `uncalled` takes nothing: it makes `use(contextStorage)` a compile error.
36
+ // `App` defaults to the app `Register` names in `@alxia/core` (`RegisteredBase`)
37
+ function contextStorage<App = RegisteredBase>(...uncalled: readonly never[]): ContextStorageMiddleware<App>;
38
+
39
+ // A middleware: it requires `App`'s context of the app that mounts it.
40
+ type ContextStorageMiddleware<App> = Middleware<
41
+ RequiresOf<StoredContext<App>, 'context'>,
42
+ Promise<Response>
43
+ > & {
44
+ context(): StoredContext<App>;
45
+ tryContext(): StoredContext<App> | undefined;
41
46
  };
42
47
 
48
+ // What `context()` returns: `App`'s context, or `BaseContext` when `App` is no app
49
+ type StoredContext<App> = [ContextOf<App>] extends [never] ? BaseContext : Mounted<ContextOf<App>>;
50
+
43
51
  function getContext<Ctx extends object = Empty>(): BaseContext & Ctx;
44
52
  function tryGetContext<Ctx extends object = Empty>(): (BaseContext & Ctx) | undefined;
45
53
  function getRequestContext(): RequestContext;
@@ -54,9 +62,11 @@ class ContextStorageError extends Error {
54
62
  type ContextStorageErrorCode = 'OUTSIDE_REQUEST' | 'NOT_ROUTED';
55
63
  ```
56
64
 
57
- `contextStorage()` returns an app plugin: pass it to `use`, called — `use(contextStorage)` fails `tsc` with `TS2769` and throws a `TypeError` at startup ([troubleshooting](troubleshooting.md#typeerror-contextstorage-is-a-factory-usecontextstorage-not-usecontextstorage)). It adds
58
- nothing to the app's type. `BaseContext`, `RequestContext` and `ContextOf`
59
- come from `@alxia/core`.
65
+ `contextStorage()` returns a middleware: pass it to `app.use`, called — `use(contextStorage)` fails `tsc` with `TS2345`, and throws where it is declared ([troubleshooting](troubleshooting.md#use-argument-1-looks-like-a-factory-contextstorage-call-it-usecontextstorage)). It adds
66
+ nothing to the app's type, but it requires `StoredContext<App>` of the app
67
+ that mounts it: `app.use` on an app that does not give that context is a compile
68
+ error. `BaseContext`, `RequestContext`, `ContextOf`, `RegisteredBase`,
69
+ `Middleware`, `RequiresOf` and `Mounted` come from `@alxia/core`.
60
70
 
61
71
  | Export | Returns | Where it would have nothing |
62
72
  | --- | --- | --- |
@@ -64,33 +74,38 @@ come from `@alxia/core`.
64
74
  | `requestContext.tryContext()` | the same | `undefined` |
65
75
  | `getContext<Ctx>()` | the route's context, as `BaseContext & Ctx`: `Ctx` is yours to state | throws a `ContextStorageError` |
66
76
  | `tryGetContext<Ctx>()` | the same | `undefined` |
67
- | `getRequestContext()` | the request as global hooks see it: `request`, `url`, `ip`, `server`, and once routing has run, `route` and `error` | throws a `ContextStorageError` coded `OUTSIDE_REQUEST` |
77
+ | `getRequestContext()` | the request as a middleware sees it: `request`, `url`, `ip`, `server`, `route` (`undefined` when no route matched) and, once a route has failed, `error` | throws a `ContextStorageError` coded `OUTSIDE_REQUEST` |
68
78
  | `tryGetRequestContext()` | the same | `undefined` |
69
79
  | `runWithContext(ctx, work)` | what `work` returns, with `ctx` as the current context while it runs | — |
70
80
 
71
81
  There is one store per copy of the package: every `contextStorage()` writes
72
- to it, and `getContext()` reads it wherever it is called. Two plugins on one
82
+ to it, and `getContext()` reads it wherever it is called. Two of them on one
73
83
  app read the same context.
74
84
 
75
85
  ## What it stores, and when
76
86
 
77
- The plugin adds two hooks. A global `around` hook opens a store for every
78
- request the app receives, wherever the plugin is used, holding the
79
- request's `RequestContext`. A route hook records the route's context in
80
- that store, for the routes declared after the plugin only. So what each
81
- function reads depends on where the code runs:
87
+ `contextStorage()` is a middleware: it opens a store around the rest of the
88
+ chain, holding the request's context, and records the route's context in
89
+ it when the request reached a route. A `use()` on the app runs on every
90
+ request, in declaration order, so what each function reads depends on
91
+ whether the middleware ran on the request, and where the code runs
92
+ relative to it:
82
93
 
83
94
  | Code running in | `getRequestContext()` | `tryGetRequestContext()` | `getContext()` | `tryGetContext()` |
84
95
  | --- | --- | --- | --- | --- |
85
- | an `onRequest` hook | the request, `route` still `undefined` | the same | throws `NOT_ROUTED` | `undefined` |
86
- | a route declared **before** the plugin, and its `onResponse` | the request | the same | throws `NOT_ROUTED` | `undefined` |
87
- | a `derive` or `wrap` declared after the plugin | the request | the same | the route's context, **before validation**: no `params`, `query` or `body` yet | the same |
88
- | the handler of a route declared after the plugin, and everything it calls | the request | the same | the context the handler receives — the same object | the same |
89
- | that route's `onError` and `onResponse` hooks | the request, with `route` and `error` | the same | the same context | the same |
90
- | an `onResponse` for a 404 | the request, `route` `undefined` | the same | throws `NOT_ROUTED` | `undefined` |
96
+ | a middleware declared **before** it | throws `OUTSIDE_REQUEST` | `undefined` | throws `OUTSIDE_REQUEST` | `undefined` |
97
+ | a middleware after it, on a request a route matched | the request, with its `route` | the same | the route's context, **before validation**: no `params`, `query` or `body` yet, and without what later middlewares add | the same |
98
+ | a middleware after it, on a request no route matched (a 404, a 405) | the request, `route` `undefined` | the same | throws `NOT_ROUTED` | `undefined` |
99
+ | the handler of a route the middleware ran on, and everything it calls | the request | the same | the context the handler receives, with what the middlewares added | the same |
100
+ | a middleware that catches an error, after it | the request, with `route` and `error` | the same | the same context | the same |
101
+ | a route declared **before** it, or outside the `group` it is used in | throws `OUTSIDE_REQUEST` | `undefined` | throws `OUTSIDE_REQUEST` | `undefined` |
91
102
  | a socket's handlers (`open`, `message`, `close`) | throws `OUTSIDE_REQUEST` | `undefined` | throws `OUTSIDE_REQUEST` | `undefined` |
92
103
  | startup, a job, a timer started at startup | throws `OUTSIDE_REQUEST` | `undefined` | throws `OUTSIDE_REQUEST` | `undefined` |
93
104
 
105
+ `contextStorage()` settles `next()`: an error the rest of the chain throws
106
+ is answered inside the store, as the route would answer it, so a
107
+ middleware that reads the context while answering an error still finds it.
108
+
94
109
  The two errors carry these messages:
95
110
 
96
111
  | `code` | `message` |
@@ -116,13 +131,13 @@ Code that runs both in and out of requests uses `tryContext()`,
116
131
 
117
132
  The context is the whole of what the handler reads: the request, `set` to
118
133
  add a header or a cookie to the response, `reply` and `redirect`, and what
119
- every hook added — so a service three calls down can set a response header,
134
+ every middleware added — so a service three calls down can set a response header,
120
135
  as `listOrders` does below.
121
136
 
122
137
  ## Reading it from outside the handler
123
138
 
124
- Keep the app's base and the plugin in a module of their own, and import the
125
- plugin from every service, repository or logger that reads it. The handlers
139
+ Keep the app's base and the middleware in a module of their own, and import it
140
+ from every service, repository or logger that reads it. The handlers
126
141
  then call those without passing anything down.
127
142
 
128
143
  ```ts
@@ -171,8 +186,8 @@ export const app = base
171
186
  ```
172
187
 
173
188
  A logger is the code that runs in and out of requests: at startup, in a 404,
174
- in a route. `tryGetRequestContext()` gives it the request wherever there is
175
- one, and `tryContext()` the user where a route was reached:
189
+ in a route. `tryGetRequestContext()` gives it the request wherever the
190
+ middleware ran, and `tryContext()` the user where a route was reached:
176
191
 
177
192
  ```ts
178
193
  // log.ts
@@ -202,8 +217,8 @@ return what a route declared next on that app would read: `ContextOf<App>`.
202
217
 
203
218
  | `App` | `context()` returns |
204
219
  | --- | --- |
205
- | none: `contextStorage()` | `BaseContext`: the request, `set`, `reply`, `redirect`, `route`, `pathParams` |
206
- | `typeof base` | `ContextOf<typeof base>`: `BaseContext` plus everything `base`'s `decorate`, `derive` and plugins added |
220
+ | none: `contextStorage()` | the context `@alxia/core`'s `Register` names, `AppContext`; with nothing registered, `BaseContext`: the request, `set`, `reply`, `redirect`, `route`, `pathParams` |
221
+ | `typeof base` | `ContextOf<typeof base>`: `BaseContext` plus everything `base`'s `decorate`, `derive` and middlewares added |
207
222
 
208
223
  ```ts
209
224
  import { alxia } from '@alxia/core';
@@ -223,24 +238,46 @@ export function whoIsAsking(): string {
223
238
  }
224
239
  ```
225
240
 
226
- Three rules follow from typing by an app:
241
+ With `Register` augmented beside `base`
242
+ ([`@alxia/core`'s `Register`](https://github.com/softistx/alxia/blob/develop/packages/core/docs/guide/types.md#register-and-appcontext)),
243
+ `contextStorage()` is `contextStorage<typeof base>()` with no import of
244
+ `base`:
245
+
246
+ ```ts
247
+ declare module '@alxia/core' {
248
+ interface Register {
249
+ context: typeof base;
250
+ }
251
+ }
252
+
253
+ export const requestContext = contextStorage(); // context().user: string
254
+ ```
255
+
256
+ Four rules follow from typing by an app:
257
+
258
+ - **The app that mounts it must give that context.** The middleware requires
259
+ what `context()` reads beyond `BaseContext`, as a `definePlugin` does:
260
+ `alxia().use(contextStorage<typeof base>())` is a compile error, since
261
+ `context()` would claim a `user` that no middleware of that app adds.
227
262
 
228
- - **Type it by the app before the plugin, never by the app that uses it.**
263
+ - **Type it by the app before the middleware, never by the app that mounts it.**
229
264
  `const app = alxia().use(requestContext)…` with
230
265
  `requestContext = contextStorage<typeof app>()` is a circular type, which
231
- `tsc` refuses with `TS7022`. Declare `base` first, as above.
232
- - **What a hook after the plugin adds is there at runtime, not in the
266
+ `tsc` refuses with `TS7022`. Declare `base` first, as above. Registered,
267
+ the same holds: give `contextStorage()` to the app after `base`, never
268
+ to the registered `base` itself.
269
+ - **What a middleware after it adds is there at runtime, not in the
233
270
  type.** `context()` knows `base`, so a `derive` added after
234
- `base.use(requestContext)` is missing from its type. Put the hooks whose
235
- values services read in `base`, or state the type with `getContext<Ctx>()`.
271
+ `base.use(requestContext)` is missing from its type. Put the middlewares
272
+ whose values services read in `base`, or state the type with `getContext<Ctx>()`.
236
273
  - **A route's own `params`, `query`, `body` and `headers` are not in it**:
237
- they belong to one route's schema, not to the app. Read them in the
238
- handler and pass them down, or state them:
274
+ they belong to one route's `validate(…)`, not to the app. Read them in
275
+ the handler and pass them down, or state them:
239
276
 
240
277
  ```ts
241
278
  import { getContext } from '@alxia/context-storage';
242
279
 
243
- // in code that only ever runs under GET /orders/:id, with a params schema
280
+ // in code that only ever runs under GET /orders/:id, after its validate({ params })
244
281
  const { params } = getContext<{ params: { id: number } }>();
245
282
  ```
246
283
 
@@ -249,10 +286,10 @@ as `hono/context-storage`'s do: nothing checks it against the route.
249
286
 
250
287
  ## Where it sits
251
288
 
252
- The plugin's `around` hook is global: it applies to every request, wherever
253
- `use` is called. Its route hook applies to the routes declared after it, in
254
- the same app or group. So **use it before the routes whose code reads the
255
- context**, and after the hooks whose values services read:
289
+ `app.use(contextStorage())` runs on every request, a 404 included, and
290
+ wraps what is declared after it, in the same app or group. So **use it
291
+ before the routes whose code reads the context**, and after the middlewares
292
+ whose values services read:
256
293
 
257
294
  ```ts
258
295
  import { alxia } from '@alxia/core';
@@ -263,27 +300,26 @@ const requestContext = contextStorage();
263
300
  const app = alxia()
264
301
  .get('/health', ({ reply }) => reply(200, 'ok')) // getContext() throws NOT_ROUTED here
265
302
  .use(requestContext)
266
- .derive(() => ({ startedAt: Date.now() })) // after the plugin: may call code that reads it
303
+ .derive(() => ({ startedAt: Date.now() })) // after it: may call code that reads it
267
304
  .get('/me', ({ reply }) => reply(200, getContext().route)); // '/me'
268
305
  ```
269
306
 
270
- A hook declared before the plugin that ends the request — a `derive`
271
- answering `401` — ends it before the plugin's route hook runs: in that
272
- request's `onResponse`, `getContext()` throws `NOT_ROUTED`, and
273
- `getRequestContext()` still answers.
307
+ A middleware declared before `contextStorage()` that ends the request, a
308
+ `401` from a guard, ends it before the middleware runs: nothing it calls
309
+ finds a store. Put the guards after `contextStorage()` when the code that
310
+ answers needs the context; put it after the observers (`logger`,
311
+ `telemetry`) and before an error-handling `try`/`catch` that reads it.
274
312
 
275
313
  | Placement | Effect |
276
314
  | --- | --- |
277
- | at the top of the chain | every route can read it; `context()` is typed `BaseContext` unless typed by an app declared before it |
315
+ | first on the app | every route after it, and every middleware after it, can read it; a request no route matches gets `getRequestContext()` too; `context()` is typed `BaseContext` unless typed by an app declared before it |
278
316
  | after `decorate` and `derive` | the usual place: typed by them, and every route after reads it |
279
- | inside a `group` | the group's routes read it; a route outside the group throws `NOT_ROUTED` |
317
+ | inside a `group` | the group's routes read it; a route outside the group, or a request no route matches, throws `OUTSIDE_REQUEST`: the middleware never ran |
280
318
  | twice | harmless: one store, one context per request |
281
319
 
282
- The plugin is an app like any other: `requestContext.get('/x', handler)` is the
283
- route method, and the context is read with `context()` and `tryContext()`.
284
- Declare routes on the app rather than on the plugin, and pass
285
- `contextStorage()` to `use` called — the uncalled form is
286
- [refused](troubleshooting.md#typeerror-contextstorage-is-a-factory-usecontextstorage-not-usecontextstorage).
320
+ `contextStorage()` is a middleware, not an app: declare routes on the app,
321
+ and pass `contextStorage()` to `app.use` called — the uncalled form is
322
+ [refused](troubleshooting.md#use-argument-1-looks-like-a-factory-contextstorage-call-it-usecontextstorage).
287
323
 
288
324
  ## What `AsyncLocalStorage` carries
289
325
 
@@ -312,7 +348,7 @@ runs:
312
348
  | a `setInterval`, queue consumer or pool started at startup | nothing: `context()` throws `OUTSIDE_REQUEST`, `tryContext()` is `undefined` |
313
349
  | a function pushed into a queue in the request, and run by something started at startup | nothing, as above |
314
350
  | an `EventEmitter` listener | the context of the code that called `emit`, since listeners run synchronously |
315
- | a WebSocket's `open`, `message` and `close` | nothing: a socket's upgrade runs outside `around` |
351
+ | a WebSocket's `open`, `message` and `close` | nothing: a socket's handlers run outside the chain |
316
352
 
317
353
  So work that outlives the request — a write-behind, an e-mail, an audit
318
354
  event — takes what it needs **before** it is detached, instead of reading
package/docs/roadmap.md CHANGED
@@ -7,7 +7,11 @@ only number on it. Every release, with each change it made, is in
7
7
 
8
8
  ## Now
9
9
 
10
- Nothing scheduled yet.
10
+ - **A middleware, not a plugin (0.4).** `app.use(contextStorage())` opens the store on every request, a 404 included: `getRequestContext()` works in every middleware after it, and an error is answered inside it, so a middleware that catches it still reads the context. `app.plugin(contextStorage())`, the deprecated plugin form, was removed with alxia 0.5: give it to `use`.
11
+ - **Typed by `Register`.** With `@alxia/core`'s `Register` naming the
12
+ base, `contextStorage()` needs no type argument: `context()` reads the
13
+ registered context. Typed either way, the middleware requires that context
14
+ of the app that mounts it, a compile error otherwise.
11
15
 
12
16
  ## Next
13
17
 
@@ -33,9 +37,9 @@ Nothing scheduled yet.
33
37
  every `await`, timer and promise, and never another request's.
34
38
  - **Typed by the app.** `contextStorage<typeof base>()` gives `context()` and
35
39
  `tryContext()` what a route declared next on `base` reads: the values its
36
- `decorate` and `derive` hooks added.
37
- - **The request in global hooks.** `getRequestContext()` reads the request
38
- before routing, in a 404 and in an `onResponse`, with the route it
40
+ `decorate` and `derive` added.
41
+ - **The request before routing.** `getRequestContext()` reads the request
42
+ before routing and in a 404, with the route it
39
43
  reached and the error it failed with; `tryGetRequestContext()` returns
40
44
  `undefined` outside a request instead of throwing.
41
45
  - **Jobs and tests.** `runWithContext(ctx, work)` runs code that reads the
@@ -43,8 +47,8 @@ Nothing scheduled yet.
43
47
  - **A refusal that says why.** Where there is no context, `getContext()`
44
48
  throws a `ContextStorageError` coded `OUTSIDE_REQUEST` or `NOT_ROUTED`,
45
49
  and `tryGetContext()` returns `undefined`.
46
- `use(contextStorage)`, the factory uncalled, fails `tsc` and throws a
47
- `TypeError` at startup, rather than leaving the routes after it unserved.
50
+ `use(contextStorage)`, the factory uncalled, fails `tsc` and throws
51
+ where it is declared (since alxia 0.5), rather than answering its routes 500.
48
52
  - **Ported from `hono/context-storage`.** One store, and `getContext()`
49
53
  read wherever it is called, as Hono's is: code written against one ports
50
54
  to the other.
@@ -7,7 +7,7 @@ behaviour that prints nothing, or an error from `tsc`. A
7
7
 
8
8
  **Runtime**
9
9
 
10
- - [`TypeError: contextStorage is a factory: use(contextStorage()), not use(contextStorage)`](#typeerror-contextstorage-is-a-factory-usecontextstorage-not-usecontextstorage)
10
+ - [`use(): argument 1 looks like a factory (contextStorage): call it, use(contextStorage())`](#use-argument-1-looks-like-a-factory-contextstorage-call-it-usecontextstorage), and `TypeError: contextStorage is a factory: use(contextStorage()), not use(contextStorage)`
11
11
  - [`ContextStorageError: getContext(): called outside a request — use tryGetContext(), or runWithContext() in a job or a test`](#contextstorageerror-getcontext-called-outside-a-request--use-trygetcontext-or-runwithcontext-in-a-job-or-a-test)
12
12
  - [`ContextStorageError: getContext(): this request reached no route declared after contextStorage() — use it earlier, or getRequestContext()`](#contextstorageerror-getcontext-this-request-reached-no-route-declared-after-contextstorage--use-it-earlier-or-getrequestcontext)
13
13
  - [A header set from a timer never reaches the response](#a-header-set-from-a-timer-never-reaches-the-response)
@@ -18,26 +18,30 @@ behaviour that prints nothing, or an error from `tsc`. A
18
18
  - [`Property 'user' does not exist on type 'BaseContext'.`](#property-user-does-not-exist-on-type-basecontext)
19
19
  - [`Property 'params' does not exist on type 'BaseContext & …'.`](#property-params-does-not-exist-on-type-basecontext--)
20
20
  - [`Object literal may only specify known properties, and 'db' does not exist in type 'BaseContext'.`](#object-literal-may-only-specify-known-properties-and-db-does-not-exist-in-type-basecontext)
21
+ - [`Property 'user' is missing in type 'BaseContext & Empty' but required in type '{ user: string; }'`](#property-user-is-missing-in-type-basecontext--empty-but-required-in-type--user-string-)
21
22
 
22
23
  ## Runtime
23
24
 
24
- ### `TypeError: contextStorage is a factory: use(contextStorage()), not use(contextStorage)`
25
+ ### `use(): argument 1 looks like a factory (contextStorage): call it, use(contextStorage())`
25
26
 
26
27
  `tsc` reports the same mistake first:
27
28
 
28
29
  ```text
29
- error TS2769: No overload matches this call.
30
- …
31
- Argument of type '<App = undefined>(...uncalled: readonly never[]) => ContextStoragePlugin<App>' is not assignable to parameter of type '(app: Alxia<Empty, Empty, "", never>) => ContextStoragePlugin<undefined>'.
30
+ error TS2345: Argument of type '<App = Alxia<Empty, "">>(...uncalled: readonly never[]) => ContextStorageMiddleware<App>' is not assignable to parameter of type '…'.
31
+ Type '<App = Alxia<Empty, "">>(...uncalled: readonly never[]) => ContextStorageMiddleware<App>' is not assignable to type '(ctx: never, next: NextFunction) => … & "this looks like a factory given uncalled: call it, as use(cors()) and not use(cors)"'.
32
+ Types of parameters 'uncalled' and 'next' are incompatible.
33
+ Type 'NextFunction' is not assignable to type 'never'.
32
34
  ```
33
35
 
34
- **When:** at startup, on `.use(contextStorage)`: the factory given to `use`
35
- without being called.
36
+ **When:** `.use(contextStorage)`, the factory given to `app.use` without
37
+ being called. It throws where the app is declared, since alxia 0.5; before,
38
+ each request the routes after it answered was a 500, with
39
+ `TypeError: contextStorage is a factory: use(contextStorage()), not use(contextStorage)`
40
+ in the server log, which a factory run past `use` still throws.
36
41
 
37
- **Why:** `use` takes a function as a plugin and calls it with the app.
38
- Called that way, `contextStorage` would return a new, empty plugin app,
39
- and every route declared after it would land on an app nobody serves; it
40
- refuses the argument instead.
42
+ **Why:** `contextStorage` is marked as a factory, so `use`, a route and
43
+ `plugin` refuse it. Run as a middleware, it would make a new middleware
44
+ each time and store nothing.
41
45
 
42
46
  **Fix:** call it, once, and keep the result:
43
47
 
@@ -58,13 +62,16 @@ and `getRequestContext()` all throw it.
58
62
  - in a job, a queue consumer, or a callback run by a `setInterval` started
59
63
  at startup — including a function pushed onto a queue during a request
60
64
  and run later by such a timer;
61
- - in a WebSocket's `open`, `message` or `close`, since a socket's upgrade
62
- runs outside the plugin's hook;
65
+ - in a route declared **before** `use(requestContext)`, or outside the
66
+ `group` it is used in: the middleware never ran on that request;
67
+ - in a middleware declared before it, which runs outside the store;
68
+ - in a WebSocket's `open`, `message` or `close`, since a socket's handlers
69
+ run outside the chain;
63
70
  - with two copies of `@alxia/context-storage` installed: each has its own
64
- store, so a plugin from one is invisible to `getContext()` from the other.
71
+ store, so the middleware from one is invisible to `getContext()` from the other.
65
72
 
66
- **Why:** the context lives in an `AsyncLocalStorage` that the plugin opens
67
- for each request. A callback reads the store that was current where it was
73
+ **Why:** the context lives in an `AsyncLocalStorage` that the middleware opens
74
+ for each request it runs on. A callback reads the store that was current where it was
68
75
  **scheduled**; anything started outside a request has none.
69
76
 
70
77
  **Fix:** in code that runs in and out of requests, use `tryContext()` —
@@ -102,7 +109,7 @@ export function auditLater(action: string): void {
102
109
  }
103
110
  ```
104
111
 
105
- In a socket handler, read what the upgrade's hooks added from
112
+ In a socket handler, read what the upgrade's middlewares added from
106
113
  `socket.data`. For two copies, `bun pm ls --all | grep context-storage`
107
114
  shows them; align the versions the app and its dependencies ask for.
108
115
 
@@ -110,39 +117,35 @@ shows them; align the versions the app and its dependencies ask for.
110
117
 
111
118
  `error.code` is `'NOT_ROUTED'`.
112
119
 
113
- **When:** in a request, but not inside a route declared after the plugin:
114
-
115
- - in a route declared **before** `use(requestContext)`, or outside the
116
- `group` it is used in;
117
- - in an `onRequest` hook, which runs before routing;
118
- - in an `onResponse` hook, for a `404`, or for a request a hook declared
119
- before the plugin refused — a `derive` answering `401`;
120
- - in code those call.
121
-
122
- **Why:** the plugin opens the store for every request, but records the
123
- route's context only for the routes declared after it: until then there is
124
- a request and no route.
125
-
126
- **Fix:** use the plugin before the routes whose code reads it:
120
+ **When:** in a request the middleware ran on, but that reached no route:
121
+ a 404 or a 405, in a middleware after `use(requestContext)`, or in code
122
+ that middleware calls.
127
123
 
128
- ```ts
129
- const app = base
130
- .use(requestContext) // before every route that reads it
131
- .get('/orders', async ({ reply }) => reply(200, await listOrders()));
132
- ```
124
+ **Why:** `use(requestContext)` on the app runs on every request, and records
125
+ the route's context only when a route matched: for an unmatched request
126
+ there is a request and no route.
133
127
 
134
- In a global hook, or in code that also runs for a 404, read the request
135
- instead, which holds wherever there is one:
128
+ **Fix:** in a middleware, or in code that also runs for a 404, read the
129
+ request instead, which holds wherever the middleware ran:
136
130
 
137
131
  ```ts
132
+ import { defineMiddleware } from '@alxia/core';
138
133
  import { getRequestContext } from '@alxia/context-storage';
139
134
 
140
- app.onResponse((response) => {
141
- const { request, route } = getRequestContext(); // route is undefined for a 404
142
- console.log(request.method, route ?? 'unmatched', response.status);
143
- });
135
+ app.use(requestContext).use(
136
+ defineMiddleware(async (_ctx, next) => {
137
+ const response = await next();
138
+ const { request, route } = getRequestContext(); // route is undefined for a 404
139
+ console.log(request.method, route ?? 'unmatched', response.status);
140
+ return response;
141
+ }),
142
+ );
144
143
  ```
145
144
 
145
+ A route declared before the middleware, or outside its `group`, throws
146
+ `OUTSIDE_REQUEST` instead (see above): mount it before the routes whose
147
+ code reads it.
148
+
146
149
  ### A header set from a timer never reaches the response
147
150
 
148
151
  **When:** a `setTimeout`, a promise that is not awaited, or a callback
@@ -175,16 +178,16 @@ error TS7022: 'requestContext' implicitly has type 'any' because it does not hav
175
178
 
176
179
  Often with `TS2448: Block-scoped variable 'requestContext' used before its declaration.`
177
180
 
178
- **When:** the plugin is typed by the app that uses it:
181
+ **When:** the middleware is typed by the app that mounts it:
179
182
 
180
183
  ```ts
181
184
  export const app = alxia().decorate({ db }).use(requestContext).get(/* … */);
182
185
  export const requestContext = contextStorage<typeof app>(); // circular
183
186
  ```
184
187
 
185
- **Why:** `app`'s type depends on the plugin, and the plugin's on `app`.
188
+ **Why:** `app`'s type depends on the middleware, and the middleware's on `app`.
186
189
 
187
- **Fix:** type it by the app as it stands **before** the plugin:
190
+ **Fix:** type it by the app as it stands **before** the middleware:
188
191
 
189
192
  ```ts
190
193
  export const base = alxia().decorate({ db });
@@ -200,18 +203,22 @@ error TS2339: Property 'user' does not exist on type 'BaseContext'.
200
203
 
201
204
  Also as `Property 'user' does not exist on type 'BaseContext & Empty & { readonly db: … }'.`
202
205
 
203
- **When:** reading from `context()` a value a hook adds, and either
206
+ **When:** reading from `context()` a value a middleware adds, and either
204
207
 
205
- - the plugin was made without an app type, `contextStorage()`; or
206
- - it is typed by `base`, and the hook adding `user` comes after it:
208
+ - the middleware was made without an app type, `contextStorage()`, and
209
+ `@alxia/core`'s `Register` names no base; or
210
+ - it is `contextStorage()` given to the registered `base` itself, which
211
+ cannot read `Register` while `base` is being typed; or
212
+ - it is typed by `base`, and the middleware adding `user` comes after it:
207
213
  `base.use(requestContext).derive(() => ({ user }))`.
208
214
 
209
215
  **Why:** `context()` returns `ContextOf<App>`: what a route declared next on
210
- `App` reads. With no `App`, that is `BaseContext`; a hook after `App` is
216
+ `App` reads. With no `App`, that is `BaseContext`; a middleware after `App` is
211
217
  not in it. At runtime the value is there.
212
218
 
213
- **Fix:** declare every hook whose values services read in `base`, then type
214
- the plugin by it:
219
+ **Fix:** declare every middleware whose values services read in `base`, then type
220
+ it by `base` — or register `base` with `@alxia/core`'s `Register` and
221
+ give `contextStorage()` to the app after `base`, never to `base` itself:
215
222
 
216
223
  ```ts
217
224
  const base = alxia()
@@ -232,9 +239,10 @@ The same for `query`, `body` and `headers`.
232
239
 
233
240
  **When:** reading a route's validated input from `requestContext.context()`.
234
241
 
235
- **Why:** those belong to one route's schema, not to the app, so the app's
236
- context does not have them. Hooks run before validation: a `derive` reads
237
- them as `undefined` even at runtime.
242
+ **Why:** those belong to one route's `validate(…)`, not to the app, so the
243
+ app's context does not have them. The app's middlewares run before a
244
+ route's own, `validate` among them: a `derive` reads the request as it
245
+ arrived, the body `undefined`, even at runtime.
238
246
 
239
247
  **Fix:** pass them from the handler, or, in code that only runs under one
240
248
  route, state them:
@@ -274,3 +282,33 @@ const ctx = {
274
282
 
275
283
  runWithContext(ctx as unknown as Ctx, () => listOrders());
276
284
  ```
285
+
286
+ ### `Property 'user' is missing in type 'BaseContext & Empty' but required in type '{ user: string; }'`
287
+
288
+ ```text
289
+ error TS2769: No overload matches this call.
290
+ …
291
+ Type 'BaseContext & Empty' is not assignable to type 'MiddlewareContext<{ user: string; }>'.
292
+ Property 'user' is missing in type 'BaseContext & Empty' but required in type '{ user: string; }'.
293
+ ```
294
+
295
+ **When:** an app uses the middleware typed by another app, `contextStorage<typeof
296
+ base>()`, or by the registered one, `contextStorage()`, and does not give
297
+ what that app's middlewares add:
298
+
299
+ ```ts
300
+ const base = alxia().derive(({ request }) => ({ user: request.headers.get('x-user') ?? 'anonymous' }));
301
+ const requestContext = contextStorage<typeof base>();
302
+
303
+ alxia().use(requestContext);
304
+ ```
305
+
306
+ **Why:** `context()` would return a `user` that no middleware of this app adds:
307
+ at runtime it would be `undefined`.
308
+
309
+ **Fix:** mount it on the app it is typed by, after `base`:
310
+
311
+ ```ts
312
+ base.use(requestContext);
313
+ ```
314
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alxia/context-storage",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "The request's context anywhere it runs — a service, a repository, a logger — through AsyncLocalStorage: alxia's hono/context-storage, typed by the app",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -41,12 +41,12 @@
41
41
  ]
42
42
  },
43
43
  "devDependencies": {
44
- "@alxia/core": "^0.3.0",
44
+ "@alxia/core": "^0.5.0",
45
45
  "@types/bun": "^1.4.2",
46
46
  "zod": "^4.6.5"
47
47
  },
48
48
  "peerDependencies": {
49
- "@alxia/core": "^0.3.0",
49
+ "@alxia/core": "^0.5.0",
50
50
  "typescript": "^6.0.3 || ^7.0.0"
51
51
  }
52
52
  }