@warlock.js/core 4.16.0 → 5.0.1
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/CHANGELOG.md +13 -0
- package/bin/warlock.js +154 -1
- package/esm/application/application.d.mts +65 -1
- package/esm/application/application.d.mts.map +1 -1
- package/esm/application/application.mjs +62 -0
- package/esm/application/application.mjs.map +1 -1
- package/esm/application/index.d.mts +1 -1
- package/esm/cli/cli-commands.manager.mjs +60 -10
- package/esm/cli/cli-commands.manager.mjs.map +1 -1
- package/esm/cli/commands/dev-server.command.mjs +1 -0
- package/esm/cli/commands/dev-server.command.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/handler-signature.check.mjs +40 -0
- package/esm/cli/commands/doctor/checks/handler-signature.check.mjs.map +1 -0
- package/esm/cli/commands/doctor/checks/index.mjs +4 -2
- package/esm/cli/commands/doctor/checks/index.mjs.map +1 -1
- package/esm/commands/cli-command.d.mts +1 -1
- package/esm/commands/cli-command.d.mts.map +1 -1
- package/esm/commands/cli-command.mjs.map +1 -1
- package/esm/commands/types.d.mts +10 -2
- package/esm/commands/types.d.mts.map +1 -1
- package/esm/config/config-handlers.d.mts +1 -2
- package/esm/config/config-handlers.d.mts.map +1 -1
- package/esm/config/config-handlers.mjs +3 -2
- package/esm/config/config-handlers.mjs.map +1 -1
- package/esm/config/locale-configuration.mjs +29 -0
- package/esm/config/locale-configuration.mjs.map +1 -0
- package/esm/connectors/assert-no-reserved-connector-names.d.mts +7 -0
- package/esm/connectors/assert-no-reserved-connector-names.d.mts.map +1 -0
- package/esm/connectors/assert-no-reserved-connector-names.mjs +44 -0
- package/esm/connectors/assert-no-reserved-connector-names.mjs.map +1 -0
- package/esm/connectors/assert-unique-connector-names.d.mts +7 -0
- package/esm/connectors/assert-unique-connector-names.d.mts.map +1 -0
- package/esm/connectors/assert-unique-connector-names.mjs +20 -0
- package/esm/connectors/assert-unique-connector-names.mjs.map +1 -0
- package/esm/connectors/connectors-manager.d.mts +38 -0
- package/esm/connectors/connectors-manager.d.mts.map +1 -1
- package/esm/connectors/connectors-manager.mjs +45 -12
- package/esm/connectors/connectors-manager.mjs.map +1 -1
- package/esm/connectors/describe-server-address.mjs +75 -0
- package/esm/connectors/describe-server-address.mjs.map +1 -0
- package/esm/connectors/http-connector.d.mts.map +1 -1
- package/esm/connectors/http-connector.mjs +6 -3
- package/esm/connectors/http-connector.mjs.map +1 -1
- package/esm/connectors/index.d.mts +4 -1
- package/esm/connectors/index.mjs +4 -1
- package/esm/connectors/register-configured-connectors.d.mts +14 -0
- package/esm/connectors/register-configured-connectors.d.mts.map +1 -0
- package/esm/connectors/register-configured-connectors.mjs +99 -0
- package/esm/connectors/register-configured-connectors.mjs.map +1 -0
- package/esm/connectors/socket-connector.d.mts.map +1 -1
- package/esm/connectors/socket-connector.mjs +4 -1
- package/esm/connectors/socket-connector.mjs.map +1 -1
- package/esm/connectors/types.d.mts +76 -1
- package/esm/connectors/types.d.mts.map +1 -1
- package/esm/connectors/types.mjs.map +1 -1
- package/esm/database/models/database-log/database-log.d.mts +50 -2
- package/esm/database/models/database-log/database-log.d.mts.map +1 -1
- package/esm/database/utils.d.mts +2 -2
- package/esm/database/utils.d.mts.map +1 -1
- package/esm/dev-server/development-server.mjs +1 -0
- package/esm/dev-server/development-server.mjs.map +1 -1
- package/esm/dev-server/layer-executor.mjs +2 -1
- package/esm/dev-server/layer-executor.mjs.map +1 -1
- package/esm/dev-server/read-config-ast.mjs +48 -0
- package/esm/dev-server/read-config-ast.mjs.map +1 -0
- package/esm/dev-server/run-typings-generation.mjs +18 -0
- package/esm/dev-server/run-typings-generation.mjs.map +1 -0
- package/esm/dev-server/type-generator.mjs +30 -39
- package/esm/dev-server/type-generator.mjs.map +1 -1
- package/esm/generations/add-command.action.mjs +169 -1
- package/esm/generations/add-command.action.mjs.map +1 -1
- package/esm/generations/stubs.mjs +93 -1
- package/esm/generations/stubs.mjs.map +1 -1
- package/esm/http/context/request-context.d.mts +14 -3
- package/esm/http/context/request-context.d.mts.map +1 -1
- package/esm/http/context/request-context.mjs +11 -1
- package/esm/http/context/request-context.mjs.map +1 -1
- package/esm/http/context/request-memo.d.mts +21 -0
- package/esm/http/context/request-memo.d.mts.map +1 -0
- package/esm/http/context/request-memo.mjs +48 -0
- package/esm/http/context/request-memo.mjs.map +1 -0
- package/esm/http/database/RequestLog.d.mts +164 -2
- package/esm/http/database/RequestLog.d.mts.map +1 -1
- package/esm/http/index.d.mts +6 -5
- package/esm/http/index.mjs +2 -1
- package/esm/http/middleware/cache-response-middleware.d.mts +2 -2
- package/esm/http/middleware/cache-response-middleware.d.mts.map +1 -1
- package/esm/http/middleware/cache-response-middleware.mjs +1 -1
- package/esm/http/middleware/cache-response-middleware.mjs.map +1 -1
- package/esm/http/middleware/concurrency-limit.middleware.mjs +1 -1
- package/esm/http/middleware/concurrency-limit.middleware.mjs.map +1 -1
- package/esm/http/middleware/idempotency.middleware.mjs +1 -1
- package/esm/http/middleware/idempotency.middleware.mjs.map +1 -1
- package/esm/http/middleware/index.d.mts +1 -1
- package/esm/http/middleware/index.mjs +1 -1
- package/esm/http/middleware/inject-request-context.d.mts +1 -8
- package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
- package/esm/http/middleware/inject-request-context.mjs +9 -22
- package/esm/http/middleware/inject-request-context.mjs.map +1 -1
- package/esm/http/middleware/ip-filter.middleware.mjs +1 -1
- package/esm/http/middleware/ip-filter.middleware.mjs.map +1 -1
- package/esm/http/middleware/maintenance.middleware.mjs +1 -1
- package/esm/http/middleware/maintenance.middleware.mjs.map +1 -1
- package/esm/http/middleware/max-body-size.middleware.mjs +1 -1
- package/esm/http/middleware/max-body-size.middleware.mjs.map +1 -1
- package/esm/http/middleware/rate-limit.middleware.mjs +1 -1
- package/esm/http/middleware/rate-limit.middleware.mjs.map +1 -1
- package/esm/http/middleware/utils/idempotency-key.d.mts +0 -14
- package/esm/http/middleware/utils/idempotency-key.d.mts.map +1 -1
- package/esm/http/middleware/utils/idempotency-key.mjs +19 -1
- package/esm/http/middleware/utils/idempotency-key.mjs.map +1 -1
- package/esm/http/request.d.mts +119 -30
- package/esm/http/request.d.mts.map +1 -1
- package/esm/http/request.mjs +98 -25
- package/esm/http/request.mjs.map +1 -1
- package/esm/http/response.mjs +1 -1
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/types.d.mts +36 -1
- package/esm/http/types.d.mts.map +1 -1
- package/esm/http/uploaded-file.d.mts +51 -19
- package/esm/http/uploaded-file.d.mts.map +1 -1
- package/esm/http/uploaded-file.mjs +36 -8
- package/esm/http/uploaded-file.mjs.map +1 -1
- package/esm/index.d.mts +17 -9
- package/esm/index.mjs +10 -4
- package/esm/production/build-contributions.mjs +115 -0
- package/esm/production/build-contributions.mjs.map +1 -0
- package/esm/production/production-builder.mjs +97 -2
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/production/resolve-build-config.d.mts +25 -0
- package/esm/production/resolve-build-config.d.mts.map +1 -0
- package/esm/production/resolve-build-config.mjs +43 -1
- package/esm/production/resolve-build-config.mjs.map +1 -1
- package/esm/repositories/adapters/cascade/cascade-query-builder.d.mts +1 -1
- package/esm/repositories/adapters/cascade/filter-applicator.mjs +20 -9
- package/esm/repositories/adapters/cascade/filter-applicator.mjs.map +1 -1
- package/esm/restful/restful.d.mts +29 -8
- package/esm/restful/restful.d.mts.map +1 -1
- package/esm/restful/restful.mjs +31 -13
- package/esm/restful/restful.mjs.map +1 -1
- package/esm/router/describe-route-for-log.mjs +29 -0
- package/esm/router/describe-route-for-log.mjs.map +1 -0
- package/esm/router/index.d.mts +3 -1
- package/esm/router/index.mjs +2 -0
- package/esm/router/log-request-lifecycle.mjs +38 -0
- package/esm/router/log-request-lifecycle.mjs.map +1 -0
- package/esm/router/positional-handler-diagnostics.d.mts +90 -0
- package/esm/router/positional-handler-diagnostics.d.mts.map +1 -0
- package/esm/router/positional-handler-diagnostics.mjs +247 -0
- package/esm/router/positional-handler-diagnostics.mjs.map +1 -0
- package/esm/router/route-registry.d.mts +34 -0
- package/esm/router/route-registry.d.mts.map +1 -0
- package/esm/router/router.d.mts.map +1 -1
- package/esm/router/router.mjs +87 -63
- package/esm/router/router.mjs.map +1 -1
- package/esm/router/types.d.mts +23 -5
- package/esm/router/types.d.mts.map +1 -1
- package/esm/storage/storage.d.mts.map +1 -1
- package/esm/storage/storage.mjs +1 -1
- package/esm/storage/storage.mjs.map +1 -1
- package/esm/storage/utils/safe-fetch.d.mts +11 -1
- package/esm/storage/utils/safe-fetch.d.mts.map +1 -1
- package/esm/storage/utils/safe-fetch.mjs +51 -4
- package/esm/storage/utils/safe-fetch.mjs.map +1 -1
- package/esm/tests/vitest-setup.mjs +1 -1
- package/esm/utils/load-environment.mjs +33 -2
- package/esm/utils/load-environment.mjs.map +1 -1
- package/esm/utils/types.d.mts +4 -0
- package/esm/utils/types.d.mts.map +1 -1
- package/esm/validation/validateAll.mjs +4 -1
- package/esm/validation/validateAll.mjs.map +1 -1
- package/esm/validation/validators/file-validator.d.mts +25 -1
- package/esm/validation/validators/file-validator.d.mts.map +1 -1
- package/esm/warlock-config/index.mjs +1 -1
- package/esm/warlock-config/normalize-build-config.mjs.map +1 -1
- package/esm/warlock-config/types.d.mts +36 -2
- package/esm/warlock-config/types.d.mts.map +1 -1
- package/package.json +15 -12
- package/skills/request-memo/SKILL.md +142 -0
- package/skills/use-request-locals/SKILL.md +144 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"normalize-build-config.mjs","names":[],"sources":["../../../../../../../core/src/warlock-config/normalize-build-config.ts"],"sourcesContent":["import type { WarlockConfig } from \"./types\";\n\ntype BuildConfig = NonNullable<WarlockConfig[\"build\"]>;\n\n/**\n * Fold the `outDirectory` alias into the canonical `outdir`.\n *\n * `outDirectory` is the name the documentation used for several releases\n * while the code only ever read `outdir`, so a config written from the docs\n * was silently ignored and the bundle still landed in `dist/`. Accepting both\n * keeps those configs working; `outdir` wins when both are set, since that is\n * the name esbuild uses and the one we tell people to prefer.\n *\n * Returns the input untouched when there is nothing to fold, so the common\n * case allocates nothing.\n */\nexport function normalizeBuildConfig<T extends BuildConfig>(build: T): T {\n if (!build.outDirectory || build.outdir) {\n return build;\n }\n\n return { ...build, outdir: build.outDirectory };\n}\n"],"mappings":";;;;;;;;;;;;;AAgBA,SAAgB,
|
|
1
|
+
{"version":3,"file":"normalize-build-config.mjs","names":[],"sources":["../../../../../../../core/src/warlock-config/normalize-build-config.ts"],"sourcesContent":["import type { WarlockConfig } from \"./types\";\n\ntype BuildConfig = NonNullable<WarlockConfig[\"build\"]>;\n\n/**\n * Fold the `outDirectory` alias into the canonical `outdir`.\n *\n * `outDirectory` is the name the documentation used for several releases\n * while the code only ever read `outdir`, so a config written from the docs\n * was silently ignored and the bundle still landed in `dist/`. Accepting both\n * keeps those configs working; `outdir` wins when both are set, since that is\n * the name esbuild uses and the one we tell people to prefer.\n *\n * Returns the input untouched when there is nothing to fold, so the common\n * case allocates nothing.\n */\nexport function normalizeBuildConfig<T extends BuildConfig>(\n build: T,\n): T & Pick<BuildConfig, \"outdir\"> {\n if (!build.outDirectory || build.outdir) {\n return build;\n }\n\n return { ...build, outdir: build.outDirectory };\n}\n"],"mappings":";;;;;;;;;;;;;AAgBA,SAAgB,qBACd,OACiC;CACjC,IAAI,CAAC,MAAM,gBAAgB,MAAM,QAC/B,OAAO;CAGT,OAAO;EAAE,GAAG;EAAO,QAAQ,MAAM;CAAa;AAChD"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { CLICommand } from "../commands/cli-command.mjs";
|
|
2
1
|
import { FileHealthCheckerContract } from "../dev-server/health-checker/file-health-checker.contract.mjs";
|
|
2
|
+
import { Connector } from "../connectors/types.mjs";
|
|
3
|
+
import { CLICommand } from "../commands/cli-command.mjs";
|
|
3
4
|
import { MigrationConstructor } from "@warlock.js/cascade";
|
|
4
5
|
import { BuildOptions } from "esbuild";
|
|
5
6
|
|
|
@@ -20,8 +21,16 @@ type WarlockConfig = {
|
|
|
20
21
|
};
|
|
21
22
|
/**
|
|
22
23
|
* Build configuration
|
|
24
|
+
*
|
|
25
|
+
* `tsconfig` and `tsconfigRaw` are deliberately omitted alongside
|
|
26
|
+
* `entryPoints`. Either one applies the app's compiler options to EVERY
|
|
27
|
+
* file esbuild touches, workspace packages included — so an app-level
|
|
28
|
+
* `verbatimModuleSyntax: true` is imposed on package sources that were
|
|
29
|
+
* never written for it, and the bundle dies at import time with
|
|
30
|
+
* `Class extends value undefined`. The builder derives what it needs from
|
|
31
|
+
* the app's tsconfig itself (see `buildAliasMapFromTsconfig`).
|
|
23
32
|
*/
|
|
24
|
-
build?: Omit<BuildOptions,
|
|
33
|
+
build?: Omit<BuildOptions, "entryPoints" | "tsconfig" | "tsconfigRaw"> & {
|
|
25
34
|
/**
|
|
26
35
|
* Output directory
|
|
27
36
|
*
|
|
@@ -91,6 +100,31 @@ type WarlockConfig = {
|
|
|
91
100
|
*/
|
|
92
101
|
sourcemap?: boolean | "inline" | "linked";
|
|
93
102
|
};
|
|
103
|
+
/**
|
|
104
|
+
* Connectors this application adds beyond the built-in ones.
|
|
105
|
+
*
|
|
106
|
+
* ONE key drives both halves of a connector's life:
|
|
107
|
+
*
|
|
108
|
+
* - `warlock build` reads this array STATICALLY and drains each
|
|
109
|
+
* connector's `build` contribution. It never calls `boot()`/`start()` —
|
|
110
|
+
* listing a connector here does not run it.
|
|
111
|
+
* - `warlock dev` and the generated production entry register the same
|
|
112
|
+
* array with the connectors manager, which is what actually boots it.
|
|
113
|
+
*
|
|
114
|
+
* A second key ("built for X" beside "boots X") was rejected precisely
|
|
115
|
+
* because the two would drift, and that drift is silent: a green build
|
|
116
|
+
* with no client bundle.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```typescript
|
|
120
|
+
* import { webConnector } from "@warlock.js/web/connector";
|
|
121
|
+
*
|
|
122
|
+
* export default defineConfig({
|
|
123
|
+
* connectors: [webConnector({ pagesDirectory: "pages" })],
|
|
124
|
+
* });
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
connectors?: Connector[];
|
|
94
128
|
/**
|
|
95
129
|
* CLI configuration
|
|
96
130
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../core/src/warlock-config/types.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../core/src/warlock-config/types.ts"],"mappings":";;;;;;;;;AAWA;;;KAAY,aAAA;EAqBF;;;EAjBR,MAAA;IACE,IAAA;IACA,IAAA;IACA,cAAA;EAAA;EAHF;;;;;;;;;;;EAiBA,KAAA,GAAQ,IAAA,CAAK,YAAA;IA8DX;;;;;IAxDA,MAAA;IA+FW;;;;;;;;;;IApFX,YAAA;IAsJF;;;;;IAhJE,OAAA;IAyLA;;AAAO;;;;;;;;;;;;;;IAxKP,YAAA;;;;;;;;;;;;;;;;IAgBA,OAAA;;;;;;IAMA,MAAA;;;;;;IAMA,SAAA;EAAA;;;;;;;;;;;;;;;;;;;;;;;;;EA2BF,UAAA,GAAa,SAAA;;;;EAKb,GAAA;IACE,QAAA,GAAW,UAAA;EAAA;;;;EAMb,SAAA;;;;IAIE,KAAA;;;;;;MAME,OAAA;;;;;;MAMA,OAAA;IAAA;;;;IAKF,cAAA,GAAiB,yBAAA;;;;;IAKjB,eAAA;;;;;;;;;IASA,eAAA;;;;;;;;;IASA,qBAAA;;;;;;;;;;IAUA,mBAAA;EAAA;;;;EAMF,QAAA;;;;;;;;;;;;;;;;;;IAkBE,UAAA,GAAa,KAAA,CAAM,oBAAA;EAAA;;;;;;;EASrB,OAAA;;;;;;;IAOE,OAAA;;;;;;IAOA,OAAA;EAAA;AAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@warlock.js/core",
|
|
3
3
|
"description": "A robust nodejs framework for building blazing fast applications",
|
|
4
|
+
"warlock": {
|
|
5
|
+
"environment": "server"
|
|
6
|
+
},
|
|
4
7
|
"bin": {
|
|
5
8
|
"warlock": "bin/warlock.js"
|
|
6
9
|
},
|
|
@@ -22,13 +25,13 @@
|
|
|
22
25
|
"@mongez/slug": "^1.0.7",
|
|
23
26
|
"@mongez/supportive-is": "^2.1.4",
|
|
24
27
|
"@mongez/time-wizard": "^1.0.6",
|
|
25
|
-
"@warlock.js/auth": "
|
|
26
|
-
"@warlock.js/cache": "
|
|
27
|
-
"@warlock.js/cascade": "
|
|
28
|
-
"@warlock.js/context": "
|
|
29
|
-
"@warlock.js/logger": "
|
|
30
|
-
"@warlock.js/seal": "
|
|
31
|
-
"@warlock.js/fs": "
|
|
28
|
+
"@warlock.js/auth": "5.0.1",
|
|
29
|
+
"@warlock.js/cache": "5.0.1",
|
|
30
|
+
"@warlock.js/cascade": "5.0.1",
|
|
31
|
+
"@warlock.js/context": "5.0.1",
|
|
32
|
+
"@warlock.js/logger": "5.0.1",
|
|
33
|
+
"@warlock.js/seal": "5.0.1",
|
|
34
|
+
"@warlock.js/fs": "5.0.1",
|
|
32
35
|
"chokidar": "^5.0.0",
|
|
33
36
|
"dayjs": "^1.11.19",
|
|
34
37
|
"es-module-lexer": "^2.0.0",
|
|
@@ -54,10 +57,10 @@
|
|
|
54
57
|
"react": "^19.2.3",
|
|
55
58
|
"react-dom": "^19.2.3",
|
|
56
59
|
"@react-email/render": "^2.0.5",
|
|
57
|
-
"@warlock.js/herald": "
|
|
58
|
-
"@warlock.js/ai": "
|
|
59
|
-
"@warlock.js/access": "
|
|
60
|
-
"@warlock.js/notifications": "
|
|
60
|
+
"@warlock.js/herald": "5.0.1",
|
|
61
|
+
"@warlock.js/ai": "5.0.1",
|
|
62
|
+
"@warlock.js/access": "5.0.1",
|
|
63
|
+
"@warlock.js/notifications": "5.0.1"
|
|
61
64
|
},
|
|
62
65
|
"peerDependenciesMeta": {
|
|
63
66
|
"sharp": {
|
|
@@ -120,7 +123,7 @@
|
|
|
120
123
|
],
|
|
121
124
|
"author": "hassanzohdy",
|
|
122
125
|
"license": "MIT",
|
|
123
|
-
"version": "
|
|
126
|
+
"version": "5.0.1",
|
|
124
127
|
"type": "module",
|
|
125
128
|
"main": "./esm/index.mjs",
|
|
126
129
|
"module": "./esm/index.mjs",
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: request-memo
|
|
3
|
+
description: 'Memoize async work for one HTTP request with `requestMemo<T>(key, fn)` — the v5 replacement for removed `fromRequest`, with single-flight promise sharing, settled-success reuse, rejection eviction, and no cross-request fallback. Triggers: `requestMemo`, `fromRequest`, `fromRequest removed`, `request-scoped cache`, `single-flight`, `current request memo`; "migrate off fromRequest", "load this once per request", "deduplicate concurrent loaders", "cache a repository lookup during one request"; typical import `import { requestMemo } from "@warlock.js/core"`. Skip: middleware-written request state — `@warlock.js/core/use-request-locals/SKILL.md`; process-wide or cross-request caching — `@warlock.js/core/use-middleware/SKILL.md`; competing patterns: dynamic `request[key]` properties, module-global `Map`, payload `request.get()` / `request.set()`.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Warlock — memoize work for one request
|
|
7
|
+
|
|
8
|
+
`requestMemo()` is the v5 replacement for the removed `fromRequest()`. Use it when several code paths in the same HTTP request may need the same asynchronously computed value and the work should run once.
|
|
9
|
+
|
|
10
|
+
## The exact signature
|
|
11
|
+
|
|
12
|
+
From `core/src/http/context/request-memo.ts`:
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
export function requestMemo<T>(key: string, fn: () => Promise<T>): Promise<T>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Import it from the package root:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { requestMemo } from "@warlock.js/core";
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`fn` takes no arguments and must return a promise. Close over the current request or the inputs needed by the loader.
|
|
25
|
+
|
|
26
|
+
## Migrate from v4 to v5
|
|
27
|
+
|
|
28
|
+
### Before — v4 `fromRequest()`
|
|
29
|
+
|
|
30
|
+
```ts title="src/app/tenants/utils/current-tenant.ts"
|
|
31
|
+
import { fromRequest, type Request } from "@warlock.js/core";
|
|
32
|
+
|
|
33
|
+
type Tenant = { id: string };
|
|
34
|
+
|
|
35
|
+
async function loadTenant(id: string): Promise<Tenant> {
|
|
36
|
+
return { id };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function currentTenant(request: Request): Promise<Tenant> {
|
|
40
|
+
const tenantId = String(request.header("x-tenant-id") ?? "");
|
|
41
|
+
|
|
42
|
+
return fromRequest("tenants.current", () => loadTenant(tenantId));
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### After — v5 `requestMemo()`
|
|
47
|
+
|
|
48
|
+
```ts title="src/app/tenants/utils/current-tenant.ts"
|
|
49
|
+
import { requestMemo, type Request } from "@warlock.js/core";
|
|
50
|
+
|
|
51
|
+
type Tenant = { id: string };
|
|
52
|
+
|
|
53
|
+
async function loadTenant(id: string): Promise<Tenant> {
|
|
54
|
+
return { id };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function currentTenant(request: Request): Promise<Tenant> {
|
|
58
|
+
const tenantId = String(request.header("x-tenant-id") ?? "");
|
|
59
|
+
|
|
60
|
+
return requestMemo("tenants.current", () => loadTenant(tenantId));
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The call-site migration is usually the import and function name. The complete v5 example above also shows the callback-shape migration: if a v4 callback used the `Request` argument supplied by `fromRequest`, capture the surrounding `request` variable because `requestMemo`'s `fn` receives no arguments.
|
|
65
|
+
|
|
66
|
+
## Single-flight behavior
|
|
67
|
+
|
|
68
|
+
Within one request, the first call for a key invokes `fn` and stores its promise. Every concurrent caller using that key receives that exact promise, so parallel consumers do not duplicate the work. After the promise resolves, later calls during the same request keep receiving the cached successful promise.
|
|
69
|
+
|
|
70
|
+
```ts title="src/app/catalog/controllers/show-product.controller.ts"
|
|
71
|
+
import { requestMemo, type RequestHandler } from "@warlock.js/core";
|
|
72
|
+
|
|
73
|
+
let loads = 0;
|
|
74
|
+
|
|
75
|
+
async function loadProduct(): Promise<{ id: string }> {
|
|
76
|
+
loads += 1;
|
|
77
|
+
return { id: "product-1" };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const showProductController: RequestHandler = async ({ response }) => {
|
|
81
|
+
const first = requestMemo("catalog.product", loadProduct);
|
|
82
|
+
const second = requestMemo("catalog.product", loadProduct);
|
|
83
|
+
const [product, sameProduct] = await Promise.all([first, second]);
|
|
84
|
+
|
|
85
|
+
return response.success({ product, sameProduct, loads });
|
|
86
|
+
};
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
For that request, `first === second` and `loads` is `1`.
|
|
90
|
+
|
|
91
|
+
A rejection is not cached. The rejected entry is deleted immediately, so a later call with the same key retries `fn`. Callers already sharing the rejected promise all observe that rejection.
|
|
92
|
+
|
|
93
|
+
## Lifetime and isolation
|
|
94
|
+
|
|
95
|
+
The memo table is attached indirectly to the current request-context store through a `WeakMap`. Each incoming request gets a fresh store, so:
|
|
96
|
+
|
|
97
|
+
- two concurrent requests never share entries, even when they use the same key;
|
|
98
|
+
- resolved values live only for the request's context lifetime;
|
|
99
|
+
- the store and its memo entries are eligible for garbage collection after the request ends;
|
|
100
|
+
- no value is written onto the `Request` object.
|
|
101
|
+
|
|
102
|
+
Calling `requestMemo()` outside the HTTP request pipeline throws synchronously. It deliberately has no process-global fallback. For data meant to survive across requests, use an explicit application cache instead.
|
|
103
|
+
|
|
104
|
+
## Why it replaces `fromRequest()`
|
|
105
|
+
|
|
106
|
+
`fromRequest()` memoized through dynamic `request[key]` properties. That only type-checked because v4's `Request` had a `[key: string]: any` index signature. v5 removed both the unsafe index signature and `fromRequest()`; `requestMemo()` keeps the one-request lifetime without mutating `Request` or turning values into `any`.
|
|
107
|
+
|
|
108
|
+
The new helper also provides real single-flight behavior: it stores the in-flight promise. The old helper awaited its callback before writing the result onto `Request`, so concurrent v4 callers could run the same callback more than once.
|
|
109
|
+
|
|
110
|
+
## Key design
|
|
111
|
+
|
|
112
|
+
Keys share one string namespace within a request. Use stable, namespaced keys and include every input that changes the result:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import { requestMemo } from "@warlock.js/core";
|
|
116
|
+
|
|
117
|
+
type Product = { id: string };
|
|
118
|
+
|
|
119
|
+
async function findProduct(id: string): Promise<Product> {
|
|
120
|
+
return { id };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function productById(id: string): Promise<Product> {
|
|
124
|
+
return requestMemo(`catalog.product:${id}`, () => findProduct(id));
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Reusing one key for different result types is a caller bug: TypeScript cannot compare the generic type arguments used at separate call sites.
|
|
129
|
+
|
|
130
|
+
## Gotchas
|
|
131
|
+
|
|
132
|
+
- **Do not call it during bootstrap, in a CLI command, or from a background job.** There is no active HTTP request context there, so it throws.
|
|
133
|
+
- **Do not omit result-changing inputs from the key.** `catalog.product` is wrong when different IDs may be loaded during one request; use `catalog.product:${id}`.
|
|
134
|
+
- **Do not use `request.set()` as a replacement.** It writes to the request input payload read by `all()`, `input()`, and `validated()`.
|
|
135
|
+
- **Failures retry.** If repeated failures must also be cached, this primitive is not that policy; catch and convert the failure into a successful result deliberately, or use another cache.
|
|
136
|
+
- **Use `request.locals` for state written by middleware and read downstream.** Memoization and request attachment are separate jobs.
|
|
137
|
+
|
|
138
|
+
## See also
|
|
139
|
+
|
|
140
|
+
- [`use-request-locals/SKILL.md`](../use-request-locals/SKILL.md) — typed per-request data written by middleware.
|
|
141
|
+
- [`write-middleware/SKILL.md`](../write-middleware/SKILL.md) — authoring and registering middleware.
|
|
142
|
+
- [`create-controller/SKILL.md`](../create-controller/SKILL.md) — consuming request-scoped values from controllers.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: use-request-locals
|
|
3
|
+
description: 'Carry typed, server-only data through one HTTP request with `request.locals`, usually written by middleware and read by downstream middleware or controllers. Augment `RequestLocals` in the module that owns each key; v5 no longer permits arbitrary `request.foo` properties. Triggers: `request.locals`, `RequestLocals`, `request.post`, `request.organization`, `Property does not exist on type Request`, `Request index signature`; "attach data to a request", "share middleware data with a controller", "type request locals", "migrate dynamic request properties"; typical type augmentation `declare module "@warlock.js/core" { interface RequestLocals { ... } }`. Skip: computed-on-demand single-flight values and removed `fromRequest` — `@warlock.js/core/request-memo/SKILL.md`; middleware mechanics — `@warlock.js/core/write-middleware/SKILL.md`; authenticated user typing — augment `RequestUser`, not `RequestLocals`; competing patterns: `(request as any).foo`, `request.set()`, module-global mutable state.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Warlock — use typed request locals
|
|
7
|
+
|
|
8
|
+
`request.locals` is the v5 home for private data that middleware writes and downstream code reads during the same request. It is separate from client input and starts as a fresh object on every `Request` instance.
|
|
9
|
+
|
|
10
|
+
## The exact declarations
|
|
11
|
+
|
|
12
|
+
From `core/src/http/types.ts`:
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
export interface RequestLocals {}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
From `core/src/http/request.ts`:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
public locals: RequestLocals = {};
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`RequestLocals` is intentionally empty and augmentable. It has no index signature: declare owned keys explicitly so reads and writes have their real types instead of `any`.
|
|
25
|
+
|
|
26
|
+
## Migrate a middleware attachment from v4 to v5
|
|
27
|
+
|
|
28
|
+
### Before — v4 dynamic `Request` property
|
|
29
|
+
|
|
30
|
+
```ts title="src/app/observability/middleware/request-timing.middleware.ts"
|
|
31
|
+
import type { Middleware } from "@warlock.js/core";
|
|
32
|
+
|
|
33
|
+
export const requestTimingMiddleware: Middleware = request => {
|
|
34
|
+
request.startedAt = Date.now();
|
|
35
|
+
};
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```ts title="src/app/observability/controllers/timing.controller.ts"
|
|
39
|
+
import type { RequestHandler } from "@warlock.js/core";
|
|
40
|
+
|
|
41
|
+
export const timingController: RequestHandler = async (request, response) => {
|
|
42
|
+
return response.success({ elapsedMs: Date.now() - request.startedAt });
|
|
43
|
+
};
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Those arbitrary properties compiled in v4 because `Request` had a `[key: string]: any` index signature. That signature is gone in v5.
|
|
47
|
+
|
|
48
|
+
### After — v5 typed `request.locals`
|
|
49
|
+
|
|
50
|
+
Declare the key once in the feature that owns and writes it:
|
|
51
|
+
|
|
52
|
+
```ts title="src/app/observability/request-locals.d.ts"
|
|
53
|
+
declare module "@warlock.js/core" {
|
|
54
|
+
interface RequestLocals {
|
|
55
|
+
startedAt?: number;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export {};
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Write it in middleware:
|
|
63
|
+
|
|
64
|
+
```ts title="src/app/observability/middleware/request-timing.middleware.ts"
|
|
65
|
+
import type { Middleware } from "@warlock.js/core";
|
|
66
|
+
|
|
67
|
+
export const requestTimingMiddleware: Middleware = ({ request }) => {
|
|
68
|
+
request.locals.startedAt = Date.now();
|
|
69
|
+
};
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Read it downstream:
|
|
73
|
+
|
|
74
|
+
```ts title="src/app/observability/controllers/timing.controller.ts"
|
|
75
|
+
import type { RequestHandler } from "@warlock.js/core";
|
|
76
|
+
|
|
77
|
+
export const timingController: RequestHandler = async ({ request, response }) => {
|
|
78
|
+
const startedAt = request.locals.startedAt;
|
|
79
|
+
|
|
80
|
+
if (startedAt === undefined) {
|
|
81
|
+
return response.serverError({ error: "request timing middleware did not run" });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return response.success({ elapsedMs: Date.now() - startedAt });
|
|
85
|
+
};
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The `?` is honest about runtime ordering: the type exists everywhere, but the value exists only after the middleware runs. Narrow it downstream unless every construction path guarantees initialization and your project deliberately declares the key as required.
|
|
89
|
+
|
|
90
|
+
## Augment with an application type
|
|
91
|
+
|
|
92
|
+
An augmentation file may import the value's type. Keeping the declaration beside the writer makes ownership visible and prevents unrelated packages from claiming the same key.
|
|
93
|
+
|
|
94
|
+
```ts title="src/app/organizations/request-locals.d.ts"
|
|
95
|
+
import type { Organization } from "./models/organization";
|
|
96
|
+
|
|
97
|
+
declare module "@warlock.js/core" {
|
|
98
|
+
interface RequestLocals {
|
|
99
|
+
organization?: Organization;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export {};
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The module specifier must be `"@warlock.js/core"`, matching the public package import whose `RequestLocals` interface is exported. Ensure the `.d.ts` file is included by the application's TypeScript configuration.
|
|
107
|
+
|
|
108
|
+
## What belongs in `locals`
|
|
109
|
+
|
|
110
|
+
Good fits are values explicitly produced by one stage and consumed by later stages:
|
|
111
|
+
|
|
112
|
+
- a resolved organization, session, feature flag, or authorization subject;
|
|
113
|
+
- request timing or tracing metadata;
|
|
114
|
+
- a model loaded by validation or routing middleware;
|
|
115
|
+
- private server state that must not appear in request input.
|
|
116
|
+
|
|
117
|
+
`locals` is not a cache API. If consumers should be able to ask for a value independently and concurrent calls must collapse into one loader, use `requestMemo()` instead.
|
|
118
|
+
|
|
119
|
+
## Isolation from input and other requests
|
|
120
|
+
|
|
121
|
+
`Request` initializes `locals` with `{}` for each new request. Request instances are not pooled, so values cannot carry into the next request.
|
|
122
|
+
|
|
123
|
+
Writing `request.locals.organization` does not affect `request.all()`, `request.input()`, or `request.validated()`. By contrast, `request.set()` writes into the input payload's `all` bag, so it is the wrong replacement for a private v4 attachment.
|
|
124
|
+
|
|
125
|
+
## Choosing the extension surface
|
|
126
|
+
|
|
127
|
+
- Use `request.locals` for per-request data written by middleware and read downstream.
|
|
128
|
+
- Use `requestMemo(key, fn)` for lazily computed, single-flight work scoped to the active request.
|
|
129
|
+
- Augment `Request` itself only for a genuine new typed member that also has a runtime implementation.
|
|
130
|
+
- Augment `RequestUser` for the authenticated `request.user` shape; do not duplicate it under locals merely to work around typing.
|
|
131
|
+
|
|
132
|
+
## Gotchas
|
|
133
|
+
|
|
134
|
+
- **Do not restore `(request as any).post`.** It recreates the exact unchecked behavior v5 removed.
|
|
135
|
+
- **Do not add `[key: string]: any` to `RequestLocals`.** Declare each key with its actual type; otherwise every typo becomes valid again.
|
|
136
|
+
- **Do not centralize keys owned by unrelated features.** The middleware or package that writes a key should own its augmentation.
|
|
137
|
+
- **Do not assume an optional local was initialized.** Middleware registration and ordering are runtime concerns; narrow the value or fail clearly downstream.
|
|
138
|
+
- **Avoid key collisions across augmentations.** Interface merging requires repeated property declarations to agree, but stable feature-prefixed names are clearer for generic metadata.
|
|
139
|
+
|
|
140
|
+
## See also
|
|
141
|
+
|
|
142
|
+
- [`request-memo/SKILL.md`](../request-memo/SKILL.md) — the `fromRequest()` migration and single-flight request memoization.
|
|
143
|
+
- [`write-middleware/SKILL.md`](../write-middleware/SKILL.md) — middleware execution and registration.
|
|
144
|
+
- [`create-controller/SKILL.md`](../create-controller/SKILL.md) — reading locals in downstream controllers.
|