@warlock.js/core 4.16.0 → 5.0.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.
Files changed (180) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/bin/warlock.js +154 -1
  3. package/esm/application/application.d.mts +65 -1
  4. package/esm/application/application.d.mts.map +1 -1
  5. package/esm/application/application.mjs +62 -0
  6. package/esm/application/application.mjs.map +1 -1
  7. package/esm/application/index.d.mts +1 -1
  8. package/esm/cli/cli-commands.manager.mjs +60 -10
  9. package/esm/cli/cli-commands.manager.mjs.map +1 -1
  10. package/esm/cli/commands/dev-server.command.mjs +1 -0
  11. package/esm/cli/commands/dev-server.command.mjs.map +1 -1
  12. package/esm/cli/commands/doctor/checks/handler-signature.check.mjs +40 -0
  13. package/esm/cli/commands/doctor/checks/handler-signature.check.mjs.map +1 -0
  14. package/esm/cli/commands/doctor/checks/index.mjs +4 -2
  15. package/esm/cli/commands/doctor/checks/index.mjs.map +1 -1
  16. package/esm/commands/cli-command.d.mts +1 -1
  17. package/esm/commands/cli-command.d.mts.map +1 -1
  18. package/esm/commands/cli-command.mjs.map +1 -1
  19. package/esm/commands/types.d.mts +10 -2
  20. package/esm/commands/types.d.mts.map +1 -1
  21. package/esm/config/config-handlers.d.mts +1 -2
  22. package/esm/config/config-handlers.d.mts.map +1 -1
  23. package/esm/config/config-handlers.mjs +3 -2
  24. package/esm/config/config-handlers.mjs.map +1 -1
  25. package/esm/config/locale-configuration.mjs +29 -0
  26. package/esm/config/locale-configuration.mjs.map +1 -0
  27. package/esm/connectors/assert-no-reserved-connector-names.d.mts +7 -0
  28. package/esm/connectors/assert-no-reserved-connector-names.d.mts.map +1 -0
  29. package/esm/connectors/assert-no-reserved-connector-names.mjs +44 -0
  30. package/esm/connectors/assert-no-reserved-connector-names.mjs.map +1 -0
  31. package/esm/connectors/assert-unique-connector-names.d.mts +7 -0
  32. package/esm/connectors/assert-unique-connector-names.d.mts.map +1 -0
  33. package/esm/connectors/assert-unique-connector-names.mjs +20 -0
  34. package/esm/connectors/assert-unique-connector-names.mjs.map +1 -0
  35. package/esm/connectors/connectors-manager.d.mts +38 -0
  36. package/esm/connectors/connectors-manager.d.mts.map +1 -1
  37. package/esm/connectors/connectors-manager.mjs +45 -12
  38. package/esm/connectors/connectors-manager.mjs.map +1 -1
  39. package/esm/connectors/describe-server-address.mjs +75 -0
  40. package/esm/connectors/describe-server-address.mjs.map +1 -0
  41. package/esm/connectors/http-connector.d.mts.map +1 -1
  42. package/esm/connectors/http-connector.mjs +6 -3
  43. package/esm/connectors/http-connector.mjs.map +1 -1
  44. package/esm/connectors/index.d.mts +4 -1
  45. package/esm/connectors/index.mjs +4 -1
  46. package/esm/connectors/register-configured-connectors.d.mts +14 -0
  47. package/esm/connectors/register-configured-connectors.d.mts.map +1 -0
  48. package/esm/connectors/register-configured-connectors.mjs +99 -0
  49. package/esm/connectors/register-configured-connectors.mjs.map +1 -0
  50. package/esm/connectors/socket-connector.d.mts.map +1 -1
  51. package/esm/connectors/socket-connector.mjs +4 -1
  52. package/esm/connectors/socket-connector.mjs.map +1 -1
  53. package/esm/connectors/types.d.mts +76 -1
  54. package/esm/connectors/types.d.mts.map +1 -1
  55. package/esm/connectors/types.mjs.map +1 -1
  56. package/esm/database/models/database-log/database-log.d.mts +50 -2
  57. package/esm/database/models/database-log/database-log.d.mts.map +1 -1
  58. package/esm/database/utils.d.mts +2 -2
  59. package/esm/database/utils.d.mts.map +1 -1
  60. package/esm/dev-server/development-server.mjs +1 -0
  61. package/esm/dev-server/development-server.mjs.map +1 -1
  62. package/esm/dev-server/layer-executor.mjs +2 -1
  63. package/esm/dev-server/layer-executor.mjs.map +1 -1
  64. package/esm/dev-server/read-config-ast.mjs +48 -0
  65. package/esm/dev-server/read-config-ast.mjs.map +1 -0
  66. package/esm/dev-server/run-typings-generation.mjs +18 -0
  67. package/esm/dev-server/run-typings-generation.mjs.map +1 -0
  68. package/esm/dev-server/type-generator.mjs +30 -39
  69. package/esm/dev-server/type-generator.mjs.map +1 -1
  70. package/esm/generations/add-command.action.mjs +169 -1
  71. package/esm/generations/add-command.action.mjs.map +1 -1
  72. package/esm/generations/stubs.mjs +93 -1
  73. package/esm/generations/stubs.mjs.map +1 -1
  74. package/esm/http/context/request-context.d.mts +14 -3
  75. package/esm/http/context/request-context.d.mts.map +1 -1
  76. package/esm/http/context/request-context.mjs +11 -1
  77. package/esm/http/context/request-context.mjs.map +1 -1
  78. package/esm/http/context/request-memo.d.mts +21 -0
  79. package/esm/http/context/request-memo.d.mts.map +1 -0
  80. package/esm/http/context/request-memo.mjs +48 -0
  81. package/esm/http/context/request-memo.mjs.map +1 -0
  82. package/esm/http/database/RequestLog.d.mts +164 -2
  83. package/esm/http/database/RequestLog.d.mts.map +1 -1
  84. package/esm/http/index.d.mts +6 -5
  85. package/esm/http/index.mjs +2 -1
  86. package/esm/http/middleware/cache-response-middleware.d.mts +2 -2
  87. package/esm/http/middleware/cache-response-middleware.d.mts.map +1 -1
  88. package/esm/http/middleware/cache-response-middleware.mjs +1 -1
  89. package/esm/http/middleware/cache-response-middleware.mjs.map +1 -1
  90. package/esm/http/middleware/concurrency-limit.middleware.mjs +1 -1
  91. package/esm/http/middleware/concurrency-limit.middleware.mjs.map +1 -1
  92. package/esm/http/middleware/idempotency.middleware.mjs +1 -1
  93. package/esm/http/middleware/idempotency.middleware.mjs.map +1 -1
  94. package/esm/http/middleware/index.d.mts +1 -1
  95. package/esm/http/middleware/index.mjs +1 -1
  96. package/esm/http/middleware/inject-request-context.d.mts +1 -8
  97. package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
  98. package/esm/http/middleware/inject-request-context.mjs +9 -22
  99. package/esm/http/middleware/inject-request-context.mjs.map +1 -1
  100. package/esm/http/middleware/ip-filter.middleware.mjs +1 -1
  101. package/esm/http/middleware/ip-filter.middleware.mjs.map +1 -1
  102. package/esm/http/middleware/maintenance.middleware.mjs +1 -1
  103. package/esm/http/middleware/maintenance.middleware.mjs.map +1 -1
  104. package/esm/http/middleware/max-body-size.middleware.mjs +1 -1
  105. package/esm/http/middleware/max-body-size.middleware.mjs.map +1 -1
  106. package/esm/http/middleware/rate-limit.middleware.mjs +1 -1
  107. package/esm/http/middleware/rate-limit.middleware.mjs.map +1 -1
  108. package/esm/http/middleware/utils/idempotency-key.d.mts +0 -14
  109. package/esm/http/middleware/utils/idempotency-key.d.mts.map +1 -1
  110. package/esm/http/middleware/utils/idempotency-key.mjs +19 -1
  111. package/esm/http/middleware/utils/idempotency-key.mjs.map +1 -1
  112. package/esm/http/request.d.mts +119 -30
  113. package/esm/http/request.d.mts.map +1 -1
  114. package/esm/http/request.mjs +98 -25
  115. package/esm/http/request.mjs.map +1 -1
  116. package/esm/http/response.mjs +1 -1
  117. package/esm/http/response.mjs.map +1 -1
  118. package/esm/http/types.d.mts +36 -1
  119. package/esm/http/types.d.mts.map +1 -1
  120. package/esm/http/uploaded-file.d.mts +51 -19
  121. package/esm/http/uploaded-file.d.mts.map +1 -1
  122. package/esm/http/uploaded-file.mjs +36 -8
  123. package/esm/http/uploaded-file.mjs.map +1 -1
  124. package/esm/index.d.mts +17 -9
  125. package/esm/index.mjs +10 -4
  126. package/esm/production/build-contributions.mjs +115 -0
  127. package/esm/production/build-contributions.mjs.map +1 -0
  128. package/esm/production/production-builder.mjs +97 -2
  129. package/esm/production/production-builder.mjs.map +1 -1
  130. package/esm/production/resolve-build-config.d.mts +25 -0
  131. package/esm/production/resolve-build-config.d.mts.map +1 -0
  132. package/esm/production/resolve-build-config.mjs +43 -1
  133. package/esm/production/resolve-build-config.mjs.map +1 -1
  134. package/esm/repositories/adapters/cascade/cascade-query-builder.d.mts +1 -1
  135. package/esm/repositories/adapters/cascade/filter-applicator.mjs +20 -9
  136. package/esm/repositories/adapters/cascade/filter-applicator.mjs.map +1 -1
  137. package/esm/restful/restful.d.mts +29 -8
  138. package/esm/restful/restful.d.mts.map +1 -1
  139. package/esm/restful/restful.mjs +31 -13
  140. package/esm/restful/restful.mjs.map +1 -1
  141. package/esm/router/describe-route-for-log.mjs +29 -0
  142. package/esm/router/describe-route-for-log.mjs.map +1 -0
  143. package/esm/router/index.d.mts +3 -1
  144. package/esm/router/index.mjs +2 -0
  145. package/esm/router/log-request-lifecycle.mjs +38 -0
  146. package/esm/router/log-request-lifecycle.mjs.map +1 -0
  147. package/esm/router/positional-handler-diagnostics.d.mts +90 -0
  148. package/esm/router/positional-handler-diagnostics.d.mts.map +1 -0
  149. package/esm/router/positional-handler-diagnostics.mjs +247 -0
  150. package/esm/router/positional-handler-diagnostics.mjs.map +1 -0
  151. package/esm/router/route-registry.d.mts +34 -0
  152. package/esm/router/route-registry.d.mts.map +1 -0
  153. package/esm/router/router.d.mts.map +1 -1
  154. package/esm/router/router.mjs +87 -63
  155. package/esm/router/router.mjs.map +1 -1
  156. package/esm/router/types.d.mts +23 -5
  157. package/esm/router/types.d.mts.map +1 -1
  158. package/esm/storage/storage.d.mts.map +1 -1
  159. package/esm/storage/storage.mjs +1 -1
  160. package/esm/storage/storage.mjs.map +1 -1
  161. package/esm/storage/utils/safe-fetch.d.mts +11 -1
  162. package/esm/storage/utils/safe-fetch.d.mts.map +1 -1
  163. package/esm/storage/utils/safe-fetch.mjs +51 -4
  164. package/esm/storage/utils/safe-fetch.mjs.map +1 -1
  165. package/esm/tests/vitest-setup.mjs +1 -1
  166. package/esm/utils/load-environment.mjs +33 -2
  167. package/esm/utils/load-environment.mjs.map +1 -1
  168. package/esm/utils/types.d.mts +4 -0
  169. package/esm/utils/types.d.mts.map +1 -1
  170. package/esm/validation/validateAll.mjs +4 -1
  171. package/esm/validation/validateAll.mjs.map +1 -1
  172. package/esm/validation/validators/file-validator.d.mts +25 -1
  173. package/esm/validation/validators/file-validator.d.mts.map +1 -1
  174. package/esm/warlock-config/index.mjs +1 -1
  175. package/esm/warlock-config/normalize-build-config.mjs.map +1 -1
  176. package/esm/warlock-config/types.d.mts +36 -2
  177. package/esm/warlock-config/types.d.mts.map +1 -1
  178. package/package.json +15 -12
  179. package/skills/request-memo/SKILL.md +142 -0
  180. 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,qBAA4C,OAAa;CACvE,IAAI,CAAC,MAAM,gBAAgB,MAAM,QAC/B,OAAO;CAGT,OAAO;EAAE,GAAG;EAAO,QAAQ,MAAM;CAAa;AAChD"}
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, 'entryPoints'> & {
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":";;;;;;;;AAUA;;;KAAY,aAAA;EAaF;;;EATR,MAAA;IACE,IAAA;IACA,IAAA;IACA,cAAA;EAAA;EAFA;;;EAQF,KAAA,GAAQ,IAAA,CAAK,YAAA;IAAL;;;;;IAMN,MAAA;IAkDA;;;;;;;;;;IAvCA,YAAA;IAqFiB;;;;;IA/EjB,OAAA;IAwIA;;;;;;;AAuBO;;;;;;;;;IA9IP,YAAA;;;;;;;;;;;;;;;;IAgBA,OAAA;;;;;;IAMA,MAAA;;;;;;IAMA,SAAA;EAAA;;;;EAMF,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"}
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": "4.16.0",
26
- "@warlock.js/cache": "4.16.0",
27
- "@warlock.js/cascade": "4.16.0",
28
- "@warlock.js/context": "4.16.0",
29
- "@warlock.js/logger": "4.16.0",
30
- "@warlock.js/seal": "4.16.0",
31
- "@warlock.js/fs": "4.16.0",
28
+ "@warlock.js/auth": "5.0.0",
29
+ "@warlock.js/cache": "5.0.0",
30
+ "@warlock.js/cascade": "5.0.0",
31
+ "@warlock.js/context": "5.0.0",
32
+ "@warlock.js/logger": "5.0.0",
33
+ "@warlock.js/seal": "5.0.0",
34
+ "@warlock.js/fs": "5.0.0",
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": "4.16.0",
58
- "@warlock.js/ai": "4.16.0",
59
- "@warlock.js/access": "4.16.0",
60
- "@warlock.js/notifications": "4.16.0"
60
+ "@warlock.js/herald": "5.0.0",
61
+ "@warlock.js/ai": "5.0.0",
62
+ "@warlock.js/access": "5.0.0",
63
+ "@warlock.js/notifications": "5.0.0"
61
64
  },
62
65
  "peerDependenciesMeta": {
63
66
  "sharp": {
@@ -120,7 +123,7 @@
120
123
  ],
121
124
  "author": "hassanzohdy",
122
125
  "license": "MIT",
123
- "version": "4.16.0",
126
+ "version": "5.0.0",
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.