threadforge 0.1.0

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 (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +152 -0
  3. package/bin/forge.js +1050 -0
  4. package/bin/host-commands.js +344 -0
  5. package/bin/platform-commands.js +570 -0
  6. package/package.json +71 -0
  7. package/shared/auth.js +475 -0
  8. package/src/core/DirectMessageBus.js +364 -0
  9. package/src/core/EndpointResolver.js +247 -0
  10. package/src/core/ForgeContext.js +2227 -0
  11. package/src/core/ForgeHost.js +122 -0
  12. package/src/core/ForgePlatform.js +145 -0
  13. package/src/core/Ingress.js +768 -0
  14. package/src/core/Interceptors.js +420 -0
  15. package/src/core/MessageBus.js +310 -0
  16. package/src/core/Prometheus.js +305 -0
  17. package/src/core/RequestContext.js +413 -0
  18. package/src/core/RoutingStrategy.js +316 -0
  19. package/src/core/Supervisor.js +1306 -0
  20. package/src/core/ThreadAllocator.js +196 -0
  21. package/src/core/WorkerChannelManager.js +879 -0
  22. package/src/core/config.js +624 -0
  23. package/src/core/host-config.js +311 -0
  24. package/src/core/network-utils.js +166 -0
  25. package/src/core/platform-config.js +308 -0
  26. package/src/decorators/ServiceProxy.js +899 -0
  27. package/src/decorators/index.js +571 -0
  28. package/src/deploy/NginxGenerator.js +865 -0
  29. package/src/deploy/PlatformManifestGenerator.js +96 -0
  30. package/src/deploy/RouteManifestGenerator.js +112 -0
  31. package/src/deploy/index.js +984 -0
  32. package/src/frontend/FrontendDevLifecycle.js +65 -0
  33. package/src/frontend/FrontendPluginOrchestrator.js +187 -0
  34. package/src/frontend/SiteResolver.js +63 -0
  35. package/src/frontend/StaticMountRegistry.js +90 -0
  36. package/src/frontend/index.js +5 -0
  37. package/src/frontend/plugins/index.js +2 -0
  38. package/src/frontend/plugins/viteFrontend.js +79 -0
  39. package/src/frontend/types.js +35 -0
  40. package/src/index.js +56 -0
  41. package/src/internals.js +31 -0
  42. package/src/plugins/PluginManager.js +537 -0
  43. package/src/plugins/ScopedPostgres.js +192 -0
  44. package/src/plugins/ScopedRedis.js +142 -0
  45. package/src/plugins/index.js +1729 -0
  46. package/src/registry/ServiceRegistry.js +796 -0
  47. package/src/scaling/ScaleAdvisor.js +442 -0
  48. package/src/services/Service.js +195 -0
  49. package/src/services/worker-bootstrap.js +676 -0
  50. package/src/templates/auth-service.js +65 -0
  51. package/src/templates/identity-service.js +75 -0
@@ -0,0 +1,196 @@
1
+ import os from "node:os";
2
+
3
+ /**
4
+ * ThreadAllocator
5
+ *
6
+ * Distributes available CPU cores across services based on
7
+ * fixed thread counts and weighted auto-allocation.
8
+ */
9
+ export class ThreadAllocator {
10
+ constructor(options = {}) {
11
+ // Allow override via environment variable or config
12
+ let envCpus = null;
13
+ if (process.env.FORGE_CPUS) {
14
+ const parsed = parseInt(process.env.FORGE_CPUS, 10);
15
+ if (!isNaN(parsed) && parsed > 0) {
16
+ envCpus = parsed;
17
+ } else {
18
+ console.warn(
19
+ `[ThreadForge] Warning: Invalid FORGE_CPUS value "${process.env.FORGE_CPUS}". ` +
20
+ `Expected a positive integer. Ignoring.`
21
+ );
22
+ }
23
+ }
24
+
25
+ const rawCpus = options.cpus ?? envCpus ?? os.cpus().length;
26
+
27
+ // Clamp to minimum of 1 to handle constrained environments where os.cpus() returns 0
28
+ this.totalCpus = Math.max(1, rawCpus);
29
+
30
+ // Log warning if clamping occurred
31
+ if (rawCpus < 1) {
32
+ console.warn(
33
+ `[ThreadForge] Warning: CPU count detected as ${rawCpus}, clamping to minimum of 1. ` +
34
+ `This may occur in constrained sandbox environments. ` +
35
+ `Override with FORGE_CPUS environment variable if needed.`
36
+ );
37
+ }
38
+
39
+ this.reservedForSupervisor = options.reserved ?? 1;
40
+ }
41
+
42
+ /**
43
+ * Calculate thread allocation for all services.
44
+ *
45
+ * @param {Object} services - Normalized service configs (keyed by name)
46
+ * @returns {Map<string, number>} - Service name → allocated thread count
47
+ */
48
+ allocate(services) {
49
+ const available = this.totalCpus - this.reservedForSupervisor;
50
+ const entries = Object.values(services);
51
+
52
+ if (entries.length === 0) {
53
+ return new Map();
54
+ }
55
+
56
+ // Phase 1: Honor fixed thread counts
57
+ let remaining = available;
58
+ const fixedServices = entries.filter((s) => typeof s.threads === "number");
59
+ const autoServices = entries.filter((s) => s.threads === "auto");
60
+
61
+ let fixedTotal = 0;
62
+ for (const s of fixedServices) {
63
+ fixedTotal += s.threads;
64
+ }
65
+
66
+ if (fixedTotal > available) {
67
+ console.warn(
68
+ `[ThreadForge] Warning: Fixed thread allocations (${fixedTotal}) exceed ` +
69
+ `available cores (${available}). Services may contend for resources.`,
70
+ );
71
+ }
72
+
73
+ remaining = Math.max(0, remaining - fixedTotal);
74
+
75
+ // Phase 2: Distribute remaining cores to 'auto' services by weight
76
+ const totalWeight = autoServices.reduce((sum, s) => sum + Math.max(1, s.weight), 0);
77
+
78
+ const allocation = new Map();
79
+
80
+ for (const s of fixedServices) {
81
+ allocation.set(s.name, s.threads);
82
+ }
83
+
84
+ if (autoServices.length > 0 && remaining > 0) {
85
+ // First pass: proportional allocation
86
+ let allocated = 0;
87
+ const provisional = [];
88
+
89
+ for (const s of autoServices) {
90
+ const share = Math.max(1, Math.round((s.weight / totalWeight) * remaining));
91
+ provisional.push({ name: s.name, threads: share });
92
+ allocated += share;
93
+ }
94
+
95
+ // Adjust if we over/under allocated due to rounding
96
+ const diff = remaining - allocated;
97
+ if (diff !== 0) {
98
+ // Sort by weight descending, give/take from highest weight services
99
+ provisional.sort((a, b) => {
100
+ const wA = services[a.name]?.weight ?? 1;
101
+ const wB = services[b.name]?.weight ?? 1;
102
+ return wB - wA;
103
+ });
104
+
105
+ let adjust = Math.abs(diff);
106
+ const direction = diff > 0 ? 1 : -1;
107
+ let i = 0;
108
+
109
+ let safetyCounter = 0;
110
+ const maxIterations = provisional.length * 2;
111
+ while (adjust > 0 && safetyCounter < maxIterations) {
112
+ safetyCounter++;
113
+ const newVal = provisional[i].threads + direction;
114
+ if (newVal >= 1) {
115
+ provisional[i].threads = newVal;
116
+ adjust--;
117
+ }
118
+ i++;
119
+ if (i >= provisional.length) i = 0;
120
+ }
121
+
122
+ if (adjust > 0) {
123
+ console.warn(`[ThreadAllocator] Could not fully adjust allocation (${adjust} threads remaining)`);
124
+ }
125
+ }
126
+
127
+ // Second pass: if total still exceeds remaining, reduce lowest-weight services
128
+ let totalProvisional = provisional.reduce((sum, p) => sum + p.threads, 0);
129
+ if (totalProvisional > remaining) {
130
+ // Sort by weight ascending so we reduce lowest-weight first
131
+ const byWeightAsc = [...provisional].sort((a, b) => {
132
+ const wA = services[a.name]?.weight ?? 1;
133
+ const wB = services[b.name]?.weight ?? 1;
134
+ return wA - wB;
135
+ });
136
+
137
+ for (const p of byWeightAsc) {
138
+ if (totalProvisional <= remaining) break;
139
+ const excess = totalProvisional - remaining;
140
+ const canReduce = p.threads - 1; // keep at least 1
141
+ if (canReduce <= 0) continue;
142
+ const reduction = Math.min(canReduce, excess);
143
+ p.threads -= reduction;
144
+ totalProvisional -= reduction;
145
+ }
146
+
147
+ if (totalProvisional > remaining) {
148
+ console.warn(
149
+ `[ThreadAllocator] Auto services allocated ${totalProvisional} threads ` +
150
+ `but only ${remaining} available. Some services may contend for resources.`,
151
+ );
152
+ }
153
+ }
154
+
155
+ for (const p of provisional) {
156
+ allocation.set(p.name, p.threads);
157
+ }
158
+ } else if (autoServices.length > 0) {
159
+ // No remaining cores, give each auto service 1 thread minimum
160
+ for (const s of autoServices) {
161
+ allocation.set(s.name, 1);
162
+ }
163
+ }
164
+
165
+ return allocation;
166
+ }
167
+
168
+ /**
169
+ * Get a summary of the allocation for display.
170
+ */
171
+ summary(services) {
172
+ const allocation = this.allocate(services);
173
+ let totalAllocated = 0;
174
+ const rows = [];
175
+
176
+ for (const [name, threads] of allocation) {
177
+ totalAllocated += threads;
178
+ const svc = services[name];
179
+ rows.push({
180
+ name,
181
+ port: svc.port,
182
+ threads,
183
+ mode: svc.mode,
184
+ weight: svc.weight,
185
+ });
186
+ }
187
+
188
+ return {
189
+ totalCpus: this.totalCpus,
190
+ reserved: this.reservedForSupervisor,
191
+ allocated: totalAllocated,
192
+ idle: Math.max(0, this.totalCpus - this.reservedForSupervisor - totalAllocated),
193
+ services: rows,
194
+ };
195
+ }
196
+ }