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,735 @@
1
+ /**
2
+ * Service Registry
3
+ *
4
+ * The registry is the brain of horizontal scaling. It answers one
5
+ * question for every proxy call: WHERE is this service?
6
+ *
7
+ * ═══════════════════════════════════════════════════════════════
8
+ * GROWTH STORY
9
+ * ═══════════════════════════════════════════════════════════════
10
+ *
11
+ * PHASE 1: Single machine ($20/mo VPS)
12
+ * Registry mode: 'embedded'
13
+ * - Runs in-process inside the supervisor
14
+ * - All services are local (UDS or colocated)
15
+ * - Registry is just a Map in memory
16
+ * - Zero operational overhead
17
+ *
18
+ * PHASE 2: 2-3 machines ($100/mo)
19
+ * Registry mode: 'multicast'
20
+ * - Each supervisor announces its services via UDP multicast
21
+ * on the local network (or via a simple HTTP gossip protocol)
22
+ * - Nodes discover each other automatically
23
+ * - No external dependencies (no etcd, no consul)
24
+ * - Services that move to another machine are auto-discovered
25
+ *
26
+ * PHASE 3: 10+ machines (serious scale)
27
+ * Registry mode: 'external'
28
+ * - Pluggable backend: Redis, etcd, Consul, or managed service
29
+ * - Full service mesh capabilities
30
+ * - Health checks, circuit breakers, canary deployments
31
+ *
32
+ * The proxy layer doesn't know or care which phase you're in.
33
+ * this.users.getUser('123') works identically in all three.
34
+ *
35
+ * ═══════════════════════════════════════════════════════════════
36
+ * REGISTRATION PROTOCOL
37
+ * ═══════════════════════════════════════════════════════════════
38
+ *
39
+ * When a service starts, it registers:
40
+ *
41
+ * {
42
+ * name: 'users',
43
+ * nodeId: 'node-abc123',
44
+ * host: '10.0.1.5',
45
+ * ports: {
46
+ * http: 4001, // HTTP port (for remote calls via /__forge/invoke)
47
+ * },
48
+ * udsPath: '/tmp/forge-1234/users-1.sock', // local only
49
+ * workers: 4,
50
+ * contract: { // what methods are available
51
+ * methods: ['getUser', 'createUser', 'listUsers'],
52
+ * events: ['user.created'],
53
+ * },
54
+ * health: {
55
+ * status: 'healthy',
56
+ * cpu: 23, // percent
57
+ * memory: 156, // MB
58
+ * rpcLatencyP50: 2, // ms
59
+ * rpcLatencyP99: 18, // ms
60
+ * pendingRequests: 12,
61
+ * lastHeartbeat: 1707500000000,
62
+ * },
63
+ * metadata: {
64
+ * version: '1.2.3',
65
+ * region: 'us-east-1',
66
+ * startedAt: 1707500000000,
67
+ * },
68
+ * }
69
+ *
70
+ * ═══════════════════════════════════════════════════════════════
71
+ * TOPOLOGY RESOLUTION
72
+ * ═══════════════════════════════════════════════════════════════
73
+ *
74
+ * When a proxy call is made, the registry resolves the transport:
75
+ *
76
+ * registry.resolve('users')
77
+ * → [
78
+ * { transport: 'local', instance: ... }, // colocated
79
+ * { transport: 'uds', path: '/tmp/...' }, // same machine
80
+ * { transport: 'http', host: '10.0.1.5:4001' }, // remote
81
+ * ]
82
+ *
83
+ * The proxy picks the BEST option:
84
+ * 1. Colocated (same process) → always preferred
85
+ * 2. UDS (same machine) → preferred over network
86
+ * 3. HTTP (closest region) → fallback
87
+ */
88
+ import crypto from "node:crypto";
89
+ import { EventEmitter } from "node:events";
90
+ import os from "node:os";
91
+ import { RegistryMode } from "../core/config-enums.js";
92
+ // ─── Base Registry ──────────────────────────────────────────
93
+ export class ServiceRegistry extends EventEmitter {
94
+ mode;
95
+ nodeId;
96
+ host;
97
+ heartbeatIntervalMs;
98
+ healthTimeoutMs;
99
+ httpBasePort;
100
+ staleReapMultiplier;
101
+ registrations;
102
+ localRegistrations;
103
+ localInstances;
104
+ _clusterSecret;
105
+ _previousClusterSecret;
106
+ _heartbeatTimer;
107
+ _reapTimer;
108
+ _backend;
109
+ _drainTimers;
110
+ _knownPeers;
111
+ _expectedClusterSize;
112
+ // REG-M4: Flapping detection — consecutive healthy heartbeats before re-promoting
113
+ _healthyStreak;
114
+ _minHealthyThreshold;
115
+ _degraded;
116
+ _multicastSocket;
117
+ _multicastAddr;
118
+ _multicastPort;
119
+ _multicastRateTimer;
120
+ constructor(options = {}) {
121
+ super();
122
+ this.mode = options.mode ?? RegistryMode.EMBEDDED;
123
+ this.nodeId = options.nodeId ?? `node-${crypto.randomBytes(4).toString("hex")}`;
124
+ this.host = options.host ?? getLocalIP();
125
+ this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? 5000;
126
+ this.healthTimeoutMs = options.healthTimeoutMs ?? 15000;
127
+ this.httpBasePort = options.httpBasePort ?? 4000;
128
+ this._clusterSecret = options.clusterSecret ?? process.env.FORGE_CLUSTER_SECRET ?? null;
129
+ this._previousClusterSecret = null;
130
+ this.staleReapMultiplier = options.staleReapMultiplier ?? 3;
131
+ this.registrations = new Map();
132
+ this.localRegistrations = new Map();
133
+ this.localInstances = new Map();
134
+ this._heartbeatTimer = null;
135
+ this._reapTimer = null;
136
+ this._backend = null;
137
+ this._drainTimers = new Map();
138
+ this._knownPeers = new Map();
139
+ this._expectedClusterSize = parseInt(process.env.FORGE_EXPECTED_CLUSTER_SIZE || "0", 10);
140
+ // REG-M4: Require 3 consecutive healthy heartbeats before re-promoting
141
+ this._healthyStreak = new Map();
142
+ this._minHealthyThreshold = 3;
143
+ this._degraded = false;
144
+ }
145
+ /**
146
+ * Start the registry. In embedded mode this is a no-op.
147
+ * In multicast/external mode, connects to peers.
148
+ */
149
+ async start() {
150
+ // Start heartbeat with jitter to prevent thundering herd across nodes
151
+ const jitter = Math.floor(Math.random() * this.heartbeatIntervalMs * 0.2);
152
+ this._heartbeatTimer = setInterval(() => {
153
+ this._sendHeartbeats();
154
+ }, this.heartbeatIntervalMs + jitter);
155
+ this._heartbeatTimer.unref();
156
+ // Start reaper (remove stale registrations)
157
+ this._reapTimer = setInterval(() => {
158
+ this._reapStale();
159
+ }, this.healthTimeoutMs);
160
+ this._reapTimer.unref();
161
+ if (this.mode === RegistryMode.MULTICAST) {
162
+ await this._startMulticast();
163
+ }
164
+ else if (this.mode === RegistryMode.EXTERNAL) {
165
+ await this._startExternal();
166
+ }
167
+ return this;
168
+ }
169
+ /**
170
+ * Register a local service.
171
+ * Called by the Supervisor when a service worker starts.
172
+ */
173
+ register(registration) {
174
+ const key = `${registration.name}@${this.nodeId}`;
175
+ const reg = {
176
+ ...registration,
177
+ name: registration.name,
178
+ nodeId: this.nodeId,
179
+ host: this.host,
180
+ ports: registration.ports ?? {},
181
+ udsPath: registration.udsPath ?? null,
182
+ workers: registration.workers ?? 0,
183
+ contract: {
184
+ methods: [],
185
+ events: [],
186
+ ...registration.contract,
187
+ },
188
+ health: {
189
+ status: "healthy",
190
+ cpu: 0,
191
+ memory: 0,
192
+ rpcLatencyP50: 0,
193
+ rpcLatencyP99: 0,
194
+ pendingRequests: 0,
195
+ lastHeartbeat: Date.now(),
196
+ ...registration.health,
197
+ },
198
+ metadata: {
199
+ version: process.env.npm_package_version ?? "0.0.0",
200
+ region: process.env.FORGE_REGION ?? "local",
201
+ startedAt: Date.now(),
202
+ ...registration.metadata,
203
+ },
204
+ };
205
+ this.registrations.set(key, reg);
206
+ this.localRegistrations.set(registration.name, reg);
207
+ this.emit("registered", reg);
208
+ this._announceToNetwork(reg);
209
+ return reg;
210
+ }
211
+ /**
212
+ * Register a colocated service instance (same process).
213
+ */
214
+ registerLocal(name, instance) {
215
+ this.localInstances.set(name, instance);
216
+ }
217
+ /**
218
+ * Deregister a service (shutting down).
219
+ */
220
+ deregister(serviceName) {
221
+ const key = `${serviceName}@${this.nodeId}`;
222
+ const reg = this.registrations.get(key);
223
+ if (reg) {
224
+ reg.health.status = "draining";
225
+ this.emit("deregistering", reg);
226
+ // Announce to network so other nodes stop routing to us
227
+ this._announceToNetwork(reg);
228
+ // Remove after drain period
229
+ const drainTimer = setTimeout(() => {
230
+ this._drainTimers.delete(key);
231
+ this.registrations.delete(key);
232
+ this.localRegistrations.delete(serviceName);
233
+ this.localInstances.delete(serviceName);
234
+ this.emit("deregistered", { name: serviceName, nodeId: this.nodeId });
235
+ }, 5000);
236
+ drainTimer.unref();
237
+ this._drainTimers.set(key, drainTimer);
238
+ }
239
+ }
240
+ /**
241
+ * Receive a registration from another node.
242
+ */
243
+ receiveRemoteRegistration(reg) {
244
+ if (!reg || typeof reg.name !== "string" || typeof reg.nodeId !== "string")
245
+ return;
246
+ if (!reg.host || typeof reg.host !== "string")
247
+ return;
248
+ if (!reg.health || typeof reg.health.status !== "string")
249
+ return;
250
+ if (reg.nodeId === this.nodeId)
251
+ return; // ignore self
252
+ const key = `${reg.name}@${reg.nodeId}`;
253
+ const existing = this.registrations.get(key);
254
+ // Set lastSeen to local clock to avoid clock skew issues with remote timestamps
255
+ reg.lastSeen = Date.now();
256
+ // Track peer nodes for split-brain detection
257
+ this._knownPeers.set(reg.nodeId, Date.now());
258
+ // REG-M4: Flapping detection — require consecutive healthy heartbeats
259
+ // before re-promoting a previously unhealthy service
260
+ if (existing && existing.health.status === "unhealthy" && reg.health.status === "healthy") {
261
+ const streak = (this._healthyStreak.get(key) ?? 0) + 1;
262
+ this._healthyStreak.set(key, streak);
263
+ if (streak < this._minHealthyThreshold) {
264
+ // Keep as unhealthy until threshold is met; update lastSeen so it doesn't get reaped
265
+ existing.lastSeen = reg.lastSeen;
266
+ existing.health.lastHeartbeat = reg.health.lastHeartbeat;
267
+ return;
268
+ }
269
+ // Threshold met — promote to healthy and clear streak counter
270
+ this._healthyStreak.delete(key);
271
+ }
272
+ else if (reg.health.status !== "healthy") {
273
+ // Reset streak counter if not healthy
274
+ this._healthyStreak.delete(key);
275
+ }
276
+ else {
277
+ // Healthy and not previously unhealthy — clear any streak tracking
278
+ this._healthyStreak.delete(key);
279
+ }
280
+ this.registrations.set(key, reg);
281
+ if (!existing) {
282
+ this.emit("discovered", reg);
283
+ }
284
+ }
285
+ /**
286
+ * Check if we can see a quorum of the expected cluster.
287
+ * Returns true if quorum is met or split-brain detection is disabled.
288
+ * A node should avoid serving traffic if it's partitioned from the majority.
289
+ */
290
+ hasQuorum() {
291
+ if (this._expectedClusterSize <= 0)
292
+ return true;
293
+ // Count recently-seen peers (within 2x health timeout)
294
+ const cutoff = Date.now() - this.healthTimeoutMs * 2;
295
+ let activePeers = 0;
296
+ for (const [, lastSeen] of this._knownPeers) {
297
+ if (lastSeen > cutoff)
298
+ activePeers++;
299
+ }
300
+ // +1 for self
301
+ const visibleNodes = activePeers + 1;
302
+ const quorum = Math.floor(this._expectedClusterSize / 2) + 1;
303
+ return visibleNodes >= quorum;
304
+ }
305
+ /**
306
+ * Update health for a local service.
307
+ */
308
+ updateHealth(serviceName, health) {
309
+ const reg = this.localRegistrations.get(serviceName);
310
+ if (reg) {
311
+ Object.assign(reg.health, health, { lastHeartbeat: Date.now() });
312
+ }
313
+ }
314
+ // ─── Resolution ─────────────────────────────────────────
315
+ /**
316
+ * Resolve endpoints for a service, ordered by preference.
317
+ *
318
+ * Returns all known instances with their transport type and
319
+ * health status, sorted by priority:
320
+ * 1. Local (colocated, same process)
321
+ * 2. UDS (same machine, different process)
322
+ * 3. HTTP (remote, same region)
323
+ * 4. HTTP (remote, different region)
324
+ */
325
+ resolve(serviceName) {
326
+ const endpoints = [];
327
+ const isColocated = this.localInstances.has(serviceName);
328
+ let localAdded = false;
329
+ for (const [_key, reg] of this.registrations) {
330
+ if (reg.name !== serviceName)
331
+ continue;
332
+ if (reg.health.status === "unhealthy")
333
+ continue;
334
+ const isLocal = reg.nodeId === this.nodeId;
335
+ const isDraining = reg.health.status === "draining";
336
+ if (isDraining)
337
+ continue;
338
+ if (isColocated && isLocal) {
339
+ if (!localAdded) {
340
+ endpoints.push({
341
+ transport: "local",
342
+ nodeId: reg.nodeId,
343
+ instance: this.localInstances.get(serviceName),
344
+ health: reg.health,
345
+ priority: 0, // highest priority
346
+ });
347
+ localAdded = true;
348
+ }
349
+ }
350
+ else if (isLocal && reg.udsPath) {
351
+ endpoints.push({
352
+ transport: "uds",
353
+ nodeId: reg.nodeId,
354
+ path: reg.udsPath,
355
+ health: reg.health,
356
+ priority: 1,
357
+ });
358
+ }
359
+ else if (reg.ports?.http) {
360
+ const sameRegion = reg.metadata?.region === (process.env.FORGE_REGION ?? "local");
361
+ endpoints.push({
362
+ transport: "http",
363
+ nodeId: reg.nodeId,
364
+ address: `${reg.host}:${reg.ports.http}`,
365
+ health: reg.health,
366
+ priority: sameRegion ? 2 : 3,
367
+ });
368
+ }
369
+ }
370
+ // REG-M5: When degraded (quorum lost), exclude remote endpoints unless they are the only option
371
+ if (this._degraded) {
372
+ const localEndpoints = endpoints.filter(e => e.transport === "local" || e.transport === "uds");
373
+ if (localEndpoints.length > 0) {
374
+ localEndpoints.sort((a, b) => a.priority - b.priority);
375
+ return localEndpoints;
376
+ }
377
+ // Fall through to all endpoints if no local ones available
378
+ }
379
+ // Sort by priority, then by health
380
+ endpoints.sort((a, b) => {
381
+ if (a.priority !== b.priority)
382
+ return a.priority - b.priority;
383
+ // Prefer lower latency
384
+ return (a.health.rpcLatencyP50 ?? 0) - (b.health.rpcLatencyP50 ?? 0);
385
+ });
386
+ return endpoints;
387
+ }
388
+ /**
389
+ * Resolve the BEST single endpoint for a service.
390
+ * Used by the proxy when making a call.
391
+ */
392
+ resolveBest(serviceName) {
393
+ const endpoints = this.resolve(serviceName);
394
+ return endpoints[0] ?? null;
395
+ }
396
+ /**
397
+ * Get all known services and their locations.
398
+ */
399
+ topology() {
400
+ const services = new Map();
401
+ for (const [, reg] of this.registrations) {
402
+ if (!services.has(reg.name)) {
403
+ services.set(reg.name, []);
404
+ }
405
+ services.get(reg.name).push({
406
+ nodeId: reg.nodeId,
407
+ host: reg.host,
408
+ transport: this.localInstances?.has(reg.name) ? "colocated" : reg.nodeId === this.nodeId ? "local" : "http",
409
+ status: reg.health.status,
410
+ cpu: reg.health.cpu,
411
+ memory: reg.health.memory,
412
+ rpcLatencyP99: reg.health.rpcLatencyP99,
413
+ pendingRequests: reg.health.pendingRequests,
414
+ workers: reg.workers,
415
+ });
416
+ }
417
+ return Object.fromEntries(services);
418
+ }
419
+ // ─── Multicast Discovery ────────────────────────────────
420
+ /**
421
+ * Multicast mode: UDP broadcast on the local network.
422
+ * Each node announces its services every heartbeat interval.
423
+ * New nodes are discovered automatically.
424
+ */
425
+ async _startMulticast() {
426
+ if (!this._clusterSecret) {
427
+ if (process.env.NODE_ENV === "production") {
428
+ throw new Error("[Registry] FORGE_CLUSTER_SECRET is required for multicast discovery in production");
429
+ }
430
+ console.warn("[Registry] FORGE_CLUSTER_SECRET not set — multicast messages are unauthenticated. Set a shared secret for production use.");
431
+ }
432
+ const dgram = await import("node:dgram");
433
+ const MULTICAST_ADDR = process.env.FORGE_MULTICAST_ADDRESS || "239.255.42.42";
434
+ const MULTICAST_PORT = parseInt(process.env.FORGE_MULTICAST_PORT || "42042", 10);
435
+ // Create UDP socket for sending/receiving announcements
436
+ const socket = dgram.createSocket({ type: "udp4", reuseAddr: true });
437
+ // Rate limiting: track message counts per source IP
438
+ const _multicastRateCounts = new Map();
439
+ const _multicastRateTimer = setInterval(() => {
440
+ _multicastRateCounts.clear();
441
+ }, 1000);
442
+ _multicastRateTimer.unref();
443
+ this._multicastRateTimer = _multicastRateTimer;
444
+ socket.on("message", (buf, rinfo) => {
445
+ // 1. Size check FIRST
446
+ if (buf.length > 8192)
447
+ return;
448
+ // 2. Rate limit per source IP: reject if > 10/sec
449
+ const srcAddr = rinfo.address;
450
+ const rateCount = (_multicastRateCounts.get(srcAddr) ?? 0) + 1;
451
+ _multicastRateCounts.set(srcAddr, rateCount);
452
+ if (rateCount > 10)
453
+ return;
454
+ // 3. JSON.parse
455
+ let msg;
456
+ try {
457
+ msg = JSON.parse(buf.toString());
458
+ }
459
+ catch {
460
+ console.warn("[ServiceRegistry] Received malformed multicast message");
461
+ return;
462
+ }
463
+ // 4. Signature verification BEFORE checking message structure
464
+ if (this._clusterSecret) {
465
+ const sig = msg.sig ?? "";
466
+ const { sig: _, ...rest } = msg;
467
+ const canonical = JSON.stringify(sortKeys(rest));
468
+ const expected = crypto.createHmac("sha256", this._clusterSecret).update(canonical).digest("hex");
469
+ try {
470
+ const sigBuf = Buffer.from(sig, "hex");
471
+ const expBuf = Buffer.from(expected, "hex");
472
+ if (sigBuf.length !== expBuf.length || !crypto.timingSafeEqual(sigBuf, expBuf)) {
473
+ return;
474
+ }
475
+ }
476
+ catch {
477
+ return;
478
+ }
479
+ }
480
+ // 5. THEN check message type, nodeId, etc.
481
+ if (!msg || typeof msg !== "object" || !msg.type || msg.type !== "forge:announce") {
482
+ return;
483
+ }
484
+ if (msg.nodeId === this.nodeId)
485
+ return; // Ignore own announcements
486
+ // Replay protection: reject stale messages
487
+ if (typeof msg.timestamp === "number") {
488
+ const MAX_MESSAGE_AGE = (this.heartbeatIntervalMs ?? 30000) * 2;
489
+ if (Math.abs(Date.now() - msg.timestamp) > MAX_MESSAGE_AGE) {
490
+ return;
491
+ }
492
+ }
493
+ for (const reg of msg.services) {
494
+ this.receiveRemoteRegistration(reg);
495
+ }
496
+ });
497
+ await new Promise((resolve) => {
498
+ socket.bind(MULTICAST_PORT, () => {
499
+ socket.addMembership(MULTICAST_ADDR);
500
+ resolve();
501
+ });
502
+ });
503
+ this._multicastSocket = socket;
504
+ this._multicastAddr = MULTICAST_ADDR;
505
+ this._multicastPort = MULTICAST_PORT;
506
+ }
507
+ /**
508
+ * Announce services to the network.
509
+ */
510
+ _announceToNetwork(reg) {
511
+ if (this.mode === RegistryMode.MULTICAST && this._multicastSocket) {
512
+ const allServices = [...this.localRegistrations.values()];
513
+ const testPayload = JSON.stringify({
514
+ type: "forge:announce",
515
+ nodeId: this.nodeId,
516
+ services: allServices,
517
+ });
518
+ if (Buffer.byteLength(testPayload) > 1400) {
519
+ // Split services into chunks that fit in a UDP packet
520
+ const chunks = [];
521
+ let current = [];
522
+ let currentSize = 0;
523
+ for (const svc of allServices) {
524
+ const svcSize = Buffer.byteLength(JSON.stringify(svc));
525
+ if (currentSize + svcSize > 1200 && current.length > 0) {
526
+ chunks.push(current);
527
+ current = [];
528
+ currentSize = 0;
529
+ }
530
+ current.push(svc);
531
+ currentSize += svcSize;
532
+ }
533
+ if (current.length > 0)
534
+ chunks.push(current);
535
+ for (const chunk of chunks) {
536
+ this._sendMulticastPayload(chunk);
537
+ }
538
+ }
539
+ else {
540
+ this._sendMulticastPayload(allServices);
541
+ }
542
+ }
543
+ if (this.mode === RegistryMode.EXTERNAL && this._backend) {
544
+ this._backend.set(`forge/services/${reg.name}/${this.nodeId}`, JSON.stringify(reg), {
545
+ ttl: Math.ceil(this.healthTimeoutMs / 1000),
546
+ });
547
+ }
548
+ }
549
+ _sendMulticastPayload(services) {
550
+ const payload = {
551
+ type: "forge:announce",
552
+ nodeId: this.nodeId,
553
+ services,
554
+ timestamp: Date.now(),
555
+ };
556
+ // Sign with HMAC if cluster secret is configured
557
+ // Use canonical JSON (recursive sorted keys) to ensure consistent signatures across environments
558
+ if (this._clusterSecret) {
559
+ const canonical = JSON.stringify(sortKeys(payload));
560
+ const sig = crypto.createHmac("sha256", this._clusterSecret).update(canonical).digest("hex");
561
+ payload.sig = sig;
562
+ }
563
+ const buf = Buffer.from(JSON.stringify(payload));
564
+ this._multicastSocket.send(buf, this._multicastPort, this._multicastAddr);
565
+ }
566
+ // ─── External Backend ───────────────────────────────────
567
+ async _startExternal() {
568
+ throw new Error("External registry backend not yet implemented. Planned backends: Redis, etcd, Consul. " +
569
+ 'Use "embedded" or "multicast" mode for now.');
570
+ }
571
+ /**
572
+ * Set an external backend (Redis, etcd, etc.)
573
+ *
574
+ * The backend must implement:
575
+ * get(key) → string
576
+ * set(key, value, options) → void
577
+ * watch(prefix, callback) → void
578
+ * delete(key) → void
579
+ */
580
+ setBackend(backend) {
581
+ this._backend = backend;
582
+ }
583
+ // ─── Health Management ──────────────────────────────────
584
+ _sendHeartbeats() {
585
+ for (const [, reg] of this.localRegistrations) {
586
+ reg.health.lastHeartbeat = Date.now();
587
+ reg.health.cpu = getCpuUsage();
588
+ reg.health.memory = Math.round(process.memoryUsage().rss / 1024 / 1024);
589
+ this._announceToNetwork(reg);
590
+ }
591
+ }
592
+ _reapStale() {
593
+ const now = Date.now();
594
+ for (const [key, reg] of this.registrations) {
595
+ if (reg.nodeId === this.nodeId)
596
+ continue; // don't reap self
597
+ // Use lastSeen (local receive time) instead of remote lastHeartbeat to avoid clock skew
598
+ const age = now - (reg.lastSeen ?? reg.health?.lastHeartbeat ?? 0);
599
+ if (age > this.healthTimeoutMs) {
600
+ if (reg.health.status !== "unhealthy") {
601
+ reg.health.status = "unhealthy";
602
+ this.emit("unhealthy", reg);
603
+ }
604
+ // Remove after multiplier * timeout
605
+ if (age > this.healthTimeoutMs * this.staleReapMultiplier) {
606
+ this.registrations.delete(key);
607
+ this.emit("removed", reg);
608
+ }
609
+ }
610
+ }
611
+ // Reap stale peers from split-brain tracking
612
+ const peerCutoff = now - this.healthTimeoutMs * this.staleReapMultiplier;
613
+ for (const [nodeId, lastSeen] of this._knownPeers) {
614
+ if (lastSeen < peerCutoff) {
615
+ this._knownPeers.delete(nodeId);
616
+ }
617
+ }
618
+ // Emit warning if quorum is lost
619
+ if (!this.hasQuorum()) {
620
+ this.emit("quorum-lost", {
621
+ expectedSize: this._expectedClusterSize,
622
+ activePeers: this._knownPeers.size,
623
+ nodeId: this.nodeId,
624
+ });
625
+ }
626
+ }
627
+ /**
628
+ * Expose the topology via HTTP for gossip / dashboards.
629
+ * The supervisor can mount this on its metrics server.
630
+ */
631
+ httpHandler(req, res) {
632
+ if (req.url === "/_forge/topology") {
633
+ res.writeHead(200, { "Content-Type": "application/json" });
634
+ res.end(JSON.stringify({
635
+ nodeId: this.nodeId,
636
+ host: this.host,
637
+ mode: this.mode,
638
+ registrations: [...this.registrations.values()],
639
+ topology: this.topology(),
640
+ }, null, 2));
641
+ return true;
642
+ }
643
+ const parsedUrl = new URL(req.url, "http://localhost");
644
+ if (parsedUrl.pathname === "/_forge/resolve" && req.method === "GET") {
645
+ const service = parsedUrl.searchParams.get("service");
646
+ if (service) {
647
+ res.writeHead(200, { "Content-Type": "application/json" });
648
+ res.end(JSON.stringify(this.resolve(service), null, 2));
649
+ }
650
+ else {
651
+ res.writeHead(400);
652
+ res.end("Missing ?service= parameter");
653
+ }
654
+ return true;
655
+ }
656
+ return false;
657
+ }
658
+ /**
659
+ * REG-M3: Rotate the cluster secret at runtime.
660
+ * The current secret becomes the previous secret (accepted during rotation),
661
+ * and the new secret becomes the current one used for signing.
662
+ */
663
+ rotateSecret(newSecret) {
664
+ if (!newSecret || typeof newSecret !== 'string') {
665
+ throw new Error('New cluster secret must be a non-empty string');
666
+ }
667
+ this._previousClusterSecret = this._clusterSecret;
668
+ this._clusterSecret = newSecret;
669
+ }
670
+ async stop() {
671
+ if (this._heartbeatTimer)
672
+ clearInterval(this._heartbeatTimer);
673
+ if (this._reapTimer)
674
+ clearInterval(this._reapTimer);
675
+ // Clear all pending drain timers from deregister()
676
+ for (const timer of this._drainTimers.values()) {
677
+ clearTimeout(timer);
678
+ }
679
+ this._drainTimers.clear();
680
+ // Immediately remove all local services (skip drain period during stop)
681
+ for (const name of [...this.localRegistrations.keys()]) {
682
+ const key = `${name}@${this.nodeId}`;
683
+ this.registrations.delete(key);
684
+ this.localRegistrations.delete(name);
685
+ this.localInstances.delete(name);
686
+ this.emit("deregistered", { name, nodeId: this.nodeId });
687
+ }
688
+ if (this._multicastRateTimer) {
689
+ clearInterval(this._multicastRateTimer);
690
+ }
691
+ if (this._multicastSocket) {
692
+ this._multicastSocket.close();
693
+ }
694
+ }
695
+ }
696
+ // ─── Utilities ──────────────────────────────────────────────
697
+ function sortKeys(obj) {
698
+ if (Array.isArray(obj))
699
+ return obj.map(sortKeys);
700
+ if (obj && typeof obj === "object") {
701
+ return Object.keys(obj)
702
+ .sort()
703
+ .reduce((acc, k) => {
704
+ acc[k] = sortKeys(obj[k]);
705
+ return acc;
706
+ }, {});
707
+ }
708
+ return obj;
709
+ }
710
+ function getLocalIP() {
711
+ const interfaces = os.networkInterfaces();
712
+ for (const name of Object.keys(interfaces)) {
713
+ for (const iface of interfaces[name]) {
714
+ if (iface.family === "IPv4" && !iface.internal) {
715
+ return iface.address;
716
+ }
717
+ }
718
+ }
719
+ return "127.0.0.1";
720
+ }
721
+ let lastCpuUsage = process.cpuUsage();
722
+ let lastCpuTime = Date.now();
723
+ function getCpuUsage() {
724
+ const now = Date.now();
725
+ const elapsed = now - lastCpuTime;
726
+ if (elapsed === 0)
727
+ return 0;
728
+ const usage = process.cpuUsage(lastCpuUsage);
729
+ lastCpuUsage = process.cpuUsage();
730
+ lastCpuTime = now;
731
+ // user + system time in microseconds / elapsed wall time in microseconds
732
+ const cpuPercent = ((usage.user + usage.system) / 1000 / elapsed) * 100;
733
+ return Math.round(cpuPercent);
734
+ }
735
+ //# sourceMappingURL=ServiceRegistry.js.map