@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 +36 -11
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -12
- package/dist/index.js.map +3 -3
- package/dist/storage.d.ts +26 -15
- package/dist/storage.d.ts.map +1 -1
- package/docs/README.md +4 -4
- package/docs/guide.md +93 -57
- package/docs/roadmap.md +10 -6
- package/docs/troubleshooting.md +92 -54
- package/package.json +3 -3
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
|
|
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
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
59
|
-
`tsc` (`
|
|
60
|
-
([troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/context-storage/docs/troubleshooting.md#
|
|
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
|
|
67
|
-
| `
|
|
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
|
|
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
|
|
1
|
+
export { ContextStorageError, type ContextStorageErrorCode, type ContextStorageMiddleware, contextStorage, getContext, getRequestContext, runWithContext, type StoredContext, tryGetContext, tryGetRequestContext, } from './storage';
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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(
|
|
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=
|
|
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
|
|
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;
|
|
8
|
-
"debugId": "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
27
|
-
*
|
|
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
|
-
/**
|
|
38
|
-
|
|
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():
|
|
44
|
+
context(): StoredContext<App>;
|
|
41
45
|
/** `tryGetContext()`, typed by `App`. */
|
|
42
|
-
tryContext():
|
|
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
|
|
46
|
-
* declared after it, every function their handlers call — however
|
|
47
|
-
* through every `await` and timer — reads it with `getContext()`,
|
|
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 =
|
|
77
|
+
export declare function contextStorage<App = RegisteredBase>(...uncalled: readonly never[]): ContextStorageMiddleware<App>;
|
|
67
78
|
//# sourceMappingURL=storage.d.ts.map
|
package/dist/storage.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AACA,OAAO,EACN,KAAK,KAAK,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5
|
-
among an app's
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
58
|
-
nothing to the app's type
|
|
59
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
request
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
|
86
|
-
| a route
|
|
87
|
-
| a
|
|
88
|
-
| the handler of a route
|
|
89
|
-
| that
|
|
90
|
-
|
|
|
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
|
|
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
|
|
125
|
-
|
|
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
|
|
175
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
|
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
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
|
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 `
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
|
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
|
-
|
|
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`
|
|
37
|
-
- **The request
|
|
38
|
-
before routing
|
|
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
|
|
47
|
-
|
|
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.
|
package/docs/troubleshooting.md
CHANGED
|
@@ -7,7 +7,7 @@ behaviour that prints nothing, or an error from `tsc`. A
|
|
|
7
7
|
|
|
8
8
|
**Runtime**
|
|
9
9
|
|
|
10
|
-
- [`
|
|
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
|
-
### `
|
|
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
|
|
30
|
-
…
|
|
31
|
-
|
|
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:**
|
|
35
|
-
|
|
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:** `
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
62
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
135
|
-
instead, which holds wherever
|
|
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.
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
206
|
+
**When:** reading from `context()` a value a middleware adds, and either
|
|
204
207
|
|
|
205
|
-
- the
|
|
206
|
-
|
|
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
|
|
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
|
|
214
|
-
|
|
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
|
|
236
|
-
context does not have them.
|
|
237
|
-
them
|
|
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.
|
|
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.
|
|
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.
|
|
49
|
+
"@alxia/core": "^0.5.0",
|
|
50
50
|
"typescript": "^6.0.3 || ^7.0.0"
|
|
51
51
|
}
|
|
52
52
|
}
|