@alxia/context-storage 0.1.1 → 0.2.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,21 +41,45 @@ 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
82
+ Pass it to `app.use` called: `use(contextStorage)`, uncalled, is refused by
59
83
  `tsc` (`TS2769`) and throws a `TypeError` at startup
60
84
  ([troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/context-storage/docs/troubleshooting.md#typeerror-contextstorage-is-a-factory-usecontextstorage-not-usecontextstorage)).
61
85
 
@@ -63,13 +87,15 @@ Pass the plugin to `use` called: `use(contextStorage)`, uncalled, is refused by
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()` |
93
+ | `ContextStoragePlugin<App>` | deprecated: the former name of `ContextStorageMiddleware` |
68
94
  | `getContext`, `tryGetContext`, `getRequestContext`, `tryGetRequestContext`, `runWithContext` | the store, untyped |
69
95
  | `ContextStorageError`, `ContextStorageErrorCode` | why there is no context |
70
96
 
71
97
  ## Documentation
72
98
 
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`.
99
+ - [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
100
  - [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
101
  - [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, type ContextStoragePlugin, 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,KAAK,oBAAoB,EACzB,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,8 @@
1
1
  // src/storage.ts
2
2
  import { AsyncLocalStorage } from "node:async_hooks";
3
3
  import {
4
- alxia
4
+ defineMiddleware,
5
+ settle
5
6
  } from "@alxia/core";
6
7
 
7
8
  class ContextStorageError extends Error {
@@ -40,18 +41,16 @@ function contextStorage(...uncalled) {
40
41
  if (uncalled.length > 0) {
41
42
  throw new TypeError("contextStorage is a factory: use(contextStorage()), not use(contextStorage)");
42
43
  }
43
- const plugin = alxia().around((request, next) => {
44
+ const middleware = defineMiddleware((ctx, next) => {
45
+ const routed = ctx.route === undefined ? undefined : ctx;
44
46
  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();
47
+ if (current !== undefined && current.request.url === ctx.url) {
48
+ current.ctx = routed ?? current.ctx;
49
+ return settle(ctx, next());
50
+ }
51
+ return storage.run({ request: ctx, ctx: routed }, () => settle(ctx, next()));
53
52
  });
54
- return Object.assign(plugin, {
53
+ return Object.assign(middleware, {
55
54
  context: () => getContext(),
56
55
  tryContext: () => tryGetContext()
57
56
  });
@@ -66,5 +65,5 @@ export {
66
65
  tryGetRequestContext
67
66
  };
68
67
 
69
- //# debugId=F3B664A2ABF678EE64756E2164756E21
68
+ //# debugId=6963925A93E2187F64756E2164756E21
70
69
  //# 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 MiddlewareMark,\n\ttype Mounted,\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\tMiddlewareMark & {\n\t\t/** `getContext()`, typed by `App`. */\n\t\tcontext(): StoredContext<App>;\n\t\t/** `tryGetContext()`, typed by `App`. */\n\t\ttryContext(): StoredContext<App> | undefined;\n\t};\n\n/** @deprecated Renamed `ContextStorageMiddleware`: it is a middleware. */\nexport type ContextStoragePlugin<App> = ContextStorageMiddleware<App>;\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, an `onError`\n * hook'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 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 middleware = defineMiddleware((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"
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;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;AAsDxC,SAAS,cAAoC,IAChD,UAC6B;AAAA,EAChC,IAAI,SAAS,SAAS,GAAG;AAAA,IAGxB,MAAM,IAAI,UACT,6EACD;AAAA,EACD;AAAA,EACA,MAAM,aAAa,iBAAiB,CAAC,KAAK,SAAS;AAAA,IAElD,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;",
8
+ "debugId": "6963925A93E2187F64756E2164756E21",
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 MiddlewareMark, 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,35 @@ 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>> & MiddlewareMark & {
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
+ /** @deprecated Renamed `ContextStorageMiddleware`: it is a middleware. */
49
+ export type ContextStoragePlugin<App> = ContextStorageMiddleware<App>;
50
+ /** What `context()` reads: the context of `App`, or the base context when `App` is no app. */
51
+ export type StoredContext<App> = [ContextOf<App>] extends [never] ? BaseContext : Mounted<ContextOf<App>>;
44
52
  /**
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.
53
+ * The request's context, anywhere it runs, as a middleware: from the
54
+ * routes declared after it, every function their handlers call — however
55
+ * deep, through every `await` and timer — reads it with `getContext()`,
56
+ * without it being passed down; the answer to an error too, an `onError`
57
+ * hook's included. Give it to `use` before the middlewares whose errors
58
+ * your own middleware answers: what the rest throws is answered inside
59
+ * it, as the route would.
49
60
  *
50
61
  * Typed by the app it is used on: give the plugin that app's type, and its
51
62
  * `context()` returns what its routes read — the `user` a session derived, the
52
- * `db` decorated.
63
+ * `db` decorated. Given none, the app `Register` names in `@alxia/core`
64
+ * (`BaseContext` when nothing is registered). Either way the app that uses
65
+ * it must give that context: using it before is a compile error.
53
66
  *
54
67
  * ```ts
55
68
  * const base = alxia().decorate({ db }).use(session(auth, { required: true }));
@@ -63,5 +76,5 @@ export type ContextStoragePlugin<App> = Alxia<Empty, Empty, '', never> & {
63
76
  * };
64
77
  * ```
65
78
  */
66
- export declare function contextStorage<App = undefined>(...uncalled: readonly never[]): ContextStoragePlugin<App>;
79
+ export declare function contextStorage<App = RegisteredBase>(...uncalled: readonly never[]): ContextStorageMiddleware<App>;
67
80
  //# 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,cAAc,EACnB,KAAK,OAAO,EACZ,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,GACA,cAAc,GAAG;IAChB,sCAAsC;IACtC,OAAO,IAAI,aAAa,CAAC,GAAG,CAAC,CAAC;IAC9B,yCAAyC;IACzC,UAAU,IAAI,aAAa,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;CAC7C,CAAC;AAEH,0EAA0E;AAC1E,MAAM,MAAM,oBAAoB,CAAC,GAAG,IAAI,wBAAwB,CAAC,GAAG,CAAC,CAAC;AAEtE,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,13 +32,23 @@ 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;
41
- };
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
+ // `ContextStoragePlugin<App>`, its name in 0.3, is a deprecated alias.
41
+ type ContextStorageMiddleware<App> = Middleware<
42
+ RequiresOf<StoredContext<App>, 'context'>,
43
+ Promise<Response>
44
+ > &
45
+ MiddlewareMark & {
46
+ context(): StoredContext<App>;
47
+ tryContext(): StoredContext<App> | undefined;
48
+ };
49
+
50
+ // What `context()` returns: `App`'s context, or `BaseContext` when `App` is no app
51
+ type StoredContext<App> = [ContextOf<App>] extends [never] ? BaseContext : Mounted<ContextOf<App>>;
42
52
 
43
53
  function getContext<Ctx extends object = Empty>(): BaseContext & Ctx;
44
54
  function tryGetContext<Ctx extends object = Empty>(): (BaseContext & Ctx) | undefined;
@@ -54,9 +64,11 @@ class ContextStorageError extends Error {
54
64
  type ContextStorageErrorCode = 'OUTSIDE_REQUEST' | 'NOT_ROUTED';
55
65
  ```
56
66
 
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`.
67
+ `contextStorage()` returns a middleware: pass it to `app.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
68
+ nothing to the app's type, but it requires `StoredContext<App>` of the app
69
+ that mounts it: `app.use` on an app that does not give that context is a compile
70
+ error. `BaseContext`, `RequestContext`, `ContextOf`, `RegisteredBase`,
71
+ `Middleware`, `MiddlewareMark`, `RequiresOf` and `Mounted` come from `@alxia/core`.
60
72
 
61
73
  | Export | Returns | Where it would have nothing |
62
74
  | --- | --- | --- |
@@ -64,33 +76,40 @@ come from `@alxia/core`.
64
76
  | `requestContext.tryContext()` | the same | `undefined` |
65
77
  | `getContext<Ctx>()` | the route's context, as `BaseContext & Ctx`: `Ctx` is yours to state | throws a `ContextStorageError` |
66
78
  | `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` |
79
+ | `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
80
  | `tryGetRequestContext()` | the same | `undefined` |
69
81
  | `runWithContext(ctx, work)` | what `work` returns, with `ctx` as the current context while it runs | — |
70
82
 
71
83
  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
84
+ to it, and `getContext()` reads it wherever it is called. Two of them on one
73
85
  app read the same context.
74
86
 
75
87
  ## What it stores, and when
76
88
 
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:
89
+ `contextStorage()` is a middleware: it opens a store around the rest of the
90
+ chain, holding the request's context, and records the route's context in
91
+ it when the request reached a route. A `use()` on the app runs on every
92
+ request, in declaration order, so what each function reads depends on
93
+ whether the middleware ran on the request, and where the code runs
94
+ relative to it:
82
95
 
83
96
  | Code running in | `getRequestContext()` | `tryGetRequestContext()` | `getContext()` | `tryGetContext()` |
84
97
  | --- | --- | --- | --- | --- |
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` |
98
+ | a middleware declared **before** it | throws `OUTSIDE_REQUEST` | `undefined` | throws `OUTSIDE_REQUEST` | `undefined` |
99
+ | 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 |
100
+ | a middleware after it, on a request no route matched (a 404, a 405) | the request, `route` `undefined` | the same | throws `NOT_ROUTED` | `undefined` |
101
+ | 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 |
102
+ | a middleware that catches an error, after it | the request, with `route` and `error` | the same | the same context | the same |
103
+ | a route declared **before** it, or outside the `group` it is used in | throws `OUTSIDE_REQUEST` | `undefined` | throws `OUTSIDE_REQUEST` | `undefined` |
104
+ | the deprecated `onRequest` and `onResponse` hooks, which run outside the chain | throws `OUTSIDE_REQUEST` | `undefined` | throws `OUTSIDE_REQUEST` | `undefined` |
105
+ | the deprecated `onError` and `onRefusal` hooks, which answer at the route boundary | the request, with `route` and `error` | the same | the same context | the same |
91
106
  | a socket's handlers (`open`, `message`, `close`) | throws `OUTSIDE_REQUEST` | `undefined` | throws `OUTSIDE_REQUEST` | `undefined` |
92
107
  | startup, a job, a timer started at startup | throws `OUTSIDE_REQUEST` | `undefined` | throws `OUTSIDE_REQUEST` | `undefined` |
93
108
 
109
+ `contextStorage()` settles `next()`: an error the rest of the chain throws
110
+ is answered inside the store, as the route would answer it, so a hook or
111
+ a middleware that reads the context while answering an error still finds it.
112
+
94
113
  The two errors carry these messages:
95
114
 
96
115
  | `code` | `message` |
@@ -116,13 +135,13 @@ Code that runs both in and out of requests uses `tryContext()`,
116
135
 
117
136
  The context is the whole of what the handler reads: the request, `set` to
118
137
  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,
138
+ every middleware added — so a service three calls down can set a response header,
120
139
  as `listOrders` does below.
121
140
 
122
141
  ## Reading it from outside the handler
123
142
 
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
143
+ Keep the app's base and the middleware in a module of their own, and import it
144
+ from every service, repository or logger that reads it. The handlers
126
145
  then call those without passing anything down.
127
146
 
128
147
  ```ts
@@ -171,8 +190,8 @@ export const app = base
171
190
  ```
172
191
 
173
192
  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:
193
+ in a route. `tryGetRequestContext()` gives it the request wherever the
194
+ middleware ran, and `tryContext()` the user where a route was reached:
176
195
 
177
196
  ```ts
178
197
  // log.ts
@@ -202,8 +221,8 @@ return what a route declared next on that app would read: `ContextOf<App>`.
202
221
 
203
222
  | `App` | `context()` returns |
204
223
  | --- | --- |
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 |
224
+ | none: `contextStorage()` | the context `@alxia/core`'s `Register` names, `AppContext`; with nothing registered, `BaseContext`: the request, `set`, `reply`, `redirect`, `route`, `pathParams` |
225
+ | `typeof base` | `ContextOf<typeof base>`: `BaseContext` plus everything `base`'s `decorate`, `derive` and middlewares added |
207
226
 
208
227
  ```ts
209
228
  import { alxia } from '@alxia/core';
@@ -223,24 +242,46 @@ export function whoIsAsking(): string {
223
242
  }
224
243
  ```
225
244
 
226
- Three rules follow from typing by an app:
245
+ With `Register` augmented beside `base`
246
+ ([`@alxia/core`'s `Register`](https://github.com/softistx/alxia/blob/develop/packages/core/docs/guide/types.md#register-and-appcontext)),
247
+ `contextStorage()` is `contextStorage<typeof base>()` with no import of
248
+ `base`:
249
+
250
+ ```ts
251
+ declare module '@alxia/core' {
252
+ interface Register {
253
+ context: typeof base;
254
+ }
255
+ }
256
+
257
+ export const requestContext = contextStorage(); // context().user: string
258
+ ```
259
+
260
+ Four rules follow from typing by an app:
261
+
262
+ - **The app that mounts it must give that context.** The middleware requires
263
+ what `context()` reads beyond `BaseContext`, as a `definePlugin` does:
264
+ `alxia().use(contextStorage<typeof base>())` is a compile error, since
265
+ `context()` would claim a `user` that no middleware of that app adds.
227
266
 
228
- - **Type it by the app before the plugin, never by the app that uses it.**
267
+ - **Type it by the app before the middleware, never by the app that mounts it.**
229
268
  `const app = alxia().use(requestContext)…` with
230
269
  `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
270
+ `tsc` refuses with `TS7022`. Declare `base` first, as above. Registered,
271
+ the same holds: give `contextStorage()` to the app after `base`, never
272
+ to the registered `base` itself.
273
+ - **What a middleware after it adds is there at runtime, not in the
233
274
  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>()`.
275
+ `base.use(requestContext)` is missing from its type. Put the middlewares
276
+ whose values services read in `base`, or state the type with `getContext<Ctx>()`.
236
277
  - **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:
278
+ they belong to one route's `validate(…)`, not to the app. Read them in
279
+ the handler and pass them down, or state them:
239
280
 
240
281
  ```ts
241
282
  import { getContext } from '@alxia/context-storage';
242
283
 
243
- // in code that only ever runs under GET /orders/:id, with a params schema
284
+ // in code that only ever runs under GET /orders/:id, after its validate({ params })
244
285
  const { params } = getContext<{ params: { id: number } }>();
245
286
  ```
246
287
 
@@ -249,10 +290,10 @@ as `hono/context-storage`'s do: nothing checks it against the route.
249
290
 
250
291
  ## Where it sits
251
292
 
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:
293
+ `app.use(contextStorage())` runs on every request, a 404 included, and
294
+ wraps what is declared after it, in the same app or group. So **use it
295
+ before the routes whose code reads the context**, and after the middlewares
296
+ whose values services read:
256
297
 
257
298
  ```ts
258
299
  import { alxia } from '@alxia/core';
@@ -263,26 +304,25 @@ const requestContext = contextStorage();
263
304
  const app = alxia()
264
305
  .get('/health', ({ reply }) => reply(200, 'ok')) // getContext() throws NOT_ROUTED here
265
306
  .use(requestContext)
266
- .derive(() => ({ startedAt: Date.now() })) // after the plugin: may call code that reads it
307
+ .derive(() => ({ startedAt: Date.now() })) // after it: may call code that reads it
267
308
  .get('/me', ({ reply }) => reply(200, getContext().route)); // '/me'
268
309
  ```
269
310
 
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.
311
+ A middleware declared before `contextStorage()` that ends the request, a
312
+ `401` from a guard, ends it before the middleware runs: nothing it calls
313
+ finds a store. Put the guards after `contextStorage()` when the code that
314
+ answers needs the context; put it after the observers (`logger`,
315
+ `telemetry`) and before an error-handling `try`/`catch` that reads it.
274
316
 
275
317
  | Placement | Effect |
276
318
  | --- | --- |
277
- | at the top of the chain | every route can read it; `context()` is typed `BaseContext` unless typed by an app declared before it |
319
+ | 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
320
  | 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` |
321
+ | 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
322
  | twice | harmless: one store, one context per request |
281
323
 
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
324
+ `contextStorage()` is a middleware, not an app: declare routes on the app,
325
+ and pass `contextStorage()` to `app.use` called — the uncalled form is
286
326
  [refused](troubleshooting.md#typeerror-contextstorage-is-a-factory-usecontextstorage-not-usecontextstorage).
287
327
 
288
328
  ## What `AsyncLocalStorage` carries
@@ -312,7 +352,7 @@ runs:
312
352
  | a `setInterval`, queue consumer or pool started at startup | nothing: `context()` throws `OUTSIDE_REQUEST`, `tryContext()` is `undefined` |
313
353
  | a function pushed into a queue in the request, and run by something started at startup | nothing, as above |
314
354
  | 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` |
355
+ | a WebSocket's `open`, `message` and `close` | nothing: a socket's handlers run outside the chain |
316
356
 
317
357
  So work that outlives the request — a write-behind, an e-mail, an audit
318
358
  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 an `onError` hook or a catching middleware still reads the context. `app.plugin(contextStorage())` still works, deprecated.
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
 
@@ -18,6 +18,7 @@ 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
 
@@ -28,16 +29,18 @@ behaviour that prints nothing, or an error from `tsc`. A
28
29
  ```text
29
30
  error TS2769: No overload matches this call.
30
31
  …
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>'.
32
+ Argument of type '<App = Alxia<Empty, "", never>>(...uncalled: readonly never[]) => ContextStorageMiddleware<App>' is not assignable to parameter of type 'ScopeMiddleware<Empty, [], MiddlewareReturn>'.
33
+ …
34
+ Type 'BaseContext & Empty' is not assignable to type 'never'.
32
35
  ```
33
36
 
34
- **When:** at startup, on `.use(contextStorage)`: the factory given to `use`
35
- without being called.
37
+ **When:** at startup, on `.use(contextStorage)`: the factory given to
38
+ `app.use` without being called.
36
39
 
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.
40
+ **Why:** `app.use` calls a function it is given with the app, as a plugin.
41
+ Called that way, `contextStorage` would be handed the app, and what
42
+ follows would be declared on a plugin nobody serves; it refuses the
43
+ argument instead.
41
44
 
42
45
  **Fix:** call it, once, and keep the result:
43
46
 
@@ -58,13 +61,17 @@ and `getRequestContext()` all throw it.
58
61
  - in a job, a queue consumer, or a callback run by a `setInterval` started
59
62
  at startup — including a function pushed onto a queue during a request
60
63
  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;
64
+ - in a route declared **before** `use(requestContext)`, or outside the
65
+ `group` it is used in: the middleware never ran on that request;
66
+ - in a middleware declared before it, or in the deprecated `onRequest` and
67
+ `onResponse` hooks, which run outside the chain;
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.
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.
125
123
 
126
- **Fix:** use the plugin before the routes whose code reads it:
127
-
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.1",
3
+ "version": "0.2.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.2.0",
44
+ "@alxia/core": "^0.4.0",
45
45
  "@types/bun": "^1.4.2",
46
46
  "zod": "^4.6.5"
47
47
  },
48
48
  "peerDependencies": {
49
- "@alxia/core": "^0.2.0",
49
+ "@alxia/core": "^0.4.0",
50
50
  "typescript": "^6.0.3 || ^7.0.0"
51
51
  }
52
52
  }