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,1707 @@
1
+ import type { Server, ServerWebSocket, WebSocketHandler } from "bun";
2
+ import {
3
+ composeWsUpgradeMiddleware,
4
+ createOriginCheckMiddleware,
5
+ createWsAuthMiddleware,
6
+ isValidSocketUser,
7
+ WsUpgradeError,
8
+ type WsAccessTokenAuthenticator,
9
+ type WsUpgradeContext,
10
+ } from "./middleware";
11
+ import {
12
+ hasHandleConnection,
13
+ hasHandleDisconnect,
14
+ hasOnGatewayInit,
15
+ hasOnGatewayShutdown,
16
+ type ClientPacket,
17
+ type ReconnectPacketData,
18
+ type ServerPacket,
19
+ type SocketContext,
20
+ type SocketUser,
21
+ type ReplayDelivery,
22
+ } from "./types";
23
+ import type { RegisteredNamespace } from "./explorer";
24
+ import { PacketCodecError } from "./packet-codec";
25
+ import { dispatchWsHandler, WsDispatchError } from "./ws-dispatch";
26
+ import {
27
+ applySessionToConnection,
28
+ createConnectionData,
29
+ detachRoomSubscriptions,
30
+ restoreRoomSubscriptions,
31
+ trySendPayload,
32
+ wrapSocket,
33
+ type WsConnectionData,
34
+ } from "./socket-wrapper";
35
+ import { TopicCache } from "./topic-cache";
36
+ import { SessionCapacityError, SessionManager, type SessionState } from "./session-manager";
37
+ import { WsRateLimiter } from "./WsRateLimiter";
38
+ import type { WebSocketAdapter } from "./adapter/adapter.interface";
39
+ import { InMemoryWebSocketAdapter } from "./adapter/in-memory.adapter";
40
+ import type { PacketCodec } from "./codec/packet-codec.interface";
41
+ import { payloadToBytes, payloadToText } from "./codec/packet-codec.interface";
42
+ import { jsonPacketCodec } from "./codec/json.codec";
43
+ import { WebSocketDeliveryError, type WebSocketDiagnostic, type ReliableSessionOwner } from "./reliable-delivery";
44
+
45
+ export interface WebSocketMemoryLimits {
46
+ readonly maxRateLimitBuckets?: number;
47
+ readonly maxTopics?: number;
48
+ readonly maxSessions?: number;
49
+ readonly maxRoomsPerSession?: number;
50
+ readonly maxRoomNameLength?: number;
51
+ readonly maxOutboundQueuePerSession?: number;
52
+ readonly maxOutboundQueueBytesPerSession?: number;
53
+ readonly maxOfflineBroadcastRecipients?: number;
54
+ readonly maxOfflineBroadcastBytes?: number;
55
+ /** Unsettled upgrade operations, including work that outlives its timeout. */
56
+ readonly maxConcurrentHandshakes?: number;
57
+ /** Complete upgrade and connection initialization deadline. Default 10s; 0 disables it. */
58
+ readonly handshakeTimeoutMs?: number;
59
+ readonly maxHandshakeAttemptsPerWindow?: number;
60
+ readonly handshakeWindowMs?: number;
61
+ readonly maxIngressPacketsPerWindow?: number;
62
+ readonly ingressWindowMs?: number;
63
+ readonly maxControlFramesPerWindow?: number;
64
+ readonly controlFrameWindowMs?: number;
65
+ readonly maxPendingMessagesPerConnection?: number;
66
+ /** Unsettled validators/handlers, including work that outlives its timeout. */
67
+ readonly maxConcurrentMessageHandlers?: number;
68
+ /** Validator + handler deadline per message. Default 30s; 0 disables it. */
69
+ readonly messageHandlingTimeoutMs?: number;
70
+ /** Total graceful shutdown drain budget. Default 5s; 0 disables it. */
71
+ readonly shutdownDrainTimeoutMs?: number;
72
+ readonly activeSessionLeaseMs?: number;
73
+ readonly leaseRenewIntervalMs?: number;
74
+ readonly maxSessionIdLength?: number;
75
+ readonly maxEventNameLength?: number;
76
+ readonly maxPacketIdLength?: number;
77
+ /** Native per-socket outgoing buffer bound. Default 1 MiB. */
78
+ readonly maxBackpressureBytes?: number;
79
+ readonly socketCloseTimeoutMs?: number;
80
+ readonly reliablePollIntervalMs?: number;
81
+ }
82
+
83
+ export interface WebSocketServerOptions {
84
+ namespaces: RegisteredNamespace[];
85
+ authenticator?: WsAccessTokenAuthenticator | null;
86
+ requireAuth?: boolean;
87
+ allowQueryToken?: boolean;
88
+ defaultCorsOrigins?: string[];
89
+ adapter?: WebSocketAdapter;
90
+ codec?: PacketCodec;
91
+ replayDelivery?: ReplayDelivery;
92
+ onDiagnostic?: (event: WebSocketDiagnostic) => void;
93
+ limits?: WebSocketMemoryLimits;
94
+ /** Expose validator/handler exception messages. Disabled by default. */
95
+ exposeHandlerErrors?: boolean;
96
+ }
97
+
98
+ export interface WebSocketServerStats {
99
+ readonly activeConnections: number;
100
+ readonly sessions: number;
101
+ readonly queuedMessages: number;
102
+ readonly topics: number;
103
+ readonly rateLimitBuckets: number;
104
+ readonly delivery: Readonly<Record<WebSocketDiagnostic["type"], number>>;
105
+ }
106
+
107
+ interface MessageQueue {
108
+ tail: Promise<void>;
109
+ pending: number;
110
+ }
111
+
112
+ class SessionOwnershipError extends Error {
113
+ public constructor() {
114
+ super("WebSocket session is already active.");
115
+ this.name = "SessionOwnershipError";
116
+ }
117
+ }
118
+
119
+ function parseReconnectSid(packet: ClientPacket): string | null {
120
+ if (packet.type !== "reconnect") {
121
+ return null;
122
+ }
123
+ const data = packet.data as ReconnectPacketData | undefined;
124
+ return data && typeof data.sid === "string" && data.sid.length > 0 ? data.sid : null;
125
+ }
126
+
127
+ /** Native Bun WebSocket runtime shared with the HTTP listener. */
128
+ export class WebSocketServer {
129
+ private readonly namespacesByPath = new Map<string, RegisteredNamespace>();
130
+ private readonly namespacesByName = new Map<string, RegisteredNamespace>();
131
+ private readonly topics: TopicCache;
132
+ private readonly sessionManager: SessionManager;
133
+ private readonly adapter: WebSocketAdapter;
134
+ private readonly codec: PacketCodec;
135
+ private readonly handlerRateLimiter: WsRateLimiter;
136
+ private readonly handshakeRateLimiter: WsRateLimiter;
137
+ private readonly ingressRateLimiter: WsRateLimiter;
138
+ private readonly controlRateLimiter: WsRateLimiter;
139
+ private readonly limits: Required<WebSocketMemoryLimits>;
140
+ private readonly rateLimitKeysBySid = new Map<string, Set<string>>();
141
+ private readonly connectionsBySid = new Map<string, ServerWebSocket<WsConnectionData>>();
142
+ private readonly physicalSockets = new Set<ServerWebSocket<WsConnectionData>>();
143
+ private readonly messageQueues = new Map<string, MessageQueue>();
144
+ private readonly openOperations = new Map<string, Promise<void>>();
145
+ private readonly drainWaiters = new Map<string, () => void>();
146
+ private readonly closeTimers = new Map<string, ReturnType<typeof setTimeout>>();
147
+ private readonly pendingOperations = new Set<Promise<void>>();
148
+ private readonly pendingWork = new Set<Promise<void>>();
149
+ private readonly connectionWork = new Map<string, Promise<void>>();
150
+ private readonly pendingAdapterOperations = new Set<Promise<void>>();
151
+ private readonly handshakeControllers = new Set<AbortController>();
152
+ private readonly messageControllers = new Map<string, AbortController>();
153
+ private readonly initializedGateways: unknown[] = [];
154
+ private readonly maxPayloadLength: number;
155
+ private activeConnections = 0;
156
+ private activeHandshakes = 0;
157
+ private activeMessageHandlers = 0;
158
+ private leaseRenewalInFlight = false;
159
+ private bunServer: Server<WsConnectionData> | null = null;
160
+ private leaseTimer?: ReturnType<typeof setInterval>;
161
+ private deliveryTimer?: ReturnType<typeof setInterval>;
162
+ private readonly deliveryCandidates = new Set<string>();
163
+ private deliveryInFlight = false;
164
+ private readonly deliveryCounters: Record<WebSocketDiagnostic["type"], number> = {
165
+ "queue-rejected": 0, "adapter-error": 0, "lease-lost": 0, "reliable-published": 0,
166
+ "reliable-rejected": 0, "reliable-acked": 0, "poll-error": 0,
167
+ };
168
+ private adapterOperationError: unknown;
169
+ private initialized = false;
170
+ private closed = false;
171
+ private closing?: Promise<void>;
172
+
173
+ public constructor(private readonly options: WebSocketServerOptions) {
174
+ validateAuthenticatedOrigins(options);
175
+ if (options.replayDelivery !== undefined && options.replayDelivery !== "transport" && options.replayDelivery !== "client-ack") {
176
+ throw new Error("WebSocket replayDelivery must be transport or client-ack.");
177
+ }
178
+ this.limits = normalizeMemoryLimits(options.limits);
179
+ if (this.limits.leaseRenewIntervalMs >= this.limits.activeSessionLeaseMs) {
180
+ throw new Error("WebSocket leaseRenewIntervalMs must be less than activeSessionLeaseMs.");
181
+ }
182
+ this.topics = new TopicCache({ maxTopics: this.limits.maxTopics });
183
+ this.handlerRateLimiter = new WsRateLimiter({ maxBuckets: this.limits.maxRateLimitBuckets });
184
+ this.handshakeRateLimiter = new WsRateLimiter({ maxBuckets: this.limits.maxRateLimitBuckets });
185
+ this.ingressRateLimiter = new WsRateLimiter({ maxBuckets: this.limits.maxRateLimitBuckets });
186
+ this.controlRateLimiter = new WsRateLimiter({ maxBuckets: this.limits.maxRateLimitBuckets });
187
+
188
+ let maxPayload = 1;
189
+ for (const namespace of options.namespaces) {
190
+ validateNamespace(namespace);
191
+ if (this.namespacesByPath.has(namespace.path)) {
192
+ throw new Error(`Duplicate WebSocket path "${namespace.path}".`);
193
+ }
194
+ if (this.namespacesByName.has(namespace.namespace)) {
195
+ throw new Error(`Duplicate WebSocket namespace "${namespace.namespace}".`);
196
+ }
197
+ this.namespacesByPath.set(namespace.path, namespace);
198
+ this.namespacesByName.set(namespace.namespace, namespace);
199
+ maxPayload = Math.max(maxPayload, namespace.maxPayloadBytes);
200
+ }
201
+ this.maxPayloadLength = maxPayload;
202
+ this.adapter = options.adapter ?? new InMemoryWebSocketAdapter({ maxEntries: this.limits.maxSessions });
203
+ this.codec = options.codec ?? jsonPacketCodec;
204
+ this.sessionManager = new SessionManager({
205
+ adapter: this.adapter,
206
+ maxOutboundQueue: this.limits.maxOutboundQueuePerSession,
207
+ maxOutboundQueueBytes: this.limits.maxOutboundQueueBytesPerSession,
208
+ maxSessions: this.limits.maxSessions,
209
+ maxRoomsPerSession: this.limits.maxRoomsPerSession,
210
+ maxRoomNameLength: this.limits.maxRoomNameLength,
211
+ maxOfflineBroadcastRecipients: this.limits.maxOfflineBroadcastRecipients,
212
+ maxOfflineBroadcastBytes: this.limits.maxOfflineBroadcastBytes,
213
+ activeLeaseMs: this.limits.activeSessionLeaseMs,
214
+ });
215
+ }
216
+
217
+ public getStats(): WebSocketServerStats {
218
+ const sessionStats = this.sessionManager.getStats();
219
+ return {
220
+ activeConnections: this.activeConnections,
221
+ sessions: sessionStats.sessions,
222
+ queuedMessages: sessionStats.queuedMessages,
223
+ topics: this.topics.getStats().topics,
224
+ delivery: { ...this.deliveryCounters },
225
+ rateLimitBuckets:
226
+ this.handlerRateLimiter.getStats().buckets
227
+ + this.handshakeRateLimiter.getStats().buckets
228
+ + this.ingressRateLimiter.getStats().buckets
229
+ + this.controlRateLimiter.getStats().buckets,
230
+ };
231
+ }
232
+
233
+ public async initialize(): Promise<void> {
234
+ if (this.initialized) {
235
+ return;
236
+ }
237
+ if (this.closed) {
238
+ throw new Error("WebSocket server is closed.");
239
+ }
240
+ try {
241
+ await this.adapter.initialize({
242
+ localPublish: (topic, payload) => {
243
+ this.bunServer?.publish(topic, this.codec.isBinary(payload) ? payload : payloadToText(payload));
244
+ },
245
+ deliveriesReady: (sids) => this.queueDeliveries(sids),
246
+ });
247
+ for (const namespace of this.options.namespaces) {
248
+ // Register before invoking the hook so a partially initialized gateway
249
+ // still receives rollback shutdown when its init hook throws.
250
+ this.initializedGateways.push(namespace.gatewayInstance);
251
+ if (hasOnGatewayInit(namespace.gatewayInstance)) {
252
+ await namespace.gatewayInstance.onGatewayInit();
253
+ }
254
+ }
255
+ this.initialized = true;
256
+ if (this.adapter.reliableRooms) {
257
+ this.deliveryTimer = setInterval(() => this.queueDeliveries(this.connectionsBySid.keys()), this.limits.reliablePollIntervalMs);
258
+ }
259
+ const leaseSpreadMs = Math.min(this.limits.leaseRenewIntervalMs,
260
+ Math.floor((this.limits.activeSessionLeaseMs - this.limits.leaseRenewIntervalMs) / 2));
261
+ const leaseTickMs = Math.max(1, Math.min(1000, Math.floor(this.limits.leaseRenewIntervalMs / 10),
262
+ Math.floor(Math.min(...this.options.namespaces.map((namespace) => namespace.sessionTtlMs)) / 10)));
263
+ this.leaseTimer = setInterval(() => {
264
+ if (this.leaseRenewalInFlight || this.closed) {
265
+ return;
266
+ }
267
+ this.leaseRenewalInFlight = true;
268
+ const renewal = this.sessionManager.renewOwnedLeases({ afterMs: this.limits.leaseRenewIntervalMs, spreadMs: leaseSpreadMs })
269
+ .then((lostSids) => {
270
+ if (lostSids.length) this.diagnose("lease-lost", lostSids.length);
271
+ for (const sid of lostSids) {
272
+ const ws = this.connectionsBySid.get(sid);
273
+ if (ws) this.closeConnection(ws, 4009, "session ownership lost");
274
+ }
275
+ })
276
+ .catch((error: unknown) => {
277
+ this.diagnose("adapter-error");
278
+ // If lease persistence is unavailable, fail closed before another
279
+ // node can legitimately claim an apparently expired session.
280
+ for (const ws of this.connectionsBySid.values()) {
281
+ this.closeConnection(ws, 1011, "session lease renewal failed");
282
+ }
283
+ throw error;
284
+ })
285
+ .finally(() => {
286
+ this.leaseRenewalInFlight = false;
287
+ });
288
+ this.trackOperation(renewal);
289
+ }, leaseTickMs);
290
+ } catch (error) {
291
+ await this.shutdownInitializedGateways().catch(() => {});
292
+ await this.adapter.close().catch(() => {});
293
+ this.closed = true;
294
+ throw error;
295
+ }
296
+ }
297
+
298
+ public async tryUpgrade(
299
+ request: Request,
300
+ server: Server<WsConnectionData>,
301
+ ): Promise<Response | undefined | null> {
302
+ if (request.headers.get("upgrade")?.toLowerCase() !== "websocket") {
303
+ return null;
304
+ }
305
+ const url = new URL(request.url);
306
+ const registered = this.namespacesByPath.get(url.pathname);
307
+ if (!registered) {
308
+ return null;
309
+ }
310
+ if (this.closed || !this.initialized) {
311
+ return new Response("WebSocket service unavailable", { status: 503 });
312
+ }
313
+ this.bunServer = server;
314
+
315
+ const address = requestAddress(server, request);
316
+ const handshakeKey = `${address}:${registered.path}`;
317
+ if (!this.handshakeRateLimiter.check(
318
+ handshakeKey,
319
+ this.limits.maxHandshakeAttemptsPerWindow,
320
+ this.limits.handshakeWindowMs,
321
+ )) {
322
+ return new Response("Too many WebSocket upgrade attempts", { status: 429 });
323
+ }
324
+ if (this.activeHandshakes >= this.limits.maxConcurrentHandshakes) {
325
+ return new Response("WebSocket service busy", { status: 503 });
326
+ }
327
+
328
+ this.activeHandshakes += 1;
329
+ const handshakeController = new AbortController();
330
+ this.handshakeControllers.add(handshakeController);
331
+ const work = this.observeWork(
332
+ () => this.performUpgrade(request, server, url, registered, handshakeController.signal),
333
+ () => {
334
+ this.handshakeControllers.delete(handshakeController);
335
+ this.activeHandshakes -= 1;
336
+ },
337
+ );
338
+ try {
339
+ return await runAbortable(
340
+ () => work,
341
+ handshakeController,
342
+ this.limits.handshakeTimeoutMs,
343
+ "WebSocket upgrade timed out.",
344
+ );
345
+ } catch (error) {
346
+ if (error instanceof WsUpgradeError) {
347
+ return new Response(error.message, { status: error.status });
348
+ }
349
+ if (error instanceof WebSocketOperationTimeoutError) {
350
+ return new Response("WebSocket upgrade timed out", { status: 504 });
351
+ }
352
+ if (handshakeController.signal.aborted && this.closed) {
353
+ return new Response("WebSocket service unavailable", { status: 503 });
354
+ }
355
+ return new Response("WebSocket upgrade failed", { status: 500 });
356
+ }
357
+ }
358
+
359
+ private async performUpgrade(
360
+ request: Request,
361
+ server: Server<WsConnectionData>,
362
+ url: URL,
363
+ registered: RegisteredNamespace,
364
+ signal: AbortSignal,
365
+ ): Promise<Response | undefined> {
366
+ const corsOrigins =
367
+ registered.corsOrigins.length > 0
368
+ ? registered.corsOrigins
369
+ : (this.options.defaultCorsOrigins ?? ["*"]);
370
+ const middlewares = [
371
+ createOriginCheckMiddleware(corsOrigins),
372
+ ...(this.options.authenticator
373
+ ? [createWsAuthMiddleware(this.options.authenticator, {
374
+ required: this.options.requireAuth,
375
+ allowQueryToken: this.options.allowQueryToken,
376
+ })]
377
+ : []),
378
+ ...registered.middleware,
379
+ ];
380
+ const ctx: WsUpgradeContext = {
381
+ request,
382
+ namespace: registered.namespace,
383
+ path: registered.path,
384
+ signal,
385
+ data: {},
386
+ };
387
+
388
+ await composeWsUpgradeMiddleware(middlewares, async () => {})(ctx);
389
+ throwIfAborted(signal);
390
+ if (this.closed) {
391
+ return new Response("WebSocket service unavailable", { status: 503 });
392
+ }
393
+
394
+ const upgradeUser = (ctx.data as { user?: unknown }).user;
395
+ if (upgradeUser !== undefined && !isValidSocketUser(upgradeUser)) {
396
+ return new Response("Unauthorized", { status: 401 });
397
+ }
398
+
399
+ const sidParam = url.searchParams.get("sid");
400
+ const requestedReplay = url.searchParams.get("replay");
401
+ if (url.searchParams.getAll("replay").length > 1 || (requestedReplay !== null && requestedReplay !== "transport" && requestedReplay !== "client-ack")) {
402
+ return new Response("Invalid replay delivery mode", { status: 400 });
403
+ }
404
+ const replayDelivery = requestedReplay ?? this.options.replayDelivery ?? "transport";
405
+ if (this.options.replayDelivery === "client-ack" && replayDelivery !== "client-ack") {
406
+ return new Response("Client replay acknowledgement required", { status: 409 });
407
+ }
408
+ if (replayDelivery === "client-ack" && !this.adapter.compareAndSwapSession) {
409
+ return new Response("Replay acknowledgement requires an atomic session adapter", { status: 503 });
410
+ }
411
+ let connectionData: WsConnectionData;
412
+ if (sidParam !== null) {
413
+ if (sidParam.length === 0 || sidParam.length > this.limits.maxSessionIdLength) {
414
+ return new Response("Invalid session id", { status: 400 });
415
+ }
416
+ let session: SessionState | undefined;
417
+ try {
418
+ session = await this.sessionManager.resolveSession(sidParam, signal);
419
+ throwIfAborted(signal);
420
+ } catch (error) {
421
+ return new Response(
422
+ error instanceof SessionCapacityError ? "WebSocket session capacity exceeded" : "Session lookup failed",
423
+ { status: error instanceof SessionCapacityError ? 503 : 500 },
424
+ );
425
+ }
426
+ if (!session || session.namespace !== registered.namespace) {
427
+ return new Response("Session not found", { status: 404 });
428
+ }
429
+ const storedUser = session.context.user;
430
+ if (storedUser !== undefined && !isValidSocketUser(storedUser)) {
431
+ return new Response("Unauthorized", { status: 401 });
432
+ }
433
+ const user = upgradeUser as SocketUser | undefined;
434
+ if (storedUser && user?.id !== storedUser.id) {
435
+ return new Response("Unauthorized", { status: 401 });
436
+ }
437
+ if (session.replayDelivery === "client-ack" && replayDelivery !== "client-ack") {
438
+ return new Response("Client replay acknowledgement required", { status: 409 });
439
+ }
440
+ if (user) {
441
+ session.context.user = cloneSocketUser(user);
442
+ this.sessionManager.updateContext(session.sid, session.context);
443
+ await this.sessionManager.flushSession(session.sid);
444
+ throwIfAborted(signal);
445
+ }
446
+ connectionData = createConnectionData(registered.namespace, { ...session.context }, session.sid, replayDelivery);
447
+ connectionData.rooms = new Set(session.rooms);
448
+ connectionData.isReconnect = true;
449
+ } else {
450
+ if (!this.sessionManager.hasCapacity()) {
451
+ return new Response("WebSocket session capacity exceeded", { status: 503 });
452
+ }
453
+ connectionData = createConnectionData(registered.namespace, { ...(ctx.data as SocketContext) }, undefined, replayDelivery);
454
+ }
455
+
456
+ if (this.closed) {
457
+ return new Response("WebSocket service unavailable", { status: 503 });
458
+ }
459
+
460
+ return server.upgrade(request, { data: connectionData })
461
+ ? undefined
462
+ : new Response("WebSocket upgrade failed", { status: 500 });
463
+ }
464
+
465
+ public createBunHandler(): WebSocketHandler<WsConnectionData> {
466
+ return {
467
+ maxPayloadLength: this.maxPayloadLength,
468
+ backpressureLimit: this.limits.maxBackpressureBytes,
469
+ closeOnBackpressureLimit: true,
470
+ open: (ws) => {
471
+ this.physicalSockets.add(ws);
472
+ const opening = this.handleOpenSafely(ws);
473
+ this.openOperations.set(ws.data.connId, opening);
474
+ this.trackOperation(opening.finally(() => {
475
+ if (this.openOperations.get(ws.data.connId) === opening) {
476
+ this.openOperations.delete(ws.data.connId);
477
+ }
478
+ this.queueDeliveries([ws.data.sid]);
479
+ }));
480
+ },
481
+ message: (ws, message) => this.enqueueMessage(ws, message),
482
+ drain: (ws) => { this.drainWaiters.get(ws.data.connId)?.(); this.queueDeliveries([ws.data.sid]); },
483
+ close: (ws, _code, reason) => {
484
+ this.physicalSockets.delete(ws);
485
+ this.trackOperation(this.handleCloseSafely(ws, reason));
486
+ },
487
+ };
488
+ }
489
+
490
+ public close(): Promise<void> {
491
+ // Every caller observes the same result, including a previous drain timeout.
492
+ return this.closing ??= this.closeRuntime();
493
+ }
494
+
495
+ private async closeRuntime(): Promise<void> {
496
+ if (this.closed && !this.initialized) return;
497
+ this.closed = true;
498
+ if (this.deliveryTimer !== undefined) clearInterval(this.deliveryTimer);
499
+ this.deliveryTimer = undefined;
500
+ this.deliveryCandidates.clear();
501
+ if (this.leaseTimer !== undefined) clearInterval(this.leaseTimer);
502
+ this.leaseTimer = undefined;
503
+
504
+ const deadline = this.limits.shutdownDrainTimeoutMs === 0
505
+ ? null : Date.now() + this.limits.shutdownDrainTimeoutMs;
506
+ this.abortMessageOperations("WebSocket server is shutting down.");
507
+ for (const controller of this.handshakeControllers) {
508
+ controller.abort(new WebSocketOperationAbortedError("WebSocket server is shutting down."));
509
+ }
510
+ for (const ws of this.physicalSockets) {
511
+ ws.data.lifetimeController.abort(new WebSocketOperationAbortedError("WebSocket server is shutting down."));
512
+ }
513
+
514
+ // Stop transport immediately. Disconnect hooks wait for their connection's
515
+ // actual work; neither a timeout nor a closed socket completes that work.
516
+ const sockets = [...this.physicalSockets];
517
+ let cursor = 0;
518
+ this.trackOperation(Promise.all(Array.from({ length: Math.min(16, sockets.length) }, async () => {
519
+ while (cursor < sockets.length) await this.handleCloseSafely(sockets[cursor++]!, "server shutdown");
520
+ })).then(() => {}));
521
+ for (const ws of sockets) ws.terminate();
522
+ for (const timer of this.closeTimers.values()) clearTimeout(timer);
523
+ this.closeTimers.clear();
524
+
525
+ // Continue ordered cleanup after a public timeout, but do not close the
526
+ // gateway/adapter underneath unresolved application callbacks.
527
+ const cleanup = this.finishClose();
528
+ if (!await settleBeforeDeadline(() => cleanup, deadline)) {
529
+ throw new WebSocketOperationTimeoutError("WebSocket shutdown timed out; cleanup remains pending.");
530
+ }
531
+ }
532
+
533
+ private async finishClose(): Promise<void> {
534
+ while (this.pendingWork.size > 0) await Promise.all([...this.pendingWork]);
535
+ await this.awaitPendingOperations();
536
+ const errors: unknown[] = [];
537
+ for (const cleanup of [
538
+ () => this.sessionManager.flushAll(),
539
+ () => this.awaitPendingAdapterOperations(),
540
+ () => this.shutdownInitializedGateways(),
541
+ () => this.adapter.close(),
542
+ ]) {
543
+ try { await cleanup(); } catch (error) { errors.push(error); }
544
+ }
545
+
546
+ this.initialized = false;
547
+ this.connectionsBySid.clear();
548
+ this.physicalSockets.clear();
549
+ this.messageQueues.clear();
550
+ this.openOperations.clear();
551
+ this.drainWaiters.clear();
552
+ this.topics.clear();
553
+ this.rateLimitKeysBySid.clear();
554
+ this.handlerRateLimiter.clear();
555
+ this.handshakeRateLimiter.clear();
556
+ this.ingressRateLimiter.clear();
557
+ this.controlRateLimiter.clear();
558
+ this.handshakeControllers.clear();
559
+ this.messageControllers.clear();
560
+ this.activeConnections = 0;
561
+ this.bunServer = null;
562
+ if (this.adapterOperationError !== undefined) {
563
+ errors.push(this.adapterOperationError);
564
+ this.adapterOperationError = undefined;
565
+ }
566
+ if (errors.length > 0) throw errors[0];
567
+ }
568
+
569
+ private wrapOptions(
570
+ ws: ServerWebSocket<WsConnectionData>,
571
+ signal?: AbortSignal,
572
+ ): Parameters<typeof wrapSocket>[2] {
573
+ const registered = this.resolveNamespace(ws);
574
+ const maxPayloadBytes = registered ? outboundLimit(registered) : this.maxPayloadLength;
575
+ return {
576
+ codec: this.codec,
577
+ ...(signal ? { signal } : {}),
578
+ maxRoomNameLength: this.limits.maxRoomNameLength,
579
+ maxRoomsPerSession: this.limits.maxRoomsPerSession,
580
+ disconnect: (code, reason) => this.closeConnection(ws, code, reason),
581
+ send: (payload) => {
582
+ if (signal?.aborted) {
583
+ return;
584
+ }
585
+ if (payloadBytes(payload) > maxPayloadBytes) {
586
+ throw new Error("WebSocket outbound payload exceeds the namespace limit.");
587
+ }
588
+ if (!trySendPayload(ws, payload)) {
589
+ this.closeForOutbound(ws, 1011, "outbound send failed");
590
+ }
591
+ },
592
+ broadcast: (room, packet) => {
593
+ if (signal?.aborted) {
594
+ return;
595
+ }
596
+ const namespace = ws.data.namespace;
597
+ const topic = this.topics.get(namespace, room);
598
+ const payload = this.codec.encodeServer(packet);
599
+ const bytes = payloadToBytes(payload);
600
+ if (bytes.byteLength > maxPayloadBytes) {
601
+ throw new Error("WebSocket broadcast payload exceeds the namespace limit.");
602
+ }
603
+ const admission = this.sessionManager.enqueueToOfflineRoomMembers(
604
+ namespace,
605
+ room,
606
+ { v: 1, ...packet },
607
+ ws.data.sid,
608
+ bytes.byteLength,
609
+ true,
610
+ );
611
+ if (admission.truncated) {
612
+ this.diagnose("queue-rejected");
613
+ throw new WebSocketDeliveryError("OFFLINE_QUEUE_CAPACITY");
614
+ }
615
+ ws.publish(topic, payload);
616
+ this.trackAdapterOperation(this.adapter.publish(topic, bytes));
617
+ },
618
+ broadcastReliable: async (room, packet, options) => {
619
+ signal?.throwIfAborted();
620
+ const store = this.adapter.reliableRooms;
621
+ if (!store) throw new WebSocketDeliveryError("RELIABLE_DELIVERY_UNAVAILABLE");
622
+ if (payloadBytes(this.codec.encodeServer({ ...packet, deliveryId: options.messageId })) > maxPayloadBytes) {
623
+ throw new WebSocketDeliveryError("PAYLOAD_CAPACITY");
624
+ }
625
+ try {
626
+ // Joining a room and publishing in one handler is an ordered operation.
627
+ await this.sessionManager.flushSession(ws.data.sid);
628
+ signal?.throwIfAborted();
629
+ const receipt = await store.publish({ ...options, namespace: ws.data.namespace, room, excludeSid: ws.data.sid, packet: { ...packet, v: 1 } });
630
+ this.diagnose("reliable-published", receipt.duplicate ? 0 : receipt.recipients);
631
+ signal?.throwIfAborted();
632
+ return receipt;
633
+ } catch (error) {
634
+ this.diagnose("reliable-rejected");
635
+ throw error;
636
+ }
637
+ },
638
+ onRoomsChanged: (sid, rooms) => {
639
+ // A replaced physical socket may still run its disconnect hook after a
640
+ // new socket has claimed the same SID. Never let that stale wrapper
641
+ // overwrite the new owner's room snapshot.
642
+ if (this.connectionsBySid.get(sid) === ws) {
643
+ this.sessionManager.updateRooms(sid, rooms);
644
+ }
645
+ },
646
+ };
647
+ }
648
+
649
+ private encodeSend(
650
+ ws: ServerWebSocket<WsConnectionData>,
651
+ packet: Omit<ServerPacket, "v"> & { v?: 1 },
652
+ ): boolean {
653
+ const payload = this.codec.encodeServer(packet);
654
+ const registered = this.resolveNamespace(ws);
655
+ if (registered && payloadBytes(payload) > outboundLimit(registered)) {
656
+ this.closeForOutbound(ws, 1009, "outbound payload too large");
657
+ return false;
658
+ }
659
+ if (!trySendPayload(ws, payload)) {
660
+ this.closeForOutbound(ws, 1011, "outbound send failed");
661
+ return false;
662
+ }
663
+ return true;
664
+ }
665
+
666
+ private closeForOutbound(ws: ServerWebSocket<WsConnectionData>, code: number, reason: string): void {
667
+ this.closeConnection(ws, code, reason);
668
+ }
669
+
670
+ private closeConnection(ws: ServerWebSocket<WsConnectionData>, code: number, reason: string): void {
671
+ ws.data.messageProcessingStopped = true;
672
+ const error = new WebSocketOperationAbortedError(reason);
673
+ ws.data.lifetimeController.abort(error);
674
+ this.messageControllers.get(ws.data.connId)?.abort(error);
675
+ ws.close(code, reason);
676
+ if (ws.data.closed || this.closeTimers.has(ws.data.connId)) return;
677
+ const timer = setTimeout(() => {
678
+ this.closeTimers.delete(ws.data.connId);
679
+ if (!ws.data.closed) ws.terminate();
680
+ }, this.limits.socketCloseTimeoutMs);
681
+ this.closeTimers.set(ws.data.connId, timer);
682
+ }
683
+
684
+ private sendError(ws: ServerWebSocket<WsConnectionData>, message: string, id?: string, code?: string): void {
685
+ this.encodeSend(ws, { type: "error", data: { message, ...(code ? { code } : {}) }, ...(id !== undefined ? { id } : {}) });
686
+ }
687
+
688
+ private resolveNamespace(ws: ServerWebSocket<WsConnectionData>): RegisteredNamespace | undefined {
689
+ return this.namespacesByName.get(ws.data.namespace);
690
+ }
691
+
692
+ private async handleOpenSafely(ws: ServerWebSocket<WsConnectionData>): Promise<void> {
693
+ const work = this.observeWork(() => this.handleOpen(ws), undefined, ws.data.connId);
694
+ try {
695
+ await runAbortable(
696
+ () => work,
697
+ ws.data.lifetimeController,
698
+ this.limits.handshakeTimeoutMs,
699
+ "WebSocket connection initialization timed out.",
700
+ );
701
+ } catch (error) {
702
+ ws.data.messageProcessingStopped = true;
703
+ ws.data.lifetimeController.abort(error);
704
+ const code = error instanceof SessionCapacityError ? 1013 : error instanceof SessionOwnershipError ? 4009 : 1011;
705
+ const reason = error instanceof SessionCapacityError
706
+ ? "session capacity exceeded"
707
+ : error instanceof SessionOwnershipError
708
+ ? "session already active"
709
+ : "connection initialization failed";
710
+ this.closeConnection(ws, code, reason);
711
+ }
712
+ }
713
+
714
+ private async handleOpen(ws: ServerWebSocket<WsConnectionData>): Promise<void> {
715
+ const signal = ws.data.lifetimeController.signal;
716
+ throwIfAborted(signal);
717
+ const registered = this.resolveNamespace(ws);
718
+ if (!registered || this.closed) {
719
+ this.closeConnection(ws, 1011, "Unknown namespace");
720
+ return;
721
+ }
722
+
723
+ let session: SessionState;
724
+ if (ws.data.isReconnect) {
725
+ const resolved = await this.sessionManager.resolveSession(ws.data.sid, signal);
726
+ throwIfAborted(signal);
727
+ if (!resolved || resolved.namespace !== registered.namespace
728
+ || (resolved.replayDelivery === "client-ack" && ws.data.replayDelivery !== "client-ack")) {
729
+ throw new SessionOwnershipError();
730
+ }
731
+ session = resolved;
732
+ applySessionToConnection(ws, session.sid, session.context, session.rooms);
733
+ } else {
734
+ session = this.sessionManager.createSession(
735
+ ws.data.sid,
736
+ ws.data.namespace,
737
+ ws.data.context,
738
+ registered.sessionTtlMs,
739
+ ws.data.replayDelivery,
740
+ );
741
+ }
742
+
743
+ await this.claimPhysicalConnection(ws, session.sid);
744
+ throwIfAborted(signal);
745
+ if (ws.data.replayDelivery === "client-ack") {
746
+ this.sessionManager.enableReplayAcknowledgements(session.sid);
747
+ await this.sessionManager.flushSession(session.sid);
748
+ throwIfAborted(signal);
749
+ }
750
+ restoreRoomSubscriptions(ws, this.topics);
751
+ ws.data.opened = true;
752
+ this.activeConnections += 1;
753
+
754
+ if (ws.data.isReconnect) {
755
+ if (!await this.sendReconnect(ws, registered, signal)) return;
756
+ } else {
757
+ await this.sessionManager.flushSession(session.sid);
758
+ throwIfAborted(signal);
759
+ if (!this.encodeSend(ws, {
760
+ type: "connected",
761
+ sid: session.sid,
762
+ namespace: session.namespace,
763
+ data: { userId: session.context.user?.id },
764
+ ...(ws.data.replayDelivery === "client-ack" ? { replayDelivery: "client-ack" as const } : {}),
765
+ })) return;
766
+ }
767
+
768
+ if (hasHandleConnection(registered.gatewayInstance)) {
769
+ await registered.gatewayInstance.handleConnection(
770
+ wrapSocket(ws, this.topics, this.wrapOptions(ws, ws.data.lifetimeController.signal)),
771
+ );
772
+ throwIfAborted(signal);
773
+ if (this.closed || ws.data.closed || this.connectionsBySid.get(session.sid) !== ws) {
774
+ return;
775
+ }
776
+ await this.sessionManager.flushSession(session.sid);
777
+ }
778
+ }
779
+
780
+ private async sendReconnect(
781
+ ws: ServerWebSocket<WsConnectionData>,
782
+ registered: RegisteredNamespace,
783
+ signal: AbortSignal,
784
+ ): Promise<boolean> {
785
+ ws.data.replaying = true;
786
+ try { return await this.sendReconnectFrames(ws, registered, signal); }
787
+ finally { ws.data.replaying = false; this.queueDeliveries([ws.data.sid]); }
788
+ }
789
+
790
+ private async sendReconnectFrames(
791
+ ws: ServerWebSocket<WsConnectionData>,
792
+ registered: RegisteredNamespace,
793
+ signal: AbortSignal,
794
+ ): Promise<boolean> {
795
+ throwIfAborted(signal);
796
+ const sid = ws.data.sid;
797
+ const clientAck = ws.data.replayDelivery === "client-ack";
798
+ const remote = clientAck && this.adapter.reliableRooms
799
+ ? (await this.adapter.reliableRooms.readPending([this.deliveryOwner(ws)]))[0]?.packets ?? []
800
+ : [];
801
+ throwIfAborted(signal);
802
+ const pending = [...this.sessionManager.peekOutbound(sid), ...remote];
803
+ const remoteIds = new Set(remote.map((packet) => packet.deliveryId));
804
+ ws.data.sentReplayIds = clientAck ? new Set<string>() : undefined;
805
+ ws.data.sentReliableIds = clientAck ? new Set<string>() : undefined;
806
+ const recordSent = (packet: ServerPacket): void => {
807
+ if (remoteIds.has(packet.deliveryId)) ws.data.sentReliableIds!.add(packet.deliveryId!);
808
+ else ws.data.sentReplayIds!.add(packet.deliveryId!);
809
+ };
810
+ const missed: ServerPacket[] = [];
811
+ const frame: Omit<ServerPacket, "v"> = {
812
+ type: "reconnected",
813
+ sid,
814
+ namespace: registered.namespace,
815
+ missed,
816
+ data: { userId: ws.data.context.user?.id },
817
+ ...(clientAck ? { replayDelivery: "client-ack" as const, replayCount: pending.length } : {}),
818
+ };
819
+ for (const packet of pending) {
820
+ const candidate = { ...frame, missed: [...missed, packet] };
821
+ if (payloadBytes(this.codec.encodeServer(candidate)) > outboundLimit(registered)) {
822
+ break;
823
+ }
824
+ missed.push(packet);
825
+ }
826
+
827
+ let accepted = 0;
828
+ try {
829
+ await this.waitForDrain(ws, signal);
830
+ if (!this.encodeSend(ws, frame)) return false;
831
+ accepted = missed.length;
832
+ if (clientAck) for (const packet of missed) recordSent(packet);
833
+ // A valid event may fit the namespace limit while not fitting inside
834
+ // the reconnect envelope. Deliver the remainder in order; wait for
835
+ // native drain rather than exhausting a healthy client's send buffer.
836
+ for (const packet of pending.slice(missed.length)) {
837
+ throwIfAborted(signal);
838
+ if ((ws.getBufferedAmount?.() ?? 0) > 0) {
839
+ if (!clientAck) this.sessionManager.acknowledgeOutbound(sid, accepted);
840
+ accepted = 0;
841
+ await this.waitForDrain(ws, signal);
842
+ }
843
+ if (!this.encodeSend(ws, packet)) return false;
844
+ accepted += 1;
845
+ if (clientAck) recordSent(packet);
846
+ }
847
+ return true;
848
+ } finally {
849
+ // Each transport prefix is committed before yielding to drain; a
850
+ // replaced connection cannot remove a new owner's replay entries.
851
+ if (!clientAck && this.connectionsBySid.get(sid) === ws) this.sessionManager.acknowledgeOutbound(sid, accepted);
852
+ await this.sessionManager.flushSession(sid);
853
+ throwIfAborted(signal);
854
+ }
855
+ }
856
+
857
+ private async waitForDrain(ws: ServerWebSocket<WsConnectionData>, signal: AbortSignal): Promise<void> {
858
+ while ((ws.getBufferedAmount?.() ?? 0) > 0) {
859
+ throwIfAborted(signal);
860
+ await new Promise<void>((resolve, reject) => {
861
+ const cleanup = (): void => {
862
+ signal.removeEventListener("abort", abort);
863
+ if (this.drainWaiters.get(ws.data.connId) === drained) this.drainWaiters.delete(ws.data.connId);
864
+ };
865
+ // Let Bun finish its native drain callback before the next write batch.
866
+ // Resuming inside that callback can stall subsequent buffered sends.
867
+ const drained = (): void => { cleanup(); setImmediate(resolve); };
868
+ const abort = (): void => { cleanup(); reject(signal.reason); };
869
+ this.drainWaiters.set(ws.data.connId, drained);
870
+ signal.addEventListener("abort", abort, { once: true });
871
+ if (signal.aborted) abort();
872
+ else if ((ws.getBufferedAmount?.() ?? 0) === 0) drained();
873
+ });
874
+ }
875
+ throwIfAborted(signal);
876
+ }
877
+
878
+ private async claimPhysicalConnection(
879
+ ws: ServerWebSocket<WsConnectionData>,
880
+ sid: string,
881
+ signal: AbortSignal = ws.data.lifetimeController.signal,
882
+ ): Promise<void> {
883
+ const claimed = await this.sessionManager.claimActiveConnection(sid, ws.data.connId);
884
+ if (!claimed) {
885
+ throw new SessionOwnershipError();
886
+ }
887
+ if (this.closed || ws.data.closed || signal.aborted) {
888
+ await this.sessionManager.releaseActiveConnection(sid, ws.data.connId, ws.data.rooms);
889
+ throw new SessionOwnershipError();
890
+ }
891
+ const previous = this.connectionsBySid.get(sid);
892
+ this.connectionsBySid.set(sid, ws);
893
+ if (previous && previous !== ws) {
894
+ previous.data.messageProcessingStopped = true;
895
+ previous.data.lifetimeController.abort(
896
+ new WebSocketOperationAbortedError("WebSocket session was replaced."),
897
+ );
898
+ this.messageControllers.get(previous.data.connId)?.abort(
899
+ new WebSocketOperationAbortedError("WebSocket session was replaced."),
900
+ );
901
+ this.closeConnection(previous, 4009, "session replaced");
902
+ }
903
+ }
904
+
905
+ private async handleReconnectMessage(
906
+ ws: ServerWebSocket<WsConnectionData>,
907
+ packet: ClientPacket,
908
+ registered: RegisteredNamespace,
909
+ signal: AbortSignal,
910
+ ): Promise<void> {
911
+ const targetSid = parseReconnectSid(packet);
912
+ if (!targetSid || targetSid.length > this.limits.maxSessionIdLength) {
913
+ this.sendError(ws, "Invalid reconnect session id", packet.id);
914
+ return;
915
+ }
916
+ const session = await this.sessionManager.resolveSession(targetSid, signal);
917
+ throwIfAborted(signal);
918
+ if (!session || session.namespace !== registered.namespace) {
919
+ this.sendError(ws, "Session not found or expired", packet.id);
920
+ return;
921
+ }
922
+ if (session.context.user !== undefined && !isValidSocketUser(session.context.user)) {
923
+ this.sendError(ws, "Unauthorized reconnect", packet.id);
924
+ return;
925
+ }
926
+ if (session.replayDelivery === "client-ack" && ws.data.replayDelivery !== "client-ack") {
927
+ this.sendError(ws, "Client replay acknowledgement required", packet.id);
928
+ return;
929
+ }
930
+
931
+ const data = packet.data as ReconnectPacketData | undefined;
932
+ let effectiveUser = ws.data.principal;
933
+ let reauthenticatedUser: SocketUser | undefined;
934
+ if (this.options.authenticator && data?.token) {
935
+ const user = await this.options.authenticator(data.token, {
936
+ signal,
937
+ namespace: registered.namespace,
938
+ path: registered.path,
939
+ });
940
+ throwIfAborted(signal);
941
+ if (user !== null && !isValidSocketUser(user)) {
942
+ this.sendError(ws, "Unauthorized reconnect", packet.id);
943
+ return;
944
+ }
945
+ if (user !== null) {
946
+ effectiveUser = user;
947
+ reauthenticatedUser = user;
948
+ } else {
949
+ // An explicitly supplied but rejected credential cannot silently fall
950
+ // back to a possibly stale connection principal.
951
+ effectiveUser = undefined;
952
+ }
953
+ }
954
+
955
+ // Session ownership is an invariant of the stored session, not of the
956
+ // optional top-level authenticator. Gateway middleware may establish the
957
+ // principal instead, and a socket that knows another user's SID must never
958
+ // be allowed to rebind to it. A token above is only an additional reauth
959
+ // mechanism which updates the effective current principal.
960
+ if (session.context.user?.id && effectiveUser?.id !== session.context.user.id) {
961
+ this.sendError(ws, "Unauthorized reconnect", packet.id);
962
+ return;
963
+ }
964
+ if (reauthenticatedUser) {
965
+ ws.data.principal = cloneSocketUser(reauthenticatedUser);
966
+ ws.data.context.user = cloneSocketUser(reauthenticatedUser);
967
+ session.context.user = cloneSocketUser(reauthenticatedUser);
968
+ this.sessionManager.updateContext(session.sid, session.context);
969
+ } else if (!session.context.user && effectiveUser) {
970
+ // An authenticated physical connection adopts an anonymous session so
971
+ // subsequent reconnects cannot silently downgrade it back to bearer-only.
972
+ session.context.user = cloneSocketUser(effectiveUser);
973
+ this.sessionManager.updateContext(session.sid, session.context);
974
+ }
975
+
976
+ const oldSid = ws.data.sid;
977
+ if (oldSid !== session.sid) {
978
+ if (this.connectionsBySid.get(oldSid) === ws) {
979
+ this.connectionsBySid.delete(oldSid);
980
+ }
981
+ await this.sessionManager.releaseActiveConnection(oldSid, ws.data.connId, ws.data.rooms);
982
+ throwIfAborted(signal);
983
+ this.forgetRateLimitKeys(oldSid);
984
+ detachRoomSubscriptions(ws, this.topics);
985
+ }
986
+
987
+ applySessionToConnection(ws, session.sid, session.context, session.rooms);
988
+ try {
989
+ await this.claimPhysicalConnection(ws, session.sid, signal);
990
+ throwIfAborted(signal);
991
+ } catch (error) {
992
+ this.closeConnection(ws,
993
+ error instanceof SessionOwnershipError ? 4009 : 1011,
994
+ error instanceof SessionOwnershipError ? "session already active" : "session persistence failed",
995
+ );
996
+ return;
997
+ }
998
+ restoreRoomSubscriptions(ws, this.topics);
999
+ if (ws.data.replayDelivery === "client-ack") {
1000
+ this.sessionManager.enableReplayAcknowledgements(session.sid);
1001
+ await this.sessionManager.flushSession(session.sid);
1002
+ throwIfAborted(signal);
1003
+ }
1004
+ if (!await this.sendReconnect(ws, registered, signal)) return;
1005
+
1006
+ if (hasHandleConnection(registered.gatewayInstance)) {
1007
+ await registered.gatewayInstance.handleConnection(wrapSocket(ws, this.topics, this.wrapOptions(ws, signal)));
1008
+ throwIfAborted(signal);
1009
+ await this.sessionManager.flushSession(session.sid);
1010
+ }
1011
+ }
1012
+
1013
+ private enqueueMessage(ws: ServerWebSocket<WsConnectionData>, raw: string | Buffer): void {
1014
+ if (this.closed || ws.data.closed || ws.data.messageProcessingStopped) {
1015
+ return;
1016
+ }
1017
+ const registered = this.resolveNamespace(ws);
1018
+ if (!registered || payloadBytes(raw) > registered.maxPayloadBytes) {
1019
+ this.closeConnection(ws, 1009, "payload too large");
1020
+ return;
1021
+ }
1022
+ const ingressKey = `ingress:${ws.data.connId}`;
1023
+ if (!this.ingressRateLimiter.check(
1024
+ ingressKey,
1025
+ this.limits.maxIngressPacketsPerWindow,
1026
+ this.limits.ingressWindowMs,
1027
+ )) {
1028
+ this.sendError(ws, "Too many messages");
1029
+ return;
1030
+ }
1031
+
1032
+ let queue = this.messageQueues.get(ws.data.connId);
1033
+ if (!queue) {
1034
+ queue = { tail: this.openOperations.get(ws.data.connId) ?? Promise.resolve(), pending: 0 };
1035
+ this.messageQueues.set(ws.data.connId, queue);
1036
+ }
1037
+ if (queue.pending >= this.limits.maxPendingMessagesPerConnection) {
1038
+ this.sendError(ws, "Too many pending messages");
1039
+ this.closeConnection(ws, 1013, "message queue exceeded");
1040
+ return;
1041
+ }
1042
+
1043
+ queue.pending += 1;
1044
+ const currentQueue = queue;
1045
+ const task = queue.tail.then(async () => {
1046
+ if (ws.data.closed || ws.data.messageProcessingStopped || this.closed) {
1047
+ return;
1048
+ }
1049
+ if (this.activeMessageHandlers >= this.limits.maxConcurrentMessageHandlers) {
1050
+ this.sendError(ws, "WebSocket service busy");
1051
+ return;
1052
+ }
1053
+ this.activeMessageHandlers += 1;
1054
+ const messageController = new AbortController();
1055
+ this.messageControllers.set(ws.data.connId, messageController);
1056
+ const work = this.observeWork(() => this.handleMessage(ws, raw, messageController.signal), () => {
1057
+ if (this.messageControllers.get(ws.data.connId) === messageController) {
1058
+ this.messageControllers.delete(ws.data.connId);
1059
+ }
1060
+ this.activeMessageHandlers -= 1;
1061
+ }, ws.data.connId);
1062
+ try {
1063
+ await runAbortable(
1064
+ () => work,
1065
+ messageController,
1066
+ this.limits.messageHandlingTimeoutMs,
1067
+ "WebSocket message processing timed out.",
1068
+ );
1069
+ } catch (error) {
1070
+ if (error instanceof WebSocketOperationTimeoutError) {
1071
+ ws.data.messageProcessingStopped = true;
1072
+ this.sendError(ws, "Message processing timed out");
1073
+ this.closeConnection(ws, 1011, "message processing timed out");
1074
+ return;
1075
+ }
1076
+ if (messageController.signal.aborted && (this.closed || ws.data.closed)) {
1077
+ return;
1078
+ }
1079
+ ws.data.messageProcessingStopped = true;
1080
+ this.sendError(ws, "Message processing failed");
1081
+ this.closeConnection(ws, 1011, "message processing failed");
1082
+ }
1083
+ }).finally(() => {
1084
+ currentQueue.pending = Math.max(0, currentQueue.pending - 1);
1085
+ if (currentQueue.pending === 0 && this.messageQueues.get(ws.data.connId) === currentQueue) {
1086
+ this.messageQueues.delete(ws.data.connId);
1087
+ }
1088
+ });
1089
+ currentQueue.tail = task;
1090
+ this.trackOperation(task);
1091
+ }
1092
+
1093
+ private async handleMessage(
1094
+ ws: ServerWebSocket<WsConnectionData>,
1095
+ raw: string | Buffer,
1096
+ signal: AbortSignal,
1097
+ ): Promise<void> {
1098
+ throwIfAborted(signal);
1099
+ const registered = this.resolveNamespace(ws);
1100
+ if (
1101
+ !registered
1102
+ || !ws.data.opened
1103
+ || this.connectionsBySid.get(ws.data.sid) !== ws
1104
+ ) {
1105
+ this.closeConnection(ws, 4009, "stale connection");
1106
+ return;
1107
+ }
1108
+
1109
+ if (!this.sessionManager.touchSession(ws.data.sid, ws.data.connId)) {
1110
+ this.closeConnection(ws, 4009, "session lease expired");
1111
+ return;
1112
+ }
1113
+ await this.sessionManager.flushSession(ws.data.sid);
1114
+ throwIfAborted(signal);
1115
+
1116
+ let packet: ClientPacket;
1117
+ try {
1118
+ packet = this.codec.decodeClient(raw);
1119
+ } catch (error) {
1120
+ this.sendError(ws, error instanceof PacketCodecError ? "Invalid packet" : "Packet decoding failed");
1121
+ return;
1122
+ }
1123
+
1124
+ if (packet.id !== undefined && (typeof packet.id !== "string" || packet.id.length > this.limits.maxPacketIdLength)) {
1125
+ this.sendError(ws, "Packet id is too long");
1126
+ return;
1127
+ }
1128
+ if (packet.namespace !== undefined && packet.namespace !== registered.namespace) {
1129
+ this.sendError(ws, "Namespace mismatch", packet.id);
1130
+ return;
1131
+ }
1132
+
1133
+ if (packet.type === "ping" || packet.type === "reconnect" || packet.type === "replay-ack") {
1134
+ const controlKey = `control:${ws.data.connId}`;
1135
+ if (!this.controlRateLimiter.check(
1136
+ controlKey,
1137
+ this.limits.maxControlFramesPerWindow,
1138
+ this.limits.controlFrameWindowMs,
1139
+ )) {
1140
+ this.sendError(ws, "Too many control frames", packet.id);
1141
+ return;
1142
+ }
1143
+ }
1144
+
1145
+ if (packet.type === "ping") {
1146
+ this.encodeSend(ws, { type: "pong" });
1147
+ return;
1148
+ }
1149
+ if (packet.type === "reconnect") {
1150
+ await this.handleReconnectMessage(ws, packet, registered, signal);
1151
+ return;
1152
+ }
1153
+ if (packet.type === "replay-ack") {
1154
+ const ids = (packet.data as { deliveryIds?: unknown } | null)?.deliveryIds;
1155
+ if (ws.data.replayDelivery !== "client-ack" || !Array.isArray(ids)
1156
+ || ids.length === 0 || ids.length > this.limits.maxOutboundQueuePerSession
1157
+ || ids.some((id) => typeof id !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(id)
1158
+ || !(ws.data.sentReplayIds?.has(id) || ws.data.sentReliableIds?.has(id) || ws.data.recentReliableAcks?.has(id)))) {
1159
+ this.sendError(ws, "Invalid replay acknowledgement", packet.id);
1160
+ return;
1161
+ }
1162
+ const reliableIds = ids.filter((id) => ws.data.sentReliableIds?.has(id) || ws.data.recentReliableAcks?.has(id));
1163
+ const remoteCount = reliableIds.length && this.adapter.reliableRooms
1164
+ ? await this.adapter.reliableRooms.acknowledge(this.deliveryOwner(ws), reliableIds) : 0;
1165
+ throwIfAborted(signal);
1166
+ const acknowledged = remoteCount + this.sessionManager.acknowledgeDeliveries(ws.data.sid, ids);
1167
+ await this.sessionManager.flushSession(ws.data.sid);
1168
+ throwIfAborted(signal);
1169
+ for (const id of reliableIds) {
1170
+ ws.data.sentReliableIds?.delete(id);
1171
+ (ws.data.recentReliableAcks ??= new Set()).add(id);
1172
+ }
1173
+ while ((ws.data.recentReliableAcks?.size ?? 0) > this.limits.maxOutboundQueuePerSession) {
1174
+ ws.data.recentReliableAcks!.delete(ws.data.recentReliableAcks!.values().next().value!);
1175
+ }
1176
+ this.diagnose("reliable-acked", acknowledged);
1177
+ this.queueDeliveries([ws.data.sid]);
1178
+ if (packet.id) this.encodeSend(ws, { type: "ack", id: packet.id, data: { acknowledged } });
1179
+ return;
1180
+ }
1181
+ if (
1182
+ packet.type !== "event"
1183
+ || typeof packet.event !== "string"
1184
+ || packet.event.length === 0
1185
+ || packet.event.length > this.limits.maxEventNameLength
1186
+ ) {
1187
+ this.sendError(ws, "Invalid event name", packet.id);
1188
+ return;
1189
+ }
1190
+
1191
+ const handler = registered.handlers.get(packet.event);
1192
+ if (!handler) {
1193
+ this.sendError(ws, "Unknown event", packet.id);
1194
+ return;
1195
+ }
1196
+
1197
+ const socket = wrapSocket(ws, this.topics, this.wrapOptions(ws, signal));
1198
+ if (handler.rateLimit) {
1199
+ let customKey: string | undefined;
1200
+ try {
1201
+ customKey = handler.rateLimit.key?.(socket);
1202
+ } catch {
1203
+ this.sendError(ws, "Rate limit policy failed", packet.id);
1204
+ return;
1205
+ }
1206
+ const key = customKey ?? `${socket.id}:${packet.event}`;
1207
+ if (customKey === undefined) {
1208
+ this.trackRateLimitKey(socket.id, key);
1209
+ }
1210
+ if (!this.handlerRateLimiter.check(key, handler.rateLimit.limit, handler.rateLimit.windowMs)) {
1211
+ this.sendError(ws, "Too many requests", packet.id);
1212
+ return;
1213
+ }
1214
+ }
1215
+
1216
+ let reply: string | Uint8Array | undefined;
1217
+ try {
1218
+ await dispatchWsHandler(handler, socket, packet, (payload) => {
1219
+ if (signal.aborted || this.closed || ws.data.closed || ws.data.messageProcessingStopped) {
1220
+ return;
1221
+ }
1222
+ if (payloadBytes(payload) > outboundLimit(registered)) {
1223
+ this.closeForOutbound(ws, 1009, "outbound payload too large");
1224
+ return;
1225
+ }
1226
+ reply = payload;
1227
+ }, signal, this.codec);
1228
+ // Acknowledging join/context before persistence lets another node observe
1229
+ // the successful reply while the corresponding room membership is absent.
1230
+ await this.sessionManager.flushSession(ws.data.sid);
1231
+ throwIfAborted(signal);
1232
+ if (reply !== undefined && !trySendPayload(ws, reply)) this.closeForOutbound(ws, 1011, "outbound send failed");
1233
+ } catch (error) {
1234
+ throwIfAborted(signal);
1235
+ const rawMessage = error instanceof WsDispatchError && error.cause instanceof Error
1236
+ ? error.cause.message
1237
+ : error instanceof Error
1238
+ ? error.message
1239
+ : undefined;
1240
+ const safeMessage = error instanceof WsDispatchError ? error.message : "Handler failed";
1241
+ const cause = error instanceof WsDispatchError ? error.cause : error;
1242
+ if (cause instanceof WebSocketDeliveryError) this.sendError(ws, cause.message, packet.id, cause.code);
1243
+ else this.sendError(ws, this.options.exposeHandlerErrors === true && rawMessage ? rawMessage : safeMessage, packet.id);
1244
+ }
1245
+ throwIfAborted(signal);
1246
+ }
1247
+
1248
+ private async handleCloseSafely(ws: ServerWebSocket<WsConnectionData>, reason: string): Promise<void> {
1249
+ try {
1250
+ await this.handleClose(ws, reason);
1251
+ } catch {
1252
+ // Close callbacks cannot report to the peer; shutdown still awaits all
1253
+ // remaining adapter/session operations.
1254
+ }
1255
+ }
1256
+
1257
+ private async handleClose(ws: ServerWebSocket<WsConnectionData>, reason: string): Promise<void> {
1258
+ const closeTimer = this.closeTimers.get(ws.data.connId);
1259
+ if (closeTimer !== undefined) clearTimeout(closeTimer);
1260
+ this.closeTimers.delete(ws.data.connId);
1261
+ if (ws.data.closed) {
1262
+ return;
1263
+ }
1264
+ ws.data.closed = true;
1265
+ ws.data.lifetimeController.abort(
1266
+ new WebSocketOperationAbortedError("WebSocket connection closed."),
1267
+ );
1268
+ this.messageControllers.get(ws.data.connId)?.abort(
1269
+ new WebSocketOperationAbortedError("WebSocket connection closed."),
1270
+ );
1271
+ if (ws.data.opened) {
1272
+ this.activeConnections = Math.max(0, this.activeConnections - 1);
1273
+ }
1274
+ this.ingressRateLimiter.forget(`ingress:${ws.data.connId}`);
1275
+ this.controlRateLimiter.forget(`control:${ws.data.connId}`);
1276
+ this.messageQueues.delete(ws.data.connId);
1277
+
1278
+ const ownsSession = this.connectionsBySid.get(ws.data.sid) === ws;
1279
+ if (ownsSession) {
1280
+ this.connectionsBySid.delete(ws.data.sid);
1281
+ await this.sessionManager.releaseActiveConnection(ws.data.sid, ws.data.connId, ws.data.rooms);
1282
+ this.forgetRateLimitKeys(ws.data.sid);
1283
+ }
1284
+
1285
+ for (const room of ws.data.rooms) {
1286
+ this.topics.delete(ws.data.namespace, room);
1287
+ }
1288
+ const registered = this.resolveNamespace(ws);
1289
+ await this.connectionWork.get(ws.data.connId);
1290
+ if (registered && ws.data.opened && hasHandleDisconnect(registered.gatewayInstance)) {
1291
+ await registered.gatewayInstance.handleDisconnect(
1292
+ wrapSocket(ws, this.topics, this.wrapOptions(ws)),
1293
+ reason,
1294
+ );
1295
+ }
1296
+ }
1297
+
1298
+ private trackRateLimitKey(sid: string, key: string): void {
1299
+ let keys = this.rateLimitKeysBySid.get(sid);
1300
+ if (!keys) {
1301
+ keys = new Set<string>();
1302
+ this.rateLimitKeysBySid.set(sid, keys);
1303
+ }
1304
+ keys.add(key);
1305
+ }
1306
+
1307
+ private forgetRateLimitKeys(sid: string): void {
1308
+ const keys = this.rateLimitKeysBySid.get(sid);
1309
+ if (!keys) {
1310
+ return;
1311
+ }
1312
+ this.handlerRateLimiter.forgetMany(keys);
1313
+ this.rateLimitKeysBySid.delete(sid);
1314
+ }
1315
+
1316
+ private trackOperation(operation: Promise<void>): void {
1317
+ const guarded = operation.catch(() => {});
1318
+ const pending = this.pendingOperations;
1319
+ pending.add(guarded);
1320
+ void guarded.finally(() => pending.delete(guarded));
1321
+ }
1322
+
1323
+ /** Tracks actual settlement separately from the caller's abortable wait. */
1324
+ private observeWork<T>(operation: () => Promise<T>, onSettled?: () => void, connId?: string): Promise<T> {
1325
+ const work = Promise.resolve().then(operation);
1326
+ const release = (): void => {
1327
+ this.pendingWork.delete(completion);
1328
+ if (connId !== undefined && this.connectionWork.get(connId) === completion) this.connectionWork.delete(connId);
1329
+ onSettled?.();
1330
+ };
1331
+ const completion = work.then(release, release);
1332
+ this.pendingWork.add(completion);
1333
+ if (connId !== undefined) this.connectionWork.set(connId, completion);
1334
+ return work;
1335
+ }
1336
+
1337
+ private trackAdapterOperation(operation: Promise<void>): void {
1338
+ const guarded = operation.catch((error: unknown) => {
1339
+ this.diagnose("adapter-error");
1340
+ this.adapterOperationError ??= error;
1341
+ });
1342
+ const pending = this.pendingAdapterOperations;
1343
+ pending.add(guarded);
1344
+ void guarded.finally(() => pending.delete(guarded));
1345
+ }
1346
+
1347
+ public async getHealth(): Promise<{ ready: boolean; reason?: "stopped" | "store" }> {
1348
+ if (!this.initialized || this.closed) return { ready: false, reason: "stopped" };
1349
+ try { await this.adapter.healthCheck?.(); return { ready: true }; }
1350
+ catch { this.diagnose("adapter-error"); return { ready: false, reason: "store" }; }
1351
+ }
1352
+
1353
+ private diagnose(type: WebSocketDiagnostic["type"], count = 1): void {
1354
+ this.deliveryCounters[type] += count;
1355
+ try { this.options.onDiagnostic?.({ type, count, timestamp: Date.now() }); }
1356
+ catch { /* Observers cannot alter delivery or resource ownership. */ }
1357
+ }
1358
+
1359
+ private deliveryOwner(ws: ServerWebSocket<WsConnectionData>): ReliableSessionOwner {
1360
+ return { sid: ws.data.sid, connId: ws.data.connId, ownerInstanceId: this.adapter.instanceId,
1361
+ ...(ws.data.sentReliableIds ? { issuedIds: [...ws.data.sentReliableIds] } : {}) };
1362
+ }
1363
+
1364
+ private queueDeliveries(sids: Iterable<string>): void {
1365
+ if (this.closed || !this.adapter.reliableRooms) return;
1366
+ for (const sid of sids) if (this.connectionsBySid.has(sid)) this.deliveryCandidates.add(sid);
1367
+ if (this.deliveryInFlight || !this.deliveryCandidates.size) return;
1368
+ this.deliveryInFlight = true;
1369
+ this.trackOperation(this.deliverCandidates().catch(() => {
1370
+ this.deliveryCandidates.clear();
1371
+ this.diagnose("poll-error");
1372
+ }).finally(() => { this.deliveryInFlight = false; }));
1373
+ }
1374
+
1375
+ private async deliverCandidates(): Promise<void> {
1376
+ while (!this.closed && this.deliveryCandidates.size) {
1377
+ const sockets: ServerWebSocket<WsConnectionData>[] = [];
1378
+ for (const sid of this.deliveryCandidates) {
1379
+ this.deliveryCandidates.delete(sid);
1380
+ const ws = this.connectionsBySid.get(sid);
1381
+ if (ws && ws.data.opened && !ws.data.closed && !ws.data.messageProcessingStopped
1382
+ && !ws.data.replaying && !this.openOperations.has(ws.data.connId) && ws.data.replayDelivery === "client-ack") sockets.push(ws);
1383
+ if (sockets.length >= 128) break;
1384
+ }
1385
+ if (!sockets.length) continue;
1386
+ const owners = sockets.map((ws) => this.deliveryOwner(ws));
1387
+ const batches = await this.adapter.reliableRooms!.readPending(owners);
1388
+ for (const batch of batches) {
1389
+ // Empty mailboxes need no local session lookup: getSession also sweeps
1390
+ // expiry. Calling it for every idle owner makes each poll quadratic.
1391
+ if (!batch.packets.length) continue;
1392
+ const index = owners.findIndex((owner) => owner.sid === batch.sid);
1393
+ const ws = sockets[index];
1394
+ if (!ws || this.closed || ws.data.closed || ws.data.messageProcessingStopped || ws.data.replaying
1395
+ || ws.data.sid !== batch.sid || this.connectionsBySid.get(batch.sid) !== ws) continue;
1396
+ const state = this.sessionManager.getSession(batch.sid);
1397
+ if (!state || state.activeConnId !== ws.data.connId || (state.activeLeaseExpiresAt ?? 0) <= Date.now()) continue;
1398
+ const issued = ws.data.sentReliableIds ??= new Set();
1399
+ for (const packet of batch.packets) {
1400
+ if (!packet.deliveryId || issued.has(packet.deliveryId) || ws.data.recentReliableAcks?.has(packet.deliveryId)) continue;
1401
+ if (issued.size >= this.limits.maxOutboundQueuePerSession || (ws.getBufferedAmount?.() ?? 0) > 0) break;
1402
+ if (!this.encodeSend(ws, packet)) break;
1403
+ issued.add(packet.deliveryId);
1404
+ }
1405
+ }
1406
+ }
1407
+ }
1408
+
1409
+ private async awaitPendingOperations(): Promise<void> {
1410
+ while (this.pendingOperations.size > 0) {
1411
+ await Promise.all([...this.pendingOperations]);
1412
+ }
1413
+ }
1414
+
1415
+ private async awaitPendingAdapterOperations(): Promise<void> {
1416
+ while (this.pendingAdapterOperations.size > 0) {
1417
+ await Promise.all([...this.pendingAdapterOperations]);
1418
+ }
1419
+ }
1420
+
1421
+ private async shutdownInitializedGateways(): Promise<void> {
1422
+ const gateways = this.initializedGateways.splice(0).reverse();
1423
+ const errors: unknown[] = [];
1424
+ for (const gateway of gateways) {
1425
+ if (hasOnGatewayShutdown(gateway)) {
1426
+ try {
1427
+ await gateway.onGatewayShutdown();
1428
+ } catch (error) {
1429
+ errors.push(error);
1430
+ }
1431
+ }
1432
+ }
1433
+ if (errors.length > 0) {
1434
+ throw errors[0];
1435
+ }
1436
+ }
1437
+
1438
+ private abortMessageOperations(message: string): void {
1439
+ for (const controller of this.messageControllers.values()) {
1440
+ controller.abort(new WebSocketOperationAbortedError(message));
1441
+ }
1442
+ }
1443
+
1444
+
1445
+ }
1446
+
1447
+ export type { WsConnectionData };
1448
+
1449
+ function validateAuthenticatedOrigins(options: WebSocketServerOptions): void {
1450
+ if (options.requireAuth === true && !options.authenticator) {
1451
+ throw new Error("WebSocket requireAuth requires an authenticator.");
1452
+ }
1453
+ if (!options.authenticator) {
1454
+ return;
1455
+ }
1456
+ for (const namespace of options.namespaces) {
1457
+ const origins = namespace.corsOrigins.length > 0
1458
+ ? namespace.corsOrigins
1459
+ : (options.defaultCorsOrigins ?? ["*"]);
1460
+ if (origins.length === 0 || origins.includes("*")) {
1461
+ throw new Error(
1462
+ `WebSocket namespace "${namespace.namespace}" uses authentication and requires explicit CORS origins.`,
1463
+ );
1464
+ }
1465
+ }
1466
+ }
1467
+
1468
+ function outboundLimit(namespace: RegisteredNamespace): number {
1469
+ return namespace.maxOutboundPayloadBytes ?? namespace.maxPayloadBytes;
1470
+ }
1471
+
1472
+ function validateNamespace(namespace: RegisteredNamespace): void {
1473
+ if (!Number.isFinite(outboundLimit(namespace)) || outboundLimit(namespace) <= 0) {
1474
+ throw new Error(`WebSocket namespace "${namespace.namespace}" requires a positive maxOutboundPayloadBytes.`);
1475
+ }
1476
+ if (!namespace.namespace.startsWith("/") || namespace.namespace.includes("\0")) {
1477
+ throw new Error(`Invalid WebSocket namespace "${namespace.namespace}".`);
1478
+ }
1479
+ if (!namespace.path.startsWith("/") || namespace.path.includes("\0") || namespace.path.includes("?") || namespace.path.includes("#")) {
1480
+ throw new Error(`Invalid WebSocket path "${namespace.path}".`);
1481
+ }
1482
+ if (!Number.isFinite(namespace.maxPayloadBytes) || namespace.maxPayloadBytes <= 0) {
1483
+ throw new Error(`WebSocket namespace "${namespace.namespace}" requires a positive maxPayloadBytes.`);
1484
+ }
1485
+ if (!Number.isFinite(namespace.sessionTtlMs) || namespace.sessionTtlMs <= 0) {
1486
+ throw new Error(`WebSocket namespace "${namespace.namespace}" requires a positive sessionTtlMs.`);
1487
+ }
1488
+ }
1489
+
1490
+ function normalizeMemoryLimits(limits: WebSocketMemoryLimits | undefined): Required<WebSocketMemoryLimits> {
1491
+ return {
1492
+ maxRateLimitBuckets: positiveLimit(limits?.maxRateLimitBuckets, 10_000, "maxRateLimitBuckets"),
1493
+ maxTopics: positiveLimit(limits?.maxTopics, 10_000, "maxTopics"),
1494
+ maxSessions: positiveLimit(limits?.maxSessions, 10_000, "maxSessions"),
1495
+ maxRoomsPerSession: positiveLimit(limits?.maxRoomsPerSession, 256, "maxRoomsPerSession"),
1496
+ maxRoomNameLength: positiveLimit(limits?.maxRoomNameLength, 256, "maxRoomNameLength"),
1497
+ maxOutboundQueuePerSession: positiveLimit(limits?.maxOutboundQueuePerSession, 100, "maxOutboundQueuePerSession"),
1498
+ maxOutboundQueueBytesPerSession: positiveLimit(
1499
+ limits?.maxOutboundQueueBytesPerSession,
1500
+ 1024 * 1024,
1501
+ "maxOutboundQueueBytesPerSession",
1502
+ ),
1503
+ maxOfflineBroadcastRecipients: positiveLimit(
1504
+ limits?.maxOfflineBroadcastRecipients,
1505
+ 1_000,
1506
+ "maxOfflineBroadcastRecipients",
1507
+ ),
1508
+ maxOfflineBroadcastBytes: positiveLimit(
1509
+ limits?.maxOfflineBroadcastBytes,
1510
+ 1024 * 1024,
1511
+ "maxOfflineBroadcastBytes",
1512
+ ),
1513
+ maxConcurrentHandshakes: positiveLimit(limits?.maxConcurrentHandshakes, 64, "maxConcurrentHandshakes"),
1514
+ handshakeTimeoutMs: nonNegativeLimit(limits?.handshakeTimeoutMs, 10_000, "handshakeTimeoutMs"),
1515
+ maxHandshakeAttemptsPerWindow: positiveLimit(
1516
+ limits?.maxHandshakeAttemptsPerWindow,
1517
+ 60,
1518
+ "maxHandshakeAttemptsPerWindow",
1519
+ ),
1520
+ handshakeWindowMs: positiveLimit(limits?.handshakeWindowMs, 60_000, "handshakeWindowMs"),
1521
+ maxIngressPacketsPerWindow: positiveLimit(
1522
+ limits?.maxIngressPacketsPerWindow,
1523
+ 120,
1524
+ "maxIngressPacketsPerWindow",
1525
+ ),
1526
+ ingressWindowMs: positiveLimit(limits?.ingressWindowMs, 10_000, "ingressWindowMs"),
1527
+ maxControlFramesPerWindow: positiveLimit(
1528
+ limits?.maxControlFramesPerWindow,
1529
+ 30,
1530
+ "maxControlFramesPerWindow",
1531
+ ),
1532
+ controlFrameWindowMs: positiveLimit(limits?.controlFrameWindowMs, 10_000, "controlFrameWindowMs"),
1533
+ maxPendingMessagesPerConnection: positiveLimit(
1534
+ limits?.maxPendingMessagesPerConnection,
1535
+ 32,
1536
+ "maxPendingMessagesPerConnection",
1537
+ ),
1538
+ maxConcurrentMessageHandlers: positiveLimit(
1539
+ limits?.maxConcurrentMessageHandlers,
1540
+ 256,
1541
+ "maxConcurrentMessageHandlers",
1542
+ ),
1543
+ messageHandlingTimeoutMs: nonNegativeLimit(
1544
+ limits?.messageHandlingTimeoutMs,
1545
+ 30_000,
1546
+ "messageHandlingTimeoutMs",
1547
+ ),
1548
+ shutdownDrainTimeoutMs: nonNegativeLimit(
1549
+ limits?.shutdownDrainTimeoutMs,
1550
+ 5_000,
1551
+ "shutdownDrainTimeoutMs",
1552
+ ),
1553
+ activeSessionLeaseMs: positiveLimit(limits?.activeSessionLeaseMs, 30_000, "activeSessionLeaseMs"),
1554
+ leaseRenewIntervalMs: positiveLimit(limits?.leaseRenewIntervalMs, 10_000, "leaseRenewIntervalMs"),
1555
+ maxSessionIdLength: positiveLimit(limits?.maxSessionIdLength, 128, "maxSessionIdLength"),
1556
+ maxEventNameLength: positiveLimit(limits?.maxEventNameLength, 128, "maxEventNameLength"),
1557
+ maxPacketIdLength: positiveLimit(limits?.maxPacketIdLength, 128, "maxPacketIdLength"),
1558
+ maxBackpressureBytes: positiveLimit(limits?.maxBackpressureBytes, 1024 * 1024, "maxBackpressureBytes"),
1559
+ socketCloseTimeoutMs: positiveLimit(limits?.socketCloseTimeoutMs, 1000, "socketCloseTimeoutMs"),
1560
+ reliablePollIntervalMs: positiveLimit(limits?.reliablePollIntervalMs, 1000, "reliablePollIntervalMs"),
1561
+ };
1562
+ }
1563
+
1564
+ function positiveLimit(value: number | undefined, fallback: number, field: string): number {
1565
+ if (value === undefined) {
1566
+ return fallback;
1567
+ }
1568
+ if (!Number.isFinite(value) || value < 1) {
1569
+ throw new Error(`WebSocket limit "${field}" must be a positive number.`);
1570
+ }
1571
+ return Math.floor(value);
1572
+ }
1573
+
1574
+ function cloneSocketUser(user: SocketUser): SocketUser {
1575
+ return { ...user };
1576
+ }
1577
+
1578
+ function nonNegativeLimit(value: number | undefined, fallback: number, field: string): number {
1579
+ if (value === undefined) {
1580
+ return fallback;
1581
+ }
1582
+ if (!Number.isFinite(value) || value < 0) {
1583
+ throw new Error(`WebSocket limit "${field}" must be a non-negative number.`);
1584
+ }
1585
+ return value === 0 ? 0 : Math.max(1, Math.floor(value));
1586
+ }
1587
+
1588
+ function payloadBytes(payload: string | Uint8Array | Buffer): number {
1589
+ return typeof payload === "string" ? Buffer.byteLength(payload) : payload.byteLength;
1590
+ }
1591
+
1592
+ function requestAddress(server: Server<WsConnectionData>, request: Request): string {
1593
+ try {
1594
+ return server.requestIP(request)?.address ?? "unknown";
1595
+ } catch {
1596
+ return "unknown";
1597
+ }
1598
+ }
1599
+
1600
+ class WebSocketOperationTimeoutError extends Error {
1601
+ public constructor(message: string) {
1602
+ super(message);
1603
+ this.name = "WebSocketOperationTimeoutError";
1604
+ }
1605
+ }
1606
+
1607
+ class WebSocketOperationAbortedError extends Error {
1608
+ public constructor(message: string) {
1609
+ super(message);
1610
+ this.name = "WebSocketOperationAbortedError";
1611
+ }
1612
+ }
1613
+
1614
+ /**
1615
+ * Observes the underlying promise even after the deadline wins. This prevents
1616
+ * late middleware/validator/handler rejection from becoming unhandled while
1617
+ * keeping the caller's wait bounded. The owner separately tracks actual work
1618
+ * settlement for admission and resource cleanup.
1619
+ */
1620
+ function runAbortable<T>(
1621
+ operation: () => Promise<T>,
1622
+ controller: AbortController,
1623
+ timeoutMs: number,
1624
+ timeoutMessage: string,
1625
+ ): Promise<T> {
1626
+ const observed = Promise.resolve().then(operation);
1627
+ return new Promise<T>((resolve, reject) => {
1628
+ let settled = false;
1629
+ let timer: ReturnType<typeof setTimeout> | undefined;
1630
+ const finish = (callback: () => void): void => {
1631
+ if (settled) {
1632
+ return;
1633
+ }
1634
+ settled = true;
1635
+ if (timer !== undefined) {
1636
+ clearTimeout(timer);
1637
+ }
1638
+ controller.signal.removeEventListener("abort", onAbort);
1639
+ callback();
1640
+ };
1641
+ const onAbort = (): void => {
1642
+ const reason = controller.signal.reason;
1643
+ finish(() => reject(reason instanceof Error ? reason : new WebSocketOperationAbortedError("Operation aborted.")));
1644
+ };
1645
+
1646
+ controller.signal.addEventListener("abort", onAbort, { once: true });
1647
+ if (controller.signal.aborted) {
1648
+ onAbort();
1649
+ }
1650
+ if (timeoutMs > 0 && !settled) {
1651
+ timer = setTimeout(() => {
1652
+ const error = new WebSocketOperationTimeoutError(timeoutMessage);
1653
+ controller.abort(error);
1654
+ }, timeoutMs);
1655
+ }
1656
+ observed.then(
1657
+ (value) => finish(() => resolve(value)),
1658
+ (error: unknown) => finish(() => reject(error)),
1659
+ );
1660
+ });
1661
+ }
1662
+
1663
+ /** Returns false on deadline without abandoning rejection observation. */
1664
+ async function settleBeforeDeadline(operation: () => Promise<void>, deadline: number | null): Promise<boolean> {
1665
+ const observed = Promise.resolve().then(operation);
1666
+ if (deadline === null) {
1667
+ await observed;
1668
+ return true;
1669
+ }
1670
+ const remaining = deadline - Date.now();
1671
+ if (remaining <= 0) {
1672
+ void observed.catch(() => {});
1673
+ return false;
1674
+ }
1675
+ return await new Promise<boolean>((resolve, reject) => {
1676
+ let settled = false;
1677
+ const timer = setTimeout(() => {
1678
+ if (!settled) {
1679
+ settled = true;
1680
+ resolve(false);
1681
+ }
1682
+ }, remaining);
1683
+ observed.then(
1684
+ () => {
1685
+ if (!settled) {
1686
+ settled = true;
1687
+ clearTimeout(timer);
1688
+ resolve(true);
1689
+ }
1690
+ },
1691
+ (error: unknown) => {
1692
+ if (!settled) {
1693
+ settled = true;
1694
+ clearTimeout(timer);
1695
+ reject(error);
1696
+ }
1697
+ },
1698
+ );
1699
+ });
1700
+ }
1701
+
1702
+ function throwIfAborted(signal: AbortSignal): void {
1703
+ if (!signal.aborted) {
1704
+ return;
1705
+ }
1706
+ throw signal.reason instanceof Error ? signal.reason : new WebSocketOperationAbortedError("Operation aborted.");
1707
+ }