threadforge 0.1.1 → 0.2.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 (358) hide show
  1. package/README.md +52 -20
  2. package/bin/forge.js +2 -1058
  3. package/bin/host-commands.d.ts +2 -0
  4. package/bin/host-commands.d.ts.map +1 -0
  5. package/bin/host-commands.js +7 -8
  6. package/bin/platform-commands.d.ts +2 -0
  7. package/bin/platform-commands.d.ts.map +1 -0
  8. package/bin/platform-commands.js +118 -36
  9. package/dist/cli/base-command.d.ts +12 -0
  10. package/dist/cli/base-command.d.ts.map +1 -0
  11. package/dist/cli/base-command.js +25 -0
  12. package/dist/cli/base-command.js.map +1 -0
  13. package/dist/cli/commands/build.d.ts +10 -0
  14. package/dist/cli/commands/build.d.ts.map +1 -0
  15. package/dist/cli/commands/build.js +110 -0
  16. package/dist/cli/commands/build.js.map +1 -0
  17. package/dist/cli/commands/deploy.d.ts +12 -0
  18. package/dist/cli/commands/deploy.d.ts.map +1 -0
  19. package/dist/cli/commands/deploy.js +143 -0
  20. package/dist/cli/commands/deploy.js.map +1 -0
  21. package/dist/cli/commands/dev.d.ts +10 -0
  22. package/dist/cli/commands/dev.d.ts.map +1 -0
  23. package/dist/cli/commands/dev.js +138 -0
  24. package/dist/cli/commands/dev.js.map +1 -0
  25. package/dist/cli/commands/generate.d.ts +10 -0
  26. package/dist/cli/commands/generate.d.ts.map +1 -0
  27. package/dist/cli/commands/generate.js +76 -0
  28. package/dist/cli/commands/generate.js.map +1 -0
  29. package/dist/cli/commands/host.d.ts +8 -0
  30. package/dist/cli/commands/host.d.ts.map +1 -0
  31. package/dist/cli/commands/host.js +20 -0
  32. package/dist/cli/commands/host.js.map +1 -0
  33. package/dist/cli/commands/init.d.ts +16 -0
  34. package/dist/cli/commands/init.d.ts.map +1 -0
  35. package/dist/cli/commands/init.js +246 -0
  36. package/dist/cli/commands/init.js.map +1 -0
  37. package/dist/cli/commands/platform.d.ts +8 -0
  38. package/dist/cli/commands/platform.d.ts.map +1 -0
  39. package/dist/cli/commands/platform.js +20 -0
  40. package/dist/cli/commands/platform.js.map +1 -0
  41. package/dist/cli/commands/restart.d.ts +8 -0
  42. package/dist/cli/commands/restart.d.ts.map +1 -0
  43. package/dist/cli/commands/restart.js +13 -0
  44. package/dist/cli/commands/restart.js.map +1 -0
  45. package/dist/cli/commands/scaffold/frontend.d.ts +10 -0
  46. package/dist/cli/commands/scaffold/frontend.d.ts.map +1 -0
  47. package/dist/cli/commands/scaffold/frontend.js +130 -0
  48. package/dist/cli/commands/scaffold/frontend.js.map +1 -0
  49. package/dist/cli/commands/scaffold/react.d.ts +7 -0
  50. package/dist/cli/commands/scaffold/react.d.ts.map +1 -0
  51. package/dist/cli/commands/scaffold/react.js +12 -0
  52. package/dist/cli/commands/scaffold/react.js.map +1 -0
  53. package/dist/cli/commands/scale.d.ts +8 -0
  54. package/dist/cli/commands/scale.d.ts.map +1 -0
  55. package/dist/cli/commands/scale.js +13 -0
  56. package/dist/cli/commands/scale.js.map +1 -0
  57. package/dist/cli/commands/start.d.ts +10 -0
  58. package/dist/cli/commands/start.d.ts.map +1 -0
  59. package/dist/cli/commands/start.js +71 -0
  60. package/dist/cli/commands/start.js.map +1 -0
  61. package/dist/cli/commands/status.d.ts +11 -0
  62. package/dist/cli/commands/status.d.ts.map +1 -0
  63. package/dist/cli/commands/status.js +60 -0
  64. package/dist/cli/commands/status.js.map +1 -0
  65. package/dist/cli/commands/stop.d.ts +10 -0
  66. package/dist/cli/commands/stop.d.ts.map +1 -0
  67. package/dist/cli/commands/stop.js +89 -0
  68. package/dist/cli/commands/stop.js.map +1 -0
  69. package/dist/cli/util/config-discovery.d.ts +8 -0
  70. package/dist/cli/util/config-discovery.d.ts.map +1 -0
  71. package/dist/cli/util/config-discovery.js +70 -0
  72. package/dist/cli/util/config-discovery.js.map +1 -0
  73. package/dist/cli/util/config-patcher.d.ts +17 -0
  74. package/dist/cli/util/config-patcher.d.ts.map +1 -0
  75. package/dist/cli/util/config-patcher.js +439 -0
  76. package/dist/cli/util/config-patcher.js.map +1 -0
  77. package/dist/cli/util/frontend-dev.d.ts +8 -0
  78. package/dist/cli/util/frontend-dev.d.ts.map +1 -0
  79. package/dist/cli/util/frontend-dev.js +117 -0
  80. package/dist/cli/util/frontend-dev.js.map +1 -0
  81. package/dist/cli/util/process.d.ts +5 -0
  82. package/dist/cli/util/process.d.ts.map +1 -0
  83. package/dist/cli/util/process.js +17 -0
  84. package/dist/cli/util/process.js.map +1 -0
  85. package/dist/cli/util/templates.d.ts +10 -0
  86. package/dist/cli/util/templates.d.ts.map +1 -0
  87. package/dist/cli/util/templates.js +157 -0
  88. package/dist/cli/util/templates.js.map +1 -0
  89. package/dist/core/AlertSink.d.ts +83 -0
  90. package/dist/core/AlertSink.d.ts.map +1 -0
  91. package/dist/core/AlertSink.js +126 -0
  92. package/dist/core/AlertSink.js.map +1 -0
  93. package/dist/core/DirectMessageBus.d.ts +88 -0
  94. package/dist/core/DirectMessageBus.d.ts.map +1 -0
  95. package/dist/core/DirectMessageBus.js +352 -0
  96. package/dist/core/DirectMessageBus.js.map +1 -0
  97. package/dist/core/EndpointResolver.d.ts +111 -0
  98. package/dist/core/EndpointResolver.d.ts.map +1 -0
  99. package/dist/core/EndpointResolver.js +336 -0
  100. package/dist/core/EndpointResolver.js.map +1 -0
  101. package/dist/core/ForgeContext.d.ts +221 -0
  102. package/dist/core/ForgeContext.d.ts.map +1 -0
  103. package/dist/core/ForgeContext.js +1169 -0
  104. package/dist/core/ForgeContext.js.map +1 -0
  105. package/dist/core/ForgeEndpoints.d.ts +71 -0
  106. package/dist/core/ForgeEndpoints.d.ts.map +1 -0
  107. package/dist/core/ForgeEndpoints.js +442 -0
  108. package/dist/core/ForgeEndpoints.js.map +1 -0
  109. package/dist/core/ForgeHost.d.ts +82 -0
  110. package/dist/core/ForgeHost.d.ts.map +1 -0
  111. package/dist/core/ForgeHost.js +107 -0
  112. package/dist/core/ForgeHost.js.map +1 -0
  113. package/dist/core/ForgePlatform.d.ts +96 -0
  114. package/dist/core/ForgePlatform.d.ts.map +1 -0
  115. package/dist/core/ForgePlatform.js +136 -0
  116. package/dist/core/ForgePlatform.js.map +1 -0
  117. package/dist/core/ForgeWebSocket.d.ts +56 -0
  118. package/dist/core/ForgeWebSocket.d.ts.map +1 -0
  119. package/dist/core/ForgeWebSocket.js +415 -0
  120. package/dist/core/ForgeWebSocket.js.map +1 -0
  121. package/dist/core/Ingress.d.ts +329 -0
  122. package/dist/core/Ingress.d.ts.map +1 -0
  123. package/dist/core/Ingress.js +694 -0
  124. package/dist/core/Ingress.js.map +1 -0
  125. package/dist/core/Interceptors.d.ts +134 -0
  126. package/dist/core/Interceptors.d.ts.map +1 -0
  127. package/dist/core/Interceptors.js +416 -0
  128. package/dist/core/Interceptors.js.map +1 -0
  129. package/dist/core/Logger.d.ts +20 -0
  130. package/dist/core/Logger.d.ts.map +1 -0
  131. package/dist/core/Logger.js +77 -0
  132. package/dist/core/Logger.js.map +1 -0
  133. package/dist/core/MessageBus.d.ts +15 -0
  134. package/dist/core/MessageBus.d.ts.map +1 -0
  135. package/dist/core/MessageBus.js +18 -0
  136. package/dist/core/MessageBus.js.map +1 -0
  137. package/dist/core/Prometheus.d.ts +80 -0
  138. package/dist/core/Prometheus.d.ts.map +1 -0
  139. package/dist/core/Prometheus.js +332 -0
  140. package/dist/core/Prometheus.js.map +1 -0
  141. package/dist/core/RequestContext.d.ts +214 -0
  142. package/dist/core/RequestContext.d.ts.map +1 -0
  143. package/dist/core/RequestContext.js +556 -0
  144. package/dist/core/RequestContext.js.map +1 -0
  145. package/dist/core/Router.d.ts +45 -0
  146. package/dist/core/Router.d.ts.map +1 -0
  147. package/dist/core/Router.js +285 -0
  148. package/dist/core/Router.js.map +1 -0
  149. package/dist/core/RoutingStrategy.d.ts +116 -0
  150. package/dist/core/RoutingStrategy.d.ts.map +1 -0
  151. package/dist/core/RoutingStrategy.js +306 -0
  152. package/dist/core/RoutingStrategy.js.map +1 -0
  153. package/dist/core/RpcConfig.d.ts +72 -0
  154. package/dist/core/RpcConfig.d.ts.map +1 -0
  155. package/dist/core/RpcConfig.js +127 -0
  156. package/dist/core/RpcConfig.js.map +1 -0
  157. package/dist/core/SignatureCache.d.ts +81 -0
  158. package/dist/core/SignatureCache.d.ts.map +1 -0
  159. package/dist/core/SignatureCache.js +172 -0
  160. package/dist/core/SignatureCache.js.map +1 -0
  161. package/dist/core/StaticFileServer.d.ts +34 -0
  162. package/dist/core/StaticFileServer.d.ts.map +1 -0
  163. package/dist/core/StaticFileServer.js +497 -0
  164. package/dist/core/StaticFileServer.js.map +1 -0
  165. package/dist/core/Supervisor.d.ts +198 -0
  166. package/dist/core/Supervisor.d.ts.map +1 -0
  167. package/dist/core/Supervisor.js +1418 -0
  168. package/dist/core/Supervisor.js.map +1 -0
  169. package/dist/core/ThreadAllocator.d.ts +52 -0
  170. package/dist/core/ThreadAllocator.d.ts.map +1 -0
  171. package/dist/core/ThreadAllocator.js +174 -0
  172. package/dist/core/ThreadAllocator.js.map +1 -0
  173. package/dist/core/WorkerChannelManager.d.ts +130 -0
  174. package/dist/core/WorkerChannelManager.d.ts.map +1 -0
  175. package/dist/core/WorkerChannelManager.js +956 -0
  176. package/dist/core/WorkerChannelManager.js.map +1 -0
  177. package/dist/core/config-enums.d.ts +41 -0
  178. package/dist/core/config-enums.d.ts.map +1 -0
  179. package/dist/core/config-enums.js +59 -0
  180. package/dist/core/config-enums.js.map +1 -0
  181. package/dist/core/config.d.ts +159 -0
  182. package/dist/core/config.d.ts.map +1 -0
  183. package/dist/core/config.js +694 -0
  184. package/dist/core/config.js.map +1 -0
  185. package/dist/core/host-config.d.ts +146 -0
  186. package/dist/core/host-config.d.ts.map +1 -0
  187. package/dist/core/host-config.js +312 -0
  188. package/dist/core/host-config.js.map +1 -0
  189. package/dist/core/ipc-errors.d.ts +27 -0
  190. package/dist/core/ipc-errors.d.ts.map +1 -0
  191. package/dist/core/ipc-errors.js +36 -0
  192. package/dist/core/ipc-errors.js.map +1 -0
  193. package/dist/core/network-utils.d.ts +35 -0
  194. package/dist/core/network-utils.d.ts.map +1 -0
  195. package/dist/core/network-utils.js +145 -0
  196. package/dist/core/network-utils.js.map +1 -0
  197. package/dist/core/platform-config.d.ts +142 -0
  198. package/dist/core/platform-config.d.ts.map +1 -0
  199. package/dist/core/platform-config.js +299 -0
  200. package/dist/core/platform-config.js.map +1 -0
  201. package/dist/decorators/ServiceProxy.d.ts +175 -0
  202. package/dist/decorators/ServiceProxy.d.ts.map +1 -0
  203. package/dist/decorators/ServiceProxy.js +969 -0
  204. package/dist/decorators/ServiceProxy.js.map +1 -0
  205. package/dist/decorators/index.d.ts +146 -0
  206. package/dist/decorators/index.d.ts.map +1 -0
  207. package/dist/decorators/index.js +545 -0
  208. package/dist/decorators/index.js.map +1 -0
  209. package/dist/deploy/NginxGenerator.d.ts +165 -0
  210. package/dist/deploy/NginxGenerator.d.ts.map +1 -0
  211. package/dist/deploy/NginxGenerator.js +781 -0
  212. package/dist/deploy/NginxGenerator.js.map +1 -0
  213. package/dist/deploy/PlatformManifestGenerator.d.ts +43 -0
  214. package/dist/deploy/PlatformManifestGenerator.d.ts.map +1 -0
  215. package/dist/deploy/PlatformManifestGenerator.js +80 -0
  216. package/dist/deploy/PlatformManifestGenerator.js.map +1 -0
  217. package/dist/deploy/RouteManifestGenerator.d.ts +42 -0
  218. package/dist/deploy/RouteManifestGenerator.d.ts.map +1 -0
  219. package/dist/deploy/RouteManifestGenerator.js +105 -0
  220. package/dist/deploy/RouteManifestGenerator.js.map +1 -0
  221. package/dist/deploy/index.d.ts +210 -0
  222. package/dist/deploy/index.d.ts.map +1 -0
  223. package/dist/deploy/index.js +918 -0
  224. package/dist/deploy/index.js.map +1 -0
  225. package/dist/frontend/FrontendDevLifecycle.d.ts +26 -0
  226. package/dist/frontend/FrontendDevLifecycle.d.ts.map +1 -0
  227. package/dist/frontend/FrontendDevLifecycle.js +60 -0
  228. package/dist/frontend/FrontendDevLifecycle.js.map +1 -0
  229. package/dist/frontend/FrontendPluginOrchestrator.d.ts +64 -0
  230. package/dist/frontend/FrontendPluginOrchestrator.d.ts.map +1 -0
  231. package/dist/frontend/FrontendPluginOrchestrator.js +167 -0
  232. package/dist/frontend/FrontendPluginOrchestrator.js.map +1 -0
  233. package/dist/frontend/SiteResolver.d.ts +33 -0
  234. package/dist/frontend/SiteResolver.d.ts.map +1 -0
  235. package/dist/frontend/SiteResolver.js +53 -0
  236. package/dist/frontend/SiteResolver.js.map +1 -0
  237. package/dist/frontend/StaticMountRegistry.d.ts +36 -0
  238. package/dist/frontend/StaticMountRegistry.d.ts.map +1 -0
  239. package/dist/frontend/StaticMountRegistry.js +94 -0
  240. package/dist/frontend/StaticMountRegistry.js.map +1 -0
  241. package/dist/frontend/index.d.ts +7 -0
  242. package/dist/frontend/index.d.ts.map +1 -0
  243. package/{src → dist}/frontend/index.js +4 -2
  244. package/dist/frontend/index.js.map +1 -0
  245. package/dist/frontend/pathUtils.d.ts +8 -0
  246. package/dist/frontend/pathUtils.d.ts.map +1 -0
  247. package/dist/frontend/pathUtils.js +17 -0
  248. package/dist/frontend/pathUtils.js.map +1 -0
  249. package/dist/frontend/plugins/index.d.ts +2 -0
  250. package/dist/frontend/plugins/index.d.ts.map +1 -0
  251. package/{src → dist}/frontend/plugins/index.js +1 -1
  252. package/dist/frontend/plugins/index.js.map +1 -0
  253. package/dist/frontend/plugins/viteFrontend.d.ts +51 -0
  254. package/dist/frontend/plugins/viteFrontend.d.ts.map +1 -0
  255. package/dist/frontend/plugins/viteFrontend.js +134 -0
  256. package/dist/frontend/plugins/viteFrontend.js.map +1 -0
  257. package/dist/frontend/types.d.ts +25 -0
  258. package/dist/frontend/types.d.ts.map +1 -0
  259. package/dist/frontend/types.js +2 -0
  260. package/dist/frontend/types.js.map +1 -0
  261. package/dist/index.d.ts +17 -0
  262. package/dist/index.d.ts.map +1 -0
  263. package/dist/index.js +32 -0
  264. package/dist/index.js.map +1 -0
  265. package/dist/internals.d.ts +21 -0
  266. package/dist/internals.d.ts.map +1 -0
  267. package/{src → dist}/internals.js +12 -14
  268. package/dist/internals.js.map +1 -0
  269. package/dist/plugins/PluginManager.d.ts +209 -0
  270. package/dist/plugins/PluginManager.d.ts.map +1 -0
  271. package/dist/plugins/PluginManager.js +365 -0
  272. package/dist/plugins/PluginManager.js.map +1 -0
  273. package/dist/plugins/ScopedPostgres.d.ts +78 -0
  274. package/dist/plugins/ScopedPostgres.d.ts.map +1 -0
  275. package/dist/plugins/ScopedPostgres.js +190 -0
  276. package/dist/plugins/ScopedPostgres.js.map +1 -0
  277. package/dist/plugins/ScopedRedis.d.ts +88 -0
  278. package/dist/plugins/ScopedRedis.d.ts.map +1 -0
  279. package/dist/plugins/ScopedRedis.js +169 -0
  280. package/dist/plugins/ScopedRedis.js.map +1 -0
  281. package/dist/plugins/index.d.ts +289 -0
  282. package/dist/plugins/index.d.ts.map +1 -0
  283. package/dist/plugins/index.js +1942 -0
  284. package/dist/plugins/index.js.map +1 -0
  285. package/dist/plugins/types.d.ts +59 -0
  286. package/dist/plugins/types.d.ts.map +1 -0
  287. package/dist/plugins/types.js +2 -0
  288. package/dist/plugins/types.js.map +1 -0
  289. package/dist/registry/ServiceRegistry.d.ts +305 -0
  290. package/dist/registry/ServiceRegistry.d.ts.map +1 -0
  291. package/dist/registry/ServiceRegistry.js +735 -0
  292. package/dist/registry/ServiceRegistry.js.map +1 -0
  293. package/dist/scaling/ScaleAdvisor.d.ts +214 -0
  294. package/dist/scaling/ScaleAdvisor.d.ts.map +1 -0
  295. package/dist/scaling/ScaleAdvisor.js +526 -0
  296. package/dist/scaling/ScaleAdvisor.js.map +1 -0
  297. package/dist/services/Service.d.ts +164 -0
  298. package/dist/services/Service.d.ts.map +1 -0
  299. package/dist/services/Service.js +106 -0
  300. package/dist/services/Service.js.map +1 -0
  301. package/dist/services/worker-bootstrap.d.ts +15 -0
  302. package/dist/services/worker-bootstrap.d.ts.map +1 -0
  303. package/dist/services/worker-bootstrap.js +744 -0
  304. package/dist/services/worker-bootstrap.js.map +1 -0
  305. package/dist/templates/auth-service.d.ts +42 -0
  306. package/dist/templates/auth-service.d.ts.map +1 -0
  307. package/dist/templates/auth-service.js +54 -0
  308. package/dist/templates/auth-service.js.map +1 -0
  309. package/dist/templates/identity-service.d.ts +50 -0
  310. package/dist/templates/identity-service.d.ts.map +1 -0
  311. package/dist/templates/identity-service.js +62 -0
  312. package/dist/templates/identity-service.js.map +1 -0
  313. package/dist/types/contract.d.ts +120 -0
  314. package/dist/types/contract.d.ts.map +1 -0
  315. package/dist/types/contract.js +69 -0
  316. package/dist/types/contract.js.map +1 -0
  317. package/package.json +78 -20
  318. package/src/core/DirectMessageBus.js +0 -364
  319. package/src/core/EndpointResolver.js +0 -259
  320. package/src/core/ForgeContext.js +0 -2236
  321. package/src/core/ForgeHost.js +0 -122
  322. package/src/core/ForgePlatform.js +0 -145
  323. package/src/core/Ingress.js +0 -768
  324. package/src/core/Interceptors.js +0 -420
  325. package/src/core/MessageBus.js +0 -321
  326. package/src/core/Prometheus.js +0 -305
  327. package/src/core/RequestContext.js +0 -413
  328. package/src/core/RoutingStrategy.js +0 -330
  329. package/src/core/Supervisor.js +0 -1349
  330. package/src/core/ThreadAllocator.js +0 -196
  331. package/src/core/WorkerChannelManager.js +0 -879
  332. package/src/core/config.js +0 -637
  333. package/src/core/host-config.js +0 -311
  334. package/src/core/network-utils.js +0 -166
  335. package/src/core/platform-config.js +0 -308
  336. package/src/decorators/ServiceProxy.js +0 -904
  337. package/src/decorators/index.js +0 -571
  338. package/src/deploy/NginxGenerator.js +0 -865
  339. package/src/deploy/PlatformManifestGenerator.js +0 -96
  340. package/src/deploy/RouteManifestGenerator.js +0 -112
  341. package/src/deploy/index.js +0 -984
  342. package/src/frontend/FrontendDevLifecycle.js +0 -65
  343. package/src/frontend/FrontendPluginOrchestrator.js +0 -187
  344. package/src/frontend/SiteResolver.js +0 -63
  345. package/src/frontend/StaticMountRegistry.js +0 -90
  346. package/src/frontend/plugins/viteFrontend.js +0 -79
  347. package/src/frontend/types.js +0 -35
  348. package/src/index.js +0 -58
  349. package/src/plugins/PluginManager.js +0 -537
  350. package/src/plugins/ScopedPostgres.js +0 -192
  351. package/src/plugins/ScopedRedis.js +0 -142
  352. package/src/plugins/index.js +0 -1756
  353. package/src/registry/ServiceRegistry.js +0 -797
  354. package/src/scaling/ScaleAdvisor.js +0 -442
  355. package/src/services/Service.js +0 -195
  356. package/src/services/worker-bootstrap.js +0 -679
  357. package/src/templates/auth-service.js +0 -65
  358. package/src/templates/identity-service.js +0 -75
@@ -1,768 +0,0 @@
1
- /**
2
- * Ingress Protection
3
- *
4
- * Multiple layers of defense between the internet and your services.
5
- * Each layer catches different failure modes:
6
- *
7
- * ┌─────────────────────────────────────────────────────────────┐
8
- * │ INTERNET │
9
- * └───────────────────────────┬─────────────────────────────────┘
10
- * ▼
11
- * ┌─────────────────────────────────────────────────────────────┐
12
- * │ Layer 1: CONNECTION LIMITER │
13
- * │ Total open connections capped at N (e.g. 10,000). │
14
- * │ New connections get TCP RST when full. │
15
- * │ Prevents file descriptor exhaustion. │
16
- * └───────────────────────────┬─────────────────────────────────┘
17
- * ▼
18
- * ┌─────────────────────────────────────────────────────────────┐
19
- * │ Layer 2: RATE LIMITER (per client) │
20
- * │ Token bucket per IP / API key. Burst-friendly. │
21
- * │ Returns 429 Too Many Requests when exceeded. │
22
- * │ Prevents single client from consuming all capacity. │
23
- * └───────────────────────────┬─────────────────────────────────┘
24
- * ▼
25
- * ┌─────────────────────────────────────────────────────────────┐
26
- * │ Layer 3: GLOBAL LOAD SHEDDER │
27
- * │ Monitors gateway CPU + event loop lag. │
28
- * │ When overloaded, sheds low-priority requests (503). │
29
- * │ High-priority requests (health checks, auth) pass through. │
30
- * │ Prevents cascade failure in the gateway itself. │
31
- * └───────────────────────────┬─────────────────────────────────┘
32
- * ▼
33
- * ┌─────────────────────────────────────────────────────────────┐
34
- * │ Layer 4: ADAPTIVE CONCURRENCY (per service) │
35
- * │ Limits in-flight requests to each downstream service. │
36
- * │ Dynamically adjusts limit based on observed latency. │
37
- * │ When a service slows down, we send FEWER requests to it. │
38
- * │ Returns 503 when the service's concurrency window is full. │
39
- * │ Prevents a slow service from consuming all gateway threads.│
40
- * └───────────────────────────┬─────────────────────────────────┘
41
- * ▼
42
- * ┌─────────────────────────────────────────────────────────────┐
43
- * │ SERVICE (users, billing, etc.) │
44
- * └─────────────────────────────────────────────────────────────┘
45
- *
46
- * Usage in a gateway service:
47
- *
48
- * import { IngressProtection } from 'threadforge/ingress';
49
- *
50
- * export default class GatewayService extends Service {
51
- * async onStart(ctx) {
52
- * this.ingress = new IngressProtection({
53
- * maxConnections: 10000,
54
- * rateLimit: { windowMs: 60000, maxRequests: 100 },
55
- * loadShedding: { eventLoopThresholdMs: 100 },
56
- * services: {
57
- * users: { maxConcurrent: 200 },
58
- * billing: { maxConcurrent: 50 },
59
- * notifications: { maxConcurrent: 100 },
60
- * },
61
- * });
62
- *
63
- * // Apply as middleware
64
- * ctx.router.use(this.ingress.middleware());
65
- * }
66
- * }
67
- */
68
-
69
- import { EventEmitter } from "node:events";
70
- import { isPrivateNetwork } from "./ForgeContext.js";
71
-
72
- // ─── Rate Limiter (Token Bucket) ────────────────────────────
73
-
74
- /**
75
- * Per-client rate limiter using the token bucket algorithm.
76
- *
77
- * Token bucket is burst-friendly: a client that hasn't made
78
- * requests in a while accumulates tokens and can burst.
79
- * A client that's been hammering the API runs out of tokens
80
- * and gets 429'd.
81
- *
82
- * Why token bucket instead of sliding window:
83
- * - Allows natural burst patterns (page load = 10 requests at once)
84
- * - Smoother than fixed windows (no "thundering herd" at window reset)
85
- * - O(1) per request (no sorted sets or sliding counters)
86
- */
87
- export class RateLimiter {
88
- /**
89
- * @param {Object} options
90
- * @param {number} [options.maxTokens=100] - Max tokens per client (burst size)
91
- * @param {number} [options.refillRate=10] - Tokens added per second
92
- * @param {number} [options.windowMs=60000] - Cleanup interval for stale buckets
93
- * @param {Function} [options.keyExtractor] - (req) => string, extracts client identity
94
- */
95
- constructor(options = {}) {
96
- this.maxTokens = options.maxTokens ?? 100;
97
- this.refillRate = options.refillRate ?? (options.maxRequests ?? 100) / 60;
98
- this.windowMs = options.windowMs ?? 60000;
99
- this.keyExtractor = options.keyExtractor ?? defaultKeyExtractor;
100
-
101
- /** @type {Map<string, {tokens: number, lastRefill: number}>} */
102
- this.buckets = new Map();
103
-
104
- // Periodically clean stale buckets
105
- this._cleanupTimer = setInterval(() => this._cleanup(), this.windowMs);
106
- this._cleanupTimer.unref();
107
- }
108
-
109
- /**
110
- * Check if a request is allowed.
111
- * @returns {{ allowed: boolean, remaining: number, retryAfter?: number }}
112
- */
113
- check(req) {
114
- const key = this.keyExtractor(req);
115
- const now = Date.now();
116
-
117
- let bucket = this.buckets.get(key);
118
- if (!bucket) {
119
- bucket = { tokens: this.maxTokens, lastRefill: now };
120
- this.buckets.set(key, bucket);
121
- }
122
-
123
- // Refill tokens based on elapsed time
124
- const elapsed = (now - bucket.lastRefill) / 1000;
125
- bucket.tokens = Math.min(this.maxTokens, bucket.tokens + elapsed * this.refillRate);
126
- bucket.lastRefill = now;
127
-
128
- if (bucket.tokens >= 1) {
129
- bucket.tokens -= 1;
130
- return {
131
- allowed: true,
132
- remaining: Math.floor(bucket.tokens),
133
- };
134
- }
135
-
136
- // Rate limited — calculate retry-after
137
- const tokensNeeded = 1 - bucket.tokens;
138
- const retryAfterMs = Math.ceil((tokensNeeded / this.refillRate) * 1000);
139
-
140
- return {
141
- allowed: false,
142
- remaining: 0,
143
- retryAfter: retryAfterMs,
144
- };
145
- }
146
-
147
- _cleanup() {
148
- const staleThreshold = Date.now() - this.windowMs * 2;
149
- for (const [key, bucket] of this.buckets) {
150
- if (bucket.lastRefill < staleThreshold) {
151
- this.buckets.delete(key);
152
- }
153
- }
154
- }
155
-
156
- get stats() {
157
- return {
158
- activeClients: this.buckets.size,
159
- maxTokens: this.maxTokens,
160
- refillRate: this.refillRate,
161
- };
162
- }
163
-
164
- stop() {
165
- if (this._cleanupTimer) clearInterval(this._cleanupTimer);
166
- }
167
- }
168
-
169
- function isValidIP(str) {
170
- if (!str) return false;
171
- // IPv4
172
- if (/^(\d{1,3}\.){3}\d{1,3}$/.test(str)) {
173
- return str.split(".").every((p) => {
174
- const n = parseInt(p, 10);
175
- return n >= 0 && n <= 255;
176
- });
177
- }
178
- // IPv6 (basic check)
179
- if (/^[0-9a-fA-F:]+$/.test(str) && str.includes(":")) return true;
180
- return false;
181
- }
182
-
183
- function isTrustedProxyAddr(address) {
184
- const trusted = process.env.FORGE_TRUSTED_PROXIES;
185
- if (!trusted) return false;
186
- if (!address) return false;
187
- const noZone = address.split("%")[0];
188
- const addr = noZone.toLowerCase().replace(/^(0{0,4}:){0,5}(0{0,4}:)?ffff:/, "").replace(/^::ffff:/, "");
189
- for (const entry of trusted.split(",")) {
190
- const trimmed = entry.trim();
191
- if (addr === trimmed) return true;
192
- }
193
- return false;
194
- }
195
-
196
- function defaultKeyExtractor(req) {
197
- const rawAddr = req.socket?.remoteAddress;
198
- const remoteAddr = rawAddr ?? `unknown_${Math.random().toString(36).slice(2, 10)}`;
199
-
200
- // H1: Only trust X-Forwarded-For from trusted proxies
201
- // If FORGE_TRUSTED_PROXIES is set, only those addresses are trusted.
202
- // Otherwise, fall back to trusting private networks for backwards compatibility.
203
- const trusted = process.env.FORGE_TRUSTED_PROXIES;
204
- const isProxyTrusted = trusted ? isTrustedProxyAddr(remoteAddr) : (rawAddr && isPrivateNetwork(remoteAddr));
205
- if (isProxyTrusted) {
206
- const xff = req.headers?.["x-forwarded-for"];
207
- if (xff) {
208
- const ips = xff.split(",").slice(0, 10).map((ip) => ip.trim());
209
- // Walk right-to-left to find first untrusted IP (the real client)
210
- for (let i = ips.length - 1; i >= 0; i--) {
211
- if (!isValidIP(ips[i])) continue;
212
- const ipTrusted = trusted ? isTrustedProxyAddr(ips[i]) : isPrivateNetwork(ips[i]);
213
- if (ips[i] && !ipTrusted) {
214
- return ips[i];
215
- }
216
- }
217
- }
218
-
219
- // Fallback to X-Real-IP (commonly set by nginx)
220
- const realIp = req.headers?.["x-real-ip"];
221
- if (realIp && isValidIP(realIp)) {
222
- return realIp;
223
- }
224
-
225
- return remoteAddr;
226
- }
227
-
228
- // Direct connection or no trusted proxies configured — use direct remote address
229
- return remoteAddr;
230
- }
231
-
232
- // ─── Load Shedder ───────────────────────────────────────────
233
-
234
- /**
235
- * Global load shedder. Monitors the gateway process health and
236
- * drops requests when overloaded.
237
- *
238
- * Monitors two signals:
239
- * 1. Event loop lag — if the event loop is delayed by > threshold,
240
- * the process is CPU-starved. Start shedding.
241
- * 2. Active requests — if we have too many in-flight, shed.
242
- *
243
- * Shedding is priority-based:
244
- * - CRITICAL: health checks, auth — never shed
245
- * - HIGH: payment webhooks — shed only at extreme load
246
- * - NORMAL: regular API calls — shed when overloaded
247
- * - LOW: analytics, logging — shed first
248
- */
249
- export class LoadShedder {
250
- /**
251
- * @param {Object} options
252
- * @param {number} [options.eventLoopThresholdMs=100] - Lag threshold to start shedding
253
- * @param {number} [options.maxActiveRequests=5000] - Max in-flight before shedding
254
- * @param {number} [options.checkIntervalMs=500] - How often to check event loop
255
- * @param {Object} [options.priorities] - Path → priority overrides
256
- */
257
- constructor(options = {}) {
258
- this.eventLoopThresholdMs = options.eventLoopThresholdMs ?? 100;
259
- this.maxActiveRequests = options.maxActiveRequests ?? 5000;
260
- this.checkIntervalMs = options.checkIntervalMs ?? 500;
261
-
262
- // Default priority rules
263
- this.priorityRules = [
264
- { pattern: /^\/(health|ready|live)/, priority: "critical" },
265
- { pattern: /^\/auth(\/|$)|^\/login(\/|$)|^\/oauth(\/|$)/, priority: "critical" },
266
- { pattern: /^\/webhook(\/|$)|^\/stripe(\/|$)/, priority: "high" },
267
- { pattern: /^\/api\//, priority: "normal" },
268
- { pattern: /^\/analytics|^\/telemetry/, priority: "low" },
269
- ...(options.priorityRules ?? []),
270
- ];
271
-
272
- this.activeRequests = 0;
273
- this.eventLoopLag = 0;
274
- this._lastCheck = Date.now();
275
-
276
- // Shedding thresholds by priority
277
- this.shedThresholds = {
278
- low: { lagMs: 50, loadRatio: 0.6 },
279
- normal: { lagMs: 100, loadRatio: 0.8 },
280
- high: { lagMs: 200, loadRatio: 0.95 },
281
- critical: { lagMs: Infinity, loadRatio: Infinity }, // never shed
282
- };
283
-
284
- // Start event loop lag measurement using recursive setTimeout to prevent drift
285
- this._lagTimerActive = true;
286
- const measureLag = () => {
287
- if (!this._lagTimerActive) return;
288
- const now = Date.now();
289
- const expected = this.checkIntervalMs;
290
- const actual = now - this._lastCheck;
291
- this.eventLoopLag = Math.max(0, actual - expected);
292
- this._lastCheck = now;
293
- this._lagTimer = setTimeout(measureLag, this.checkIntervalMs);
294
- this._lagTimer.unref();
295
- };
296
- this._lagTimer = setTimeout(measureLag, this.checkIntervalMs);
297
- this._lagTimer.unref();
298
- }
299
-
300
- /**
301
- * Should this request be accepted or shed?
302
- * @param {Object} req - HTTP request
303
- * @returns {{ accept: boolean, reason?: string, priority: string }}
304
- */
305
- shouldAccept(req) {
306
- const priority = this._getPriority(req);
307
- const threshold = this.shedThresholds[priority];
308
- const loadRatio = this.activeRequests / this.maxActiveRequests;
309
-
310
- // Check event loop lag
311
- if (this.eventLoopLag > threshold.lagMs) {
312
- return {
313
- accept: false,
314
- reason: `Event loop lag ${this.eventLoopLag}ms exceeds ${threshold.lagMs}ms threshold for ${priority} priority`,
315
- priority,
316
- };
317
- }
318
-
319
- // Check active request count
320
- if (loadRatio > threshold.loadRatio) {
321
- return {
322
- accept: false,
323
- reason: `Load at ${(loadRatio * 100).toFixed(0)}% exceeds ${(threshold.loadRatio * 100).toFixed(0)}% threshold for ${priority} priority`,
324
- priority,
325
- };
326
- }
327
-
328
- return { accept: true, priority };
329
- }
330
-
331
- _getPriority(req) {
332
- const url = req.url ?? "/";
333
- for (const rule of this.priorityRules) {
334
- if (rule.pattern.test(url)) {
335
- return rule.priority;
336
- }
337
- }
338
- return "normal";
339
- }
340
-
341
- trackRequest() {
342
- this.activeRequests++;
343
- return () => {
344
- this.activeRequests--;
345
- };
346
- }
347
-
348
- get stats() {
349
- return {
350
- activeRequests: this.activeRequests,
351
- maxActiveRequests: this.maxActiveRequests,
352
- eventLoopLagMs: this.eventLoopLag,
353
- loadPercent: ((this.activeRequests / this.maxActiveRequests) * 100).toFixed(1),
354
- shedding:
355
- this.eventLoopLag > this.shedThresholds.normal.lagMs || this.activeRequests > this.maxActiveRequests * 0.8,
356
- };
357
- }
358
-
359
- stop() {
360
- this._lagTimerActive = false;
361
- if (this._lagTimer) clearTimeout(this._lagTimer);
362
- }
363
- }
364
-
365
- // ─── Adaptive Concurrency Limiter ───────────────────────────
366
-
367
- /**
368
- * Adaptive Concurrency Limiter (per downstream service)
369
- *
370
- * The core problem: how many concurrent requests should we send
371
- * to the users service? Too few = wasted capacity. Too many =
372
- * overwhelm the service, latency spikes, cascade failure.
373
- *
374
- * Fixed limits are fragile — they're either too conservative
375
- * (wasting capacity during normal load) or too aggressive
376
- * (causing failures during peak).
377
- *
378
- * Instead, we use the Vegas algorithm (from TCP congestion control):
379
- *
380
- * 1. Start with a small concurrency window (e.g., 10)
381
- * 2. Measure round-trip latency for each request
382
- * 3. Track the minimum observed latency (the "no-load" baseline)
383
- * 4. If current latency ≈ baseline → increase the window
384
- * (service has capacity, give it more)
385
- * 5. If current latency >> baseline → decrease the window
386
- * (service is queuing, back off)
387
- *
388
- * This automatically adapts to:
389
- * - Fast services (high window, more concurrent requests)
390
- * - Slow services (low window, fewer concurrent requests)
391
- * - Services that degrade under load (window shrinks as latency rises)
392
- * - Services that recover (window grows as latency drops)
393
- *
394
- * Inspired by Netflix's concurrency-limits library.
395
- */
396
- export class AdaptiveConcurrencyLimiter {
397
- /**
398
- * @param {string} serviceName
399
- * @param {Object} options
400
- * @param {number} [options.initialLimit=20] - Starting concurrency limit
401
- * @param {number} [options.minLimit=5] - Floor
402
- * @param {number} [options.maxLimit=500] - Ceiling
403
- * @param {number} [options.smoothing=0.2] - EMA smoothing for latency
404
- * @param {number} [options.tolerance=2.0] - Ratio of current/min latency
405
- * before we reduce the limit
406
- */
407
- constructor(serviceName, options = {}) {
408
- this.serviceName = serviceName;
409
- this.limit = options.initialLimit ?? 20;
410
- this.minLimit = options.minLimit ?? 5;
411
- this.maxLimit = options.maxLimit ?? 500;
412
- this.smoothing = options.smoothing ?? 0.2;
413
- this.tolerance = options.tolerance ?? 2.0;
414
-
415
- this.inFlight = 0;
416
- this.minLatency = Infinity;
417
- this.smoothedLatency = 0;
418
-
419
- // Metrics
420
- this.totalRequests = 0;
421
- this.totalRejected = 0;
422
- this.totalSuccesses = 0;
423
- this.totalFailures = 0;
424
-
425
- // Latency percentiles (ring buffer)
426
- this._latencies = new Float64Array(1000);
427
- this._latencyIdx = 0;
428
- this._latencyCount = 0;
429
-
430
- // Windowed minimum latency reset
431
- this._minLatencyWindow = new Float64Array(1000);
432
- this._minLatencyWindowIdx = 0;
433
- this._minLatencyWindowCount = 0;
434
- this._minLatencyObservations = 0;
435
- }
436
-
437
- /**
438
- * Try to acquire a concurrency slot.
439
- * @returns {{ acquired: boolean, release?: Function }}
440
- */
441
- tryAcquire() {
442
- this.totalRequests++;
443
-
444
- if (this.inFlight >= this.limit) {
445
- this.totalRejected++;
446
- return { acquired: false };
447
- }
448
-
449
- this.inFlight++;
450
- const startTime = performance.now();
451
-
452
- const release = (success = true) => {
453
- this.inFlight--;
454
- const latency = performance.now() - startTime;
455
- this._recordLatency(latency, success);
456
- };
457
-
458
- return { acquired: true, release };
459
- }
460
-
461
- _recordLatency(latencyMs, success) {
462
- if (success) {
463
- this.totalSuccesses++;
464
-
465
- // Track minimum latency with windowed reset
466
- this._minLatencyWindow[this._minLatencyWindowIdx % this._minLatencyWindow.length] = latencyMs;
467
- this._minLatencyWindowIdx++;
468
- this._minLatencyWindowCount = Math.min(this._minLatencyWindowCount + 1, this._minLatencyWindow.length);
469
- this._minLatencyObservations++;
470
-
471
- if (latencyMs < this.minLatency) {
472
- this.minLatency = latencyMs;
473
- }
474
-
475
- // Every 1000 observations, recalculate minLatency from the window
476
- if (this._minLatencyObservations >= 1000) {
477
- this._minLatencyObservations = 0;
478
- let windowMin = Infinity;
479
- for (let i = 0; i < this._minLatencyWindowCount; i++) {
480
- if (this._minLatencyWindow[i] < windowMin) {
481
- windowMin = this._minLatencyWindow[i];
482
- }
483
- }
484
- this.minLatency = windowMin;
485
- }
486
-
487
- // Exponential moving average
488
- if (this.smoothedLatency === 0) {
489
- this.smoothedLatency = latencyMs;
490
- } else {
491
- this.smoothedLatency = this.smoothing * latencyMs + (1 - this.smoothing) * this.smoothedLatency;
492
- }
493
-
494
- // Store in ring buffer for percentile calculation
495
- this._latencies[this._latencyIdx % this._latencies.length] = latencyMs;
496
- this._latencyIdx++;
497
- this._latencyCount = Math.min(this._latencyCount + 1, this._latencies.length);
498
-
499
- // Adjust limit using Vegas algorithm
500
- this._adjustLimit();
501
- } else {
502
- this.totalFailures++;
503
- // On failure, aggressively reduce limit
504
- this.limit = Math.max(this.minLimit, Math.floor(this.limit * 0.75));
505
- }
506
- }
507
-
508
- _adjustLimit() {
509
- if (this.minLatency === Infinity || this.smoothedLatency === 0) return;
510
-
511
- const ratio = this.smoothedLatency / this.minLatency;
512
-
513
- if (ratio < this.tolerance) {
514
- // Latency is close to baseline — we have room, increase slowly
515
- // The gradient determines how aggressively we grow
516
- const gradient = Math.max(0, (this.tolerance - ratio) / this.tolerance);
517
- const increase = Math.ceil(gradient * 2);
518
- this.limit = Math.min(this.maxLimit, this.limit + increase);
519
- } else {
520
- // Latency is elevated — service is queuing, reduce
521
- // Reduce proportionally to how far over the tolerance we are
522
- const overshoot = ratio / this.tolerance;
523
- const decrease = Math.ceil(overshoot);
524
- this.limit = Math.max(this.minLimit, this.limit - decrease);
525
- }
526
- }
527
-
528
- get stats() {
529
- return {
530
- service: this.serviceName,
531
- currentLimit: this.limit,
532
- inFlight: this.inFlight,
533
- utilization: `${((this.inFlight / this.limit) * 100).toFixed(0)}%`,
534
- minLatencyMs: this.minLatency === Infinity ? null : this.minLatency.toFixed(2),
535
- smoothedLatencyMs: this.smoothedLatency.toFixed(2),
536
- p99LatencyMs: this._getPercentile(0.99)?.toFixed(2) ?? null,
537
- totalRequests: this.totalRequests,
538
- totalRejected: this.totalRejected,
539
- rejectionRate: `${((this.totalRejected / Math.max(1, this.totalRequests)) * 100).toFixed(1)}%`,
540
- };
541
- }
542
-
543
- _getPercentile(p) {
544
- if (this._latencyCount === 0) return null;
545
- const sorted = Array.from(this._latencies.slice(0, this._latencyCount)).sort((a, b) => a - b);
546
- const idx = Math.floor(sorted.length * p);
547
- return sorted[idx];
548
- }
549
- }
550
-
551
- // ─── IngressProtection (combines all layers) ────────────────
552
-
553
- /**
554
- * Combined ingress protection middleware.
555
- *
556
- * Applies all layers in order: connection limit → rate limit →
557
- * load shedding → route → adaptive concurrency → service.
558
- */
559
- export class IngressProtection extends EventEmitter {
560
- /**
561
- * @param {Object} options
562
- * @param {number} [options.maxConnections=10000]
563
- * @param {Object} [options.rateLimit]
564
- * @param {Object} [options.loadShedding]
565
- * @param {Object} [options.services] - Per-service concurrency config
566
- */
567
- constructor(options = {}) {
568
- super();
569
-
570
- this.maxConnections = options.maxConnections ?? 10000;
571
- this.activeConnections = 0;
572
-
573
- this.rateLimiter = new RateLimiter(options.rateLimit ?? {});
574
- this.loadShedder = new LoadShedder(options.loadShedding ?? {});
575
-
576
- // Per-service adaptive concurrency limiters
577
- /** @type {Map<string, AdaptiveConcurrencyLimiter>} */
578
- this.serviceLimiters = new Map();
579
-
580
- if (options.services) {
581
- for (const [name, config] of Object.entries(options.services)) {
582
- this.serviceLimiters.set(name, new AdaptiveConcurrencyLimiter(name, config));
583
- }
584
- }
585
-
586
- this._metricsInterval = setInterval(() => {
587
- this.emit("metrics", this.stats);
588
- }, 10000);
589
- this._metricsInterval.unref();
590
- }
591
-
592
- /**
593
- * Get or create a concurrency limiter for a service.
594
- */
595
- getServiceLimiter(serviceName) {
596
- if (!this.serviceLimiters.has(serviceName)) {
597
- this.serviceLimiters.set(serviceName, new AdaptiveConcurrencyLimiter(serviceName));
598
- }
599
- return this.serviceLimiters.get(serviceName);
600
- }
601
-
602
- /**
603
- * Returns a middleware function for the ForgeContext router.
604
- *
605
- * Usage:
606
- * ctx.router.use(this.ingress.middleware());
607
- */
608
- middleware() {
609
- return async (req, res, next) => {
610
- // Layer 1: Connection limit
611
- if (this.activeConnections >= this.maxConnections) {
612
- res.writeHead(503, { "Retry-After": "5" });
613
- res.end(JSON.stringify({ error: "Server at connection capacity" }));
614
- return;
615
- }
616
- this.activeConnections++;
617
- let connectionCounted = true;
618
- req.on("close", () => {
619
- if (connectionCounted) {
620
- this.activeConnections--;
621
- connectionCounted = false;
622
- }
623
- });
624
-
625
- // Layer 2: Rate limiting
626
- const rateCheck = this.rateLimiter.check(req);
627
- if (!rateCheck.allowed) {
628
- const retryAfterSec = Math.ceil(rateCheck.retryAfter / 1000);
629
- res.writeHead(429, {
630
- "Retry-After": String(retryAfterSec),
631
- "X-RateLimit-Remaining": "0",
632
- "X-RateLimit-Reset": String(retryAfterSec),
633
- });
634
- res.end(
635
- JSON.stringify({
636
- error: "Rate limit exceeded",
637
- retryAfter: retryAfterSec,
638
- }),
639
- );
640
- if (connectionCounted) {
641
- connectionCounted = false;
642
- this.activeConnections--;
643
- }
644
- return;
645
- }
646
-
647
- // Layer 3: Load shedding
648
- const shedCheck = this.loadShedder.shouldAccept(req);
649
- if (!shedCheck.accept) {
650
- res.writeHead(503, { "Retry-After": "1" });
651
- // Don't expose internal load/lag details to clients
652
- res.end(
653
- JSON.stringify({
654
- error: "Service temporarily overloaded",
655
- }),
656
- );
657
- if (connectionCounted) {
658
- connectionCounted = false;
659
- this.activeConnections--;
660
- }
661
- return;
662
- }
663
-
664
- // Track active request
665
- const releaseRequest = this.loadShedder.trackRequest();
666
-
667
- // Add headers
668
- res.setHeader("X-RateLimit-Remaining", String(rateCheck.remaining));
669
-
670
- // Track completion by wrapping res.end — also hook close for premature disconnects
671
- let requestReleased = false;
672
- const doRelease = () => {
673
- if (!requestReleased) {
674
- requestReleased = true;
675
- releaseRequest();
676
- // Clean up close listener to avoid accumulation
677
- if (typeof res.removeListener === "function") {
678
- res.removeListener("close", doRelease);
679
- }
680
- }
681
- };
682
-
683
- const origEnd = res.end.bind(res);
684
- res.end = (...args) => {
685
- doRelease();
686
- return origEnd(...args);
687
- };
688
-
689
- // If client disconnects before res.end(), still release
690
- if (typeof res.on === "function") res.on("close", doRelease);
691
-
692
- // Pass through to router
693
- if (next) next();
694
- };
695
- }
696
-
697
- /**
698
- * Wrap a proxy call with adaptive concurrency limiting.
699
- *
700
- * Usage in the gateway:
701
- * const user = await this.ingress.withConcurrencyLimit('users', () =>
702
- * this.users.getUser(id)
703
- * );
704
- *
705
- * Or in the proxy interceptor layer:
706
- * The ServiceProxy automatically calls this if ingress is configured.
707
- */
708
- async withConcurrencyLimit(serviceName, fn) {
709
- const limiter = this.getServiceLimiter(serviceName);
710
- const slot = limiter.tryAcquire();
711
-
712
- if (!slot.acquired) {
713
- const err = new Error(
714
- `Service "${serviceName}" at concurrency limit (${limiter.limit}). ` +
715
- `${limiter.inFlight} requests in flight.`,
716
- );
717
- err.code = "CONCURRENCY_LIMIT";
718
- err.statusCode = 503;
719
- throw err;
720
- }
721
-
722
- try {
723
- const result = await fn();
724
- slot.release(true);
725
- return result;
726
- } catch (err) {
727
- slot.release(false);
728
- throw err;
729
- }
730
- }
731
-
732
- get stats() {
733
- const serviceStats = {};
734
- for (const [name, limiter] of this.serviceLimiters) {
735
- serviceStats[name] = limiter.stats;
736
- }
737
-
738
- return {
739
- connections: {
740
- active: this.activeConnections,
741
- max: this.maxConnections,
742
- },
743
- rateLimiter: this.rateLimiter.stats,
744
- loadShedder: this.loadShedder.stats,
745
- services: serviceStats,
746
- };
747
- }
748
-
749
- /**
750
- * HTTP handler for ingress stats endpoint.
751
- * Mount on the metrics server:
752
- * GET /ingress → full stats
753
- */
754
- httpHandler(req, res) {
755
- if (req.url === "/ingress") {
756
- res.writeHead(200, { "Content-Type": "application/json" });
757
- res.end(JSON.stringify(this.stats, null, 2));
758
- return true;
759
- }
760
- return false;
761
- }
762
-
763
- stop() {
764
- this.rateLimiter.stop();
765
- this.loadShedder.stop();
766
- if (this._metricsInterval) clearInterval(this._metricsInterval);
767
- }
768
- }