@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 +35 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -12
- package/dist/index.js.map +3 -3
- package/dist/storage.d.ts +28 -15
- package/dist/storage.d.ts.map +1 -1
- package/docs/README.md +4 -4
- package/docs/guide.md +97 -57
- package/docs/roadmap.md +5 -1
- package/docs/troubleshooting.md +88 -50
- package/package.json +3 -3
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
|
|
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
|
|
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
|
|
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()` |
|
|
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
|
|
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
|
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,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
|
-
|
|
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
|
|
44
|
+
const middleware = defineMiddleware((ctx, next) => {
|
|
45
|
+
const routed = ctx.route === undefined ? undefined : ctx;
|
|
44
46
|
const current = storage.getStore();
|
|
45
|
-
if (current
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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(
|
|
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=
|
|
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
|
|
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;
|
|
8
|
-
"debugId": "
|
|
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
|
|
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
|
|
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,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
|
-
/**
|
|
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>> & MiddlewareMark & {
|
|
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
|
+
/** @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
|
|
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
|
|
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 =
|
|
79
|
+
export declare function contextStorage<App = RegisteredBase>(...uncalled: readonly never[]): ContextStorageMiddleware<App>;
|
|
67
80
|
//# 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,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
|
|
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,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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
58
|
-
nothing to the app's type
|
|
59
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
request
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
|
86
|
-
| a route
|
|
87
|
-
| a
|
|
88
|
-
| the handler of a route
|
|
89
|
-
| that
|
|
90
|
-
|
|
|
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
|
|
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
|
|
125
|
-
|
|
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
|
|
175
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
|
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
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
|
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 `
|
|
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
|
-
|
|
283
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
package/docs/troubleshooting.md
CHANGED
|
@@ -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 =
|
|
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
|
|
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`
|
|
38
|
-
Called that way, `contextStorage` would
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
62
|
-
|
|
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
|
|
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.
|
|
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
|
-
**
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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.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.
|
|
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.
|
|
49
|
+
"@alxia/core": "^0.4.0",
|
|
50
50
|
"typescript": "^6.0.3 || ^7.0.0"
|
|
51
51
|
}
|
|
52
52
|
}
|