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
@@ -0,0 +1,969 @@
1
+ /**
2
+ * ServiceProxy v2
3
+ *
4
+ * Auto-generates proxy clients with:
5
+ * - Pluggable routing strategies (round-robin, hash, least-pending)
6
+ * - Interceptor chains (deadline, logging, circuit breaker, retry)
7
+ * - Pending request tracking (for least-pending routing)
8
+ * - Contract-based method validation
9
+ */
10
+ import { createHmac } from "node:crypto";
11
+ import http from "node:http";
12
+ import { isPrivateNetwork, isTrustedProxy } from "../core/ForgeContext.js";
13
+ import { AdaptiveConcurrencyLimiter } from "../core/Ingress.js";
14
+ import { bulkheadInterceptor, CircuitBreaker, deadlineInterceptor, isRetryable, loggingInterceptor, metricsInterceptor, runInterceptorChain, } from "../core/Interceptors.js";
15
+ import { RequestContext } from "../core/RequestContext.js";
16
+ import { createStrategy } from "../core/RoutingStrategy.js";
17
+ import { resolveRpcOptionsForTarget } from "../core/RpcConfig.js";
18
+ import { createSignatureCacheFromEnv } from "../core/SignatureCache.js";
19
+ import { getContract } from "./index.js";
20
+ const DEFAULT_MAX_SOCKETS = 256;
21
+ const DEFAULT_MAX_SOCKETS_PER_HOST = 64;
22
+ const DEFAULT_KEEPALIVE_MS = 30_000;
23
+ let _rpcAgent = null;
24
+ /**
25
+ * Get or create the shared HTTP agent for remote RPC connections.
26
+ * Uses connection pooling with keepalive to avoid per-request TCP setup.
27
+ */
28
+ export function _getRpcAgent(config) {
29
+ if (_rpcAgent)
30
+ return _rpcAgent;
31
+ const maxSockets = config?.maxSockets
32
+ ?? (parseInt(process.env.FORGE_RPC_MAX_SOCKETS || "", 10)
33
+ || DEFAULT_MAX_SOCKETS);
34
+ const maxSocketsPerHost = config?.maxSocketsPerHost
35
+ ?? (parseInt(process.env.FORGE_RPC_MAX_SOCKETS_PER_HOST || "", 10)
36
+ || DEFAULT_MAX_SOCKETS_PER_HOST);
37
+ const keepAliveMs = config?.keepAliveMs
38
+ ?? (parseInt(process.env.FORGE_RPC_KEEPALIVE_MS || "", 10)
39
+ || DEFAULT_KEEPALIVE_MS);
40
+ _rpcAgent = new http.Agent({
41
+ keepAlive: true,
42
+ keepAliveMsecs: keepAliveMs,
43
+ maxSockets: maxSocketsPerHost,
44
+ maxTotalSockets: maxSockets,
45
+ });
46
+ return _rpcAgent;
47
+ }
48
+ /**
49
+ * Destroy the shared RPC agent (for test teardown / graceful shutdown).
50
+ */
51
+ export function _destroyRpcAgent() {
52
+ if (_rpcAgent) {
53
+ _rpcAgent.destroy();
54
+ _rpcAgent = null;
55
+ }
56
+ }
57
+ // ─── Module state ───────────────────────────────────────────
58
+ const _WIRED_SERVICES = new WeakSet();
59
+ // P-PERF-29: Global signature cache for internal RPC optimization
60
+ let _signatureCache = null;
61
+ /**
62
+ * Initialize the signature cache from environment variables.
63
+ * Called once on module load.
64
+ */
65
+ function _initSignatureCache() {
66
+ if (_signatureCache)
67
+ return;
68
+ _signatureCache = createSignatureCacheFromEnv();
69
+ if (_signatureCache?.isEnabled()) {
70
+ console.log("[ThreadForge] Signature caching enabled for internal RPC (FORGE_CACHE_SIGNATURES=true)");
71
+ }
72
+ }
73
+ // Initialize signature cache on module load
74
+ _initSignatureCache();
75
+ /**
76
+ * Get or compute a signature for internal RPC.
77
+ * Uses the cache if enabled, otherwise computes per-request.
78
+ */
79
+ function _getInternalSignature() {
80
+ const internalSecret = process.env.FORGE_INTERNAL_SECRET;
81
+ if (!internalSecret) {
82
+ throw new Error("FORGE_INTERNAL_SECRET not set");
83
+ }
84
+ if (_signatureCache && _signatureCache.isEnabled()) {
85
+ return _signatureCache.getSignature("POST", "/__forge/invoke");
86
+ }
87
+ // Fallback: compute per-request (original behavior)
88
+ const ts = String(Date.now());
89
+ const sig = createHmac("sha256", internalSecret).update(`POST:/__forge/invoke:${ts}`).digest("hex");
90
+ return { signature: sig, timestamp: ts };
91
+ }
92
+ /**
93
+ * Stop the signature cache (for cleanup/testing).
94
+ */
95
+ export function _stopSignatureCache() {
96
+ if (_signatureCache) {
97
+ _signatureCache.stop();
98
+ _signatureCache = null;
99
+ }
100
+ }
101
+ // ─── LRU-style cache with oldest-20% eviction ────────────────────────
102
+ const MAX_CACHE_SIZE = 1000;
103
+ const EVICT_COUNT = Math.floor(MAX_CACHE_SIZE * 0.2); // 200
104
+ /**
105
+ * Evict the oldest 20% of entries from a Map whose values are
106
+ * `{ fn, order }` objects. Entries are sorted by insertion order
107
+ * and the lowest-order entries are removed.
108
+ */
109
+ function evictOldest(cache) {
110
+ if (cache.size <= MAX_CACHE_SIZE)
111
+ return;
112
+ // Map iteration order is insertion order, so the first EVICT_COUNT entries are oldest
113
+ let removed = 0;
114
+ for (const key of cache.keys()) {
115
+ if (removed >= EVICT_COUNT)
116
+ break;
117
+ cache.delete(key);
118
+ removed++;
119
+ }
120
+ }
121
+ // ─── Simple token-bucket rate limiter (per-route, per-IP) ────────────
122
+ const _rateLimitBuckets = new Map();
123
+ const MAX_RATE_LIMIT_BUCKETS = 100_000;
124
+ let _rateLimitCleanupTimer = null;
125
+ function _rateLimitCleanupFn() {
126
+ const now = Date.now();
127
+ for (const [key, bucket] of _rateLimitBuckets) {
128
+ if (now - bucket.windowStart > 120_000) {
129
+ _rateLimitBuckets.delete(key);
130
+ }
131
+ }
132
+ }
133
+ /** Start (or restart) the periodic rate-limit bucket cleanup timer. */
134
+ export function _startRateLimitCleanup() {
135
+ if (_rateLimitCleanupTimer)
136
+ return;
137
+ _rateLimitCleanupTimer = setInterval(_rateLimitCleanupFn, 60_000);
138
+ if (_rateLimitCleanupTimer && typeof _rateLimitCleanupTimer === "object" && "unref" in _rateLimitCleanupTimer) {
139
+ _rateLimitCleanupTimer.unref();
140
+ }
141
+ }
142
+ /** Stop the periodic rate-limit bucket cleanup timer (for test teardown). */
143
+ export function _stopRateLimitCleanup() {
144
+ if (_rateLimitCleanupTimer) {
145
+ clearInterval(_rateLimitCleanupTimer);
146
+ _rateLimitCleanupTimer = null;
147
+ }
148
+ }
149
+ // Auto-start the cleanup timer on module load
150
+ _startRateLimitCleanup();
151
+ function parseRateLimit(spec) {
152
+ const m = /^(\d+)\/(sec|min|hour)$/i.exec(spec);
153
+ if (!m)
154
+ return null;
155
+ const limit = parseInt(m[1], 10);
156
+ const windowMs = { sec: 1000, min: 60_000, hour: 3_600_000 }[m[2].toLowerCase()];
157
+ return { limit, windowMs };
158
+ }
159
+ let _lastEvictionCheck = 0;
160
+ function checkRateLimit(routeKey, ip, config) {
161
+ const now = Date.now();
162
+ if (_rateLimitBuckets.size > MAX_RATE_LIMIT_BUCKETS && now - _lastEvictionCheck >= 10_000) {
163
+ _lastEvictionCheck = now;
164
+ setImmediate(() => {
165
+ const cutoff = Date.now();
166
+ for (const [k, b] of _rateLimitBuckets) {
167
+ if (cutoff - b.windowStart > 120_000)
168
+ _rateLimitBuckets.delete(k);
169
+ }
170
+ });
171
+ }
172
+ const bucketKey = `${routeKey}:${ip}`;
173
+ let bucket = _rateLimitBuckets.get(bucketKey);
174
+ if (!bucket || now - bucket.windowStart >= config.windowMs) {
175
+ bucket = { count: 1, windowStart: now };
176
+ _rateLimitBuckets.set(bucketKey, bucket);
177
+ return true;
178
+ }
179
+ bucket.count++;
180
+ return bucket.count <= config.limit;
181
+ }
182
+ /** Sentinel returned by handleProxyRequest when the message isn't a proxy request. */
183
+ export const NOT_HANDLED = Symbol("NOT_HANDLED");
184
+ /**
185
+ * Per-target concurrency limiters, shared across all proxies on this node.
186
+ */
187
+ const _concurrencyLimiters = new Map();
188
+ function getConcurrencyLimiter(targetName, config = {}) {
189
+ if (!_concurrencyLimiters.has(targetName)) {
190
+ _concurrencyLimiters.set(targetName, new AdaptiveConcurrencyLimiter(targetName, config));
191
+ }
192
+ return _concurrencyLimiters.get(targetName);
193
+ }
194
+ /** Expose for status endpoints */
195
+ export function getAllConcurrencyStats() {
196
+ const stats = {};
197
+ for (const [name, limiter] of _concurrencyLimiters) {
198
+ stats[name] = limiter.stats;
199
+ }
200
+ return stats;
201
+ }
202
+ /**
203
+ * Build proxy clients for all services this service connects to.
204
+ */
205
+ export function buildServiceProxies(ctx, serviceClasses, localServices = new Map(), options = {}) {
206
+ const proxies = {};
207
+ const { rpcConfig, ...baseOptions } = options;
208
+ // Ensure the shared RPC connection pool is initialized
209
+ _getRpcAgent(options.pool);
210
+ for (const [serviceName, ServiceClass] of serviceClasses) {
211
+ if (serviceName === ctx.serviceName)
212
+ continue;
213
+ const contract = getContract(ServiceClass);
214
+ // Resolve per-target RPC options and merge into proxy options
215
+ const targetRpcOptions = resolveRpcOptionsForTarget(rpcConfig, serviceName);
216
+ const mergedOptions = {
217
+ ...baseOptions,
218
+ ...targetRpcOptions,
219
+ };
220
+ proxies[serviceName] = createServiceProxy(ctx, serviceName, contract, localServices.get(serviceName) ?? null, mergedOptions);
221
+ }
222
+ return proxies;
223
+ }
224
+ /**
225
+ * Create a proxy client for a single target service.
226
+ */
227
+ export function createServiceProxy(ctx, targetName, contract, localInstance, options) {
228
+ // Wire routing strategy to EndpointResolver if configured
229
+ const routingStrategy = contract?.routingStrategy ?? options.routingStrategy;
230
+ if (routingStrategy && ctx._endpointResolver) {
231
+ ctx._endpointResolver.setStrategy(targetName, createStrategy(routingStrategy));
232
+ }
233
+ // Build interceptor chain (retry is handled as an outer loop, not in the chain)
234
+ const interceptors = buildInterceptors(ctx, targetName, contract, options);
235
+ // Retry config (service-level default; per-method overrides in createProxiedMethod)
236
+ const retryConfig = contract?.retry ?? options.retry;
237
+ // Circuit breakers are per-endpoint (host:port), so different endpoints
238
+ // can trip independently. Colocated calls share a single CB per service.
239
+ const cbOptions = contract?.circuitBreaker ??
240
+ options.circuitBreaker ??
241
+ {};
242
+ const MAX_ENDPOINT_CBS = 100;
243
+ const endpointCircuitBreakers = new Map();
244
+ // Shared CB for colocated (non-network) calls
245
+ const colocatedCircuitBreaker = new CircuitBreaker(cbOptions);
246
+ /**
247
+ * Get or create a circuit breaker for a specific endpoint.
248
+ */
249
+ function getCircuitBreaker(endpointKey) {
250
+ if (!endpointKey)
251
+ return colocatedCircuitBreaker;
252
+ let cb = endpointCircuitBreakers.get(endpointKey);
253
+ if (!cb) {
254
+ // Cap to prevent unbounded growth
255
+ if (endpointCircuitBreakers.size >= MAX_ENDPOINT_CBS) {
256
+ // Evict the oldest entry
257
+ const firstKey = endpointCircuitBreakers.keys().next().value;
258
+ if (firstKey !== undefined) {
259
+ endpointCircuitBreakers.delete(firstKey);
260
+ }
261
+ }
262
+ cb = new CircuitBreaker(cbOptions);
263
+ endpointCircuitBreakers.set(endpointKey, cb);
264
+ }
265
+ return cb;
266
+ }
267
+ /** Remove a stale endpoint's CB */
268
+ function removeEndpointCB(endpointKey) {
269
+ endpointCircuitBreakers.delete(endpointKey);
270
+ }
271
+ if (contract) {
272
+ return buildContractProxy(ctx, targetName, contract, localInstance, interceptors, { getCircuitBreaker, removeEndpointCB, colocatedCircuitBreaker, endpointCircuitBreakers }, retryConfig);
273
+ }
274
+ return buildDynamicProxy(ctx, targetName, localInstance, interceptors, { getCircuitBreaker, removeEndpointCB, colocatedCircuitBreaker, endpointCircuitBreakers }, retryConfig);
275
+ }
276
+ /**
277
+ * Build the interceptor chain for a proxy.
278
+ *
279
+ * Returns an array with a null placeholder at index 1 for the
280
+ * per-attempt circuit breaker interceptor. This avoids allocating
281
+ * a new array on every call — the caller just swaps slot [1].
282
+ *
283
+ * Layout: [deadline, CB_SLOT, concurrency, metrics?, logging]
284
+ */
285
+ function buildInterceptors(ctx, targetName, contract, options) {
286
+ const chain = [];
287
+ // Deadline propagation (always on)
288
+ const timeout = contract?.defaultTimeout ??
289
+ options.defaultTimeout ??
290
+ 5000;
291
+ chain.push(deadlineInterceptor(timeout));
292
+ // Slot [1] reserved for circuit breaker (swapped per-attempt)
293
+ chain.push(null);
294
+ // Bulkhead: limits concurrent outgoing calls per service
295
+ const bulkheadConfig = contract?.bulkhead ??
296
+ options.bulkhead ??
297
+ {};
298
+ chain.push(bulkheadInterceptor(bulkheadConfig));
299
+ // Adaptive concurrency limiting (always on for non-local services)
300
+ const concurrencyConfig = contract?.concurrency ??
301
+ options.concurrency ??
302
+ {};
303
+ const limiter = getConcurrencyLimiter(targetName, concurrencyConfig);
304
+ chain.push(concurrencyInterceptor(limiter));
305
+ // Metrics (always on if available)
306
+ if (ctx.metrics) {
307
+ chain.push(metricsInterceptor(ctx.metrics));
308
+ }
309
+ // Logging (debug level)
310
+ chain.push(loggingInterceptor(ctx.logger));
311
+ return chain;
312
+ }
313
+ /**
314
+ * Adaptive concurrency interceptor.
315
+ * Automatically wraps every proxy call with concurrency limiting.
316
+ * No manual wrapping needed — it's invisible to the developer.
317
+ */
318
+ function concurrencyInterceptor(limiter) {
319
+ return async function adaptiveConcurrency(callCtx, next) {
320
+ const slot = limiter.tryAcquire();
321
+ if (!slot.acquired) {
322
+ const err = new Error(`Service "${callCtx.target}" at concurrency limit (${limiter.limit}). ` +
323
+ `${limiter.inFlight} in flight. Rejecting ${callCtx.method}.`);
324
+ err.code = "CONCURRENCY_LIMIT";
325
+ err.statusCode = 503;
326
+ err.localAdmission = true;
327
+ throw err;
328
+ }
329
+ try {
330
+ const result = await next();
331
+ slot.release(true);
332
+ return result;
333
+ }
334
+ catch (err) {
335
+ slot.release(false);
336
+ throw err;
337
+ }
338
+ };
339
+ }
340
+ /**
341
+ * Build a contract-based proxy with typed methods.
342
+ */
343
+ function buildContractProxy(ctx, targetName, contract, localInstance, interceptors, cbBag, retryConfig) {
344
+ const proxy = {};
345
+ for (const [methodName, meta] of contract.methods) {
346
+ proxy[methodName] = createProxiedMethod(ctx, targetName, methodName, meta, localInstance, contract, interceptors, cbBag, retryConfig);
347
+ }
348
+ // Generic call method with private method guard, contract validation, and caching
349
+ const $callCache = new Map();
350
+ proxy.$call = (methodName, ...args) => {
351
+ if (typeof methodName !== "string" || methodName.startsWith("_")) {
352
+ throw new Error(`Cannot call private method "${methodName}" via $call`);
353
+ }
354
+ if (contract && !contract.methods.has(methodName)) {
355
+ throw new Error(`Method "${methodName}" is not exposed on ${targetName}`);
356
+ }
357
+ evictOldest($callCache);
358
+ let fn = $callCache.get(methodName);
359
+ if (!fn) {
360
+ const methodMeta = contract?.methods.get(methodName) ?? { name: methodName, options: {} };
361
+ fn = createProxiedMethod(ctx, targetName, methodName, methodMeta, localInstance, contract, interceptors, cbBag, retryConfig);
362
+ $callCache.set(methodName, fn);
363
+ }
364
+ return fn(...args);
365
+ };
366
+ // Invalidate method cache for hot-reload scenarios
367
+ proxy.$invalidate = () => {
368
+ $callCache.clear();
369
+ for (const key of Object.keys(proxy)) {
370
+ if (key.startsWith("$"))
371
+ continue;
372
+ delete proxy[key];
373
+ }
374
+ // Re-create proxied methods from contract
375
+ for (const [methodName, meta] of contract.methods) {
376
+ proxy[methodName] = createProxiedMethod(ctx, targetName, methodName, meta, localInstance, contract, interceptors, cbBag, retryConfig);
377
+ }
378
+ };
379
+ // Metadata
380
+ proxy.$name = targetName;
381
+ proxy.$methods = [...contract.methods.keys()];
382
+ proxy.$isLocal = !!localInstance;
383
+ proxy.$circuitBreaker = cbBag.colocatedCircuitBreaker;
384
+ proxy.$endpointCircuitBreakers = cbBag.endpointCircuitBreakers;
385
+ return proxy;
386
+ }
387
+ /**
388
+ * Build a dynamic proxy (no contract — any method name accepted).
389
+ */
390
+ function buildDynamicProxy(ctx, targetName, localInstance, interceptors, cbBag, retryConfig) {
391
+ const methodCache = new Map();
392
+ return new Proxy({}, {
393
+ get(_, methodName) {
394
+ if (methodName === "$name")
395
+ return targetName;
396
+ if (methodName === "$isLocal")
397
+ return !!localInstance;
398
+ if (methodName === "$circuitBreaker")
399
+ return cbBag.colocatedCircuitBreaker;
400
+ if (methodName === "$endpointCircuitBreakers")
401
+ return cbBag.endpointCircuitBreakers;
402
+ if (methodName === "$invalidate")
403
+ return () => {
404
+ methodCache.clear();
405
+ };
406
+ if (methodName === "then")
407
+ return undefined;
408
+ evictOldest(methodCache);
409
+ if (methodCache.has(methodName))
410
+ return methodCache.get(methodName);
411
+ const fn = createProxiedMethod(ctx, targetName, methodName, {}, localInstance, null, interceptors, cbBag, retryConfig);
412
+ methodCache.set(methodName, fn);
413
+ return fn;
414
+ },
415
+ });
416
+ }
417
+ /**
418
+ * Create a single proxied method with full interceptor + routing support.
419
+ *
420
+ * Retry is implemented as an outer loop around the interceptor chain so that
421
+ * each attempt gets a fresh chain execution (deadline, circuit breaker,
422
+ * metrics, logging all run on every attempt).
423
+ */
424
+ function createProxiedMethod(ctx, targetName, methodName, meta, localInstance, contract, interceptors, cbBag, retryConfig) {
425
+ // Resolve retry settings: per-method overrides service-level
426
+ const metaOptions = meta.options;
427
+ const methodRetry = metaOptions?.retry ?? retryConfig;
428
+ const retryDisabled = methodRetry === false;
429
+ const maxAttempts = retryDisabled ? 1 : (methodRetry?.maxAttempts ?? 3);
430
+ const baseDelayMs = retryDisabled ? 0 : (methodRetry?.baseDelayMs ?? 100);
431
+ const maxDelayMs = retryDisabled ? 0 : (methodRetry?.maxDelayMs ?? 2000);
432
+ const idempotent = metaOptions?.idempotent;
433
+ return async (...args) => {
434
+ // Enforce localOnly: reject remote calls for methods marked localOnly
435
+ if (metaOptions?.localOnly && !localInstance) {
436
+ throw new Error(`Method '${methodName}' on service '${targetName}' is marked localOnly and cannot be called remotely`);
437
+ }
438
+ // Capture the current RequestContext (from the calling service's request)
439
+ const rctx = RequestContext.current();
440
+ const callCtx = {
441
+ from: ctx.serviceName,
442
+ target: targetName,
443
+ method: methodName,
444
+ args,
445
+ deadline: rctx?.deadline ?? null,
446
+ timeout: metaOptions?.timeout ?? 5000,
447
+ metadata: Object.create(null),
448
+ attempt: 0,
449
+ // Flag for interceptor chain: colocated calls skip circuit breaker + bulkhead
450
+ isColocated: !!localInstance,
451
+ // Propagated context
452
+ correlationId: rctx?.correlationId ?? null,
453
+ traceId: rctx?.traceId ?? null,
454
+ auth: rctx?.auth ?? null,
455
+ };
456
+ let lastError;
457
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
458
+ callCtx.attempt = attempt;
459
+ // Resolve endpoint ONCE per attempt — used for both circuit breaker
460
+ // selection and the actual HTTP call. This avoids round-robin advancing
461
+ // twice, which would cause circuit breaker state to accumulate against
462
+ // a different endpoint than the one actually called.
463
+ let endpointKey = null;
464
+ let resolvedEndpoint = null;
465
+ if (!localInstance) {
466
+ const resolved = ctx._endpointResolver?.resolve(targetName) ?? null;
467
+ // RT-C2: Handle broadcast strategy returning all endpoints
468
+ if (Array.isArray(resolved) && resolved.length > 0) {
469
+ resolvedEndpoint = resolved[0];
470
+ // Fan out to all endpoints, return first successful response
471
+ const broadcastPromises = resolved.map(async (ep) => {
472
+ const headers = { "Content-Type": "application/json" };
473
+ const rctx = RequestContext.current();
474
+ if (rctx)
475
+ Object.assign(headers, rctx.toHeaders());
476
+ const internalSecret = process.env.FORGE_INTERNAL_SECRET;
477
+ if (internalSecret) {
478
+ const { signature: sig, timestamp: ts } = _getInternalSignature();
479
+ headers["x-forge-internal-sig"] = sig;
480
+ headers["x-forge-internal-ts"] = ts;
481
+ }
482
+ const resp = await fetch(`http://${ep.host}:${ep.port}/__forge/invoke`, {
483
+ method: "POST",
484
+ headers,
485
+ body: JSON.stringify({ method: methodName, args }),
486
+ signal: AbortSignal.timeout(callCtx.timeout ?? 5000),
487
+ keepalive: true,
488
+ });
489
+ const data = (await resp.json());
490
+ if (data.error)
491
+ throw new Error(data.error);
492
+ return data.result;
493
+ });
494
+ return Promise.any(broadcastPromises);
495
+ }
496
+ resolvedEndpoint = Array.isArray(resolved) ? null : resolved;
497
+ if (resolvedEndpoint) {
498
+ endpointKey = `${resolvedEndpoint.host}:${resolvedEndpoint.port}`;
499
+ }
500
+ else {
501
+ const port = ctx._servicePorts?.[targetName];
502
+ if (port)
503
+ endpointKey = `127.0.0.1:${port}`;
504
+ }
505
+ // COR-C1: Notify strategy of pending request (enables LeastPendingStrategy)
506
+ if (endpointKey && ctx._endpointResolver) {
507
+ ctx._endpointResolver.acquireEndpoint(targetName, endpointKey);
508
+ }
509
+ }
510
+ callCtx.resolvedEndpoint = resolvedEndpoint;
511
+ const circuitBreaker = cbBag.getCircuitBreaker(endpointKey);
512
+ // Abort early if circuit breaker is open and not yet ready for a probe
513
+ if (circuitBreaker.state === "open" && Date.now() < circuitBreaker.nextAttemptTime) {
514
+ const err = new Error(`Circuit breaker OPEN for ${targetName}.${methodName}` +
515
+ ` — service appears unavailable. Retry after ${new Date(circuitBreaker.nextAttemptTime).toISOString()}`);
516
+ err.code = "CIRCUIT_OPEN";
517
+ throw err;
518
+ }
519
+ // Build per-attempt interceptor chain with the correct circuit breaker.
520
+ // Must copy because concurrent calls share the `interceptors` template —
521
+ // mutating slot [1] in-place would race with other in-flight chains.
522
+ const attemptChain = interceptors.slice();
523
+ attemptChain[1] = circuitBreaker.createInterceptor();
524
+ let targetHost = "127.0.0.1";
525
+ let targetPort;
526
+ try {
527
+ const result = await runInterceptorChain(attemptChain, callCtx, async (finalCtx) => {
528
+ const extCtx = finalCtx;
529
+ // === Colocated: direct function call with context propagation ===
530
+ if (localInstance) {
531
+ // Validate method is exposed in the contract (prevents calling private methods via colocated dispatch)
532
+ if (contract && !contract.methods.has(methodName)) {
533
+ throw new Error(`Method '${methodName}' is not exposed on service '${targetName}'`);
534
+ }
535
+ const method = localInstance.service[methodName];
536
+ if (typeof method === "function") {
537
+ const callFn = () => {
538
+ if (rctx) {
539
+ const childCtx = rctx.child(targetName, methodName);
540
+ if (extCtx.deadline)
541
+ childCtx.deadline = extCtx.deadline;
542
+ return RequestContext.run(childCtx, () => method.apply(localInstance.service, args));
543
+ }
544
+ return method.apply(localInstance.service, args);
545
+ };
546
+ if (extCtx.deadline) {
547
+ const timeLeft = extCtx.deadline - Date.now();
548
+ if (timeLeft <= 0)
549
+ throw new Error(`Deadline exceeded for ${targetName}.${methodName}`);
550
+ let timer = null;
551
+ const timeoutPromise = new Promise((_, reject) => {
552
+ timer = setTimeout(() => reject(new Error(`Deadline exceeded for colocated call ${targetName}.${methodName}`)), timeLeft);
553
+ });
554
+ return Promise.race([callFn(), timeoutPromise]).finally(() => {
555
+ if (timer)
556
+ clearTimeout(timer);
557
+ });
558
+ }
559
+ return callFn();
560
+ }
561
+ throw new Error(`${targetName} has no method "${methodName}"`);
562
+ }
563
+ // === Remote: HTTP invoke when endpoint is known, otherwise IPC request fallback ===
564
+ // Calculate effective timeout respecting deadline.
565
+ let effectiveTimeout = extCtx.timeout ?? 5000;
566
+ if (extCtx.deadline) {
567
+ const remaining = extCtx.deadline - Date.now();
568
+ if (remaining <= 0)
569
+ throw new Error(`Deadline exceeded for ${targetName}.${methodName}`);
570
+ effectiveTimeout = Math.min(effectiveTimeout, remaining);
571
+ }
572
+ targetHost = "127.0.0.1";
573
+ targetPort = undefined;
574
+ // Use the endpoint resolved once at the top of this attempt
575
+ // (stored in callCtx) to avoid double round-robin advancement.
576
+ const endpoint = extCtx.resolvedEndpoint;
577
+ if (endpoint) {
578
+ targetHost = endpoint.host;
579
+ targetPort = endpoint.port;
580
+ }
581
+ else {
582
+ targetPort = ctx._servicePorts?.[targetName];
583
+ }
584
+ if (!targetPort) {
585
+ // Internal/background services may not expose HTTP ports. Use IPC request path.
586
+ if (typeof ctx.request === "function") {
587
+ const payload = {
588
+ __forge_method: methodName,
589
+ __forge_args: args,
590
+ };
591
+ if (extCtx.deadline) {
592
+ payload.__forge_deadline = extCtx.deadline;
593
+ }
594
+ return ctx.request(targetName, payload, effectiveTimeout);
595
+ }
596
+ throw new Error(`No endpoint known for service "${targetName}". ` +
597
+ `Check that it's listed in forge.config and has a port.`);
598
+ }
599
+ const headers = { "Content-Type": "application/json" };
600
+ if (rctx) {
601
+ Object.assign(headers, rctx.toHeaders());
602
+ // Override deadline with remaining time from callCtx (shrinks on retry)
603
+ if (extCtx.deadline) {
604
+ const remaining = extCtx.deadline - Date.now();
605
+ if (remaining <= 0) {
606
+ throw new Error(`Deadline exceeded for ${targetName}.${methodName}`);
607
+ }
608
+ headers["x-forge-deadline"] = String(remaining);
609
+ }
610
+ }
611
+ // P-PERF-29: Sign request with HMAC (uses cache if enabled)
612
+ const internalSecret = process.env.FORGE_INTERNAL_SECRET;
613
+ if (internalSecret) {
614
+ const { signature: sig, timestamp: ts } = _getInternalSignature();
615
+ headers["x-forge-internal-sig"] = sig;
616
+ headers["x-forge-internal-ts"] = ts;
617
+ }
618
+ const resp = await fetch(`http://${targetHost}:${targetPort}/__forge/invoke`, {
619
+ method: "POST",
620
+ headers,
621
+ body: JSON.stringify({
622
+ method: methodName,
623
+ args,
624
+ }),
625
+ signal: AbortSignal.timeout(effectiveTimeout),
626
+ keepalive: true,
627
+ });
628
+ // M-11: Enforce response body size limit to prevent OOM from oversized responses
629
+ const MAX_RESPONSE_SIZE = 10 * 1024 * 1024;
630
+ const contentLength = resp.headers.get("content-length");
631
+ if (contentLength && parseInt(contentLength, 10) > MAX_RESPONSE_SIZE) {
632
+ throw new Error(`Response from ${targetHost}:${targetPort} exceeds 10MB limit`);
633
+ }
634
+ // DEC-M4: Stream body with size limit to guard against chunked transfers without Content-Length
635
+ const reader = resp.body?.getReader();
636
+ if (!reader) {
637
+ throw new Error(`No response body from ${targetHost}:${targetPort}`);
638
+ }
639
+ const chunks = [];
640
+ let totalSize = 0;
641
+ for (;;) {
642
+ const { done, value } = await reader.read();
643
+ if (done)
644
+ break;
645
+ totalSize += value.byteLength;
646
+ if (totalSize > MAX_RESPONSE_SIZE) {
647
+ reader.cancel();
648
+ throw new Error(`Response from ${targetHost}:${targetPort} exceeds 10MB limit`);
649
+ }
650
+ chunks.push(value);
651
+ }
652
+ const bodyText = new TextDecoder().decode(chunks.length === 1 ? chunks[0] : Buffer.concat(chunks));
653
+ const data = JSON.parse(bodyText);
654
+ if (data.error) {
655
+ const err = new Error(data.error);
656
+ err.statusCode = resp.status;
657
+ if (typeof data.code === "string") {
658
+ err.code = data.code;
659
+ if (data.code === "CONCURRENCY_LIMIT" || data.code === "BULKHEAD_FULL") {
660
+ err.localAdmission = true;
661
+ }
662
+ }
663
+ throw err;
664
+ }
665
+ // REG-M2: Record success to reset failure counter
666
+ if (targetHost && targetPort && ctx._endpointResolver) {
667
+ ctx._endpointResolver.recordSuccess(targetHost, targetPort);
668
+ }
669
+ return data.result;
670
+ });
671
+ return result;
672
+ }
673
+ catch (err) {
674
+ lastError = err;
675
+ // REG-M2: Record connection failures for passive health checking
676
+ if (targetHost && targetPort && ctx._endpointResolver) {
677
+ const errCode = err.code;
678
+ if (errCode === "ECONNREFUSED" || errCode === "ECONNRESET" || errCode === "ETIMEDOUT" || errCode === "UND_ERR_CONNECT_TIMEOUT") {
679
+ ctx._endpointResolver.recordFailure(targetHost, targetPort);
680
+ }
681
+ }
682
+ // Never retry circuit-open errors — the breaker is tripped
683
+ if (err.code === "CIRCUIT_OPEN") {
684
+ throw err;
685
+ }
686
+ if (!isRetryable(err, idempotent) || attempt >= maxAttempts - 1) {
687
+ throw err;
688
+ }
689
+ // Check deadline before retrying
690
+ if (callCtx.deadline && Date.now() >= callCtx.deadline) {
691
+ throw new Error(`Deadline exceeded after ${attempt + 1} attempts to ${callCtx.target}.${callCtx.method}`);
692
+ }
693
+ // INT-M2: Full jitter per AWS best practice: random(0, min(maxDelay, base * 2^attempt))
694
+ let delay = Math.random() * Math.min(maxDelayMs, baseDelayMs * 2 ** attempt);
695
+ if (callCtx.deadline) {
696
+ const timeLeft = callCtx.deadline - Date.now();
697
+ if (timeLeft <= 0) {
698
+ throw new Error(`Deadline exceeded after ${attempt + 1} attempts to ${callCtx.target}.${callCtx.method}`);
699
+ }
700
+ delay = Math.min(delay, timeLeft);
701
+ }
702
+ await new Promise((resolve) => setTimeout(resolve, delay));
703
+ }
704
+ finally {
705
+ // COR-C1: Release pending slot so LeastPendingStrategy sees accurate counts
706
+ if (!localInstance && endpointKey && ctx._endpointResolver) {
707
+ ctx._endpointResolver.releaseEndpoint(targetName, endpointKey);
708
+ }
709
+ }
710
+ }
711
+ throw lastError;
712
+ };
713
+ }
714
+ // ─── Request Handler (receiving side) ───────────────────────
715
+ /**
716
+ * Handle an incoming proxy-style RPC request on the receiving service.
717
+ *
718
+ * Detects the __forge_method convention, checks deadlines,
719
+ * validates the method is exposed, and dispatches.
720
+ */
721
+ export function handleProxyRequest(service, from, payload) {
722
+ if (payload?.__forge_method) {
723
+ const methodName = payload.__forge_method;
724
+ const args = payload.__forge_args ?? [];
725
+ // Check deadline propagation (value is an absolute timestamp in ms)
726
+ let absoluteDeadline = null;
727
+ if (payload.__forge_deadline) {
728
+ absoluteDeadline = Number(payload.__forge_deadline);
729
+ const remaining = absoluteDeadline - Date.now();
730
+ if (remaining <= 0) {
731
+ throw new Error(`Deadline already exceeded for ${service.ctx?.serviceName}.${methodName}` + ` (caller: ${from})`);
732
+ }
733
+ }
734
+ // Contract/whitelist check first — reject unexposed methods before probing internals
735
+ const contract = getContract(service.constructor);
736
+ if (contract && !contract.methods.has(methodName)) {
737
+ throw new Error(`Method "${methodName}" is not exposed on this service`);
738
+ }
739
+ // Reject private methods when there is no contract to gate access
740
+ if (!contract && methodName.startsWith("_")) {
741
+ throw new Error(`Method "${methodName}" is private and cannot be invoked remotely`);
742
+ }
743
+ const method = service[methodName];
744
+ if (typeof method !== "function") {
745
+ throw new Error(`Service "${service.ctx?.serviceName}" has no method "${methodName}". ` +
746
+ `Available: ${getExposedMethods(service).join(", ") || "none"}`);
747
+ }
748
+ // Enforce deadline during execution if set
749
+ if (absoluteDeadline) {
750
+ const remaining = absoluteDeadline - Date.now();
751
+ if (remaining <= 0) {
752
+ throw new Error(`Deadline already exceeded for ${service.ctx?.serviceName}.${methodName} (caller: ${from})`);
753
+ }
754
+ let timer = null;
755
+ const timeoutPromise = new Promise((_, reject) => {
756
+ timer = setTimeout(() => reject(new Error("Deadline exceeded during execution")), remaining);
757
+ if (timer)
758
+ timer.unref();
759
+ });
760
+ // Re-check deadline immediately before race to close the window
761
+ const currentRemaining = absoluteDeadline - Date.now();
762
+ if (currentRemaining <= 0) {
763
+ clearTimeout(timer);
764
+ throw new Error("Deadline exceeded before execution start");
765
+ }
766
+ return Promise.race([method.apply(service, args), timeoutPromise]).finally(() => {
767
+ if (timer)
768
+ clearTimeout(timer);
769
+ });
770
+ }
771
+ return method.apply(service, args);
772
+ }
773
+ return NOT_HANDLED;
774
+ }
775
+ function getExposedMethods(service) {
776
+ const contract = getContract(service.constructor);
777
+ if (contract)
778
+ return [...contract.methods.keys()];
779
+ return Object.getOwnPropertyNames(Object.getPrototypeOf(service)).filter((m) => m !== "constructor" && !m.startsWith("_") && typeof service[m] === "function");
780
+ }
781
+ /**
782
+ * Auto-register HTTP routes from @Route decorator / contract metadata.
783
+ *
784
+ * For every route declared via `@Route(method, path)` or the plain-JS
785
+ * `static contract = { routes: [...] }`, this function registers the
786
+ * corresponding handler on the service's HTTP router.
787
+ *
788
+ * **Contract route handler signature:**
789
+ *
790
+ * async handler(body, params, query) -> result
791
+ *
792
+ * - `body` — parsed request body (`req.body`)
793
+ * - `params` — URL path parameters (`req.params`), e.g. `{ id: '42' }` for `/users/:id`
794
+ * - `query` — query-string parameters (`req.query`), e.g. `{ page: '2' }`
795
+ * - Return value is automatically serialized as JSON via `res.json(result)`.
796
+ * POST routes respond with 201; all other methods respond with 200.
797
+ * Errors are caught and returned as `{ error: message }` with 404 (if the
798
+ * message contains "not found") or 500.
799
+ *
800
+ * **How this differs from manual `ctx.router` routes:**
801
+ *
802
+ * When you register routes manually in `onStart(ctx)`, the handler receives
803
+ * the raw `(req, res)` pair and you are responsible for sending the response:
804
+ *
805
+ * ctx.router.get('/health', (req, res) => {
806
+ * res.json({ ok: true });
807
+ * });
808
+ *
809
+ * Contract-based handlers are higher-level: the framework supplies the
810
+ * individual pieces of the request as arguments and handles serialization
811
+ * and status codes for you.
812
+ *
813
+ * @example
814
+ * // ── Contract / decorator approach ──────────────────────
815
+ * // Handler receives (body, params, query) and returns a value.
816
+ *
817
+ * class UserService extends Service {
818
+ * \@Expose()
819
+ * \@Route('GET', '/users/:id')
820
+ * async getUser(body, params, query) {
821
+ * return this.db.findUser(params.id); // auto-serialized, 200 OK
822
+ * }
823
+ *
824
+ * \@Expose()
825
+ * \@Route('POST', '/users')
826
+ * async createUser(body, params, query) {
827
+ * return this.db.insert(body); // auto-serialized, 201 Created
828
+ * }
829
+ * }
830
+ *
831
+ * // ── Manual approach (in onStart) ──────────────────────
832
+ * // Handler receives (req, res) and must call res.json() itself.
833
+ *
834
+ * async onStart(ctx) {
835
+ * ctx.router.get('/users/:id', async (req, res) => {
836
+ * const user = await this.db.findUser(req.params.id);
837
+ * res.json(user); // you choose the status code
838
+ * });
839
+ * }
840
+ */
841
+ export function autoRegisterRoutes(service, ctx) {
842
+ const contract = getContract(service.constructor);
843
+ if (!contract || contract.routes.length === 0)
844
+ return;
845
+ for (const route of contract.routes) {
846
+ const handler = service[route.handlerName];
847
+ if (typeof handler !== "function") {
848
+ throw new Error(`Route handler "${route.handlerName}" not found on service`);
849
+ }
850
+ if (ctx.serviceType === "edge") {
851
+ const method = route.httpMethod.toLowerCase();
852
+ const routeRateLimit = route.rateLimit ? parseRateLimit(route.rateLimit) : null;
853
+ const routeKey = `${method}:${route.path}`;
854
+ ctx.router[method](route.path, async (req, res) => {
855
+ const rctx = req.ctx ??
856
+ (req.headers
857
+ ? RequestContext.fromPropagation(req.headers)
858
+ : new RequestContext());
859
+ // @Auth enforcement -- defense-in-depth even without ForgeProxy
860
+ if (route.auth) {
861
+ if (!rctx.auth) {
862
+ res.json({ error: "Authentication required" }, 401);
863
+ return;
864
+ }
865
+ // DEC-H1: Support role-based auth — auth string format "required:role1,role2"
866
+ if (typeof route.auth === "string" && route.auth.includes(":")) {
867
+ const roles = route.auth
868
+ .split(":")[1]
869
+ ?.split(",")
870
+ .map((r) => r.trim())
871
+ .filter(Boolean) ?? [];
872
+ if (roles.length > 0 && !roles.includes(rctx.auth.role)) {
873
+ res.json({ error: "Insufficient permissions" }, 403);
874
+ return;
875
+ }
876
+ }
877
+ }
878
+ // @RateLimit enforcement -- per-route, per-IP token bucket
879
+ if (routeRateLimit) {
880
+ const rawAddr = req.socket?.remoteAddress ?? "";
881
+ let ip;
882
+ // Trust X-Forwarded-For only from explicitly trusted proxies (FORGE_TRUSTED_PROXIES),
883
+ // or fall back to any private network if FORGE_TRUSTED_PROXIES is not configured.
884
+ const trustXff = process.env.FORGE_TRUSTED_PROXIES ? isTrustedProxy(rawAddr) : isPrivateNetwork(rawAddr);
885
+ if (trustXff) {
886
+ ip = req.headers?.["x-forwarded-for"]?.split(",")[0]?.trim() || rawAddr;
887
+ }
888
+ else {
889
+ ip = rawAddr || "unknown";
890
+ }
891
+ if (!checkRateLimit(routeKey, ip, routeRateLimit)) {
892
+ res.json({ error: "Rate limit exceeded" }, 429);
893
+ return;
894
+ }
895
+ }
896
+ await RequestContext.run(rctx, async () => {
897
+ try {
898
+ const result = await handler.call(service, req.body, req.params, req.query);
899
+ const statusCode = route.httpMethod === "POST" ? 201 : 200;
900
+ res.json(result, statusCode);
901
+ }
902
+ catch (err) {
903
+ let message;
904
+ let code;
905
+ if (err == null) {
906
+ message = "Unknown error";
907
+ code = 500;
908
+ }
909
+ else if (typeof err === "string") {
910
+ message = err;
911
+ code = 500;
912
+ }
913
+ else {
914
+ message = err.message ?? "Unknown error";
915
+ if (err.statusCode) {
916
+ code = err.statusCode;
917
+ }
918
+ else if (message.toLowerCase().includes("not found")) {
919
+ code = 404;
920
+ }
921
+ else {
922
+ code = 500;
923
+ }
924
+ }
925
+ // Don't leak internals on 5xx errors
926
+ if (code >= 500) {
927
+ res.json({ error: "Internal server error" }, code);
928
+ }
929
+ else {
930
+ res.json({ error: message }, code);
931
+ }
932
+ }
933
+ });
934
+ });
935
+ }
936
+ }
937
+ }
938
+ /**
939
+ * Auto-wire event subscriptions from @On decorators.
940
+ */
941
+ export function autoWireSubscriptions(service, _ctx) {
942
+ if (_WIRED_SERVICES.has(service))
943
+ return; // Prevent double-wrapping on hot reload
944
+ const contract = getContract(service.constructor);
945
+ if (!contract || contract.subscriptions.length === 0)
946
+ return;
947
+ _WIRED_SERVICES.add(service);
948
+ const originalOnMessage = service.onMessage.bind(service);
949
+ service.onMessage = async (from, payload) => {
950
+ if (payload?.__forge_event) {
951
+ for (const sub of contract.subscriptions) {
952
+ if (sub.service === from && sub.event === payload.__forge_event) {
953
+ const handler = service[sub.handlerName];
954
+ if (typeof handler === "function") {
955
+ try {
956
+ await handler.call(service, payload.__forge_data);
957
+ }
958
+ catch (err) {
959
+ _ctx?.logger?.error?.(`Subscription handler ${sub.handlerName} failed for event ${sub.event}: ${err.message}`);
960
+ }
961
+ }
962
+ }
963
+ }
964
+ return;
965
+ }
966
+ return originalOnMessage(from, payload);
967
+ };
968
+ }
969
+ //# sourceMappingURL=ServiceProxy.js.map