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,694 @@
1
+ /**
2
+ * ThreadForge Configuration v2
3
+ *
4
+ * Key design changes:
5
+ *
6
+ * 1. Services declare their TYPE:
7
+ * - "edge" → binds an HTTP port, receives external traffic
8
+ * - "internal" → no port, only reachable via IPC from other services
9
+ * - "background" → no port, no inbound IPC, runs tasks (cron, queue consumer)
10
+ *
11
+ * Only edge services need ports. Internal services live behind the
12
+ * IPC bus and are never directly exposed.
13
+ *
14
+ * 2. Services declare DEPENDENCIES (connects):
15
+ * Instead of a full mesh (N*(N-1)/2 channels), services declare
16
+ * which other services they talk to. This means 8 services might
17
+ * only need 8 channels instead of 28.
18
+ *
19
+ * 3. Services can be COLOCATED into process groups:
20
+ * Lightweight internal services can share a process. One Node.js
21
+ * event loop handles multiple services via in-process function calls
22
+ * (zero IPC overhead). Only CPU-heavy services need isolated processes.
23
+ *
24
+ * This means 8 services might run in 3 processes instead of 8,
25
+ * saving ~200MB of memory.
26
+ *
27
+ * 4. Thread allocation is per-PROCESS GROUP, not per-service.
28
+ */
29
+ import { validateRpcOptions } from "./RpcConfig.js";
30
+ import { LogLevel, LOG_LEVELS, REGISTRY_MODES, RoutingStrategyName, ROUTING_STRATEGY_NAMES, SERVICE_MODES, SERVICE_TYPES, ServiceMode, ServiceType, } from "./config-enums.js";
31
+ export { LogLevel, LOG_LEVELS, RegistryMode, REGISTRY_MODES, RoutingStrategyName, ROUTING_STRATEGY_NAMES, ServiceMode, SERVICE_MODES, ServiceType, SERVICE_TYPES, } from "./config-enums.js";
32
+ const WEBSOCKET_PATH_RE = /^\/[a-zA-Z0-9/_-]*$/;
33
+ function normalizeWebsocketConfig(serviceName, serviceType, websocket) {
34
+ if (websocket === undefined || websocket === null || websocket === false)
35
+ return null;
36
+ if (serviceType !== ServiceType.EDGE) {
37
+ throw new Error(`Service "${serviceName}": websocket config is only valid for edge services`);
38
+ }
39
+ let paths = ["/ws"];
40
+ let enabled = true;
41
+ if (websocket === true) {
42
+ paths = ["/ws"];
43
+ }
44
+ else if (Array.isArray(websocket)) {
45
+ paths = websocket;
46
+ }
47
+ else if (typeof websocket === "object") {
48
+ enabled = websocket.enabled !== false;
49
+ if (websocket.paths !== undefined) {
50
+ if (!Array.isArray(websocket.paths)) {
51
+ throw new Error(`Service "${serviceName}": websocket.paths must be an array`);
52
+ }
53
+ paths = websocket.paths;
54
+ }
55
+ }
56
+ else {
57
+ throw new Error(`Service "${serviceName}": websocket must be boolean, array of paths, or { enabled, paths } object`);
58
+ }
59
+ if (!enabled)
60
+ return { enabled: false, paths: [] };
61
+ if (paths.length === 0) {
62
+ throw new Error(`Service "${serviceName}": websocket.paths must include at least one path when enabled`);
63
+ }
64
+ const normalizedPaths = [...new Set(paths)].map((p) => {
65
+ if (typeof p !== "string" || !WEBSOCKET_PATH_RE.test(p)) {
66
+ throw new Error(`Service "${serviceName}": invalid websocket path "${p}". Use absolute paths containing [a-zA-Z0-9/_-]`);
67
+ }
68
+ return p;
69
+ });
70
+ return { enabled: true, paths: normalizedPaths };
71
+ }
72
+ /**
73
+ * Validate and normalize service config.
74
+ */
75
+ function normalizeService(name, config) {
76
+ const VALID_SERVICE_NAME = /^[a-z][a-z0-9_-]*(:[a-z][a-z0-9_-]*)?$/;
77
+ if (!VALID_SERVICE_NAME.test(name)) {
78
+ throw new Error(`Invalid service name "${name}": must start with a lowercase letter and contain only [a-z0-9_-] (or use appname:servicename format)`);
79
+ }
80
+ if (name.length > 63) {
81
+ throw new Error(`Service name "${name}" exceeds max length of 63 characters`);
82
+ }
83
+ const type = config.type ?? ServiceType.INTERNAL;
84
+ if (!SERVICE_TYPES.includes(type)) {
85
+ throw new Error(`Service "${name}" has invalid type: ${type}. Use: ${SERVICE_TYPES.join(", ")}`);
86
+ }
87
+ const modeInput = config.mode;
88
+ if (modeInput && !SERVICE_MODES.includes(modeInput)) {
89
+ const hint = modeInput === "worker"
90
+ ? ` mode 'worker' (worker_threads) is not yet implemented. Use mode '${ServiceMode.CLUSTER}' (default).`
91
+ : "";
92
+ throw new Error(`Service "${name}": invalid mode "${modeInput}".${hint} Valid modes: ${SERVICE_MODES.join(", ")}`);
93
+ }
94
+ // Remote services don't need an entry file — they're on another machine
95
+ if (type === ServiceType.REMOTE) {
96
+ if (!config.address) {
97
+ throw new Error(`Remote service "${name}" requires an address (e.g., 'http://billing.internal:4001')`);
98
+ }
99
+ if (!/^https?:\/\/.+/.test(config.address)) {
100
+ throw new Error(`Remote service "${name}": address must be a valid HTTP(S) URL, got "${config.address}"`);
101
+ }
102
+ try {
103
+ new URL(config.address);
104
+ }
105
+ catch {
106
+ throw new Error(`Remote service "${name}": address is not a parseable URL: "${config.address}"`);
107
+ }
108
+ // CFG-2: Validate routing.strategy for remote services too
109
+ const remoteRouting = { strategy: config.routing?.strategy ?? RoutingStrategyName.ROUND_ROBIN };
110
+ if (remoteRouting.strategy && !ROUTING_STRATEGY_NAMES.includes(remoteRouting.strategy)) {
111
+ throw new Error(`Service "${name}": invalid routing strategy "${remoteRouting.strategy}". Valid strategies: ${ROUTING_STRATEGY_NAMES.join(", ")}`);
112
+ }
113
+ if (config.connects && config.connects.length > 0) {
114
+ console.warn(`[ThreadForge] Warning: Remote service "${name}" specifies "connects" which has no effect. ` +
115
+ `Remote services are on another machine and do not participate in local channel resolution.`);
116
+ }
117
+ return {
118
+ name,
119
+ entry: null,
120
+ type: ServiceType.REMOTE,
121
+ port: null,
122
+ address: config.address,
123
+ connects: [],
124
+ group: null,
125
+ threads: 0,
126
+ weight: 0,
127
+ mode: ServiceMode.CLUSTER,
128
+ env: {},
129
+ routing: remoteRouting,
130
+ websocket: null,
131
+ };
132
+ }
133
+ if (!config.entry) {
134
+ throw new Error(`Service "${name}" is missing required field: entry`);
135
+ }
136
+ // Only edge services need a port
137
+ if (type === ServiceType.EDGE && !config.port) {
138
+ throw new Error(`Edge service "${name}" requires a port`);
139
+ }
140
+ if (type !== ServiceType.EDGE && config.port && !config.group && !config.remotePort) {
141
+ console.warn(`[ThreadForge] Note: Service "${name}" has type "${type}" with port ${config.port}. ` +
142
+ `This port will be used for cross-machine HTTP access (/__forge/invoke, /__forge/event).`);
143
+ }
144
+ if (config.group?.startsWith("_isolated:")) {
145
+ throw new Error(`Service "${name}": group name cannot start with "_isolated:" (reserved prefix)`);
146
+ }
147
+ if (config.env) {
148
+ if (typeof config.env !== "object" || Array.isArray(config.env)) {
149
+ throw new Error(`Service "${name}": env must be a plain object`);
150
+ }
151
+ for (const [k, v] of Object.entries(config.env)) {
152
+ if (typeof v !== "string") {
153
+ throw new Error(`Service "${name}": env.${k} must be a string, got ${typeof v}`);
154
+ }
155
+ }
156
+ }
157
+ const connects = config.connects ?? [];
158
+ if (!Array.isArray(connects)) {
159
+ throw new Error(`Service "${name}" connects must be an array of service names`);
160
+ }
161
+ for (const c of connects) {
162
+ if (typeof c !== "string" || c.length === 0) {
163
+ throw new Error(`Service "${name}": each element in connects must be a non-empty string, got ${JSON.stringify(c)}`);
164
+ }
165
+ }
166
+ if (config.port !== undefined && config.port !== null) {
167
+ if (!Number.isInteger(config.port) || config.port < 1 || config.port > 65535) {
168
+ throw new Error(`Service "${name}": port must be an integer between 1-65535, got ${config.port}`);
169
+ }
170
+ }
171
+ if (config.weight !== undefined) {
172
+ if (typeof config.weight !== "number" || !Number.isInteger(config.weight) || config.weight < 1) {
173
+ throw new Error(`Service "${name}": weight must be a positive integer, got ${config.weight}`);
174
+ }
175
+ }
176
+ if (config.prefix !== undefined && config.prefix !== null) {
177
+ if (typeof config.prefix !== "string" || !config.prefix.startsWith("/")) {
178
+ throw new Error(`Service "${name}": prefix must start with /, got "${config.prefix}"`);
179
+ }
180
+ if (config.prefix.includes("?") || config.prefix.includes("#")) {
181
+ throw new Error(`Service "${name}": prefix cannot contain query strings or fragments`);
182
+ }
183
+ }
184
+ const websocket = normalizeWebsocketConfig(name, type, config.websocket);
185
+ // CFG-1: Validate threads — must be "auto" or a positive integer
186
+ const threads = config.threads ?? "auto";
187
+ if (threads !== "auto") {
188
+ if (typeof threads !== "number" || !Number.isInteger(threads) || threads <= 0 || !Number.isFinite(threads)) {
189
+ throw new Error(`Service "${name}": threads must be "auto" or a positive integer, got ${JSON.stringify(threads)}`);
190
+ }
191
+ }
192
+ // CFG-2: Validate routing.strategy
193
+ const routing = { strategy: config.routing?.strategy ?? RoutingStrategyName.ROUND_ROBIN };
194
+ if (routing.strategy && !ROUTING_STRATEGY_NAMES.includes(routing.strategy)) {
195
+ throw new Error(`Service "${name}": invalid routing strategy "${routing.strategy}". Valid strategies: ${ROUTING_STRATEGY_NAMES.join(", ")}`);
196
+ }
197
+ // Validate RPC configuration shape
198
+ validateRpcOptions(name, "rpc", config.rpc);
199
+ if (config.rpcTargets) {
200
+ if (typeof config.rpcTargets !== "object" || Array.isArray(config.rpcTargets)) {
201
+ throw new Error(`Service "${name}": rpcTargets must be an object`);
202
+ }
203
+ for (const [targetName, targetOpts] of Object.entries(config.rpcTargets)) {
204
+ validateRpcOptions(name, `rpcTargets.${targetName}`, targetOpts);
205
+ }
206
+ }
207
+ return {
208
+ name,
209
+ entry: config.entry,
210
+ type,
211
+ port: config.port ?? null,
212
+ connects,
213
+ group: config.group ?? null, // colocation group name
214
+ threads,
215
+ weight: config.weight ?? 1,
216
+ mode: config.mode ?? ServiceMode.CLUSTER,
217
+ env: config.env ?? {},
218
+ routing,
219
+ prefix: config.prefix ?? null,
220
+ plugins: config.plugins ?? null, // null = all plugins, [...] = specific
221
+ websocket,
222
+ rpc: config.rpc,
223
+ rpcTargets: config.rpcTargets,
224
+ };
225
+ }
226
+ /**
227
+ * Resolve colocation groups.
228
+ *
229
+ * Services in the same group run in the same process(es).
230
+ * They communicate via direct function calls (zero overhead).
231
+ * Services without a group get their own isolated process.
232
+ *
233
+ * Returns a map of group name → array of service configs.
234
+ */
235
+ function resolveGroups(services) {
236
+ const groups = new Map();
237
+ for (const [name, svc] of Object.entries(services)) {
238
+ // Remote services don't run locally — no process group needed
239
+ if (svc.type === ServiceType.REMOTE)
240
+ continue;
241
+ const groupName = svc.group ?? `_isolated:${name}`;
242
+ if (!groups.has(groupName)) {
243
+ groups.set(groupName, {
244
+ name: groupName,
245
+ services: [],
246
+ threads: 0,
247
+ weight: 0,
248
+ hasEdge: false,
249
+ port: null,
250
+ });
251
+ }
252
+ const group = groups.get(groupName);
253
+ group.services.push(svc);
254
+ /**
255
+ * H4: Max-weight semantics for colocation groups.
256
+ *
257
+ * The group's weight equals its heaviest member service. This is intentional:
258
+ * since all services in a group share the same OS process (same event loop),
259
+ * the heaviest service determines the group's resource needs — it is the
260
+ * bottleneck. For example, a group with one weight-10 and two weight-1
261
+ * services gets the same thread allocation as a single weight-10 service,
262
+ * because the weight-10 service dominates the process's CPU budget.
263
+ */
264
+ group.weight = Math.max(group.weight, svc.weight);
265
+ if (svc.type === ServiceType.EDGE) {
266
+ if (group.hasEdge && group.port !== svc.port) {
267
+ throw new Error(`Group "${groupName}": multiple edge services with different ports (${group.port} vs ${svc.port}). ` +
268
+ `Edge services in the same group must use the same port.`);
269
+ }
270
+ group.hasEdge = true;
271
+ group.port = svc.port;
272
+ }
273
+ // Group inherits the highest thread count / weight of its members
274
+ if (typeof svc.threads === "number" && svc.threads > 0) {
275
+ group.threads = typeof group.threads === "number" ? Math.max(group.threads, svc.threads) : svc.threads;
276
+ }
277
+ else if (svc.threads === "auto" && group.threads === 0) {
278
+ group.threads = "auto";
279
+ }
280
+ }
281
+ return groups;
282
+ }
283
+ /**
284
+ * Resolve the channel topology from dependency declarations.
285
+ *
286
+ * Instead of full mesh, only create channels between services
287
+ * that actually declare they need to talk to each other.
288
+ *
289
+ * Returns array of { from, to } pairs.
290
+ */
291
+ function detectCycles(services) {
292
+ const WHITE = 0, GRAY = 1, BLACK = 2;
293
+ const color = {};
294
+ const parent = {};
295
+ for (const name of Object.keys(services)) {
296
+ color[name] = WHITE;
297
+ }
298
+ function dfs(name) {
299
+ color[name] = GRAY;
300
+ for (const target of services[name]?.connects ?? []) {
301
+ if (!services[target])
302
+ continue;
303
+ if (color[target] === GRAY) {
304
+ // Found a back edge — reconstruct the cycle path
305
+ const cycle = [target];
306
+ let cur = name;
307
+ while (cur !== target) {
308
+ cycle.push(cur);
309
+ cur = parent[cur];
310
+ }
311
+ cycle.push(target);
312
+ cycle.reverse();
313
+ // Skip bidirectional pairs (A→B→A) — those are valid IPC channels
314
+ if (cycle.length === 3 && cycle[0] === cycle[2])
315
+ continue;
316
+ throw new Error(`Circular dependency detected: ${cycle.join(" \u2192 ")}`);
317
+ }
318
+ if (color[target] === WHITE) {
319
+ parent[target] = name;
320
+ dfs(target);
321
+ }
322
+ }
323
+ color[name] = BLACK;
324
+ }
325
+ for (const name of Object.keys(services)) {
326
+ if (color[name] === WHITE) {
327
+ dfs(name);
328
+ }
329
+ }
330
+ }
331
+ function resolveChannels(services) {
332
+ const channels = [];
333
+ const seen = new Set();
334
+ for (const [name, svc] of Object.entries(services)) {
335
+ for (const target of svc.connects) {
336
+ if (!services[target]) {
337
+ throw new Error(`Service "${name}" declares connection to "${target}", but no such service exists.`);
338
+ }
339
+ // Deduplicate bidirectional
340
+ const key = [name, target].sort().join("<->");
341
+ if (!seen.has(key)) {
342
+ seen.add(key);
343
+ channels.push({ from: name, to: target });
344
+ }
345
+ }
346
+ }
347
+ detectCycles(services);
348
+ return channels;
349
+ }
350
+ /**
351
+ * Detect circular dependencies at the group level.
352
+ * Self-loops (a group connecting to itself) are allowed — that's colocated.
353
+ */
354
+ function detectGroupCycles(services, groups) {
355
+ // Build service→group mapping
356
+ const svcToGroup = {};
357
+ for (const [groupName, group] of groups) {
358
+ for (const svc of group.services) {
359
+ svcToGroup[svc.name] = groupName;
360
+ }
361
+ }
362
+ // Build group-level adjacency
363
+ const groupAdj = new Map();
364
+ for (const [name, svc] of Object.entries(services)) {
365
+ const fromGroup = svcToGroup[name];
366
+ if (!fromGroup)
367
+ continue;
368
+ for (const target of svc.connects) {
369
+ const toGroup = svcToGroup[target];
370
+ if (!toGroup || toGroup === fromGroup)
371
+ continue; // skip self-loops
372
+ if (!groupAdj.has(fromGroup))
373
+ groupAdj.set(fromGroup, new Set());
374
+ groupAdj.get(fromGroup).add(toGroup);
375
+ }
376
+ }
377
+ // DFS cycle detection
378
+ const WHITE = 0, GRAY = 1, BLACK = 2;
379
+ const color = {};
380
+ const parent = {};
381
+ for (const [groupName] of groups) {
382
+ color[groupName] = WHITE;
383
+ }
384
+ function dfs(g) {
385
+ color[g] = GRAY;
386
+ for (const target of groupAdj.get(g) ?? []) {
387
+ if (color[target] === GRAY) {
388
+ const cycle = [target];
389
+ let cur = g;
390
+ while (cur !== target) {
391
+ cycle.push(cur);
392
+ cur = parent[cur];
393
+ }
394
+ cycle.push(target);
395
+ cycle.reverse();
396
+ // Skip bidirectional pairs (A→B→A)
397
+ if (cycle.length === 3 && cycle[0] === cycle[2])
398
+ continue;
399
+ throw new Error(`Circular group dependency detected: ${cycle.join(" \u2192 ")}`);
400
+ }
401
+ if (color[target] === WHITE) {
402
+ parent[target] = g;
403
+ dfs(target);
404
+ }
405
+ }
406
+ color[g] = BLACK;
407
+ }
408
+ for (const [groupName] of groups) {
409
+ if (color[groupName] === WHITE)
410
+ dfs(groupName);
411
+ }
412
+ }
413
+ /**
414
+ * Define services for the ThreadForge runtime.
415
+ */
416
+ export function defineServices(servicesMap, options = {}) {
417
+ if (!servicesMap || typeof servicesMap !== "object") {
418
+ throw new Error("defineServices() requires a services object");
419
+ }
420
+ // D5: Accept plugins as top-level key in servicesMap or in options.
421
+ // If plugins appears inside the services object, emit a deprecation warning
422
+ // and prefer options.plugins when both are provided.
423
+ let plugins;
424
+ if (options.plugins) {
425
+ plugins = options.plugins;
426
+ }
427
+ else if (servicesMap.plugins) {
428
+ console.warn(`[ThreadForge] Deprecation: "plugins" inside the services object is deprecated. ` +
429
+ `Pass it as a separate top-level key: defineServices({ ...services }, { plugins: [...] })`);
430
+ plugins = servicesMap.plugins;
431
+ }
432
+ else {
433
+ plugins = [];
434
+ }
435
+ const filteredMap = { ...servicesMap };
436
+ delete filteredMap.plugins;
437
+ if (options.frontendPlugins !== undefined && !Array.isArray(options.frontendPlugins)) {
438
+ throw new Error("defineServices(): frontendPlugins must be an array when provided");
439
+ }
440
+ if (options.sites !== undefined && (typeof options.sites !== "object" || Array.isArray(options.sites))) {
441
+ throw new Error("defineServices(): sites must be an object map when provided");
442
+ }
443
+ const entries = Object.entries(filteredMap);
444
+ if (entries.length === 0) {
445
+ throw new Error("defineServices() requires at least one service");
446
+ }
447
+ // H1: Check for reserved service names
448
+ for (const [name] of entries) {
449
+ if (name === "plugins") {
450
+ throw new Error(`"plugins" is a reserved name and cannot be used as a service name`);
451
+ }
452
+ }
453
+ // Check for duplicate ports among all services
454
+ const ports = new Map();
455
+ for (const [name, config] of entries) {
456
+ if (config.port) {
457
+ if (ports.has(config.port)) {
458
+ throw new Error(`Port ${config.port} is used by both "${ports.get(config.port)}" and "${name}"`);
459
+ }
460
+ ports.set(config.port, name);
461
+ }
462
+ }
463
+ // Validate metricsPort
464
+ if (options.metricsPort !== undefined && options.metricsPort !== null) {
465
+ if (typeof options.metricsPort !== "number" || !Number.isInteger(options.metricsPort) ||
466
+ options.metricsPort < 1 || options.metricsPort > 65535) {
467
+ throw new Error(`metricsPort must be an integer between 1-65535, got ${JSON.stringify(options.metricsPort)}`);
468
+ }
469
+ }
470
+ // Check for metricsPort collision
471
+ const metricsPort = options.metricsPort ?? 9090;
472
+ for (const [name, config] of entries) {
473
+ if (config.port === metricsPort) {
474
+ throw new Error(`Service "${name}" port ${config.port} conflicts with metricsPort (${metricsPort})`);
475
+ }
476
+ }
477
+ // Normalize all services
478
+ const services = {};
479
+ for (const [name, config] of entries) {
480
+ services[name] = normalizeService(name, config);
481
+ }
482
+ // Validate thread counts and plugin arrays
483
+ for (const [name, svc] of Object.entries(services)) {
484
+ if (svc.type !== ServiceType.REMOTE && typeof svc.threads === "number") {
485
+ if (!Number.isInteger(svc.threads) || svc.threads <= 0 || !Number.isFinite(svc.threads)) {
486
+ throw new Error(`Service "${name}": threads must be a positive integer or "auto", got ${svc.threads}`);
487
+ }
488
+ }
489
+ if (svc.plugins) {
490
+ if (!Array.isArray(svc.plugins)) {
491
+ throw new Error(`Service "${name}": plugins must be an array`);
492
+ }
493
+ for (const p of svc.plugins) {
494
+ if (typeof p !== "string" || p.trim() === "") {
495
+ throw new Error(`Service "${name}": plugin name must be a non-empty string`);
496
+ }
497
+ }
498
+ if (new Set(svc.plugins).size !== svc.plugins.length) {
499
+ throw new Error(`Service "${name}": duplicate plugin entries detected`);
500
+ }
501
+ }
502
+ }
503
+ // Self-reference check
504
+ for (const [name, svc] of Object.entries(services)) {
505
+ if (svc.connects.includes(name)) {
506
+ throw new Error(`Service "${name}" cannot connect to itself.`);
507
+ }
508
+ }
509
+ // Validate rpcTargets keys against connects[] (requires full service map)
510
+ for (const [name, svc] of Object.entries(services)) {
511
+ if (svc.rpcTargets) {
512
+ for (const target of Object.keys(svc.rpcTargets)) {
513
+ if (!svc.connects.includes(target)) {
514
+ throw new Error(`Service "${name}": rpcTargets key "${target}" is not in connects array. ` +
515
+ `Add "${target}" to connects or remove it from rpcTargets.`);
516
+ }
517
+ }
518
+ }
519
+ }
520
+ // Resolve topology
521
+ const groups = resolveGroups(services);
522
+ const channels = resolveChannels(services);
523
+ // M3: Detect group-level circular dependencies
524
+ detectGroupCycles(services, groups);
525
+ // M-14: Warn on unknown option keys to catch typos early
526
+ const KNOWN_OPTIONS = new Set([
527
+ "metricsPort",
528
+ "logging",
529
+ "watch",
530
+ "registryMode",
531
+ "host",
532
+ "httpBasePort",
533
+ "ingress",
534
+ "plugins",
535
+ "frontendPlugins",
536
+ "sites",
537
+ // Internal keys used by platform/host modes
538
+ "_configUrl",
539
+ "_isHostMode",
540
+ "_isPlatformMode",
541
+ "_hostMeta",
542
+ "_hostMetaJSON",
543
+ ]);
544
+ for (const key of Object.keys(options)) {
545
+ if (!KNOWN_OPTIONS.has(key) && !key.startsWith("_")) {
546
+ console.warn(`[ThreadForge] Unknown option "${key}" in defineServices(). Known options: ${[...KNOWN_OPTIONS].filter((k) => !k.startsWith("_")).join(", ")}`);
547
+ }
548
+ }
549
+ // Validate logging.level
550
+ if (options.logging?.level !== undefined) {
551
+ if (typeof options.logging.level !== "string" || !LOG_LEVELS.includes(options.logging.level)) {
552
+ throw new Error(`Invalid logging.level: ${JSON.stringify(options.logging.level)}. Valid levels: ${LOG_LEVELS.join(", ")}`);
553
+ }
554
+ }
555
+ if (options.registryMode !== undefined) {
556
+ if (typeof options.registryMode !== "string" || !REGISTRY_MODES.includes(options.registryMode)) {
557
+ throw new Error(`Invalid registryMode: ${JSON.stringify(options.registryMode)}. Valid modes: ${REGISTRY_MODES.join(", ")}`);
558
+ }
559
+ }
560
+ // Only pass through known option keys to prevent typos polluting the config
561
+ // A12: Namespace internal fields into _internal sub-object
562
+ const _internal = {
563
+ configUrl: options._configUrl,
564
+ isHostMode: options._isHostMode,
565
+ isPlatformMode: options._isPlatformMode,
566
+ hostMeta: options._hostMeta,
567
+ hostMetaJSON: options._hostMetaJSON,
568
+ };
569
+ const result = {
570
+ services,
571
+ groups: Object.fromEntries(groups),
572
+ channels,
573
+ plugins,
574
+ frontendPlugins: options.frontendPlugins ?? [],
575
+ sites: options.sites ?? {},
576
+ metricsPort: options.metricsPort !== undefined ? options.metricsPort : 9090,
577
+ logging: { level: LogLevel.INFO, structured: true, ...(options.logging ?? {}) },
578
+ watch: options.watch ?? false,
579
+ registryMode: options.registryMode,
580
+ host: options.host,
581
+ httpBasePort: options.httpBasePort,
582
+ ingress: options.ingress,
583
+ _internal,
584
+ // Backward compat: expose legacy underscore keys
585
+ get _configUrl() {
586
+ return this._internal.configUrl;
587
+ },
588
+ set _configUrl(value) {
589
+ this._internal.configUrl = value;
590
+ },
591
+ get _isHostMode() {
592
+ return this._internal.isHostMode;
593
+ },
594
+ set _isHostMode(value) {
595
+ this._internal.isHostMode = value;
596
+ },
597
+ get _isPlatformMode() {
598
+ return this._internal.isPlatformMode;
599
+ },
600
+ set _isPlatformMode(value) {
601
+ this._internal.isPlatformMode = value;
602
+ },
603
+ get _hostMeta() {
604
+ return this._internal.hostMeta;
605
+ },
606
+ set _hostMeta(value) {
607
+ this._internal.hostMeta = value;
608
+ },
609
+ get _hostMetaJSON() {
610
+ return this._internal.hostMetaJSON;
611
+ },
612
+ set _hostMetaJSON(value) {
613
+ this._internal.hostMetaJSON = value;
614
+ },
615
+ };
616
+ for (const svc of Object.values(services)) {
617
+ Object.freeze(svc);
618
+ Object.freeze(svc.connects);
619
+ if (svc.env)
620
+ Object.freeze(svc.env);
621
+ if (svc.routing)
622
+ Object.freeze(svc.routing);
623
+ if (svc.websocket)
624
+ Object.freeze(svc.websocket);
625
+ }
626
+ Object.freeze(services);
627
+ for (const group of Object.values(result.groups)) {
628
+ Object.freeze(group.services);
629
+ Object.freeze(group);
630
+ }
631
+ Object.freeze(result.groups);
632
+ Object.freeze(channels);
633
+ Object.freeze(result.logging);
634
+ return result;
635
+ }
636
+ /**
637
+ * Detect if a config object is already normalized by defineServices().
638
+ * This lets loadConfig accept both:
639
+ * 1) raw service maps
640
+ * 2) pre-normalized defineServices(...) exports
641
+ */
642
+ function isNormalizedConfig(config) {
643
+ if (!config || typeof config !== "object" || Array.isArray(config))
644
+ return false;
645
+ const obj = config;
646
+ if (!obj.services || typeof obj.services !== "object" || Array.isArray(obj.services))
647
+ return false;
648
+ if (!obj.groups || typeof obj.groups !== "object" || Array.isArray(obj.groups))
649
+ return false;
650
+ if (!Array.isArray(obj.channels))
651
+ return false;
652
+ return Object.values(obj.services).every((svc) => {
653
+ return (svc &&
654
+ typeof svc === "object" &&
655
+ !Array.isArray(svc) &&
656
+ typeof svc.name === "string" &&
657
+ typeof svc.type === "string" &&
658
+ Array.isArray(svc.connects));
659
+ });
660
+ }
661
+ /**
662
+ * Load a forge config file from disk.
663
+ */
664
+ export async function loadConfig(configPath) {
665
+ try {
666
+ const module = (await import(configPath));
667
+ const config = module.default ?? module;
668
+ // Accept configs that already exported defineServices(...) output.
669
+ if (isNormalizedConfig(config)) {
670
+ return config;
671
+ }
672
+ // Accept configs exported via definePlatform(...) / defineHost(...).
673
+ // Use lazy imports to avoid circular module initialization at load time.
674
+ if (config && typeof config === "object" && !Array.isArray(config)) {
675
+ if (config._isPlatformConfig) {
676
+ const { resolveAndDefinePlatform } = (await import("./platform-config.js"));
677
+ return await resolveAndDefinePlatform(config);
678
+ }
679
+ if (config._isHostConfig) {
680
+ const { resolveAndDefine } = (await import("./host-config.js"));
681
+ return await resolveAndDefine(config);
682
+ }
683
+ }
684
+ // Raw object exports are normalized/validated here.
685
+ return defineServices(config);
686
+ }
687
+ catch (err) {
688
+ if (err.code === "ERR_MODULE_NOT_FOUND") {
689
+ throw new Error(`Config file not found or import error: ${configPath}\n ${err.message}`);
690
+ }
691
+ throw err;
692
+ }
693
+ }
694
+ //# sourceMappingURL=config.js.map