threadforge 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. package/README.md +52 -20
  2. package/bin/forge.js +2 -1058
  3. package/bin/host-commands.d.ts +2 -0
  4. package/bin/host-commands.d.ts.map +1 -0
  5. package/bin/host-commands.js +7 -8
  6. package/bin/platform-commands.d.ts +2 -0
  7. package/bin/platform-commands.d.ts.map +1 -0
  8. package/bin/platform-commands.js +118 -36
  9. package/dist/cli/base-command.d.ts +12 -0
  10. package/dist/cli/base-command.d.ts.map +1 -0
  11. package/dist/cli/base-command.js +25 -0
  12. package/dist/cli/base-command.js.map +1 -0
  13. package/dist/cli/commands/build.d.ts +10 -0
  14. package/dist/cli/commands/build.d.ts.map +1 -0
  15. package/dist/cli/commands/build.js +110 -0
  16. package/dist/cli/commands/build.js.map +1 -0
  17. package/dist/cli/commands/deploy.d.ts +12 -0
  18. package/dist/cli/commands/deploy.d.ts.map +1 -0
  19. package/dist/cli/commands/deploy.js +143 -0
  20. package/dist/cli/commands/deploy.js.map +1 -0
  21. package/dist/cli/commands/dev.d.ts +10 -0
  22. package/dist/cli/commands/dev.d.ts.map +1 -0
  23. package/dist/cli/commands/dev.js +138 -0
  24. package/dist/cli/commands/dev.js.map +1 -0
  25. package/dist/cli/commands/generate.d.ts +10 -0
  26. package/dist/cli/commands/generate.d.ts.map +1 -0
  27. package/dist/cli/commands/generate.js +76 -0
  28. package/dist/cli/commands/generate.js.map +1 -0
  29. package/dist/cli/commands/host.d.ts +8 -0
  30. package/dist/cli/commands/host.d.ts.map +1 -0
  31. package/dist/cli/commands/host.js +20 -0
  32. package/dist/cli/commands/host.js.map +1 -0
  33. package/dist/cli/commands/init.d.ts +16 -0
  34. package/dist/cli/commands/init.d.ts.map +1 -0
  35. package/dist/cli/commands/init.js +246 -0
  36. package/dist/cli/commands/init.js.map +1 -0
  37. package/dist/cli/commands/platform.d.ts +8 -0
  38. package/dist/cli/commands/platform.d.ts.map +1 -0
  39. package/dist/cli/commands/platform.js +20 -0
  40. package/dist/cli/commands/platform.js.map +1 -0
  41. package/dist/cli/commands/restart.d.ts +8 -0
  42. package/dist/cli/commands/restart.d.ts.map +1 -0
  43. package/dist/cli/commands/restart.js +13 -0
  44. package/dist/cli/commands/restart.js.map +1 -0
  45. package/dist/cli/commands/scaffold/frontend.d.ts +10 -0
  46. package/dist/cli/commands/scaffold/frontend.d.ts.map +1 -0
  47. package/dist/cli/commands/scaffold/frontend.js +130 -0
  48. package/dist/cli/commands/scaffold/frontend.js.map +1 -0
  49. package/dist/cli/commands/scaffold/react.d.ts +7 -0
  50. package/dist/cli/commands/scaffold/react.d.ts.map +1 -0
  51. package/dist/cli/commands/scaffold/react.js +12 -0
  52. package/dist/cli/commands/scaffold/react.js.map +1 -0
  53. package/dist/cli/commands/scale.d.ts +8 -0
  54. package/dist/cli/commands/scale.d.ts.map +1 -0
  55. package/dist/cli/commands/scale.js +13 -0
  56. package/dist/cli/commands/scale.js.map +1 -0
  57. package/dist/cli/commands/start.d.ts +10 -0
  58. package/dist/cli/commands/start.d.ts.map +1 -0
  59. package/dist/cli/commands/start.js +71 -0
  60. package/dist/cli/commands/start.js.map +1 -0
  61. package/dist/cli/commands/status.d.ts +11 -0
  62. package/dist/cli/commands/status.d.ts.map +1 -0
  63. package/dist/cli/commands/status.js +60 -0
  64. package/dist/cli/commands/status.js.map +1 -0
  65. package/dist/cli/commands/stop.d.ts +10 -0
  66. package/dist/cli/commands/stop.d.ts.map +1 -0
  67. package/dist/cli/commands/stop.js +89 -0
  68. package/dist/cli/commands/stop.js.map +1 -0
  69. package/dist/cli/util/config-discovery.d.ts +8 -0
  70. package/dist/cli/util/config-discovery.d.ts.map +1 -0
  71. package/dist/cli/util/config-discovery.js +70 -0
  72. package/dist/cli/util/config-discovery.js.map +1 -0
  73. package/dist/cli/util/config-patcher.d.ts +17 -0
  74. package/dist/cli/util/config-patcher.d.ts.map +1 -0
  75. package/dist/cli/util/config-patcher.js +439 -0
  76. package/dist/cli/util/config-patcher.js.map +1 -0
  77. package/dist/cli/util/frontend-dev.d.ts +8 -0
  78. package/dist/cli/util/frontend-dev.d.ts.map +1 -0
  79. package/dist/cli/util/frontend-dev.js +117 -0
  80. package/dist/cli/util/frontend-dev.js.map +1 -0
  81. package/dist/cli/util/process.d.ts +5 -0
  82. package/dist/cli/util/process.d.ts.map +1 -0
  83. package/dist/cli/util/process.js +17 -0
  84. package/dist/cli/util/process.js.map +1 -0
  85. package/dist/cli/util/templates.d.ts +10 -0
  86. package/dist/cli/util/templates.d.ts.map +1 -0
  87. package/dist/cli/util/templates.js +157 -0
  88. package/dist/cli/util/templates.js.map +1 -0
  89. package/dist/core/AlertSink.d.ts +83 -0
  90. package/dist/core/AlertSink.d.ts.map +1 -0
  91. package/dist/core/AlertSink.js +126 -0
  92. package/dist/core/AlertSink.js.map +1 -0
  93. package/dist/core/DirectMessageBus.d.ts +88 -0
  94. package/dist/core/DirectMessageBus.d.ts.map +1 -0
  95. package/dist/core/DirectMessageBus.js +352 -0
  96. package/dist/core/DirectMessageBus.js.map +1 -0
  97. package/dist/core/EndpointResolver.d.ts +111 -0
  98. package/dist/core/EndpointResolver.d.ts.map +1 -0
  99. package/dist/core/EndpointResolver.js +336 -0
  100. package/dist/core/EndpointResolver.js.map +1 -0
  101. package/dist/core/ForgeContext.d.ts +221 -0
  102. package/dist/core/ForgeContext.d.ts.map +1 -0
  103. package/dist/core/ForgeContext.js +1169 -0
  104. package/dist/core/ForgeContext.js.map +1 -0
  105. package/dist/core/ForgeEndpoints.d.ts +71 -0
  106. package/dist/core/ForgeEndpoints.d.ts.map +1 -0
  107. package/dist/core/ForgeEndpoints.js +442 -0
  108. package/dist/core/ForgeEndpoints.js.map +1 -0
  109. package/dist/core/ForgeHost.d.ts +82 -0
  110. package/dist/core/ForgeHost.d.ts.map +1 -0
  111. package/dist/core/ForgeHost.js +107 -0
  112. package/dist/core/ForgeHost.js.map +1 -0
  113. package/dist/core/ForgePlatform.d.ts +96 -0
  114. package/dist/core/ForgePlatform.d.ts.map +1 -0
  115. package/dist/core/ForgePlatform.js +136 -0
  116. package/dist/core/ForgePlatform.js.map +1 -0
  117. package/dist/core/ForgeWebSocket.d.ts +56 -0
  118. package/dist/core/ForgeWebSocket.d.ts.map +1 -0
  119. package/dist/core/ForgeWebSocket.js +415 -0
  120. package/dist/core/ForgeWebSocket.js.map +1 -0
  121. package/dist/core/Ingress.d.ts +329 -0
  122. package/dist/core/Ingress.d.ts.map +1 -0
  123. package/dist/core/Ingress.js +694 -0
  124. package/dist/core/Ingress.js.map +1 -0
  125. package/dist/core/Interceptors.d.ts +134 -0
  126. package/dist/core/Interceptors.d.ts.map +1 -0
  127. package/dist/core/Interceptors.js +416 -0
  128. package/dist/core/Interceptors.js.map +1 -0
  129. package/dist/core/Logger.d.ts +20 -0
  130. package/dist/core/Logger.d.ts.map +1 -0
  131. package/dist/core/Logger.js +77 -0
  132. package/dist/core/Logger.js.map +1 -0
  133. package/dist/core/MessageBus.d.ts +15 -0
  134. package/dist/core/MessageBus.d.ts.map +1 -0
  135. package/dist/core/MessageBus.js +18 -0
  136. package/dist/core/MessageBus.js.map +1 -0
  137. package/dist/core/Prometheus.d.ts +80 -0
  138. package/dist/core/Prometheus.d.ts.map +1 -0
  139. package/dist/core/Prometheus.js +332 -0
  140. package/dist/core/Prometheus.js.map +1 -0
  141. package/dist/core/RequestContext.d.ts +214 -0
  142. package/dist/core/RequestContext.d.ts.map +1 -0
  143. package/dist/core/RequestContext.js +556 -0
  144. package/dist/core/RequestContext.js.map +1 -0
  145. package/dist/core/Router.d.ts +45 -0
  146. package/dist/core/Router.d.ts.map +1 -0
  147. package/dist/core/Router.js +285 -0
  148. package/dist/core/Router.js.map +1 -0
  149. package/dist/core/RoutingStrategy.d.ts +116 -0
  150. package/dist/core/RoutingStrategy.d.ts.map +1 -0
  151. package/dist/core/RoutingStrategy.js +306 -0
  152. package/dist/core/RoutingStrategy.js.map +1 -0
  153. package/dist/core/RpcConfig.d.ts +72 -0
  154. package/dist/core/RpcConfig.d.ts.map +1 -0
  155. package/dist/core/RpcConfig.js +127 -0
  156. package/dist/core/RpcConfig.js.map +1 -0
  157. package/dist/core/SignatureCache.d.ts +81 -0
  158. package/dist/core/SignatureCache.d.ts.map +1 -0
  159. package/dist/core/SignatureCache.js +172 -0
  160. package/dist/core/SignatureCache.js.map +1 -0
  161. package/dist/core/StaticFileServer.d.ts +34 -0
  162. package/dist/core/StaticFileServer.d.ts.map +1 -0
  163. package/dist/core/StaticFileServer.js +497 -0
  164. package/dist/core/StaticFileServer.js.map +1 -0
  165. package/dist/core/Supervisor.d.ts +198 -0
  166. package/dist/core/Supervisor.d.ts.map +1 -0
  167. package/dist/core/Supervisor.js +1418 -0
  168. package/dist/core/Supervisor.js.map +1 -0
  169. package/dist/core/ThreadAllocator.d.ts +52 -0
  170. package/dist/core/ThreadAllocator.d.ts.map +1 -0
  171. package/dist/core/ThreadAllocator.js +174 -0
  172. package/dist/core/ThreadAllocator.js.map +1 -0
  173. package/dist/core/WorkerChannelManager.d.ts +130 -0
  174. package/dist/core/WorkerChannelManager.d.ts.map +1 -0
  175. package/dist/core/WorkerChannelManager.js +956 -0
  176. package/dist/core/WorkerChannelManager.js.map +1 -0
  177. package/dist/core/config-enums.d.ts +41 -0
  178. package/dist/core/config-enums.d.ts.map +1 -0
  179. package/dist/core/config-enums.js +59 -0
  180. package/dist/core/config-enums.js.map +1 -0
  181. package/dist/core/config.d.ts +159 -0
  182. package/dist/core/config.d.ts.map +1 -0
  183. package/dist/core/config.js +694 -0
  184. package/dist/core/config.js.map +1 -0
  185. package/dist/core/host-config.d.ts +146 -0
  186. package/dist/core/host-config.d.ts.map +1 -0
  187. package/dist/core/host-config.js +312 -0
  188. package/dist/core/host-config.js.map +1 -0
  189. package/dist/core/ipc-errors.d.ts +27 -0
  190. package/dist/core/ipc-errors.d.ts.map +1 -0
  191. package/dist/core/ipc-errors.js +36 -0
  192. package/dist/core/ipc-errors.js.map +1 -0
  193. package/dist/core/network-utils.d.ts +35 -0
  194. package/dist/core/network-utils.d.ts.map +1 -0
  195. package/dist/core/network-utils.js +145 -0
  196. package/dist/core/network-utils.js.map +1 -0
  197. package/dist/core/platform-config.d.ts +142 -0
  198. package/dist/core/platform-config.d.ts.map +1 -0
  199. package/dist/core/platform-config.js +299 -0
  200. package/dist/core/platform-config.js.map +1 -0
  201. package/dist/decorators/ServiceProxy.d.ts +175 -0
  202. package/dist/decorators/ServiceProxy.d.ts.map +1 -0
  203. package/dist/decorators/ServiceProxy.js +969 -0
  204. package/dist/decorators/ServiceProxy.js.map +1 -0
  205. package/dist/decorators/index.d.ts +146 -0
  206. package/dist/decorators/index.d.ts.map +1 -0
  207. package/dist/decorators/index.js +545 -0
  208. package/dist/decorators/index.js.map +1 -0
  209. package/dist/deploy/NginxGenerator.d.ts +165 -0
  210. package/dist/deploy/NginxGenerator.d.ts.map +1 -0
  211. package/dist/deploy/NginxGenerator.js +781 -0
  212. package/dist/deploy/NginxGenerator.js.map +1 -0
  213. package/dist/deploy/PlatformManifestGenerator.d.ts +43 -0
  214. package/dist/deploy/PlatformManifestGenerator.d.ts.map +1 -0
  215. package/dist/deploy/PlatformManifestGenerator.js +80 -0
  216. package/dist/deploy/PlatformManifestGenerator.js.map +1 -0
  217. package/dist/deploy/RouteManifestGenerator.d.ts +42 -0
  218. package/dist/deploy/RouteManifestGenerator.d.ts.map +1 -0
  219. package/dist/deploy/RouteManifestGenerator.js +105 -0
  220. package/dist/deploy/RouteManifestGenerator.js.map +1 -0
  221. package/dist/deploy/index.d.ts +210 -0
  222. package/dist/deploy/index.d.ts.map +1 -0
  223. package/dist/deploy/index.js +918 -0
  224. package/dist/deploy/index.js.map +1 -0
  225. package/dist/frontend/FrontendDevLifecycle.d.ts +26 -0
  226. package/dist/frontend/FrontendDevLifecycle.d.ts.map +1 -0
  227. package/dist/frontend/FrontendDevLifecycle.js +60 -0
  228. package/dist/frontend/FrontendDevLifecycle.js.map +1 -0
  229. package/dist/frontend/FrontendPluginOrchestrator.d.ts +64 -0
  230. package/dist/frontend/FrontendPluginOrchestrator.d.ts.map +1 -0
  231. package/dist/frontend/FrontendPluginOrchestrator.js +167 -0
  232. package/dist/frontend/FrontendPluginOrchestrator.js.map +1 -0
  233. package/dist/frontend/SiteResolver.d.ts +33 -0
  234. package/dist/frontend/SiteResolver.d.ts.map +1 -0
  235. package/dist/frontend/SiteResolver.js +53 -0
  236. package/dist/frontend/SiteResolver.js.map +1 -0
  237. package/dist/frontend/StaticMountRegistry.d.ts +36 -0
  238. package/dist/frontend/StaticMountRegistry.d.ts.map +1 -0
  239. package/dist/frontend/StaticMountRegistry.js +94 -0
  240. package/dist/frontend/StaticMountRegistry.js.map +1 -0
  241. package/dist/frontend/index.d.ts +7 -0
  242. package/dist/frontend/index.d.ts.map +1 -0
  243. package/{src → dist}/frontend/index.js +4 -2
  244. package/dist/frontend/index.js.map +1 -0
  245. package/dist/frontend/pathUtils.d.ts +8 -0
  246. package/dist/frontend/pathUtils.d.ts.map +1 -0
  247. package/dist/frontend/pathUtils.js +17 -0
  248. package/dist/frontend/pathUtils.js.map +1 -0
  249. package/dist/frontend/plugins/index.d.ts +2 -0
  250. package/dist/frontend/plugins/index.d.ts.map +1 -0
  251. package/{src → dist}/frontend/plugins/index.js +1 -1
  252. package/dist/frontend/plugins/index.js.map +1 -0
  253. package/dist/frontend/plugins/viteFrontend.d.ts +51 -0
  254. package/dist/frontend/plugins/viteFrontend.d.ts.map +1 -0
  255. package/dist/frontend/plugins/viteFrontend.js +134 -0
  256. package/dist/frontend/plugins/viteFrontend.js.map +1 -0
  257. package/dist/frontend/types.d.ts +25 -0
  258. package/dist/frontend/types.d.ts.map +1 -0
  259. package/dist/frontend/types.js +2 -0
  260. package/dist/frontend/types.js.map +1 -0
  261. package/dist/index.d.ts +17 -0
  262. package/dist/index.d.ts.map +1 -0
  263. package/dist/index.js +32 -0
  264. package/dist/index.js.map +1 -0
  265. package/dist/internals.d.ts +21 -0
  266. package/dist/internals.d.ts.map +1 -0
  267. package/{src → dist}/internals.js +12 -14
  268. package/dist/internals.js.map +1 -0
  269. package/dist/plugins/PluginManager.d.ts +209 -0
  270. package/dist/plugins/PluginManager.d.ts.map +1 -0
  271. package/dist/plugins/PluginManager.js +365 -0
  272. package/dist/plugins/PluginManager.js.map +1 -0
  273. package/dist/plugins/ScopedPostgres.d.ts +78 -0
  274. package/dist/plugins/ScopedPostgres.d.ts.map +1 -0
  275. package/dist/plugins/ScopedPostgres.js +190 -0
  276. package/dist/plugins/ScopedPostgres.js.map +1 -0
  277. package/dist/plugins/ScopedRedis.d.ts +88 -0
  278. package/dist/plugins/ScopedRedis.d.ts.map +1 -0
  279. package/dist/plugins/ScopedRedis.js +169 -0
  280. package/dist/plugins/ScopedRedis.js.map +1 -0
  281. package/dist/plugins/index.d.ts +289 -0
  282. package/dist/plugins/index.d.ts.map +1 -0
  283. package/dist/plugins/index.js +1942 -0
  284. package/dist/plugins/index.js.map +1 -0
  285. package/dist/plugins/types.d.ts +59 -0
  286. package/dist/plugins/types.d.ts.map +1 -0
  287. package/dist/plugins/types.js +2 -0
  288. package/dist/plugins/types.js.map +1 -0
  289. package/dist/registry/ServiceRegistry.d.ts +305 -0
  290. package/dist/registry/ServiceRegistry.d.ts.map +1 -0
  291. package/dist/registry/ServiceRegistry.js +735 -0
  292. package/dist/registry/ServiceRegistry.js.map +1 -0
  293. package/dist/scaling/ScaleAdvisor.d.ts +214 -0
  294. package/dist/scaling/ScaleAdvisor.d.ts.map +1 -0
  295. package/dist/scaling/ScaleAdvisor.js +526 -0
  296. package/dist/scaling/ScaleAdvisor.js.map +1 -0
  297. package/dist/services/Service.d.ts +164 -0
  298. package/dist/services/Service.d.ts.map +1 -0
  299. package/dist/services/Service.js +106 -0
  300. package/dist/services/Service.js.map +1 -0
  301. package/dist/services/worker-bootstrap.d.ts +15 -0
  302. package/dist/services/worker-bootstrap.d.ts.map +1 -0
  303. package/dist/services/worker-bootstrap.js +744 -0
  304. package/dist/services/worker-bootstrap.js.map +1 -0
  305. package/dist/templates/auth-service.d.ts +42 -0
  306. package/dist/templates/auth-service.d.ts.map +1 -0
  307. package/dist/templates/auth-service.js +54 -0
  308. package/dist/templates/auth-service.js.map +1 -0
  309. package/dist/templates/identity-service.d.ts +50 -0
  310. package/dist/templates/identity-service.d.ts.map +1 -0
  311. package/dist/templates/identity-service.js +62 -0
  312. package/dist/templates/identity-service.js.map +1 -0
  313. package/dist/types/contract.d.ts +120 -0
  314. package/dist/types/contract.d.ts.map +1 -0
  315. package/dist/types/contract.js +69 -0
  316. package/dist/types/contract.js.map +1 -0
  317. package/package.json +78 -20
  318. package/src/core/DirectMessageBus.js +0 -364
  319. package/src/core/EndpointResolver.js +0 -259
  320. package/src/core/ForgeContext.js +0 -2236
  321. package/src/core/ForgeHost.js +0 -122
  322. package/src/core/ForgePlatform.js +0 -145
  323. package/src/core/Ingress.js +0 -768
  324. package/src/core/Interceptors.js +0 -420
  325. package/src/core/MessageBus.js +0 -321
  326. package/src/core/Prometheus.js +0 -305
  327. package/src/core/RequestContext.js +0 -413
  328. package/src/core/RoutingStrategy.js +0 -330
  329. package/src/core/Supervisor.js +0 -1349
  330. package/src/core/ThreadAllocator.js +0 -196
  331. package/src/core/WorkerChannelManager.js +0 -879
  332. package/src/core/config.js +0 -637
  333. package/src/core/host-config.js +0 -311
  334. package/src/core/network-utils.js +0 -166
  335. package/src/core/platform-config.js +0 -308
  336. package/src/decorators/ServiceProxy.js +0 -904
  337. package/src/decorators/index.js +0 -571
  338. package/src/deploy/NginxGenerator.js +0 -865
  339. package/src/deploy/PlatformManifestGenerator.js +0 -96
  340. package/src/deploy/RouteManifestGenerator.js +0 -112
  341. package/src/deploy/index.js +0 -984
  342. package/src/frontend/FrontendDevLifecycle.js +0 -65
  343. package/src/frontend/FrontendPluginOrchestrator.js +0 -187
  344. package/src/frontend/SiteResolver.js +0 -63
  345. package/src/frontend/StaticMountRegistry.js +0 -90
  346. package/src/frontend/plugins/viteFrontend.js +0 -79
  347. package/src/frontend/types.js +0 -35
  348. package/src/index.js +0 -58
  349. package/src/plugins/PluginManager.js +0 -537
  350. package/src/plugins/ScopedPostgres.js +0 -192
  351. package/src/plugins/ScopedRedis.js +0 -142
  352. package/src/plugins/index.js +0 -1756
  353. package/src/registry/ServiceRegistry.js +0 -797
  354. package/src/scaling/ScaleAdvisor.js +0 -442
  355. package/src/services/Service.js +0 -195
  356. package/src/services/worker-bootstrap.js +0 -679
  357. package/src/templates/auth-service.js +0 -65
  358. package/src/templates/identity-service.js +0 -75
@@ -1,797 +0,0 @@
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
-
89
- import crypto from "node:crypto";
90
- import { EventEmitter } from "node:events";
91
- import os from "node:os";
92
-
93
- /**
94
- * @typedef {Object} ServiceRegistration
95
- * @property {string} name
96
- * @property {string} nodeId
97
- * @property {string} host
98
- * @property {{http?: number}} ports
99
- * @property {string|null} udsPath
100
- * @property {number} workers
101
- * @property {{methods: string[], events: string[]}} contract
102
- * @property {ServiceHealth} health
103
- * @property {Object} metadata
104
- */
105
-
106
- /**
107
- * @typedef {Object} ServiceHealth
108
- * @property {'healthy'|'degraded'|'unhealthy'|'draining'} status
109
- * @property {number} cpu
110
- * @property {number} memory
111
- * @property {number} rpcLatencyP50
112
- * @property {number} rpcLatencyP99
113
- * @property {number} pendingRequests
114
- * @property {number} lastHeartbeat
115
- */
116
-
117
- /**
118
- * @typedef {Object} ResolvedEndpoint
119
- * @property {'local'|'uds'|'http'} transport
120
- * @property {string} nodeId
121
- * @property {object} [instance] - for 'local'
122
- * @property {string} [path] - for 'uds'
123
- * @property {string} [address] - for 'http'
124
- * @property {ServiceHealth} health
125
- * @property {number} priority - lower = preferred
126
- */
127
-
128
- // ─── Base Registry ──────────────────────────────────────────
129
-
130
- export class ServiceRegistry extends EventEmitter {
131
- /**
132
- * @param {Object} options
133
- * @param {string} [options.mode='embedded'] - 'embedded' | 'multicast' | 'external'
134
- * @param {string} [options.nodeId] - Unique node identifier
135
- * @param {string} [options.host] - This node's reachable address
136
- * @param {number} [options.heartbeatIntervalMs=5000]
137
- * @param {number} [options.healthTimeoutMs=15000] - Mark unhealthy after this
138
- * @param {number} [options.httpBasePort=4000] - Base port for auto HTTP binding
139
- */
140
- constructor(options = {}) {
141
- super();
142
-
143
- this.mode = options.mode ?? "embedded";
144
- this.nodeId = options.nodeId ?? `node-${crypto.randomBytes(4).toString("hex")}`;
145
- this.host = options.host ?? getLocalIP();
146
- this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? 5000;
147
- this.healthTimeoutMs = options.healthTimeoutMs ?? 15000;
148
- this.httpBasePort = options.httpBasePort ?? 4000;
149
- this._clusterSecret = options.clusterSecret ?? process.env.FORGE_CLUSTER_SECRET ?? null;
150
- this.staleReapMultiplier = options.staleReapMultiplier ?? 3;
151
-
152
- /**
153
- * All known service registrations across all nodes.
154
- * Key: `${serviceName}@${nodeId}`
155
- * @type {Map<string, ServiceRegistration>}
156
- */
157
- this.registrations = new Map();
158
-
159
- /**
160
- * Services running on THIS node.
161
- * @type {Map<string, ServiceRegistration>}
162
- */
163
- this.localRegistrations = new Map();
164
-
165
- /**
166
- * Colocated service instances (same process).
167
- * @type {Map<string, {service: object, ctx: object}>}
168
- */
169
- this.localInstances = new Map();
170
-
171
- /** @type {NodeJS.Timeout|null} */
172
- this._heartbeatTimer = null;
173
-
174
- /** @type {NodeJS.Timeout|null} */
175
- this._reapTimer = null;
176
-
177
- /** @type {Object|null} - multicast/external backend */
178
- this._backend = null;
179
-
180
- /** @type {Map<string, NodeJS.Timeout>} pending drain timers from deregister() */
181
- this._drainTimers = new Map();
182
-
183
- /**
184
- * Split-brain protection: track known peer nodes for quorum checks.
185
- * @type {Map<string, number>} nodeId → last seen timestamp
186
- */
187
- this._knownPeers = new Map();
188
-
189
- /** Expected cluster size for quorum calculation (0 = disabled) */
190
- this._expectedClusterSize = parseInt(process.env.FORGE_EXPECTED_CLUSTER_SIZE || '0', 10);
191
- }
192
-
193
- /**
194
- * Start the registry. In embedded mode this is a no-op.
195
- * In multicast/external mode, connects to peers.
196
- */
197
- async start() {
198
- // Start heartbeat with jitter to prevent thundering herd across nodes
199
- const jitter = Math.floor(Math.random() * this.heartbeatIntervalMs * 0.2);
200
- this._heartbeatTimer = setInterval(() => {
201
- this._sendHeartbeats();
202
- }, this.heartbeatIntervalMs + jitter);
203
- this._heartbeatTimer.unref();
204
-
205
- // Start reaper (remove stale registrations)
206
- this._reapTimer = setInterval(() => {
207
- this._reapStale();
208
- }, this.healthTimeoutMs);
209
- this._reapTimer.unref();
210
-
211
- if (this.mode === "multicast") {
212
- await this._startMulticast();
213
- } else if (this.mode === "external") {
214
- await this._startExternal();
215
- }
216
-
217
- return this;
218
- }
219
-
220
- /**
221
- * Register a local service.
222
- * Called by the Supervisor when a service worker starts.
223
- */
224
- register(registration) {
225
- const key = `${registration.name}@${this.nodeId}`;
226
-
227
- const reg = {
228
- ...registration,
229
- nodeId: this.nodeId,
230
- host: this.host,
231
- health: {
232
- status: "healthy",
233
- cpu: 0,
234
- memory: 0,
235
- rpcLatencyP50: 0,
236
- rpcLatencyP99: 0,
237
- pendingRequests: 0,
238
- lastHeartbeat: Date.now(),
239
- ...registration.health,
240
- },
241
- metadata: {
242
- version: process.env.npm_package_version ?? "0.0.0",
243
- region: process.env.FORGE_REGION ?? "local",
244
- startedAt: Date.now(),
245
- ...registration.metadata,
246
- },
247
- };
248
-
249
- this.registrations.set(key, reg);
250
- this.localRegistrations.set(registration.name, reg);
251
-
252
- this.emit("registered", reg);
253
- this._announceToNetwork(reg);
254
-
255
- return reg;
256
- }
257
-
258
- /**
259
- * Register a colocated service instance (same process).
260
- */
261
- registerLocal(name, instance) {
262
- this.localInstances.set(name, instance);
263
- }
264
-
265
- /**
266
- * Deregister a service (shutting down).
267
- */
268
- deregister(serviceName) {
269
- const key = `${serviceName}@${this.nodeId}`;
270
- const reg = this.registrations.get(key);
271
-
272
- if (reg) {
273
- reg.health.status = "draining";
274
- this.emit("deregistering", reg);
275
-
276
- // Announce to network so other nodes stop routing to us
277
- this._announceToNetwork(reg);
278
-
279
- // Remove after drain period
280
- const drainTimer = setTimeout(() => {
281
- this._drainTimers.delete(key);
282
- this.registrations.delete(key);
283
- this.localRegistrations.delete(serviceName);
284
- this.localInstances.delete(serviceName);
285
- this.emit("deregistered", { name: serviceName, nodeId: this.nodeId });
286
- }, 5000);
287
- drainTimer.unref();
288
- this._drainTimers.set(key, drainTimer);
289
- }
290
- }
291
-
292
- /**
293
- * Receive a registration from another node.
294
- */
295
- receiveRemoteRegistration(reg) {
296
- if (!reg || typeof reg.name !== 'string' || typeof reg.nodeId !== 'string') return;
297
- if (!reg.host || typeof reg.host !== 'string') return;
298
- if (!reg.health || typeof reg.health.status !== 'string') return;
299
-
300
- if (reg.nodeId === this.nodeId) return; // ignore self
301
-
302
- const key = `${reg.name}@${reg.nodeId}`;
303
- const existing = this.registrations.get(key);
304
-
305
- // Set lastSeen to local clock to avoid clock skew issues with remote timestamps
306
- reg.lastSeen = Date.now();
307
-
308
- // Track peer nodes for split-brain detection
309
- this._knownPeers.set(reg.nodeId, Date.now());
310
-
311
- this.registrations.set(key, reg);
312
-
313
- if (!existing) {
314
- this.emit("discovered", reg);
315
- }
316
- }
317
-
318
- /**
319
- * Check if we can see a quorum of the expected cluster.
320
- * Returns true if quorum is met or split-brain detection is disabled.
321
- * A node should avoid serving traffic if it's partitioned from the majority.
322
- */
323
- hasQuorum() {
324
- if (this._expectedClusterSize <= 0) return true;
325
-
326
- // Count recently-seen peers (within 2x health timeout)
327
- const cutoff = Date.now() - this.healthTimeoutMs * 2;
328
- let activePeers = 0;
329
- for (const [, lastSeen] of this._knownPeers) {
330
- if (lastSeen > cutoff) activePeers++;
331
- }
332
-
333
- // +1 for self
334
- const visibleNodes = activePeers + 1;
335
- const quorum = Math.floor(this._expectedClusterSize / 2) + 1;
336
- return visibleNodes >= quorum;
337
- }
338
-
339
- /**
340
- * Update health for a local service.
341
- */
342
- updateHealth(serviceName, health) {
343
- const reg = this.localRegistrations.get(serviceName);
344
- if (reg) {
345
- Object.assign(reg.health, health, { lastHeartbeat: Date.now() });
346
- }
347
- }
348
-
349
- // ─── Resolution ─────────────────────────────────────────
350
-
351
- /**
352
- * Resolve endpoints for a service, ordered by preference.
353
- *
354
- * Returns all known instances with their transport type and
355
- * health status, sorted by priority:
356
- * 1. Local (colocated, same process)
357
- * 2. UDS (same machine, different process)
358
- * 3. HTTP (remote, same region)
359
- * 4. HTTP (remote, different region)
360
- *
361
- * @param {string} serviceName
362
- * @returns {ResolvedEndpoint[]}
363
- */
364
- resolve(serviceName) {
365
- const endpoints = [];
366
-
367
- const isColocated = this.localInstances.has(serviceName);
368
- let localAdded = false;
369
-
370
- for (const [_key, reg] of this.registrations) {
371
- if (reg.name !== serviceName) continue;
372
- if (reg.health.status === "unhealthy") continue;
373
-
374
- const isLocal = reg.nodeId === this.nodeId;
375
- const isDraining = reg.health.status === "draining";
376
-
377
- if (isDraining) continue;
378
-
379
- if (isColocated && isLocal) {
380
- if (!localAdded) {
381
- endpoints.push({
382
- transport: "local",
383
- nodeId: reg.nodeId,
384
- instance: this.localInstances.get(serviceName),
385
- health: reg.health,
386
- priority: 0, // highest priority
387
- });
388
- localAdded = true;
389
- }
390
- } else if (isLocal && reg.udsPath) {
391
- endpoints.push({
392
- transport: "uds",
393
- nodeId: reg.nodeId,
394
- path: reg.udsPath,
395
- health: reg.health,
396
- priority: 1,
397
- });
398
- } else if (reg.ports?.http) {
399
- const sameRegion = reg.metadata?.region === (process.env.FORGE_REGION ?? "local");
400
- endpoints.push({
401
- transport: "http",
402
- nodeId: reg.nodeId,
403
- address: `${reg.host}:${reg.ports.http}`,
404
- health: reg.health,
405
- priority: sameRegion ? 2 : 3,
406
- });
407
- }
408
- }
409
-
410
- // Sort by priority, then by health
411
- endpoints.sort((a, b) => {
412
- if (a.priority !== b.priority) return a.priority - b.priority;
413
- // Prefer lower latency
414
- return (a.health.rpcLatencyP50 ?? 0) - (b.health.rpcLatencyP50 ?? 0);
415
- });
416
-
417
- return endpoints;
418
- }
419
-
420
- /**
421
- * Resolve the BEST single endpoint for a service.
422
- * Used by the proxy when making a call.
423
- *
424
- * @param {string} serviceName
425
- * @returns {ResolvedEndpoint|null}
426
- */
427
- resolveBest(serviceName) {
428
- const endpoints = this.resolve(serviceName);
429
- return endpoints[0] ?? null;
430
- }
431
-
432
- /**
433
- * Get all known services and their locations.
434
- */
435
- topology() {
436
- const services = new Map();
437
-
438
- for (const [, reg] of this.registrations) {
439
- if (!services.has(reg.name)) {
440
- services.set(reg.name, []);
441
- }
442
- services.get(reg.name).push({
443
- nodeId: reg.nodeId,
444
- host: reg.host,
445
- transport: this.localInstances?.has(reg.name) ? "colocated"
446
- : reg.nodeId === this.nodeId ? "local"
447
- : "http",
448
- status: reg.health.status,
449
- cpu: reg.health.cpu,
450
- workers: reg.workers,
451
- });
452
- }
453
-
454
- return Object.fromEntries(services);
455
- }
456
-
457
- // ─── Multicast Discovery ────────────────────────────────
458
-
459
- /**
460
- * Multicast mode: UDP broadcast on the local network.
461
- * Each node announces its services every heartbeat interval.
462
- * New nodes are discovered automatically.
463
- */
464
- async _startMulticast() {
465
- if (!this._clusterSecret) {
466
- if (process.env.NODE_ENV === 'production') {
467
- throw new Error('[Registry] FORGE_CLUSTER_SECRET is required for multicast discovery in production');
468
- }
469
- console.warn(
470
- "[Registry] FORGE_CLUSTER_SECRET not set — multicast messages are unauthenticated. Set a shared secret for production use.",
471
- );
472
- }
473
-
474
- const dgram = await import("node:dgram");
475
- const MULTICAST_ADDR = process.env.FORGE_MULTICAST_ADDRESS || "239.255.42.42";
476
- const MULTICAST_PORT = parseInt(process.env.FORGE_MULTICAST_PORT || "42042", 10);
477
-
478
- // Create UDP socket for sending/receiving announcements
479
- const socket = dgram.createSocket({ type: "udp4", reuseAddr: true });
480
-
481
- // Rate limiting: track message counts per source IP
482
- const _multicastRateCounts = new Map();
483
- const _multicastRateTimer = setInterval(() => { _multicastRateCounts.clear(); }, 1000);
484
- _multicastRateTimer.unref();
485
- this._multicastRateTimer = _multicastRateTimer;
486
-
487
- socket.on("message", (buf, rinfo) => {
488
- // 1. Size check FIRST
489
- if (buf.length > 8192) return;
490
-
491
- // 2. Rate limit per source IP: reject if > 10/sec
492
- const srcAddr = rinfo.address;
493
- const rateCount = (_multicastRateCounts.get(srcAddr) ?? 0) + 1;
494
- _multicastRateCounts.set(srcAddr, rateCount);
495
- if (rateCount > 10) return;
496
-
497
- // 3. JSON.parse
498
- let msg;
499
- try {
500
- msg = JSON.parse(buf.toString());
501
- } catch (err) {
502
- console.warn("[ServiceRegistry] Received malformed multicast message");
503
- return;
504
- }
505
-
506
- // 4. Signature verification BEFORE checking message structure
507
- if (this._clusterSecret) {
508
- const sig = msg.sig ?? "";
509
- const { sig: _, ...rest } = msg;
510
- const canonical = JSON.stringify(sortKeys(rest));
511
- const expected = crypto.createHmac("sha256", this._clusterSecret)
512
- .update(canonical)
513
- .digest("hex");
514
- try {
515
- const sigBuf = Buffer.from(sig, "hex");
516
- const expBuf = Buffer.from(expected, "hex");
517
- if (sigBuf.length !== expBuf.length || !crypto.timingSafeEqual(sigBuf, expBuf)) {
518
- return;
519
- }
520
- } catch {
521
- return;
522
- }
523
- }
524
-
525
- // 5. THEN check message type, nodeId, etc.
526
- if (!msg || typeof msg !== 'object' || !msg.type || msg.type !== 'forge:announce') {
527
- return;
528
- }
529
- if (msg.nodeId === this.nodeId) return; // Ignore own announcements
530
-
531
- // Replay protection: reject stale messages
532
- if (typeof msg.timestamp === 'number') {
533
- const MAX_MESSAGE_AGE = (this.heartbeatIntervalMs ?? 30000) * 2;
534
- if (Math.abs(Date.now() - msg.timestamp) > MAX_MESSAGE_AGE) {
535
- return;
536
- }
537
- }
538
-
539
- for (const reg of msg.services) {
540
- this.receiveRemoteRegistration(reg);
541
- }
542
- });
543
-
544
- await new Promise((resolve) => {
545
- socket.bind(MULTICAST_PORT, () => {
546
- socket.addMembership(MULTICAST_ADDR);
547
- resolve();
548
- });
549
- });
550
-
551
- this._multicastSocket = socket;
552
- this._multicastAddr = MULTICAST_ADDR;
553
- this._multicastPort = MULTICAST_PORT;
554
- }
555
-
556
- /**
557
- * Announce services to the network.
558
- */
559
- _announceToNetwork(reg) {
560
- if (this.mode === "multicast" && this._multicastSocket) {
561
- const allServices = [...this.localRegistrations.values()];
562
- const testPayload = JSON.stringify({
563
- type: "forge:announce",
564
- nodeId: this.nodeId,
565
- services: allServices,
566
- });
567
-
568
- if (Buffer.byteLength(testPayload) > 1400) {
569
- // Split services into chunks that fit in a UDP packet
570
- const chunks = [];
571
- let current = [];
572
- let currentSize = 0;
573
- for (const svc of allServices) {
574
- const svcSize = Buffer.byteLength(JSON.stringify(svc));
575
- if (currentSize + svcSize > 1200 && current.length > 0) {
576
- chunks.push(current);
577
- current = [];
578
- currentSize = 0;
579
- }
580
- current.push(svc);
581
- currentSize += svcSize;
582
- }
583
- if (current.length > 0) chunks.push(current);
584
-
585
- for (const chunk of chunks) {
586
- this._sendMulticastPayload(chunk);
587
- }
588
- } else {
589
- this._sendMulticastPayload(allServices);
590
- }
591
- }
592
-
593
- if (this.mode === "external" && this._backend) {
594
- this._backend.set(`forge/services/${reg.name}/${this.nodeId}`, JSON.stringify(reg), {
595
- ttl: Math.ceil(this.healthTimeoutMs / 1000),
596
- });
597
- }
598
- }
599
-
600
- _sendMulticastPayload(services) {
601
- const payload = {
602
- type: "forge:announce",
603
- nodeId: this.nodeId,
604
- services,
605
- timestamp: Date.now(),
606
- };
607
-
608
- // Sign with HMAC if cluster secret is configured
609
- // Use canonical JSON (recursive sorted keys) to ensure consistent signatures across environments
610
- if (this._clusterSecret) {
611
- const canonical = JSON.stringify(sortKeys(payload));
612
- const sig = crypto.createHmac("sha256", this._clusterSecret).update(canonical).digest("hex");
613
- payload.sig = sig;
614
- }
615
-
616
- const buf = Buffer.from(JSON.stringify(payload));
617
- this._multicastSocket.send(buf, this._multicastPort, this._multicastAddr);
618
- }
619
-
620
- // ─── External Backend ───────────────────────────────────
621
-
622
- async _startExternal() {
623
- throw new Error(
624
- 'External registry backend not yet implemented. Planned backends: Redis, etcd, Consul. ' +
625
- 'Use "embedded" or "multicast" mode for now.'
626
- );
627
- }
628
-
629
- /**
630
- * Set an external backend (Redis, etcd, etc.)
631
- *
632
- * The backend must implement:
633
- * get(key) → string
634
- * set(key, value, options) → void
635
- * watch(prefix, callback) → void
636
- * delete(key) → void
637
- */
638
- setBackend(backend) {
639
- this._backend = backend;
640
- }
641
-
642
- // ─── Health Management ──────────────────────────────────
643
-
644
- _sendHeartbeats() {
645
- for (const [, reg] of this.localRegistrations) {
646
- reg.health.lastHeartbeat = Date.now();
647
- reg.health.cpu = getCpuUsage();
648
- reg.health.memory = Math.round(process.memoryUsage().rss / 1024 / 1024);
649
- this._announceToNetwork(reg);
650
- }
651
- }
652
-
653
- _reapStale() {
654
- const now = Date.now();
655
-
656
- for (const [key, reg] of this.registrations) {
657
- if (reg.nodeId === this.nodeId) continue; // don't reap self
658
-
659
- // Use lastSeen (local receive time) instead of remote lastHeartbeat to avoid clock skew
660
- const age = now - (reg.lastSeen ?? reg.health?.lastHeartbeat ?? 0);
661
- if (age > this.healthTimeoutMs) {
662
- if (reg.health.status !== "unhealthy") {
663
- reg.health.status = "unhealthy";
664
- this.emit("unhealthy", reg);
665
- }
666
-
667
- // Remove after multiplier * timeout
668
- if (age > this.healthTimeoutMs * this.staleReapMultiplier) {
669
- this.registrations.delete(key);
670
- this.emit("removed", reg);
671
- }
672
- }
673
- }
674
-
675
- // Reap stale peers from split-brain tracking
676
- const peerCutoff = now - this.healthTimeoutMs * this.staleReapMultiplier;
677
- for (const [nodeId, lastSeen] of this._knownPeers) {
678
- if (lastSeen < peerCutoff) {
679
- this._knownPeers.delete(nodeId);
680
- }
681
- }
682
-
683
- // Emit warning if quorum is lost
684
- if (!this.hasQuorum()) {
685
- this.emit("quorum-lost", {
686
- expectedSize: this._expectedClusterSize,
687
- activePeers: this._knownPeers.size,
688
- nodeId: this.nodeId,
689
- });
690
- }
691
- }
692
-
693
- /**
694
- * Expose the topology via HTTP for gossip / dashboards.
695
- * The supervisor can mount this on its metrics server.
696
- */
697
- httpHandler(req, res) {
698
- if (req.url === "/_forge/topology") {
699
- res.writeHead(200, { "Content-Type": "application/json" });
700
- res.end(
701
- JSON.stringify(
702
- {
703
- nodeId: this.nodeId,
704
- host: this.host,
705
- mode: this.mode,
706
- registrations: [...this.registrations.values()],
707
- topology: this.topology(),
708
- },
709
- null,
710
- 2,
711
- ),
712
- );
713
- return true;
714
- }
715
-
716
- const parsedUrl = new URL(req.url, "http://localhost");
717
- if (parsedUrl.pathname === "/_forge/resolve" && req.method === "GET") {
718
- const service = parsedUrl.searchParams.get("service");
719
- if (service) {
720
- res.writeHead(200, { "Content-Type": "application/json" });
721
- res.end(JSON.stringify(this.resolve(service), null, 2));
722
- } else {
723
- res.writeHead(400);
724
- res.end("Missing ?service= parameter");
725
- }
726
- return true;
727
- }
728
-
729
- return false;
730
- }
731
-
732
- async stop() {
733
- if (this._heartbeatTimer) clearInterval(this._heartbeatTimer);
734
- if (this._reapTimer) clearInterval(this._reapTimer);
735
-
736
- // Clear all pending drain timers from deregister()
737
- for (const timer of this._drainTimers.values()) {
738
- clearTimeout(timer);
739
- }
740
- this._drainTimers.clear();
741
-
742
- // Immediately remove all local services (skip drain period during stop)
743
- for (const name of [...this.localRegistrations.keys()]) {
744
- const key = `${name}@${this.nodeId}`;
745
- this.registrations.delete(key);
746
- this.localRegistrations.delete(name);
747
- this.localInstances.delete(name);
748
- this.emit("deregistered", { name, nodeId: this.nodeId });
749
- }
750
-
751
- if (this._multicastRateTimer) {
752
- clearInterval(this._multicastRateTimer);
753
- }
754
- if (this._multicastSocket) {
755
- this._multicastSocket.close();
756
- }
757
- }
758
- }
759
-
760
- // ─── Utilities ──────────────────────────────────────────────
761
-
762
- function sortKeys(obj) {
763
- if (Array.isArray(obj)) return obj.map(sortKeys);
764
- if (obj && typeof obj === 'object') {
765
- return Object.keys(obj).sort().reduce((acc, k) => { acc[k] = sortKeys(obj[k]); return acc; }, {});
766
- }
767
- return obj;
768
- }
769
-
770
- function getLocalIP() {
771
- const interfaces = os.networkInterfaces();
772
- for (const name of Object.keys(interfaces)) {
773
- for (const iface of interfaces[name]) {
774
- if (iface.family === "IPv4" && !iface.internal) {
775
- return iface.address;
776
- }
777
- }
778
- }
779
- return "127.0.0.1";
780
- }
781
-
782
- let lastCpuUsage = process.cpuUsage();
783
- let lastCpuTime = Date.now();
784
-
785
- function getCpuUsage() {
786
- const now = Date.now();
787
- const elapsed = now - lastCpuTime;
788
- if (elapsed === 0) return 0;
789
-
790
- const usage = process.cpuUsage(lastCpuUsage);
791
- lastCpuUsage = process.cpuUsage();
792
- lastCpuTime = now;
793
-
794
- // user + system time in microseconds / elapsed wall time in microseconds
795
- const cpuPercent = ((usage.user + usage.system) / 1000 / elapsed) * 100;
796
- return Math.round(cpuPercent);
797
- }