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