bazis 0.96.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.
Files changed (513) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -0
  3. package/cli/build.ts +147 -0
  4. package/cli/codegen.ts +40 -0
  5. package/cli/generateModule.ts +187 -0
  6. package/cli/generateProject.ts +183 -0
  7. package/cli/main.ts +125 -0
  8. package/cli/moduleRegistration.ts +127 -0
  9. package/cli/naming.ts +124 -0
  10. package/cli/parseCli.ts +151 -0
  11. package/cli/templates/module.ts +497 -0
  12. package/cli/templates/pack.ts +76 -0
  13. package/cli/templates/passport.ts +178 -0
  14. package/core/agent/AgentContextBuilder.ts +567 -0
  15. package/core/agent/AgentDataDefinition.ts +8 -0
  16. package/core/agent/AgentRegistry.ts +539 -0
  17. package/core/agent/AgentRuntime.ts +47 -0
  18. package/core/agent/AgentToolExecutor.ts +2130 -0
  19. package/core/agent/AgentToolHooks.ts +39 -0
  20. package/core/agent/ModelProvider.ts +21 -0
  21. package/core/agent/README.md +229 -0
  22. package/core/agent/Tool.contract.ts +20 -0
  23. package/core/agent/decorators/Agent.ts +11 -0
  24. package/core/agent/decorators/Prompt.ts +11 -0
  25. package/core/agent/decorators/Task.ts +29 -0
  26. package/core/agent/decorators/Tool.ts +12 -0
  27. package/core/agent/errors.ts +43 -0
  28. package/core/agent/generated/catalog.ts +26 -0
  29. package/core/agent/index.ts +170 -0
  30. package/core/agent/internal/AgentExecutionDriver.ts +1497 -0
  31. package/core/agent/internal/AgentExecutionState.ts +775 -0
  32. package/core/agent/internal/AgentJsonSchema.ts +150 -0
  33. package/core/agent/internal/AgentModelBinding.ts +57 -0
  34. package/core/agent/internal/Schema.contract.ts +36 -0
  35. package/core/agent/internal/ToolAudit.projector.ts +163 -0
  36. package/core/agent/internal/ToolContract.validator.ts +102 -0
  37. package/core/agent/metadata.ts +398 -0
  38. package/core/agent/moduleContributions-v1.ts +113 -0
  39. package/core/agent/semantic.ts +903 -0
  40. package/core/agent/session/codec.ts +33 -0
  41. package/core/agent/session/contracts.ts +96 -0
  42. package/core/agent/session/entities.ts +84 -0
  43. package/core/agent/session/protector.ts +92 -0
  44. package/core/app/index.ts +5 -0
  45. package/core/app/runApp.ts +293 -0
  46. package/core/app/ui/index.ts +29 -0
  47. package/core/app/ui/uiProfileResolver.ts +966 -0
  48. package/core/app/ui/uiSurfaceHosting.ts +38 -0
  49. package/core/app/ui/uiSurfaceHttp.ts +476 -0
  50. package/core/background/BackgroundService.ts +223 -0
  51. package/core/background/backgroundModule.ts +42 -0
  52. package/core/background/decorator.ts +43 -0
  53. package/core/background/delay.ts +27 -0
  54. package/core/background/index.ts +15 -0
  55. package/core/cache/COMPATIBILITY.md +13 -0
  56. package/core/cache/ICache.ts +71 -0
  57. package/core/cache/MemoryCache.ts +394 -0
  58. package/core/cache/README.md +83 -0
  59. package/core/cache/SPEC.md +1355 -0
  60. package/core/cache/cacheModule.ts +21 -0
  61. package/core/cache/decorators/Cacheable.ts +38 -0
  62. package/core/cache/decorators/CacheableRedis.ts +39 -0
  63. package/core/cache/decorators/OutputCache.ts +33 -0
  64. package/core/cache/decorators/OutputRedisCache.ts +33 -0
  65. package/core/cache/decorators/cacheableMetadata.ts +78 -0
  66. package/core/cache/decorators/cacheableRedisMetadata.ts +81 -0
  67. package/core/cache/decorators/outputCacheMetadata.ts +71 -0
  68. package/core/cache/decorators/outputRedisCacheMetadata.ts +77 -0
  69. package/core/cache/di/classProviderHook.ts +26 -0
  70. package/core/cache/distributed/CacheCodec.ts +5 -0
  71. package/core/cache/distributed/DistributedCache.ts +403 -0
  72. package/core/cache/distributed/DistributedCacheDriver.ts +83 -0
  73. package/core/cache/distributed/DistributedCacheStores.ts +28 -0
  74. package/core/cache/distributed/IDistributedCache.ts +40 -0
  75. package/core/cache/distributed/NamedCacheRegistry.ts +23 -0
  76. package/core/cache/distributed/codecs.ts +32 -0
  77. package/core/cache/distributed/index.ts +19 -0
  78. package/core/cache/errors/CacheError.ts +16 -0
  79. package/core/cache/http/CachedHttpPayload.ts +213 -0
  80. package/core/cache/http/applyClientCacheHeaders.ts +61 -0
  81. package/core/cache/http/buildOutputCacheKey.ts +154 -0
  82. package/core/cache/http/buildRedisOutputCacheKey.ts +16 -0
  83. package/core/cache/http/composeOutputCache.ts +130 -0
  84. package/core/cache/http/outputCacheMiddleware.ts +111 -0
  85. package/core/cache/http/outputCacheSecurityWarning.ts +58 -0
  86. package/core/cache/http/outputRedisCacheMiddleware.ts +118 -0
  87. package/core/cache/index.ts +97 -0
  88. package/core/cache/internal/AutomaticCacheKey.ts +106 -0
  89. package/core/cache/internal/CacheKeyGuard.ts +56 -0
  90. package/core/cache/internal/buildCacheProviders.ts +146 -0
  91. package/core/cache/internal/normalizeCacheKey.ts +9 -0
  92. package/core/cache/internal/resolveCachePolicy.ts +32 -0
  93. package/core/cache/internal/resolveCacheableRedisPolicy.ts +17 -0
  94. package/core/cache/internal/resolveOutputRedisCachePolicy.ts +17 -0
  95. package/core/cache/providers/cachedProviders.ts +68 -0
  96. package/core/cache/services/cacheProxy.ts +243 -0
  97. package/core/cache/tokens/CACHE_POLICIES.ts +5 -0
  98. package/core/cache/tokens/DISTRIBUTED_CACHE.ts +30 -0
  99. package/core/cache/types/CacheEntry.ts +7 -0
  100. package/core/cache/types/CacheOptions.ts +60 -0
  101. package/core/cache/types/CachePolicy.ts +74 -0
  102. package/core/cache/types/CacheableRedisOptions.ts +23 -0
  103. package/core/cache/types/OutputRedisCacheOptions.ts +25 -0
  104. package/core/di/README.md +104 -0
  105. package/core/di/ServiceCollection.ts +185 -0
  106. package/core/di/ServiceProvider.ts +689 -0
  107. package/core/di/ServiceScope.ts +61 -0
  108. package/core/di/container.ts +38 -0
  109. package/core/di/errors/AmbiguousNamedDependencyError.ts +10 -0
  110. package/core/di/errors/AsyncResolutionRequiredError.ts +12 -0
  111. package/core/di/errors/CircularDependencyError.ts +10 -0
  112. package/core/di/errors/ClassDependenciesMismatchError.ts +15 -0
  113. package/core/di/errors/DiError.ts +6 -0
  114. package/core/di/errors/InvalidProviderError.ts +9 -0
  115. package/core/di/errors/ModuleEncapsulationError.ts +7 -0
  116. package/core/di/errors/ModuleOwnedProviderConflictError.ts +8 -0
  117. package/core/di/errors/NamedDependencyNotFoundError.ts +7 -0
  118. package/core/di/errors/OptionsValidationError.ts +7 -0
  119. package/core/di/errors/ProviderNotFoundError.ts +10 -0
  120. package/core/di/errors/ScopeDisposedError.ts +7 -0
  121. package/core/di/errors/ScopedServiceFromRootError.ts +9 -0
  122. package/core/di/errors/ServiceValidationError.ts +7 -0
  123. package/core/di/errors/index.ts +14 -0
  124. package/core/di/extensions/activator.ts +39 -0
  125. package/core/di/extensions/application.ts +117 -0
  126. package/core/di/extensions/hosted-service.ts +124 -0
  127. package/core/di/extensions/index.ts +23 -0
  128. package/core/di/extensions/options-reloadable.ts +137 -0
  129. package/core/di/extensions/options.ts +91 -0
  130. package/core/di/extensions/service-provider-token.ts +21 -0
  131. package/core/di/index.ts +97 -0
  132. package/core/di/internal/GraphValidator.ts +119 -0
  133. package/core/di/internal/HostedServicePlan.ts +158 -0
  134. package/core/di/internal/NamedTokenIndex.ts +55 -0
  135. package/core/di/internal/OpenGenericRegistration.ts +14 -0
  136. package/core/di/internal/ResolutionPlan.ts +18 -0
  137. package/core/di/internal/ResolutionScopeState.ts +19 -0
  138. package/core/di/internal/ResolutionTracker.ts +153 -0
  139. package/core/di/internal/ScopeLifecycle.ts +159 -0
  140. package/core/di/internal/ServiceRegistration.ts +14 -0
  141. package/core/di/internal/ServiceRegistry.ts +252 -0
  142. package/core/di/internal/classDeps.ts +38 -0
  143. package/core/di/internal/disposal.ts +58 -0
  144. package/core/di/internal/providerDeps.ts +26 -0
  145. package/core/di/module/DI.ts +146 -0
  146. package/core/di/module/Global.ts +25 -0
  147. package/core/di/module/Module.ts +44 -0
  148. package/core/di/module/ModuleRegistrar.ts +69 -0
  149. package/core/di/module/autoDeps.ts +39 -0
  150. package/core/di/module/classProviderHooks.ts +66 -0
  151. package/core/di/module/collectModuleConfigs.ts +57 -0
  152. package/core/di/module/collectModuleControllers.ts +53 -0
  153. package/core/di/module/collectModuleUiProfiles.ts +53 -0
  154. package/core/di/module/createContainer.ts +261 -0
  155. package/core/di/module/encapsulation.ts +416 -0
  156. package/core/di/module/encapsulationHooks.ts +36 -0
  157. package/core/di/module/generatedProviderAttachments.ts +102 -0
  158. package/core/di/module/index.ts +41 -0
  159. package/core/di/module/moduleExtensions.ts +35 -0
  160. package/core/di/module/moduleNameBinding.ts +136 -0
  161. package/core/di/module/moduleOwnedProviderContributors.ts +409 -0
  162. package/core/di/module/providerSelection.ts +90 -0
  163. package/core/di/module/shortcuts.ts +111 -0
  164. package/core/di/module/types/BazisModule.ts +71 -0
  165. package/core/di/module/types/DiRegistrar.ts +24 -0
  166. package/core/di/module/types/index.ts +2 -0
  167. package/core/di/provider/index.ts +26 -0
  168. package/core/di/provider/providerGuards.ts +17 -0
  169. package/core/di/provider/types/AsyncFactoryProvider.ts +15 -0
  170. package/core/di/provider/types/ClassProvider.ts +18 -0
  171. package/core/di/provider/types/FactoryProvider.ts +20 -0
  172. package/core/di/provider/types/KeyedDependency.ts +20 -0
  173. package/core/di/provider/types/LazyDependency.ts +31 -0
  174. package/core/di/provider/types/NamedDependency.ts +16 -0
  175. package/core/di/provider/types/Provider.ts +10 -0
  176. package/core/di/provider/types/ProviderDefinition.ts +11 -0
  177. package/core/di/provider/types/ProviderDependencyList.ts +11 -0
  178. package/core/di/provider/types/ProviderLifetime.ts +1 -0
  179. package/core/di/provider/types/ProviderOwnership.ts +7 -0
  180. package/core/di/provider/types/ResolvedDeps.ts +18 -0
  181. package/core/di/provider/types/ValueProvider.ts +6 -0
  182. package/core/di/provider/types/index.ts +13 -0
  183. package/core/di/token.ts +84 -0
  184. package/core/di/types/BuildServiceProviderOptions.ts +4 -0
  185. package/core/di/types/ServiceKey.ts +1 -0
  186. package/core/di/types/ServiceResolver.ts +16 -0
  187. package/core/di/types/index.ts +3 -0
  188. package/core/generatedFingerprint.ts +60 -0
  189. package/core/generatedRuntime.ts +187 -0
  190. package/core/grpc/Grpc.module.ts +23 -0
  191. package/core/grpc/GrpcBinding.contract.ts +35 -0
  192. package/core/grpc/GrpcCall.ts +73 -0
  193. package/core/grpc/GrpcClient.ts +172 -0
  194. package/core/grpc/GrpcClientCall.ts +267 -0
  195. package/core/grpc/GrpcError.ts +19 -0
  196. package/core/grpc/GrpcRequest.binding.ts +45 -0
  197. package/core/grpc/GrpcServer.ts +204 -0
  198. package/core/grpc/GrpcStatus.ts +8 -0
  199. package/core/grpc/GrpcTransport.ts +174 -0
  200. package/core/grpc/Metadata.ts +73 -0
  201. package/core/grpc/ProtoCodec.ts +225 -0
  202. package/core/grpc/ProtoParser.ts +192 -0
  203. package/core/grpc/ProtoReader.ts +34 -0
  204. package/core/grpc/ProtoSchema.ts +52 -0
  205. package/core/grpc/ServerCredentials.ts +30 -0
  206. package/core/grpc/clientContracts.ts +39 -0
  207. package/core/grpc/contracts.ts +45 -0
  208. package/core/grpc/decorators.ts +82 -0
  209. package/core/grpc/grpcClientProvider.ts +9 -0
  210. package/core/grpc/index.ts +17 -0
  211. package/core/grpc/moduleControllers.ts +38 -0
  212. package/core/grpc/protobuf.ts +80 -0
  213. package/core/grpc/serviceDefinition.ts +20 -0
  214. package/core/http/Authorization/Authorize.ts +52 -0
  215. package/core/http/Authorization/authorizeComposer.ts +31 -0
  216. package/core/http/Authorization/metadata.ts +158 -0
  217. package/core/http/Binding/ParameterBinder.ts +109 -0
  218. package/core/http/Binding/RequestModel.ts +27 -0
  219. package/core/http/Binding/autoBindings.ts +125 -0
  220. package/core/http/Binding/bindings.ts +19 -0
  221. package/core/http/Binding/convert.ts +48 -0
  222. package/core/http/Binding/generatedSpec.ts +18 -0
  223. package/core/http/Binding/listModelRegistry.ts +47 -0
  224. package/core/http/Binding/modelBinder.ts +345 -0
  225. package/core/http/Binding/modelValidator.ts +30 -0
  226. package/core/http/Binding/requestModelRegistry.ts +117 -0
  227. package/core/http/Decorators/attributes.ts +92 -0
  228. package/core/http/Decorators/controller.ts +24 -0
  229. package/core/http/Decorators/metadata.ts +107 -0
  230. package/core/http/Decorators/routes.ts +131 -0
  231. package/core/http/Errors/HttpError.ts +92 -0
  232. package/core/http/HttpContext/HttpContext.ts +148 -0
  233. package/core/http/HttpContext/ResponseBuilder.ts +37 -0
  234. package/core/http/HttpContext/inspectableRedirects.ts +47 -0
  235. package/core/http/HttpContext/principal.ts +24 -0
  236. package/core/http/HttpContext/responseLifetime.ts +45 -0
  237. package/core/http/HttpServer.ts +627 -0
  238. package/core/http/Middleware/accessLog.ts +90 -0
  239. package/core/http/Middleware/cors.ts +160 -0
  240. package/core/http/Middleware/errorHandler.ts +100 -0
  241. package/core/http/Middleware/pipeline.ts +21 -0
  242. package/core/http/Middleware/rateLimit.ts +104 -0
  243. package/core/http/Middleware/securityHeaders.ts +118 -0
  244. package/core/http/Middleware/types.ts +19 -0
  245. package/core/http/OpenApi/generatedOpenApiRegistry.ts +102 -0
  246. package/core/http/OpenApi/openApiDocs.ts +75 -0
  247. package/core/http/OpenApi/openApiDocument.ts +356 -0
  248. package/core/http/README.md +139 -0
  249. package/core/http/Results/HttpResult.ts +19 -0
  250. package/core/http/Results/normalize.ts +93 -0
  251. package/core/http/Results/results.ts +70 -0
  252. package/core/http/Routing/Router.ts +201 -0
  253. package/core/http/Routing/RouterBuilder.ts +184 -0
  254. package/core/http/Routing/template.ts +112 -0
  255. package/core/http/SPEC.md +981 -0
  256. package/core/http/Versioning/VersionReader.ts +20 -0
  257. package/core/http/WebSocketUpgrade.ts +29 -0
  258. package/core/http/composeRouteMiddlewareComposers.ts +21 -0
  259. package/core/http/correlation/createCorrelationIdMiddleware.ts +79 -0
  260. package/core/http/httpModule.ts +48 -0
  261. package/core/http/index.ts +140 -0
  262. package/core/http/middlewareTokens.ts +48 -0
  263. package/core/http/options.ts +134 -0
  264. package/core/http-client/index.ts +105 -0
  265. package/core/infra/Infra.ts +158 -0
  266. package/core/infra/InfraConnector.ts +86 -0
  267. package/core/infra/InfraLifecycle.ts +86 -0
  268. package/core/infra/cache/RedisDistributedCacheBackend.ts +88 -0
  269. package/core/infra/cache/RedisDistributedCacheDriver.ts +186 -0
  270. package/core/infra/cache/index.ts +7 -0
  271. package/core/infra/connectorConfig.ts +29 -0
  272. package/core/infra/connectorIdentity.ts +39 -0
  273. package/core/infra/connectors/codex/CodexAppServer.ts +146 -0
  274. package/core/infra/connectors/codex/CodexAppServerClient.ts +258 -0
  275. package/core/infra/connectors/codex/CodexPolicy.ts +114 -0
  276. package/core/infra/connectors/codex/CodexTurn.ts +133 -0
  277. package/core/infra/connectors/codex/contracts.ts +82 -0
  278. package/core/infra/connectors/codex.ts +26 -0
  279. package/core/infra/connectors/llm.ts +78 -0
  280. package/core/infra/connectors/llmRouter.ts +443 -0
  281. package/core/infra/connectors/openaiCompatible.ts +590 -0
  282. package/core/infra/connectors/openaiTextStream.ts +134 -0
  283. package/core/infra/connectors/opensearch.ts +307 -0
  284. package/core/infra/connectors/postgres.ts +105 -0
  285. package/core/infra/connectors/redis.ts +132 -0
  286. package/core/infra/index.ts +58 -0
  287. package/core/kernel/ApplicationLifetime.ts +91 -0
  288. package/core/kernel/Bazis.ts +73 -0
  289. package/core/kernel/Environment.ts +53 -0
  290. package/core/kernel/Kernel.ts +391 -0
  291. package/core/kernel/KernelBuilder.ts +172 -0
  292. package/core/kernel/LifecycleCoordinator.ts +204 -0
  293. package/core/kernel/README.md +66 -0
  294. package/core/kernel/SupervisedHostedService.ts +147 -0
  295. package/core/kernel/config/ConfigRegistry.ts +48 -0
  296. package/core/kernel/config/Configuration.ts +110 -0
  297. package/core/kernel/config/README.md +62 -0
  298. package/core/kernel/config/Secret.ts +29 -0
  299. package/core/kernel/config/addConfigOptions.ts +63 -0
  300. package/core/kernel/config/defineConfig.ts +211 -0
  301. package/core/kernel/config/sources.ts +123 -0
  302. package/core/kernel/correlation/requestContext.ts +57 -0
  303. package/core/kernel/errors/ConfigKeyMissingError.ts +7 -0
  304. package/core/kernel/errors/KernelError.ts +7 -0
  305. package/core/kernel/errors/ShutdownTimeoutError.ts +7 -0
  306. package/core/kernel/errors/StartupAbortedError.ts +7 -0
  307. package/core/kernel/errors/StartupTimeoutError.ts +7 -0
  308. package/core/kernel/errors/index.ts +5 -0
  309. package/core/kernel/events/EventBus.ts +140 -0
  310. package/core/kernel/events/EventToken.ts +17 -0
  311. package/core/kernel/events/eventSubscribers.ts +126 -0
  312. package/core/kernel/events/kernelEvents.ts +16 -0
  313. package/core/kernel/events/onEvent.ts +32 -0
  314. package/core/kernel/health/HealthCheckContracts.ts +42 -0
  315. package/core/kernel/health/HealthService.ts +93 -0
  316. package/core/kernel/index.ts +96 -0
  317. package/core/kernel/internal/awaitAbortable.ts +18 -0
  318. package/core/kernel/internal/awaitShutdown.ts +21 -0
  319. package/core/kernel/internal/reportDiagnosticFailure.ts +8 -0
  320. package/core/kernel/internal/validateTimeout.ts +8 -0
  321. package/core/kernel/lifecycleHooks.ts +18 -0
  322. package/core/kernel/logging/ConsoleLogger.ts +68 -0
  323. package/core/kernel/logging/Logger.ts +22 -0
  324. package/core/kernel/types/ConfigSource.ts +9 -0
  325. package/core/kernel/types/EnvironmentName.ts +1 -0
  326. package/core/kernel/types/KernelOptions.ts +23 -0
  327. package/core/kernel/types/LifecycleHook.ts +18 -0
  328. package/core/kernel/types/index.ts +4 -0
  329. package/core/orm/DATABASE_PROVIDER.ts +5 -0
  330. package/core/orm/OrmHostedPlan.validator.ts +55 -0
  331. package/core/orm/OrmLifecycle.ts +146 -0
  332. package/core/orm/OrmOwnedStoreLifecycle.ts +96 -0
  333. package/core/orm/buildOrmProviders.ts +152 -0
  334. package/core/orm/databaseConnector.ts +108 -0
  335. package/core/orm/encapsulationHook.ts +19 -0
  336. package/core/orm/index.ts +95 -0
  337. package/core/orm/listQuery.ts +198 -0
  338. package/core/orm/ormModule.ts +173 -0
  339. package/core/orm/ownedStoreContributions.ts +195 -0
  340. package/core/orm/registerRepositories.ts +27 -0
  341. package/core/orm/repository.ts +45 -0
  342. package/core/orm/strictSchemaHostedIdentity.ts +17 -0
  343. package/core/scripts/agent-codegen.ts +1166 -0
  344. package/core/scripts/di-generate-target.ts +90 -0
  345. package/core/scripts/di-generate.ts +1837 -0
  346. package/core/scripts/orm-predicate-codegen.ts +33 -0
  347. package/core/scripts/request-model-codegen.ts +326 -0
  348. package/core/websocket/DESIGN.md +297 -0
  349. package/core/websocket/WsRateLimiter.ts +101 -0
  350. package/core/websocket/adapter/adapter.interface.ts +45 -0
  351. package/core/websocket/adapter/in-memory.adapter.ts +190 -0
  352. package/core/websocket/adapter/redis-delivery-scripts.ts +135 -0
  353. package/core/websocket/adapter/redis-delivery-store.ts +86 -0
  354. package/core/websocket/adapter/redis-operations.ts +33 -0
  355. package/core/websocket/adapter/redis.adapter.ts +218 -0
  356. package/core/websocket/codec/binary.codec.ts +87 -0
  357. package/core/websocket/codec/json.codec.ts +34 -0
  358. package/core/websocket/codec/packet-codec.interface.ts +17 -0
  359. package/core/websocket/decorators.ts +165 -0
  360. package/core/websocket/explorer.ts +126 -0
  361. package/core/websocket/index.ts +78 -0
  362. package/core/websocket/middleware.ts +146 -0
  363. package/core/websocket/packet-codec.ts +75 -0
  364. package/core/websocket/reliable-delivery.ts +56 -0
  365. package/core/websocket/session-creation.ts +16 -0
  366. package/core/websocket/session-manager.ts +856 -0
  367. package/core/websocket/socket-wrapper.ts +314 -0
  368. package/core/websocket/topic-cache.ts +59 -0
  369. package/core/websocket/types.ts +122 -0
  370. package/core/websocket/websocketModule.ts +96 -0
  371. package/core/websocket/ws-dispatch.ts +123 -0
  372. package/core/websocket/ws-server.ts +1707 -0
  373. package/index.ts +281 -0
  374. package/library/boundary/diagnostics-v1.ts +136 -0
  375. package/library/boundary/index.ts +73 -0
  376. package/library/boundary/json/decodeBoundedJsonV1.ts +518 -0
  377. package/library/boundary/json/limits-v1.ts +46 -0
  378. package/library/boundary/json/normalizeBoundedJsonV1.ts +256 -0
  379. package/library/boundary/json/types-v1.ts +11 -0
  380. package/library/boundary/schema/boundarySchemaHashV1.ts +6 -0
  381. package/library/boundary/schema/collectBoundaryValueDiagnosticsV1.ts +325 -0
  382. package/library/boundary/schema/decodeBoundarySchemaV1.ts +646 -0
  383. package/library/boundary/schema/index.ts +8 -0
  384. package/library/boundary/schema/types-v1.ts +99 -0
  385. package/library/boundary/schema/validateBoundaryValueV1.ts +58 -0
  386. package/library/boundary/serialization/canonicalJsonV1.ts +66 -0
  387. package/library/http-client/HttpClient.ts +969 -0
  388. package/library/http-client/HttpClientFactory.ts +45 -0
  389. package/library/http-client/errors.ts +68 -0
  390. package/library/http-client/index.ts +43 -0
  391. package/library/http-client/interceptors.ts +40 -0
  392. package/library/http-client/progress.ts +62 -0
  393. package/library/http-client/redirectProtocol.ts +6 -0
  394. package/library/http-client/serialize.ts +133 -0
  395. package/library/http-client/types.ts +174 -0
  396. package/library/jsonapi/ListQueryError.ts +25 -0
  397. package/library/jsonapi/ListRequest.ts +37 -0
  398. package/library/jsonapi/decorators.ts +161 -0
  399. package/library/jsonapi/document.ts +124 -0
  400. package/library/jsonapi/index.ts +40 -0
  401. package/library/jsonapi/parseListQuery.ts +291 -0
  402. package/library/jsonapi/types.ts +125 -0
  403. package/library/jwt/JwtEncoder.ts +40 -0
  404. package/library/jwt/JwtKeyRing.ts +164 -0
  405. package/library/jwt/JwtValidator.ts +210 -0
  406. package/library/jwt/TokenIssuer.ts +149 -0
  407. package/library/jwt/TokenService.ts +51 -0
  408. package/library/jwt/base64url.ts +61 -0
  409. package/library/jwt/claims.ts +36 -0
  410. package/library/jwt/errors.ts +44 -0
  411. package/library/jwt/index.ts +59 -0
  412. package/library/jwt/limits.ts +8 -0
  413. package/library/jwt/signing/Hs256Algorithm.ts +62 -0
  414. package/library/jwt/signing/Rs256Algorithm.ts +133 -0
  415. package/library/jwt/signing/SigningAlgorithm.ts +18 -0
  416. package/library/jwt/signing/pem.ts +30 -0
  417. package/library/openapi/codegen.ts +875 -0
  418. package/library/openapi/document.ts +326 -0
  419. package/library/openapi/index.ts +13 -0
  420. package/library/openapi/types.ts +69 -0
  421. package/library/openapi/ui.ts +182 -0
  422. package/library/orm/DatabaseFacade.ts +209 -0
  423. package/library/orm/DbContext.ts +107 -0
  424. package/library/orm/DbContextFactory.ts +25 -0
  425. package/library/orm/DbContextOptions.ts +33 -0
  426. package/library/orm/Metadata/DynamicModelBuilder.ts +497 -0
  427. package/library/orm/Metadata/KeyTuple.ts +36 -0
  428. package/library/orm/Metadata/ModelBuilder.ts +299 -0
  429. package/library/orm/Metadata/OrmModel.ts +80 -0
  430. package/library/orm/Metadata/ValueConverter.ts +57 -0
  431. package/library/orm/Metadata/conventions.ts +54 -0
  432. package/library/orm/Metadata/decorators.ts +396 -0
  433. package/library/orm/Metadata/types.ts +140 -0
  434. package/library/orm/Providers/OwnedStoreCatalog.reader.ts +1802 -0
  435. package/library/orm/Providers/PostgresDialect.ts +234 -0
  436. package/library/orm/Providers/PostgresProvider.ts +1494 -0
  437. package/library/orm/Providers/PostgresSchema.decoder.ts +107 -0
  438. package/library/orm/Providers/limits.ts +29 -0
  439. package/library/orm/Providers/operationDeadline.ts +43 -0
  440. package/library/orm/Providers/ormOwnedStoreRuntime.ts +19 -0
  441. package/library/orm/Providers/ormTransactionRuntime.ts +117 -0
  442. package/library/orm/Providers/propertyConversion.ts +14 -0
  443. package/library/orm/Providers/resolvedForeignKey.ts +16 -0
  444. package/library/orm/Providers/transactionCallbacks.ts +45 -0
  445. package/library/orm/Providers/transactionOutcome.ts +82 -0
  446. package/library/orm/Providers/transactionScopes.ts +78 -0
  447. package/library/orm/Providers/types.ts +207 -0
  448. package/library/orm/Query/DbSet.ts +78 -0
  449. package/library/orm/Query/EntityQuery.ts +247 -0
  450. package/library/orm/Query/ImmediateMutations.ts +226 -0
  451. package/library/orm/Query/IncludeLoader.ts +200 -0
  452. package/library/orm/Query/QueryPlan.ts +55 -0
  453. package/library/orm/Query/SqlTranslator.ts +195 -0
  454. package/library/orm/Query/conditions.ts +127 -0
  455. package/library/orm/Query/materialize.ts +39 -0
  456. package/library/orm/README.md +256 -0
  457. package/library/orm/Repository/README.md +684 -0
  458. package/library/orm/Repository/Repository.ts +68 -0
  459. package/library/orm/Saving/CommandBuilder.ts +139 -0
  460. package/library/orm/Saving/ExecutionStrategy.ts +128 -0
  461. package/library/orm/Saving/SaveExecutor.ts +413 -0
  462. package/library/orm/Saving/applyConventions.ts +64 -0
  463. package/library/orm/Schema/CheckExpression.ts +327 -0
  464. package/library/orm/Schema/ExactSchemaVerifier.ts +168 -0
  465. package/library/orm/Schema/ExpectedSchema.ts +43 -0
  466. package/library/orm/Schema/MigrationRunner.ts +146 -0
  467. package/library/orm/Schema/Migrator.ts +73 -0
  468. package/library/orm/Schema/OrmOwnedStore.ts +123 -0
  469. package/library/orm/Schema/OwnedStoreAdmission.ts +752 -0
  470. package/library/orm/Schema/OwnedStoreCanonical.ts +273 -0
  471. package/library/orm/Schema/OwnedStoreCatalog.ts +1325 -0
  472. package/library/orm/Schema/SafeAdditiveSchema.ts +100 -0
  473. package/library/orm/Schema/SchemaAdmission.ts +62 -0
  474. package/library/orm/Schema/SchemaDiffer.ts +78 -0
  475. package/library/orm/Schema/formatSchemaDifferences.ts +62 -0
  476. package/library/orm/Schema/introspection.ts +52 -0
  477. package/library/orm/Schema/physicalColumnTypes.ts +37 -0
  478. package/library/orm/Schema/tableKey.ts +26 -0
  479. package/library/orm/Tracking/ChangeTracker.ts +448 -0
  480. package/library/orm/Tracking/EntityState.ts +13 -0
  481. package/library/orm/Transactions/OrmTransaction.ts +30 -0
  482. package/library/orm/Transactions/TransactionScopeCoordinator.ts +377 -0
  483. package/library/orm/errors.ts +190 -0
  484. package/library/orm/index.ts +162 -0
  485. package/library/orm/runtime.ts +14 -0
  486. package/library/redaction/index.ts +166 -0
  487. package/library/ui/UiProfile.ts +77 -0
  488. package/library/ui/compiled-v1.ts +131 -0
  489. package/library/ui/compiler/compileUiSurfaceV1.ts +717 -0
  490. package/library/ui/compiler/contracts-v1.ts +25 -0
  491. package/library/ui/compiler/index.ts +12 -0
  492. package/library/ui/compiler/openApiIndex.ts +246 -0
  493. package/library/ui/defineUiProfile.ts +340 -0
  494. package/library/ui/diagnostics-v1.ts +33 -0
  495. package/library/ui/index.ts +129 -0
  496. package/library/ui/profile-v1.ts +191 -0
  497. package/library/ui/serialization.ts +130 -0
  498. package/library/ui/uiProfileAuthoring.ts +289 -0
  499. package/library/ui/uiProfileAuthoringMetadata.ts +78 -0
  500. package/library/validation/MessageRegistry.ts +144 -0
  501. package/library/validation/README.md +62 -0
  502. package/library/validation/RuleCompiler.ts +183 -0
  503. package/library/validation/RuleEngine.ts +401 -0
  504. package/library/validation/SPEC.md +579 -0
  505. package/library/validation/ValidationError.ts +37 -0
  506. package/library/validation/ValidationResult.ts +63 -0
  507. package/library/validation/Validator.ts +101 -0
  508. package/library/validation/index.ts +16 -0
  509. package/library/validation/metadata.ts +69 -0
  510. package/library/validation/modelValidatorAdapter.ts +119 -0
  511. package/library/validation/types/ValidationCode.ts +69 -0
  512. package/library/validation/types/ValidatorOptions.ts +153 -0
  513. package/package.json +80 -0
@@ -0,0 +1,1355 @@
1
+ # The Cache module (`@/core/cache`): specification
2
+
3
+ In-memory and distributed (multi-instance) caching in the ASP.NET Core style:
4
+ `[OutputCache]` + `IMemoryCache` / `IDistributedCache`. Four decorators on two levels
5
+ (HTTP and services), no external npm dependencies, compatible with `bun build --compile`.
6
+
7
+ **Backend-agnostic:** the framework (`@/core/cache`) knows only the `IDistributedCache`
8
+ abstraction. A concrete backend (Redis and so on) lives in `@/core/infra/*` and is
9
+ enabled with `redisConnect(redisConfig, { cache: "distributed" })` in `@Infra`.
10
+ `@/core/cache` itself has no `import … from "bun"`/Redis at all.
11
+
12
+ Quick navigation:
13
+ - [1. What it is and why](#1-what-it-is-and-why)
14
+ - [2. Architecture: two levels × two backends](#2-architecture-two-levels--two-backends)
15
+ - [3. Quick start](#3-quick-start)
16
+ - [4. `memory()` / advanced cache module configuration](#4-memory--advanced-cache-module-configuration)
17
+ - [5. Named policies](#5-named-policies)
18
+ - [6. `@OutputCache` — HTTP output cache (memory)](#6-outputcache--http-output-cache-memory)
19
+ - [7. `@OutputRedisCache` — HTTP output cache (distributed)](#7-outputrediscache--http-output-cache-distributed)
20
+ - [8. `@Cacheable`: service method cache (memory)](#8-cacheable-service-method-cache-memory)
21
+ - [9. `@CacheableRedis`: service method cache (distributed)](#9-cacheableredis-service-method-cache-distributed)
22
+ - [10. DI providers: `cachedSingleton` / `cachedScoped` (+ auto-hook)](#10-di-providers-cachedsingleton--cachedscoped--auto-hook)
23
+ - [11. `ICache`: programmatic API](#11-icache-programmatic-api)
24
+ - [12. Output cache key building](#12-output-cache-key-building)
25
+ - [13. Anti-stampede and concurrent miss](#13-anti-stampede-and-concurrent-miss)
26
+ - [14. Tag invalidation](#14-tag-invalidation)
27
+ - [15. Integration with the HTTP pipeline](#15-integration-with-the-http-pipeline)
28
+ - [15.1. Pipeline diagrams (Mermaid)](#151-pipeline-diagrams-mermaid)
29
+ - [16. The distributed backend and `@/core/infra`](#16-the-distributed-backend-and-coreinfra)
30
+ - [17. Security](#17-security)
31
+ - [18. Production scenarios](#18-production-scenarios)
32
+ - [19. Limits and anti-patterns](#19-limits-and-anti-patterns)
33
+ - [20. FAQ](#20-faq)
34
+ - [21. Folder map](#21-folder-map)
35
+
36
+ ---
37
+
38
+ ## 1. What it is and why
39
+
40
+ The module solves two different caching tasks:
41
+
42
+ | Task | Decorator | What is cached | .NET counterpart |
43
+ | --- | --- | --- | --- |
44
+ | **Output cache** | `@OutputCache` / `@OutputRedisCache` | the ready HTTP response (status + headers + body) | `[OutputCache]` |
45
+ | **Method cache** | `@Cacheable` / `@CacheableRedis` | the return value of a service method | `IMemoryCache` + `@Cacheable` |
46
+
47
+ **Why two levels:**
48
+
49
+ - **HTTP (output cache)**: the controller action ran once → repeated GET/HEAD are served without running the action and often without touching services/the database. Fits public catalog/list endpoints.
50
+ - **Service (method cache)**: the result of a business method is cached regardless of HTTP. Fits when one service is called from several controllers or background jobs, or when `@CacheableRedis` is needed across pods.
51
+
52
+ **Why memory and Redis:**
53
+
54
+ | Backend | Scope | When |
55
+ | --- | --- | --- |
56
+ | **In-memory** (`MemoryCache`) | one process | dev, single instance, `@Cacheable` by default |
57
+ | **Distributed** (`IDistributedCache`, a backend from `@/core/infra/*`) | all application instances | horizontal scaling, `@OutputRedisCache`, `@CacheableRedis` |
58
+
59
+ > "Redis" in the decorator names means "distributed level". The backend is connected
60
+ > separately; the core works with the `IDistributedCache` abstraction.
61
+
62
+ Public API import:
63
+
64
+ ```ts
65
+ import {
66
+ memory,
67
+ OutputCache,
68
+ OutputRedisCache,
69
+ Cacheable,
70
+ CacheableRedis,
71
+ cachedScoped,
72
+ ICache,
73
+ } from "@/core/cache";
74
+ import { composeRouteMiddlewareComposers } from "@/core/http";
75
+ import { redisConnect } from "@/core/infra";
76
+ ```
77
+
78
+ ---
79
+
80
+ ## 2. Architecture: two levels × two backends
81
+
82
+ ```
83
+ ┌─────────────────────────────────────────────────────────────────┐
84
+ │ HTTP Request │
85
+ └───────────────────────────────┬─────────────────────────────────┘
86
+ │
87
+ authorize (@Authorize) → route middleware → binding → output cache
88
+ │
89
+ ┌─────────────────┴─────────────────┐
90
+ │ │
91
+ @OutputCache (memory) @OutputRedisCache (distributed)
92
+ │ │
93
+ ▼ ▼
94
+ ICache<CachedHttpPayload> DISTRIBUTED_OUTPUT_CACHE (registry)
95
+ │ │
96
+ └─────────────────┬─────────────────┘
97
+ │ cache HIT → response without the action
98
+ │ cache MISS → action → store payload
99
+ ▼
100
+ Controller action
101
+ │
102
+ ▼
103
+ Service (DI proxy)
104
+ ┌─────────────────┴─────────────────┐
105
+ │ │
106
+ @Cacheable (memory) @CacheableRedis (distributed)
107
+ │ │
108
+ ▼ ▼
109
+ ICache DISTRIBUTED_SERVICE_CACHE (registry)
110
+ (method return value) ({prefix}svc:… keys)
111
+ ```
112
+
113
+ ### Four decorators: summary table
114
+
115
+ | Decorator | Level | Store | DI / HTTP wiring |
116
+ | --- | --- | --- | --- |
117
+ | `@OutputCache` | controller action | `ICache` in-memory | the cache module's `ROUTE_MIDDLEWARE_COMPOSER` registration (automatic) |
118
+ | `@OutputRedisCache` | controller action | `IDistributedCache` | the same + `redisConnect(config, { cache: "distributed" })` in Infra |
119
+ | `@Cacheable` | service method | `ICache` in-memory | `cachedSingleton` / `cachedScoped` (or the auto-hook) |
120
+ | `@CacheableRedis` | service method | `IDistributedCache` | `cachedSingleton` / `cachedScoped` + `redisConnect(config, { cache: "distributed" })` in Infra |
121
+
122
+ **Important:** put decorators on the **implementation class**, not on a TypeScript interface. DI registers the interface token → the proxy wraps the implementation.
123
+
124
+ ### 2.1. Overview diagram (Mermaid)
125
+
126
+ ```mermaid
127
+ flowchart TB
128
+ subgraph HTTP["HTTP layer"]
129
+ REQ[Request] --> GLOBAL[server middleware]
130
+ GLOBAL --> AUTH[authorize @Authorize]
131
+ AUTH --> ROUTE[route middleware + binding]
132
+ ROUTE --> OC{output cache decorator?}
133
+ OC -->|@OutputCache| MEM_OUT[(ICache memory)]
134
+ OC -->|@OutputRedisCache| REDIS_OUT[(IDistributedCache)]
135
+ OC -->|none| ACT[Controller action]
136
+ MEM_OUT -->|HIT| RESP[Response]
137
+ REDIS_OUT -->|HIT| RESP
138
+ MEM_OUT -->|MISS| ACT
139
+ REDIS_OUT -->|MISS| ACT
140
+ ACT --> RESP
141
+ end
142
+
143
+ subgraph SVC["Service layer"]
144
+ ACT --> SVC_CALL[Service method call]
145
+ SVC_CALL --> CB{method decorator?}
146
+ CB -->|@Cacheable| MEM_SVC[(ICache memory)]
147
+ CB -->|@CacheableRedis| REDIS_SVC[(IDistributedCache svc:)]
148
+ CB -->|none| IMPL[UserService impl]
149
+ MEM_SVC -->|HIT| RET[return value]
150
+ REDIS_SVC -->|HIT| RET
151
+ MEM_SVC -->|MISS| IMPL
152
+ REDIS_SVC -->|MISS| IMPL
153
+ IMPL --> RET
154
+ end
155
+ ```
156
+
157
+ ---
158
+
159
+ ## 3. Quick start
160
+
161
+ ### 3.1. Minimum: in-memory output + method cache
162
+
163
+ ```ts
164
+ import { Cacheable, OutputCache, cachedScoped, memory } from "@/core/cache";
165
+ import { runApp } from "@/core/app";
166
+
167
+ await runApp(AppModule, {
168
+ cache: memory(),
169
+ http: { prefix: "api" },
170
+ });
171
+ ```
172
+
173
+ Controller:
174
+
175
+ ```ts
176
+ @Controller("products")
177
+ export class ProductsController {
178
+ @Get("items")
179
+ @OutputCache({ policy: "catalog" })
180
+ list(limit = 20, page = 1) {
181
+ return this.service.list(limit, page);
182
+ }
183
+ }
184
+ ```
185
+
186
+ Service:
187
+
188
+ ```ts
189
+ class UserService implements IUserStore {
190
+ @Cacheable({ policy: "userById", key: (...args) => String(args[0]) })
191
+ async byId(id: number) {
192
+ return this.users.find(id);
193
+ }
194
+ }
195
+
196
+ // UsersModule:
197
+ providers: [
198
+ cachedScoped(IUserStore, UserService, [repositoryFor(User)]),
199
+ ],
200
+ ```
201
+
202
+ ### 3.2. With a distributed backend (multi-instance)
203
+
204
+ The backend is connected from `@/core/infra/*`; the framework stays backend-agnostic:
205
+
206
+ ```ts
207
+ import { memory } from "@/core/cache";
208
+ import { Infra, redisConnect } from "@/core/infra";
209
+ import { runApp } from "@/core/app";
210
+ import { redisConfig } from "@/app/config/redis.config";
211
+ import { AppModule } from "@/app/modules/App.module";
212
+
213
+ @Infra({
214
+ cache: redisConnect(redisConfig, { cache: "distributed" }),
215
+ })
216
+ class AppInfra {}
217
+
218
+ await runApp(AppModule, { cache: memory(), infra: AppInfra, http: {} });
219
+ ```
220
+
221
+ ```ts
222
+ @Get("items")
223
+ @OutputRedisCache({ policy: "catalog" })
224
+ list() { /* … */ }
225
+ ```
226
+
227
+ ```ts
228
+ class ProductService {
229
+ @CacheableRedis({ seconds: 120, key: (...args) => String(args[0]), tags: ["products"] })
230
+ async bySku(sku: string) { /* … */ }
231
+ }
232
+
233
+ // The same provider as for memory: the distributed level turns on
234
+ // automatically when Infra publishes the registry of distributed stores in DI.
235
+ cachedScoped(IProductService, ProductService, deps);
236
+ ```
237
+
238
+ ---
239
+
240
+ ## 4. `memory()` / advanced cache module configuration
241
+
242
+ ```ts
243
+ const module = memory({
244
+ maxEntries?: number,
245
+ maxInFlight?: number,
246
+ defaultTtlSeconds?: number,
247
+ maxKeyLength?: number,
248
+ maxValueBytes?: number,
249
+ });
250
+
251
+ // Advanced internal builder used by hosting/infra integration:
252
+ const advanced = buildCacheModule({
253
+ policies?: CachePolicyRegistry,
254
+ outputCache?: CacheOutputCacheOptions,
255
+ token?: typeof ICache, // default ICache
256
+ imports?: BazisModule[],
257
+ healthCheck?: boolean, // default true
258
+ });
259
+ ```
260
+
261
+ ### 4.1. `CacheOptions` — in-memory store
262
+
263
+ | Field | Type | Default | Description |
264
+ | --- | --- | --- | --- |
265
+ | `maxEntries` | `number` | ∞ | Max entries; LRU eviction on overflow |
266
+ | `maxInFlight` | `number` | `1024` | A positive safe integer. The limit of running factories; a new miss at the full limit throws `CacheCapacityError` before the factory starts |
267
+ | `defaultTtlSeconds` | `number` | — | The default TTL for `ICache.set` without an explicit `ttlSeconds` |
268
+ | `maxKeyLength` | `number` | `256` | Max key length; a longer key is rejected |
269
+ | `maxValueBytes` | `number` | ∞ | The value size limit (UTF-8 bytes); `set` throws when exceeded |
270
+
271
+ ### 4.2. `CacheOutputCacheOptions`
272
+
273
+ | Field | Type | Default | Description |
274
+ | --- | --- | --- | --- |
275
+ | `enabled` | `boolean` | `true` | The global switch of the HTTP output cache |
276
+ | `insecureAuthorizedRouteBehavior` | `"throw" \| "warn" \| "ignore"` | `"throw"` | The policy for an `@Authorize` route without per-user isolation |
277
+ | `requireAuthenticationByDefault` | `boolean` | `false` | Treat every route without `@AllowAnonymous` as protected in that check (for applications that authenticate every route) |
278
+
279
+ ### 4.3. Distributed stores
280
+
281
+ Connection: `redisConnect(redisConfig, { cache: "distributed" })` in `@Infra`.
282
+ The connector publishes `DistributedCacheStores` and the HTTP/service registries through
283
+ DI. The cache module takes the policies and the memory/output cache settings; Infra
284
+ manages the external backend's resources. Redis parameters are in §16.
285
+
286
+ ### 4.4. What the module registers
287
+
288
+ | Token / service | Purpose |
289
+ | --- | --- |
290
+ | `ICache` | the `MemoryCache` singleton |
291
+ | `CACHE_OPTIONS` | validated options |
292
+ | `CACHE_POLICIES` | named policies from the config |
293
+ | `HEALTH_CHECK` | `cache:memory` |
294
+
295
+ Infra separately publishes `DISTRIBUTED_CACHE_BACKEND`, `DISTRIBUTED_OUTPUT_CACHE`,
296
+ `DISTRIBUTED_SERVICE_CACHE`, its lifecycle and the connection health.
297
+
298
+ ---
299
+
300
+ ## 5. Named policies
301
+
302
+ Policies are reusable presets in `buildCacheModule({ policies })`. A decorator refers to one with `policy: "name"`.
303
+ Inline decorator fields **override** the policy (policy → inline, inline wins).
304
+
305
+ ```ts
306
+ buildCacheModule({
307
+ policies: {
308
+ catalog: {
309
+ seconds: 60,
310
+ varyByQuery: ["limit", "page"],
311
+ tags: ["catalog"],
312
+ clientCache: { public: true, maxAge: 30 },
313
+ },
314
+ userById: {
315
+ seconds: 300,
316
+ tags: ["users"],
317
+ key: (...args) => `user:${String(args[0])}`, // for @Cacheable
318
+ },
319
+ },
320
+ });
321
+ ```
322
+
323
+ ```ts
324
+ @OutputCache({ policy: "catalog" })
325
+ @Cacheable({ policy: "userById" })
326
+ ```
327
+
328
+ The `CachePolicy` type combines the output cache and method cache fields: one policy can be used on both levels (extra fields are ignored on the other level).
329
+
330
+ ---
331
+
332
+ ## 6. `@OutputCache` — HTTP output cache (memory)
333
+
334
+ Caches the **full HTTP response** after the action runs. On a cache hit the controller is **not called**.
335
+
336
+ ### 6.1. Field reference
337
+
338
+ #### Common (`CachePolicyBase`)
339
+
340
+ | Field | Type | Default | Description |
341
+ | --- | --- | --- | --- |
342
+ | `seconds` | `number` | — | **Required** (inline or in the policy). The entry TTL |
343
+ | `policy` | `string` | — | A policy name from `buildCacheModule({ policies })` |
344
+ | `tags` | `string[]` | — | Tags for `ICache.evictByTag(tag)` |
345
+ | `enabled` | `boolean` | `true` | `false`: the middleware is not attached (the metadata is kept) |
346
+ | `noStore` | `boolean` | `false` | `true`: neither read nor write the cache |
347
+
348
+ #### HTTP-specific (`OutputCachePolicyFields`)
349
+
350
+ | Field | Type | Default | Description |
351
+ | --- | --- | --- | --- |
352
+ | `varyByQuery` | `string[]` \| `"*"` | `"*"` | Query parameters in the key. `[]` explicitly ignores the query |
353
+ | `varyByRoute` | `string[]` | — | Route params (`:id`, …) in the key |
354
+ | `varyByHeader` | `string[]` | — | Request header values in the key |
355
+ | `varyByUser` | `boolean` | `false` | A separate key per principal `subject` (an `@Authorize` check stores the principal; authorization runs before the cache) |
356
+ | `varyByClaim` | `string` | — | A separate key per claim value (for example role) |
357
+ | `maxBodyBytes` | `number` | `16777216` | The max body materialized for one entry; `0` explicitly disables the limit |
358
+ | `bodyReadTimeoutMs` | `number` | `5000` | The overall body materialization timeout; `0` explicitly disables the timeout |
359
+ | `unlessAuthenticated` | `boolean` | `false` | `true`: do not cache for signed-in users |
360
+ | `allowAuthenticatedShared` | `boolean` | `false` | Explicitly allow a shared entry for an optional-auth request if the response does not depend on the principal |
361
+ | `methods` | `string[]` | `GET`, `HEAD` | The HTTP methods the cache works for |
362
+ | `statusCodes` | `number[]` | `[200]` | Which status codes to store |
363
+ | `clientCache` | `ClientCacheOptions` | — | `Cache-Control` for the browser/CDN (the Response Cache layer) |
364
+ | `when` | `(ctx) => boolean \| Promise<boolean>` | — | `false` → skip read/write |
365
+
366
+ If the body exceeds `maxBodyBytes` or does not materialize within `bodyReadTimeoutMs`,
367
+ the response goes to the client unchanged but does not get into the cache.
368
+
369
+ #### `ClientCacheOptions`
370
+
371
+ | Field | Type | Description |
372
+ | --- | --- | --- |
373
+ | `maxAge` | `number` | `max-age=N` in `Cache-Control` |
374
+ | `public` | `boolean` | the `public` directive |
375
+ | `private` | `boolean` | the `private` directive |
376
+ | `noCache` | `boolean` | the `no-cache` directive |
377
+
378
+ For `varyByUser`/`varyByClaim` the client policy always becomes `private`;
379
+ `clientCache.public: true` together with personalization is a startup error.
380
+
381
+ ### 6.2. Examples
382
+
383
+ **A public catalog with pagination:**
384
+
385
+ ```ts
386
+ @Get("items")
387
+ @OutputCache({
388
+ seconds: 120,
389
+ varyByQuery: ["limit", "page"],
390
+ tags: ["catalog"],
391
+ clientCache: { public: true, maxAge: 60 },
392
+ })
393
+ list(limit = 20, page = 1) {
394
+ return this.repo.list(limit, page);
395
+ }
396
+ ```
397
+
398
+ **A protected list isolated per user:**
399
+
400
+ ```ts
401
+ @Controller("users")
402
+ @Authorize()
403
+ export class UsersController {
404
+ @Get()
405
+ @OutputCache({ policy: "users" })
406
+ // policy: { seconds: 30, varyByQuery: ["limit"], varyByUser: true, tags: ["users"] }
407
+ list(limit = 100) { /* … */ }
408
+ }
409
+ ```
410
+
411
+ **Conditional caching:**
412
+
413
+ ```ts
414
+ @Get("report")
415
+ @OutputCache({
416
+ seconds: 300,
417
+ when: (ctx) => ctx.url.searchParams.get("preview") !== "1",
418
+ })
419
+ report() { /* … */ }
420
+ ```
421
+
422
+ **Only for anonymous users:**
423
+
424
+ ```ts
425
+ @Get("landing-stats")
426
+ @OutputCache({ seconds: 60, unlessAuthenticated: true })
427
+ stats() { /* one cache for all guests; signed-in users always get fresh data */ }
428
+ ```
429
+
430
+ ### 6.3. What is stored in the cache
431
+
432
+ The response is serialized as a `CachedHttpPayload`:
433
+
434
+ ```ts
435
+ { status: number; headers: Record<string, string>; body: Uint8Array }
436
+ ```
437
+
438
+ Sensitive response headers are **neither stored nor replayed**:
439
+ `Set-Cookie`, `Authorization`, `Cookie`, `WWW-Authenticate`, `Proxy-Authenticate`, `Proxy-Authorization`.
440
+
441
+ The body is read whole (`arrayBuffer`): **streaming responses are not supported**.
442
+
443
+ ---
444
+
445
+ ## 7. `@OutputRedisCache` — HTTP output cache (distributed)
446
+
447
+ The semantics are identical to `@OutputCache`; the store is the distributed `IDistributedCache`.
448
+ It is shared by all application instances with one backend.
449
+
450
+ ### 7.1. Extra fields
451
+
452
+ | Field | Type | Default | Description |
453
+ | --- | --- | --- | --- |
454
+ | `connection` | `string` | `"default"` | The backend connection name |
455
+ | `keyPrefix` | `string` | from the connection | An extra key prefix for **this route** |
456
+ | `lockSeconds` | `number` | from the connection | Overrides the TTL of the anti-stampede lock on a miss |
457
+
458
+ Inherits all `@OutputCache` fields (see §6.1).
459
+
460
+ ### 7.2. Connection
461
+
462
+ ```ts
463
+ import { Infra, redisConnect } from "@/core/infra";
464
+ import { redisConfig } from "@/app/config/redis.config";
465
+
466
+ @Infra({
467
+ cache: redisConnect(redisConfig, { cache: "distributed" }),
468
+ })
469
+ class AppInfra {}
470
+ ```
471
+
472
+ ```ts
473
+ @Get("feed")
474
+ @OutputRedisCache({ seconds: 30, connection: "analytics", varyByQuery: ["cursor"] })
475
+ feed() { /* … */ }
476
+ ```
477
+
478
+ ### 7.3. Pipeline (Redis)
479
+
480
+ ```
481
+ MISS:
482
+ GET key → miss
483
+ SET lock:{key} NX EX lockSeconds
484
+ → run action (one pod)
485
+ → SET payload EX seconds
486
+ → SADD tag:{tag} payloadKey
487
+
488
+ HIT:
489
+ GET key → deserialize CachedHttpPayload → Response
490
+ ```
491
+
492
+ Concurrent miss:
493
+ - **on one pod**: in-process `inFlight` dedup;
494
+ - **between pods**: `SET NX EX` + polling for the peer result up to `lockSeconds`.
495
+
496
+ ### 7.4. Limits
497
+
498
+ - Without the `DISTRIBUTED_OUTPUT_CACHE` DI registry, the first request to a route with `@OutputRedisCache` gets a `CacheError`.
499
+ - `@OutputCache` + `@OutputRedisCache` on one action is a **warning**; the distributed one wins.
500
+ - The connection must be ready before the first request: `InfraLifecycle` connects Redis, default phase −100.
501
+
502
+ ---
503
+
504
+ ## 8. `@Cacheable`: service method cache (memory)
505
+
506
+ Intercepts the method call through the DI proxy. The **return value** is cached (an object reference).
507
+
508
+ ### 8.1. Field reference
509
+
510
+ | Field | Type | Default | Description |
511
+ | --- | --- | --- | --- |
512
+ | `seconds` | `number` | — | **Required** (inline or in the policy) |
513
+ | `policy` | `string` | — | A named policy |
514
+ | `tags` | `string[]` | — | Tag invalidation |
515
+ | `enabled` / `noStore` | `boolean` | — | As in the output cache |
516
+ | `key` | `string` \| `(...args) => string` | auto | The entry key. Without `key`: SHA-256 of the class/method and the canonical argument graph; unsupported input bypasses the cache |
517
+ | `unless` | `(...args) => boolean` | — | `true` → skip the cache for this call |
518
+
519
+ ### 8.2. Examples
520
+
521
+ The automatic key supports primitives, regular arrays, a standard Date, Map/Set without
522
+ extra properties and records with the Object.prototype/null prototype and enumerable
523
+ string data fields. Map/Set are walked with the built-in iterators in insertion order;
524
+ nested collections are supported. Record fields are still sorted. The graph of the whole
525
+ argument list distinguishes a repeated reference from equal copies. Object keys of a Map
526
+ and Set elements get a stable identity within the service proxy, including their
527
+ occurrences in other arguments. The encoder UUID distinguishes such identities even for
528
+ different proxies with a shared cacheNamespace. So `map.has(knownObject)` is not mixed up
529
+ with looking up another equal object. The graph content is recomputed on every call;
530
+ mutations affect the next key.
531
+
532
+ For classes with hidden state, subclasses of built-in types, accessors,
533
+ functions/symbols, symbol/non-enumerable fields, extra properties of built-in objects,
534
+ cycles, Proxy and an invalid Date, the wrapper automatically calls the original method
535
+ without the cache. The method is called once, both backends and the distributed registry
536
+ are not touched; the encoder runs no getters/Proxy traps. This needs no explicit `key`.
537
+ Errors of the method, an explicitly set `key` and the backend are not suppressed.
538
+ For CacheableRedis a missing backend is checked after deciding to use the cache.
539
+
540
+ Regular records/arrays stay data by value: the field order of a record, the descriptor
541
+ flags and the arbitrary external identity of nested objects are not part of the key.
542
+ Arguments are not frozen: the key is a snapshot at entry, so do not change the data of an
543
+ unfinished operation. For other equivalence semantics set an application `key`. Getters
544
+ and setters of the cached service itself run on the original instance and support native
545
+ private fields.
546
+
547
+ ```ts
548
+ class UserService implements IUserStore {
549
+ @Cacheable({
550
+ seconds: 300,
551
+ key: (...args: readonly unknown[]) => `user:${String(args[0])}`,
552
+ tags: ["users"],
553
+ })
554
+ async byId(id: number) {
555
+ return this.users.find(id);
556
+ }
557
+
558
+ @Cacheable({
559
+ policy: "userById",
560
+ unless: (id: unknown) => Number(id) < 0,
561
+ })
562
+ async byIdWithPolicy(id: number) { /* … */ }
563
+ }
564
+ ```
565
+
566
+ **The `key` is always `(...args: readonly unknown[]) => string`** (a TypeScript constraint of the proxy).
567
+
568
+ ### 8.3. Class-level vs method-level
569
+
570
+ ```ts
571
+ @Cacheable({ seconds: 60, tags: ["reports"] })
572
+ class ReportService {
573
+ daily() { /* cached — inherits class metadata */ }
574
+ live() { /* cached — unless overridden */ }
575
+
576
+ @Cacheable({ seconds: 5 })
577
+ live() { /* method metadata overrides class for this method */ }
578
+ }
579
+ ```
580
+
581
+ ---
582
+
583
+ ## 9. `@CacheableRedis`: service method cache (distributed)
584
+
585
+ A method cache shared between instances. Values are serialized as **JSON** (`jsonCacheCodec`).
586
+
587
+ ### 9.1. Fields
588
+
589
+ All `@Cacheable` fields (§8.1) plus:
590
+
591
+ | Field | Type | Default | Description |
592
+ | --- | --- | --- | --- |
593
+ | `connection` | `string` | `"default"` | The distributed connection name |
594
+ | `lockSeconds` | `number` | from the connection | The distributed lock TTL |
595
+
596
+ ### 9.2. Key namespace
597
+
598
+ | Store | Key pattern |
599
+ | --- | --- |
600
+ | HTTP output (codec payload) | `{keyPrefix}{routeKey}` |
601
+ | Service (codec JSON) | `{keyPrefix}svc:{methodKey}` |
602
+
603
+ The HTTP and service caches **do not overlap** even with one connection.
604
+
605
+ ### 9.3. A hybrid class example
606
+
607
+ ```ts
608
+ class UserService implements IUserStore {
609
+ @Cacheable({ policy: "userById", key: (...args) => String(args[0]) })
610
+ async byId(id: number) { /* memory: a fast local cache */ }
611
+
612
+ @CacheableRedis({ policy: "userByName", key: (...args) => String(args[0]) })
613
+ async findByName(name: string) { /* Redis: a lookup shared across pods */ }
614
+ }
615
+ ```
616
+
617
+ **One method, one decorator.** `@Cacheable` + `@CacheableRedis` on one method → a runtime error.
618
+
619
+ ---
620
+
621
+ ## 10. DI providers: `cachedSingleton` / `cachedScoped` (+ auto-hook)
622
+
623
+ One provider covers both levels. `@Cacheable` → the in-memory `ICache`; `@CacheableRedis` →
624
+ the distributed `DISTRIBUTED_SERVICE_CACHE`, **automatically** if the registry is
625
+ registered through Infra/DI. There are no separate `redisCached*` / `hybridCached*` anymore.
626
+
627
+ | Provider | Decorators | DI dependencies | Scope |
628
+ | --- | --- | --- | --- |
629
+ | `cachedSingleton` | `@Cacheable` and/or `@CacheableRedis` | `ICache`, `CACHE_POLICIES`, (optional) `DISTRIBUTED_SERVICE_CACHE` | singleton |
630
+ | `cachedScoped` | `@Cacheable` and/or `@CacheableRedis` | the same | scoped |
631
+
632
+ Removed aliases: `src/bazis/core/cache/COMPATIBILITY.md`.
633
+
634
+ ```ts
635
+ // Interface token ← proxy ← implementation class with decorators.
636
+ // Works for both @Cacheable and @CacheableRedis (if the backend is configured).
637
+ cachedScoped(IUserStore, UserService, [repositoryFor(User)]);
638
+ ```
639
+
640
+ ### 10.0. Out of the box (auto-hook)
641
+
642
+ `memory()` registers a hook on the core `singleton()` / `scoped()` providers: if a class
643
+ carries `@Cacheable` / `@CacheableRedis`, the regular registration is **automatically**
644
+ wrapped with a caching proxy. An explicit `cachedScoped` is needed only when you want to
645
+ be completely explicit.
646
+
647
+ ### 10.1. Interface vs implementation: a common question
648
+
649
+ ```ts
650
+ interface IUserStore {
651
+ byId(id: number): Promise<User | undefined>;
652
+ }
653
+
654
+ class UserService implements IUserStore {
655
+ @Cacheable({ seconds: 60, key: (...args) => String(args[0]) })
656
+ async byId(id: number) { /* … */ }
657
+ }
658
+
659
+ // ✅ correct
660
+ cachedScoped(IUserStore, UserService, deps);
661
+
662
+ // ❌ a decorator on an interface does not work
663
+ // With memory() connected, a plain singleton(IUserStore, UserService)
664
+ // also gets the proxy through the auto-hook.
665
+ ```
666
+
667
+ ### 10.2. Module encapsulation
668
+
669
+ The application must install the cache module once with `runApp(AppModule, { cache: memory() })`
670
+ or a root import of `memory()`. A feature module registers only its own service:
671
+
672
+ ```ts
673
+ @Module({
674
+ imports: [UsersDataModule],
675
+ providers: [cachedScoped(IUserStore, UserService, deps)],
676
+ })
677
+ class UsersModule {}
678
+ ```
679
+
680
+ ---
681
+
682
+ ## 11. `ICache`: programmatic API
683
+
684
+ Direct use without decorators (invalidation, custom cache logic):
685
+
686
+ ```ts
687
+ constructor(private readonly cache: ICache) {}
688
+
689
+ // a sync/async factory with dedup
690
+ const value = await this.cache.getOrCreateAsync(
691
+ "config:features",
692
+ () => this.loadFeatures(),
693
+ { ttlSeconds: 300, tags: ["config"] },
694
+ );
695
+
696
+ // group cleanup
697
+ this.cache.evictByTag("users");
698
+ ```
699
+
700
+ | Method | Description |
701
+ | --- | --- |
702
+ | `get(key)` | Reads an entry; `undefined` if it is missing or the TTL expired |
703
+ | `set(key, value, options?)` | Writes; **throws** when limits are violated |
704
+ | `getOrCreate(key, factory, options?)` | A sync/async factory; dedup if the factory returned a Promise |
705
+ | `getOrCreateAsync(key, factory, options?)` | Async + in-process anti-stampede |
706
+ | `remove(key)` | Removes one entry |
707
+ | `clear()` | Clears the whole store |
708
+ | `evictByTag(tag)` | Removes all entries with the tag |
709
+ | `list()` / `size` | Introspection (without expired entries) |
710
+
711
+ `getOrCreate*` does not throw on value-size overflow: graceful degradation (the value is returned but may not be stored). The `maxInFlight` limit is the exception: a new miss at the full limit gets `CacheCapacityError`.
712
+
713
+ Direct `ICache` keys longer than `maxKeyLength` (default 256) are rejected.
714
+ The built-in HTTP/service key builders hash a canonical representation with SHA-256 before writing.
715
+
716
+ ---
717
+
718
+ ## 12. Output cache key building
719
+
720
+ The `buildOutputCacheKey(ctx, routeName, options)` function builds a JSON tuple that
721
+ always includes the HTTP method, the action identity, the request `origin` and the actual
722
+ `pathname`, then returns `http:<sha256>`. Raw query/header/user values never get into the keyspace.
723
+
724
+ ```
725
+ http:<sha256(JSON tuple)>
726
+ ```
727
+
728
+ | Vary option | Tuple component | Example |
729
+ | --- | --- | --- |
730
+ | `varyByQuery: ["limit"]` | `["query","limit",["20"]]` | all repeated values of the named params |
731
+ | `varyByQuery: "*"` | all query names sorted | each name + all its values |
732
+ | `varyByRoute: ["id"]` | `["route","id","42"]` | |
733
+ | `varyByHeader: ["Accept-Language"]` | `["header","accept-language","ru"]` | the header name lowercased |
734
+ | `varyByUser: true` | auth state + `sub` | an empty authenticated `sub` disables the cache for the request |
735
+ | `varyByClaim: "role"` | auth state + claim type/value | an empty claim disables the cache for the request |
736
+ | API version | `["version","2"]` | if `ctx.apiVersion` is set |
737
+
738
+ An example of the final key:
739
+
740
+ ```
741
+ http:8f5d…<64 hex chars>
742
+ ```
743
+
744
+ ---
745
+
746
+ ## 13. Anti-stampede and concurrent miss
747
+
748
+ **The problem:** 100 concurrent misses on one key → 100 identical database queries.
749
+
750
+ **The solution:**
751
+
752
+ | Store | Mechanism |
753
+ | --- | --- |
754
+ | Memory (`MemoryCache`, output + `@Cacheable`) | the `inFlight` Map: one factory per key in the process |
755
+ | Redis output / `@CacheableRedis` | `SET lock NX EX` + in-process dedup + polling the peer |
756
+
757
+ If the Redis lock is not acquired, the instance waits for the peer pod's result (polling every 50 ms up to `lockSeconds`).
758
+
759
+ ---
760
+
761
+ ## 14. Tag invalidation
762
+
763
+ ```ts
764
+ @OutputCache({ seconds: 300, tags: ["catalog", "products"] })
765
+ @Cacheable({ seconds: 300, tags: ["users"] })
766
+ ```
767
+
768
+ ```ts
769
+ import { DISTRIBUTED_OUTPUT_CACHE, DISTRIBUTED_SERVICE_CACHE } from "@/core/cache";
770
+
771
+ // After a data mutation:
772
+ cache.evictByTag("catalog"); // memory output + method cache
773
+
774
+ // The distributed level (resolve the registry from DI):
775
+ await services.resolve(DISTRIBUTED_OUTPUT_CACHE).resolve("default").evictByTag("catalog");
776
+ await services.resolve(DISTRIBUTED_SERVICE_CACHE).resolve("default").evictByTag("users");
777
+ ```
778
+
779
+ The backend keeps the tag index as a SET: `SADD tag:{tag} memberKeys` (+ `EXPIRE` for the entry TTL)
780
+ → `SMEMBERS` + `DEL` on evict. The SET has a TTL, so the tag index does not grow without bound.
781
+
782
+ **Note:** invalidation is **not automatic**: the application calls `evictByTag` after POST/PUT/DELETE (like `IOutputCacheStore.EvictByTagAsync` in ASP.NET).
783
+
784
+ ---
785
+
786
+ ## 15. Integration with the HTTP pipeline
787
+
788
+ The order on a route with `@Authorize` and an output cache:
789
+
790
+ ```
791
+ 1. server middleware ← httpModule({ middleware }) + DI SERVER_MIDDLEWARE
792
+ 2. authorize ← built-in @Authorize / @AllowAnonymous check
793
+ 3. route middleware ← controller and method @Middleware
794
+ 4. binding + ActionFilter.before
795
+ 5. outputCache ← route composer from the cache module (DI)
796
+ 6. controller action
797
+ ```
798
+
799
+ Connection: installing the cache module is enough. `memory()` (or `buildCacheModule(...)`) registers the output cache composer through the `ROUTE_MIDDLEWARE_COMPOSER` DI token, and `httpModule` picks it up:
800
+
801
+ ```ts
802
+ await runApp(AppModule, {
803
+ cache: memory(), // registers the output cache ROUTE_MIDDLEWARE_COMPOSER
804
+ http: {},
805
+ });
806
+ ```
807
+
808
+ No manual `routeMiddlewareComposer` wiring is needed; an explicit
809
+ `httpModule({ routeMiddlewareComposer })` is combined with the DI composers.
810
+
811
+ **Authorization before the cache** is built into the route chain: for `varyByUser` / `varyByClaim` the `@Authorize` check must store the principal in `ctx.state` (`PRINCIPAL_STATE_KEY`) before the key is built.
812
+
813
+ On a **cache hit** step 6 does not run: the response is faster in the HTTP logs, and there are no action side effects.
814
+
815
+ ### 15.1. Pipeline diagrams (Mermaid)
816
+
817
+ #### Middleware order
818
+
819
+ ```mermaid
820
+ sequenceDiagram
821
+ participant C as Client
822
+ participant H as HttpServer
823
+ participant A as authorize
824
+ participant O as outputCache
825
+ participant X as Controller action
826
+
827
+ C->>H: GET /api/users?limit=100
828
+ H->>A: after server middleware
829
+ A->>A: run @Authorize checks, store ctx.state principal
830
+ H->>O: after route middleware and binding
831
+ alt cache HIT
832
+ O-->>C: cached Response (action skipped)
833
+ else cache MISS
834
+ O->>X: await next()
835
+ X->>X: UserService.list()
836
+ X-->>O: Response
837
+ O->>O: store CachedHttpPayload
838
+ O-->>C: Response
839
+ end
840
+ ```
841
+
842
+ #### Output cache — decision flow
843
+
844
+ ```mermaid
845
+ flowchart TD
846
+ START([outputCache middleware]) --> EN{enabled and not noStore?}
847
+ EN -->|no| NEXT[await next]
848
+ EN -->|yes| METH{method in GET HEAD?}
849
+ METH -->|no| NEXT
850
+ METH -->|yes| UNA{unlessAuthenticated and logged in?}
851
+ UNA -->|yes| NEXT
852
+ UNA -->|no| WHEN{when ctx returns true?}
853
+ WHEN -->|no| NEXT
854
+ WHEN -->|yes| KEY[buildOutputCacheKey]
855
+ KEY --> GET{cache.get key}
856
+ GET -->|hit| HIT[return cached Response]
857
+ GET -->|miss| LOCK[getOrCreateAsync factory]
858
+ LOCK --> RUN[await next - run action]
859
+ RUN --> ST{status in statusCodes?}
860
+ ST -->|yes| STORE[set payload with TTL and tags]
861
+ ST -->|no| SKIP[do not store]
862
+ STORE --> OUT[return Response]
863
+ SKIP --> OUT
864
+ NEXT --> OUT2[return Response]
865
+ ```
866
+
867
+ #### Concurrent miss (anti-stampede)
868
+
869
+ ```mermaid
870
+ sequenceDiagram
871
+ participant R1 as Request 1
872
+ participant R2 as Request 2
873
+ participant C as MemoryCache / RedisCache
874
+ participant DB as Action / DB
875
+
876
+ par same cache key
877
+ R1->>C: getOrCreateAsync(key)
878
+ R2->>C: getOrCreateAsync(key)
879
+ end
880
+ C->>C: first caller acquires lock / inFlight
881
+ C->>DB: single factory execution
882
+ DB-->>C: value
883
+ C-->>R1: value
884
+ C-->>R2: value (deduped)
885
+ ```
886
+
887
+ #### Redis distributed lock (between pods)
888
+
889
+ ```mermaid
890
+ sequenceDiagram
891
+ participant P1 as Pod 1
892
+ participant P2 as Pod 2
893
+ participant R as Redis
894
+
895
+ P1->>R: SET lock NX EX
896
+ R-->>P1: OK
897
+ P2->>R: SET lock NX EX
898
+ R-->>P2: null (busy)
899
+ P2->>R: poll GET key
900
+ P1->>P1: run action
901
+ P1->>R: SET payload EX
902
+ P1->>R: DEL lock
903
+ R-->>P2: payload hit
904
+ P2-->>P2: return cached value
905
+ ```
906
+
907
+ #### `@Cacheable` proxy (service layer)
908
+
909
+ ```mermaid
910
+ flowchart LR
911
+ CTRL[Controller] -->|resolve IUserStore| PROXY[Cacheable Proxy]
912
+ PROXY --> META{@Cacheable on method?}
913
+ META -->|no| BIND[bind original method]
914
+ META -->|yes| KEY[build key from args]
915
+ KEY --> G{ICache.get}
916
+ G -->|hit| RET1[return cached value]
917
+ G -->|miss| CALL[call UserService method]
918
+ CALL --> SET[ICache.set with TTL]
919
+ SET --> RET2[return fresh value]
920
+ ```
921
+
922
+ ---
923
+
924
+ ## 16. The distributed backend and `@/core/infra`
925
+
926
+ ### Responsibility and contracts
927
+
928
+ `DistributedCache` owns TTL, tags, the fencing lock, request coalescing and value limits.
929
+ `DistributedCacheDriver` implements the low-level commands; for Redis that is
930
+ `RedisDistributedCacheDriver` from Infra.
931
+ `DistributedCacheStores` provides the names, the HTTP/service registries and `ping()`
932
+ without managing the connection. `RedisDistributedCacheBackend` builds these registries on
933
+ top of the client owned by `redisConnect` and its `InfraLifecycle`.
934
+
935
+ The contract and the DI token `DISTRIBUTED_CACHE_BACKEND` use `DistributedCacheStores`.
936
+ It is available through the public Cache API and has no lifecycle methods.
937
+ The Redis backend has no `start/stop` either: the connection and health belong to the
938
+ Redis connector. Cache does not register the backend as a HostedService.
939
+
940
+ ### A single backend
941
+
942
+ An application allows one unkeyed `DISTRIBUTED_CACHE_BACKEND` registration.
943
+ When the module container is built, Cache checks the final registrations after configure.
944
+ Two backends, even with different `connection` values or in different modules, give a
945
+ `CacheError` before clients are created; the order of imports does not pick a winner.
946
+ The shared HTTP/service registries belong to the one chosen backend.
947
+ DI handles a repeated import of the same module once; separate containers do not conflict.
948
+ An explicit replacement through `configure.replace` is counted by its result.
949
+ The check works in `createContainer`/kernel, not in a standalone ServiceCollection.
950
+
951
+ One `redisConnect(config, { cache: ... })` provides one named connection. Other Redis
952
+ clients are allowed with separate `token` values without the cache mode.
953
+ A custom backend may provide several names through NamedCacheRegistry; there is no
954
+ automatic merging of several backends and no `connections` field in `redisConnect`.
955
+
956
+ ### Redis parameters
957
+
958
+ The `config` declaration holds the required `url: string | Secret`. The connector's second
959
+ argument takes `token?` and `cache?: "distributed" | { mode: "distributed", ...tuning }`.
960
+ Without a cache mode only the client is registered. Tuning is optional:
961
+
962
+ | Field | Type | Default | Description |
963
+ | --- | --- | --- | --- |
964
+ | `connection` | `string` | `default` | The name for decorators and NamedCacheRegistry |
965
+ | `keyPrefix` | `string` | `bazis:cache:` | A shared prefix; the backend adds `out:` and `svc:` |
966
+ | `defaultLockSeconds` | `number` | `10` | A positive finite lock time, seconds |
967
+ | `maxKeyLength` | `number` | `512` | A positive integer, the key length |
968
+ | `maxValueBytes` | `number` | No limit | A positive integer; the limit of a serialized value |
969
+ | `pollIntervalMs` | `number` | `50` | A positive finite interval for waiting for a result, ms |
970
+
971
+ The connector reads the `defineConfig` view of its kernel. Env overrides follow the
972
+ kernel/config rules and the application declaration; generic `REDIS_URL`/`REDIS_KEY_PREFIX`
973
+ are not read automatically.
974
+
975
+ ### Startup and shutdown
976
+
977
+ `InfraLifecycle` creates the client lazily and publishes it through DI. The backend and the
978
+ registries may be resolved before start; I/O is allowed once the connection is ready. At
979
+ start Infra calls `RedisClient.connect()` (default phase −100), and on an error or at
980
+ shutdown `close()` exactly once. The backend neither opens nor closes the client. Reusing a
981
+ stopped kernel is not supported.
982
+
983
+ ### Lock atomicity (fencing) and health
984
+
985
+ The lock is taken with `SET lock token NX EX`. Release is an atomic Lua `compare-and-del`
986
+ (`if get==token then del`); a slow worker cannot delete someone else's lock.
987
+
988
+ `cache:memory` shows the entries. The Infra Redis path registers `infra:<name>` with a real
989
+ PING. A custom connector defines its own connection check.
990
+ Without a distributed backend the memory operations are available; `@OutputRedisCache` and
991
+ `@CacheableRedis` need the matching DI registries.
992
+
993
+ Composition checks: [cache-composition.test.ts](../infra/test/cache-composition.test.ts).
994
+ Algorithm checks with a stub driver do not replace physical Redis acceptance.
995
+
996
+ ---
997
+
998
+ ## 17. Security
999
+
1000
+ ### 17.1. Startup guard
1001
+
1002
+ At HTTP startup the module checks: `@OutputCache` / `@OutputRedisCache` on a route with `@Authorize` (or with `requireAuthenticationByDefault`) **without** `varyByUser: true` and **without** `unlessAuthenticated: true` → a fail-fast error by default:
1003
+
1004
+ ```
1005
+ [cache] @OutputCache on UsersController.list is on an authorized route
1006
+ without varyByUser or unlessAuthenticated — …
1007
+ ```
1008
+
1009
+ For a migration you can temporarily enable the warning mode:
1010
+ `buildCacheModule({ outputCache: { insecureAuthorizedRouteBehavior: "warn" } })`.
1011
+ Disabling it fully (`"ignore"`) is acceptable only with an external security policy.
1012
+
1013
+ ### 17.2. Recommendations by scenario
1014
+
1015
+ | Scenario | Recommendation |
1016
+ | --- | --- |
1017
+ | A public GET catalog | `@OutputCache({ seconds: 60, varyByQuery: [...] })` |
1018
+ | A protected per-user list | `varyByUser: true` |
1019
+ | Shared stats only for guests | `unlessAuthenticated: true` |
1020
+ | Personal data | `@Cacheable` in the service with an explicit `key`, not the output cache |
1021
+ | A multi-instance public API | `@OutputRedisCache` |
1022
+ | An auth lookup shared between pods | `@CacheableRedis` on `findByName` or similar |
1023
+
1024
+ ### 17.3. What must not be cached through the output cache
1025
+
1026
+ - Responses with `Set-Cookie` (session establishment)
1027
+ - Streaming / chunked responses
1028
+ - Personal data without `varyByUser`
1029
+ - Mutating methods (POST/PUT/DELETE): not cached by default (`methods: GET, HEAD`)
1030
+
1031
+ ---
1032
+
1033
+ ## 18. Production scenarios
1034
+
1035
+ In the osnova application the composition root is `src/index.ts` + `src/app/infra/App.infra.ts`:
1036
+
1037
+ ```ts
1038
+ import { memory } from "@/core/cache";
1039
+ import { Infra, redisConnect } from "@/core/infra";
1040
+ import { redisConfig } from "@/app/config/redis.config";
1041
+
1042
+ @Infra({
1043
+ cache: redisConnect(redisConfig, { cache: "distributed" }),
1044
+ })
1045
+ class AppInfra {}
1046
+
1047
+ await runApp(AppModule, {
1048
+ infra: AppInfra,
1049
+ cache: memory(),
1050
+ http: { port: 3000, prefix: "api" },
1051
+ });
1052
+ ```
1053
+
1054
+ **Service** (one and the same provider for memory and distributed):
1055
+
1056
+ ```ts
1057
+ class UserService implements IUserStore {
1058
+ @Cacheable({ policy: "userById", key: (...args) => String(args[0]) })
1059
+ async byId(id: number) { /* … */ }
1060
+
1061
+ @CacheableRedis({ policy: "userByName", key: (...args) => String(args[0]) })
1062
+ async findByName(name: string) { /* … */ }
1063
+ }
1064
+
1065
+ // UsersModule:
1066
+ imports: [UsersDataModule],
1067
+ providers: [cachedScoped(IUserStore, UserService, deps)],
1068
+ ```
1069
+
1070
+ `@CacheableRedis.findByName` uses the distributed level automatically when the backend is
1071
+ configured; without a backend such a call throws a clear error (see §19).
1072
+
1073
+ ### Checking the output cache (manual)
1074
+
1075
+ 1. `GET /api/users?limit=100` with a JWT → a list of N users
1076
+ 2. `POST /api/users` → create a new one
1077
+ 3. A repeated `GET /api/users?limit=100` → **still N** (stale until the TTL expires / evictByTag)
1078
+
1079
+ Run these steps against a running application or its built binary.
1080
+
1081
+ ---
1082
+
1083
+ ## 19. Limits and anti-patterns
1084
+
1085
+ ### v1 limits
1086
+
1087
+ | # | Limit |
1088
+ | --- | --- |
1089
+ | 1 | `@OutputCache` / `@Cacheable`: one process (not shared) |
1090
+ | 2 | `@CacheableRedis` values are JSON-serializable only (Date → string, class instances lose their prototype) |
1091
+ | 3 | `@Cacheable` / `@CacheableRedis` without an explicit `key` encode supported data and Map/Set per §8.2; unsupported arguments, including cycles and hidden state, automatically bypass the cache |
1092
+ | 4 | **References** to objects are cached in memory: mutating after get changes the "cache" |
1093
+ | 5 | The output cache materializes the full body within `maxBodyBytes`/`bodyReadTimeoutMs`; large or slow streams are served without the cache |
1094
+ | 6 | Tag invalidation is manual (`evictByTag`), not tied to ORM events |
1095
+ | 7 | No `@CacheEvict` decorator (invalidate explicitly in code) |
1096
+ | 8 | The Redis backend is only Bun's built-in `RedisClient` (another backend is your own `DistributedCacheDriver`) |
1097
+
1098
+ **What improved in this version:**
1099
+
1100
+ - The distributed lock is released atomically (fencing-token Lua `compare-and-del`): no Redlock bug.
1101
+ - `maxValueBytes` applies both to the distributed level and to binary values (the output payload).
1102
+ - `MemoryCache.size` is O(1) (no full scan on every access).
1103
+ - InfraLifecycle releases the client on a connection error; the coordinator rolls back services that already started.
1104
+ - The Redis connector's `infra:<name>` health check does a real `PING`.
1105
+ - One caching proxy (`wrapCachedService`) for both levels: no duplication and no fake `ICache`.
1106
+
1107
+ ### Anti-patterns
1108
+
1109
+ ```ts
1110
+ // ❌ A decorator on an interface
1111
+ interface IUserStore {
1112
+ @Cacheable({ seconds: 60 }) // does not work
1113
+ byId(id: number): Promise<User>;
1114
+ }
1115
+
1116
+ // ✅ A regular singleton/scoped: the auto-hook wraps a class with @Cacheable/@CacheableRedis
1117
+ singleton(IUserStore, UserService); // the proxy applies automatically (memory() is connected)
1118
+
1119
+ // ❌ @OutputRedisCache without distributed stores in Infra/DI
1120
+ memory({}); // + @OutputRedisCache → CacheError on the first request
1121
+
1122
+ // ❌ Both output decorators on an action
1123
+ @OutputCache({ seconds: 60 })
1124
+ @OutputRedisCache({ seconds: 60 }) // warning, the distributed one wins
1125
+
1126
+ // ❌ Both method decorators on one method
1127
+ @Cacheable({ seconds: 10 })
1128
+ @CacheableRedis({ seconds: 10 }) // runtime throw
1129
+
1130
+ // ❌ Output cache of personal data without varyByUser
1131
+ @Authorize(isSignedIn)
1132
+ @OutputCache({ seconds: 60 }) // startup throw + data leak risk
1133
+ ```
1134
+
1135
+ ---
1136
+
1137
+ ## 20. FAQ
1138
+
1139
+ ### `@OutputCache` or `@Cacheable`: which one?
1140
+
1141
+ | Criterion | `@OutputCache` | `@Cacheable` |
1142
+ | --- | --- | --- |
1143
+ | Caches | the whole HTTP response | the method return value |
1144
+ | Level | controller | service |
1145
+ | Vary by query/header/user | ✅ | ❌ (only a `key` from args) |
1146
+ | Client `Cache-Control` | ✅ `clientCache` | ❌ |
1147
+ | Reuse outside HTTP | ❌ | ✅ (jobs, other controllers) |
1148
+ | Streaming response | ❌ | ✅ (the method may stream, but the value cache does not) |
1149
+
1150
+ **Rule:** a public GET endpoint → `@OutputCache`. A method called from several places, or a cache needed without HTTP → `@Cacheable`.
1151
+
1152
+ ---
1153
+
1154
+ ### `@OutputCache` or `@OutputRedisCache`?
1155
+
1156
+ | | Memory | Redis |
1157
+ | --- | --- | --- |
1158
+ | Instances | 1 process | N pods |
1159
+ | Dependencies | only `memory()` | + `redisConnect(config, { cache: "distributed" })` in `@Infra` |
1160
+ | Latency | ~μs | ~ms (network) |
1161
+ | Eviction tags | in-process | shared in Redis |
1162
+
1163
+ One pod / dev → `@OutputCache`. Production horizontal scale + the same GET for everyone → `@OutputRedisCache`.
1164
+
1165
+ Separate decorators (not `provider: "redis"` in options): the store is explicit in the metadata, easier to grep and fail-fast without a Redis config.
1166
+
1167
+ ---
1168
+
1169
+ ### I put `@Cacheable` on a service, but the cache does not work
1170
+
1171
+ Checklist:
1172
+
1173
+ 1. The decorator is on the **class `UserService`**, not on `interface IUserStore`.
1174
+ 2. The root app installs `cache: memory()` or imports `memory()` (auto-hook + `ICache`, `CACHE_POLICIES`).
1175
+ 3. The registration goes through `singleton`/`scoped` (the auto-hook wraps it) or explicitly `cachedScoped`/`cachedSingleton`.
1176
+ 4. The options have **`seconds`** (inline or in the `policy`).
1177
+ 5. For `@CacheableRedis`, infra connects `redisConnect(redisConfig, { cache: "distributed" })`, otherwise the call throws.
1178
+
1179
+ ---
1180
+
1181
+ ### The controller injects an interface: does the cache on the implementation work?
1182
+
1183
+ **Yes**, if:
1184
+
1185
+ ```ts
1186
+ // UserService.ts: the decorator is here
1187
+ @Cacheable({ seconds: 60 })
1188
+ async byId(id: number) { ... }
1189
+
1190
+ // UsersModule.ts: registration on the interface token
1191
+ cachedScoped(IUserStore, UserService, deps);
1192
+ ```
1193
+
1194
+ The controller gets the proxy registered as `IUserStore`. The metadata is read from the `UserService` class (`Symbol.metadata`), not from the interface.
1195
+
1196
+ ---
1197
+
1198
+ ### What is `varyByQuery`?
1199
+
1200
+ Query parameters take part in the **cache key**. Requests with different values are different entries:
1201
+
1202
+ ```
1203
+ GET /items?limit=20 → http:<digest A>
1204
+ GET /items?limit=50 → http:<digest B> (a separate cache entry)
1205
+ ```
1206
+
1207
+ `varyByQuery: "*"` means all query params (sorted), and it is the secure default.
1208
+ Only an explicit `varyByQuery: []` ignores the query for a response proven to be query-independent.
1209
+
1210
+ ---
1211
+
1212
+ ### After a POST the GET data did not update: is it a bug?
1213
+
1214
+ **No**, if you did not call `evictByTag`. The output cache keeps a snapshot of the response until the TTL expires.
1215
+
1216
+ ```ts
1217
+ await this.users.add(dto);
1218
+ await this.cache.evictByTag("users"); // or ICache from DI after the mutation
1219
+ ```
1220
+
1221
+ A manual check: after a POST the new record is not visible in the cached list until `evictByTag` or the TTL.
1222
+
1223
+ ---
1224
+
1225
+ ### Is there an `X-Cache: HIT` header?
1226
+
1227
+ **No** in v1. Signs of a hit:
1228
+
1229
+ - a repeated request is faster in the HTTP logs;
1230
+ - controller side effects do not fire (the stale data test);
1231
+ - `ICache.size` grows after the first miss.
1232
+
1233
+ ---
1234
+
1235
+ ### Can `@OutputCache` and `@OutputRedisCache` be on one action?
1236
+
1237
+ Technically yes, but **do not**: a startup warning, and Redis wins. Pick one store.
1238
+
1239
+ ---
1240
+
1241
+ ### Can `@Cacheable` and `@CacheableRedis` be on one method?
1242
+
1243
+ **No**: a runtime error. On **different** methods of one class use a regular `cachedScoped` (one proxy routes memory and distributed).
1244
+
1245
+ ---
1246
+
1247
+ ### `cachedSingleton` or `cachedScoped`?
1248
+
1249
+ | | singleton | scoped |
1250
+ | --- | --- | --- |
1251
+ | Proxy instance | one per DI registration in the container | one per DI registration in the scope |
1252
+ | `@Cacheable` cache | the shared ICache store, automatic keys are isolated per instance | the same ICache store, automatic keys are isolated per instance |
1253
+ | ORM `DbContext` | ⚠️ careful | ✅ the typical case |
1254
+
1255
+ A `UserService` with an ORM → **`cachedScoped`**. A stateless read-only service without scoped deps → `cachedSingleton` is fine.
1256
+ An explicit `key` in `cachedScoped` and `cachedSingleton` is a sharing contract: include the
1257
+ tenant/user in it if the result is personalized. Without an explicit `key` the framework
1258
+ isolates the automatic key per instance, so hidden constructor state does not leak into the
1259
+ response of another registration or request. This also applies to `@CacheableRedis`:
1260
+ sharing between processes and reuse after a restart need an explicit application `key`.
1261
+ DI creates the object with a regular class provider and then applies the proxy;
1262
+ constructor deps and lifecycle are not bypassed.
1263
+
1264
+ ---
1265
+
1266
+ ### Is Redis needed for the application to work?
1267
+
1268
+ **No.** Without a backend `@OutputCache` + `@Cacheable` (memory) work. `@OutputRedisCache`
1269
+ throws a CacheError on the first request, and `@CacheableRedis` on a method call, if the
1270
+ distributed stores are not registered through Infra/DI. The usual connection:
1271
+ `redisConnect(config, { cache: "distributed" })` in `infraModule`.
1272
+
1273
+ ---
1274
+
1275
+ ### How to invalidate the cache after an update?
1276
+
1277
+ ```ts
1278
+ import { ICache, DISTRIBUTED_OUTPUT_CACHE, DISTRIBUTED_SERVICE_CACHE } from "@/core/cache";
1279
+
1280
+ // memory output + @Cacheable
1281
+ cache.evictByTag("users");
1282
+
1283
+ // distributed HTTP output / service cache (resolve the registry from DI):
1284
+ await services.resolve(DISTRIBUTED_OUTPUT_CACHE).resolve("default").evictByTag("catalog");
1285
+ await services.resolve(DISTRIBUTED_SERVICE_CACHE).resolve("default").evictByTag("users");
1286
+ ```
1287
+
1288
+ Automatic invalidation on ORM `saveChanges` is **not implemented** (v1).
1289
+
1290
+ ---
1291
+
1292
+ ### Is it compatible with `bun build --compile`?
1293
+
1294
+ **Yes.** Metadata goes through TC39 decorators (`Symbol.metadata`), without runtime reflection npm packages.
1295
+ The `@/core/cache` core does not import Redis at all; the backend lives in `@/core/infra/cache`
1296
+ and is connected through `redisConnect(...)`.
1297
+
1298
+ ---
1299
+
1300
+ ### Where is a working example?
1301
+
1302
+ | File | What |
1303
+ | --- | --- |
1304
+ | `src/app/infra/App.infra.ts` (the osnova application) | the application composition root: infra manifest → cache backend |
1305
+ | `src/bazis/core/infra/connectors/redis.ts` | the Redis connector factory |
1306
+ | `src/bazis/core/infra/cache/` | the Redis backend adapter over Bun Redis APIs |
1307
+ | `src/bazis/core/cache/test/cache.distributed.test.ts` | the distributed logic without Redis |
1308
+ | `src/bazis/core/infra/test/redisCache.test.ts` | Redis backend integration without a real Redis |
1309
+
1310
+ ---
1311
+
1312
+ ## 21. Folder map
1313
+
1314
+ | Path | Purpose |
1315
+ | --- | --- |
1316
+ | `cacheModule.ts` | `memory()`: the cache module factory |
1317
+ | `ICache.ts` / `MemoryCache.ts` | The in-memory contract + LRU/TTL/dedup |
1318
+ | `decorators/OutputCache.ts` | HTTP metadata (memory) |
1319
+ | `decorators/OutputRedisCache.ts` | HTTP metadata (Redis) |
1320
+ | `decorators/Cacheable.ts` | Service metadata (memory) |
1321
+ | `decorators/CacheableRedis.ts` | Service metadata (Redis) |
1322
+ | `decorators/*Metadata.ts` | TC39 metadata readers |
1323
+ | `http/outputCacheMiddleware.ts` | The per-route memory middleware |
1324
+ | `http/outputRedisCacheMiddleware.ts` | The per-route Redis middleware |
1325
+ | `http/composeOutputCache.ts` | The route middleware composer |
1326
+ | `http/buildOutputCacheKey.ts` | The cache key builder |
1327
+ | `http/CachedHttpPayload.ts` | Response serialization + header stripping |
1328
+ | `http/applyClientCacheHeaders.ts` | The client Cache-Control layer |
1329
+ | `http/outputCacheSecurityWarning.ts` | Startup security warnings |
1330
+ | `distributed/IDistributedCache.ts` | The backend-agnostic contract |
1331
+ | `distributed/DistributedCache.ts` | All the policy (fencing lock, anti-stampede, tags, size) |
1332
+ | `distributed/DistributedCacheDriver.ts` | Low-level primitives (implemented by the backend) |
1333
+ | `distributed/DistributedCacheStores.ts` | Store names, registries and ping without lifecycle |
1334
+ | `distributed/NamedCacheRegistry.ts` | Resolving named connections |
1335
+ | `distributed/CacheCodec.ts`, `codecs.ts` | Serialization (JSON / HTTP payload) |
1336
+ | `tokens/DISTRIBUTED_CACHE.ts` | The `DISTRIBUTED_*` DI tokens |
1337
+ | `services/cacheProxy.ts` | The single `@Cacheable` + `@CacheableRedis` proxy |
1338
+ | `providers/cachedProviders.ts` | DI registration helpers |
1339
+ | `di/classProviderHook.ts` | The auto-hook on `singleton`/`scoped` |
1340
+ | `internal/resolveCachePolicy.ts` | Policy merge + `requireCacheSeconds` |
1341
+ | `internal/normalizeCacheKey.ts` | Long key hashing |
1342
+ | `types/*.ts` | Options + policy types |
1343
+ | `test/*.test.ts` | Unit + e2e tests |
1344
+
1345
+ The Redis backend (infrastructure, separate from the framework core):
1346
+
1347
+ | Path | Purpose |
1348
+ | --- | --- |
1349
+ | `@/core/infra/cache/RedisDistributedCacheDriver.ts` | Redis client primitives (+ Lua fencing release) |
1350
+ | `@/core/infra/cache/RedisDistributedCacheBackend.ts` | Stores and ping on top of the Redis client |
1351
+ | `@/core/infra/connectors/redis.ts` | The connector + config validation |
1352
+
1353
+ ---
1354
+
1355
+ *Binary build:* `bun run build:bin` → `bin/bazis` (the CLI).