jsgui3-server 0.0.148 → 0.0.149

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 (134) hide show
  1. package/.github/workflows/control-scan-manifest-check.yml +31 -0
  2. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-071799b982906680f5fd699d.js +40 -0
  3. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-07352945ad5c92654fcb8b65.js +39 -0
  4. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-138a601fadb6191ea314c6fd.js +39 -0
  5. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-171f6c381c2cadf2e9fa7087.js +39 -0
  6. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-1d973388156b84a04373fac9.js +39 -0
  7. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-20e117bc8a10d2cd16234bbe.js +40 -0
  8. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-2b028a82b0e5efddba42425f.js +39 -0
  9. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-4518556cd5c7e059e82b22b8.js +40 -0
  10. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-5bac1aa0f213902f718ed74f.js +40 -0
  11. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-5f9996ac7822caf777d92f56.js +39 -0
  12. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-60a92c702e65fd9cf748e3ec.js +39 -0
  13. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-6164c1f8f738995c541895d2.js +44 -0
  14. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-6718a85eb9e5aa782dd47a05.js +45 -0
  15. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-69e280f14e37aee76a1d4675.js +39 -0
  16. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-7570d1b030d44b111ed59c4c.js +39 -0
  17. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-7798c9bbd55e510d5039f936.js +42 -0
  18. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-78cd511ea1ef18ecb03d1be5.js +40 -0
  19. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-7d482e0b95bcb5e3c543118b.js +43 -0
  20. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-80e9476d1127c55b40fdb36f.js +40 -0
  21. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-810ced55d5320a3088a05b13.js +40 -0
  22. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-8423565f1a40e329afc8c6cf.js +40 -0
  23. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-900bef783b8cee36506ec282.js +39 -0
  24. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-a1a37aff6416fdad74040ddf.js +39 -0
  25. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-ad48d5e8eda40f175b4df090.js +39 -0
  26. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-aec5a2d963015528c9099462.js +39 -0
  27. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-af9d34e0f1722fab9e28c269.js +39 -0
  28. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-b818e4015e2f1fe86280b5ab.js +41 -0
  29. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-bcb2541adc70b7aba61768c5.js +44 -0
  30. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-bfe89d2c78ed44f95ed7dd73.js +40 -0
  31. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-c06f04806a1e688e1187110c.js +40 -0
  32. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-c3f3adf904f585afc544b96a.js +39 -0
  33. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-d45acb873e1d8e32d5e60f2e.js +39 -0
  34. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-db06f132533706f4a0163b8c.js +39 -0
  35. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-f660f40d78b135fc8560a862.js +39 -0
  36. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-f9dee4ec18a96e09bee06bae.js +39 -0
  37. package/README.md +85 -3
  38. package/admin-ui/client.js +8 -8
  39. package/dev-status.svg +139 -0
  40. package/docs/api-reference.md +301 -43
  41. package/docs/books/jsgui3-bundling-research-book/00-table-of-contents.md +35 -0
  42. package/docs/books/jsgui3-bundling-research-book/01-pipeline-and-runtime-semantics.md +34 -0
  43. package/docs/books/jsgui3-bundling-research-book/02-javascript-bundling-core.md +36 -0
  44. package/docs/books/jsgui3-bundling-research-book/03-style-extraction-and-css-compilation.md +35 -0
  45. package/docs/books/jsgui3-bundling-research-book/04-static-publishing-and-delivery.md +39 -0
  46. package/docs/books/jsgui3-bundling-research-book/05-current-limits-and-size-bloat-vectors.md +25 -0
  47. package/docs/books/jsgui3-bundling-research-book/06-unused-module-elimination-strategy.md +77 -0
  48. package/docs/books/jsgui3-bundling-research-book/07-jsgui3-html-control-and-mixin-pruning.md +63 -0
  49. package/docs/books/jsgui3-bundling-research-book/08-test-and-verification-methodology.md +43 -0
  50. package/docs/books/jsgui3-bundling-research-book/09-roadmap-and-rollout.md +42 -0
  51. package/docs/books/jsgui3-bundling-research-book/10-further-research-strategies-and-upgrades.md +211 -0
  52. package/docs/books/jsgui3-bundling-research-book/README.md +35 -0
  53. package/docs/bundling-system-deep-dive.md +9 -4
  54. package/docs/comprehensive-documentation.md +49 -18
  55. package/docs/configuration-reference.md +152 -27
  56. package/docs/core/README.md +19 -0
  57. package/docs/core/jsgui3-server-core-book/00-table-of-contents.md +21 -0
  58. package/docs/core/jsgui3-server-core-book/01-startup-readiness-state-machine.md +41 -0
  59. package/docs/core/jsgui3-server-core-book/02-resource-abstraction-and-lifecycle.md +92 -0
  60. package/docs/core/jsgui3-server-core-book/03-resource-pool-and-event-topology.md +47 -0
  61. package/docs/core/jsgui3-server-core-book/04-sse-publisher-semantics.md +41 -0
  62. package/docs/core/jsgui3-server-core-book/05-serve-factory-resource-wiring.md +46 -0
  63. package/docs/core/jsgui3-server-core-book/06-e2e-testing-methodology.md +48 -0
  64. package/docs/core/jsgui3-server-core-book/07-defect-detection-and-hardening-loop.md +47 -0
  65. package/docs/publishers-guide.md +59 -4
  66. package/docs/resources-guide.md +184 -35
  67. package/docs/simple-server-api-design.md +72 -17
  68. package/docs/system-architecture.md +18 -14
  69. package/examples/controls/15) window, observable SSE/server.js +6 -1
  70. package/examples/controls/19) window, auto observable ui/server.js +9 -0
  71. package/examples/controls/20) window, task manager app/README.md +133 -0
  72. package/examples/controls/20) window, task manager app/client.js +797 -0
  73. package/examples/controls/20) window, task manager app/server.js +178 -0
  74. package/examples/controls/6) window, color_palette/client.js +165 -68
  75. package/examples/controls/9) window, date picker/client.js +362 -76
  76. package/examples/controls/9b) window, shared data.model mirrored date pickers/client.js +104 -83
  77. package/examples/jsgui3-html/06) theming/client.js +22 -1
  78. package/examples/jsgui3-html/10) binding-debugger/client.js +137 -1
  79. package/http/responders/static/Static_Route_HTTP_Responder.js +52 -34
  80. package/lab/experiments/capture-color-controls.js +196 -0
  81. package/lab/results/screenshots/color-controls/full_page.png +0 -0
  82. package/lab/results/screenshots/color-controls/section_1_color_grid_12x12.png +0 -0
  83. package/lab/results/screenshots/color-controls/section_2_color_grid_4x2.png +0 -0
  84. package/lab/results/screenshots/color-controls/section_3_color_palette.png +0 -0
  85. package/lab/results/screenshots/color-controls/section_4_palette_comparison.png +0 -0
  86. package/lab/results/screenshots/color-controls/section_5_raw_swatches.png +0 -0
  87. package/lab/results/screenshots/color-controls/section_6_optimized_crayola.png +0 -0
  88. package/lab/results/screenshots/color-controls/section_7_pastel_palette.png +0 -0
  89. package/lab/results/screenshots/color-controls/section_8_extended_144.png +0 -0
  90. package/lab/screenshot-utils.js +248 -0
  91. package/module.js +11 -4
  92. package/package.json +12 -2
  93. package/publishers/Publishers.js +4 -3
  94. package/publishers/helpers/assigners/static-compressed-response-buffers/Single_Control_Webpage_Server_Static_Compressed_Response_Buffers_Assigner.js +5 -5
  95. package/publishers/http-sse-publisher.js +341 -0
  96. package/resources/process-resource.js +950 -0
  97. package/resources/processors/bundlers/js/esbuild/Advanced_JS_Bundler_Using_ESBuild.js +129 -33
  98. package/resources/processors/bundlers/js/esbuild/Core_JS_Non_Minifying_Bundler_Using_ESBuild.js +18 -7
  99. package/resources/processors/bundlers/js/esbuild/JSGUI3_HTML_Control_Optimizer.js +829 -0
  100. package/resources/remote-process-resource.js +355 -0
  101. package/resources/server-resource-pool.js +354 -41
  102. package/serve-factory.js +441 -259
  103. package/server.js +89 -13
  104. package/tests/README.md +66 -4
  105. package/tests/admin-ui-render.test.js +24 -0
  106. package/tests/assigners.test.js +56 -40
  107. package/tests/bundling-default-control-elimination.puppeteer.test.js +260 -0
  108. package/tests/configuration-validation.test.js +21 -18
  109. package/tests/content-analysis.test.js +7 -6
  110. package/tests/control-optimizer-cache-behavior.test.js +52 -0
  111. package/tests/control-scan-manifest-regression.test.js +144 -0
  112. package/tests/end-to-end.test.js +15 -14
  113. package/tests/error-handling.test.js +222 -179
  114. package/tests/fixtures/bundling-default-button-client.js +37 -0
  115. package/tests/fixtures/bundling-default-window-client.js +34 -0
  116. package/tests/fixtures/control_scan_manifest_expectations.json +48 -0
  117. package/tests/fixtures/resource-monitor-client.js +319 -0
  118. package/tests/helpers/puppeteer-e2e-harness.js +317 -0
  119. package/tests/http-sse-publisher.test.js +136 -0
  120. package/tests/performance.test.js +69 -65
  121. package/tests/process-resource.test.js +138 -0
  122. package/tests/publishers.test.js +7 -7
  123. package/tests/remote-process-resource.test.js +160 -0
  124. package/tests/sass-controls.e2e.test.js +7 -1
  125. package/tests/serve-resources.test.js +270 -0
  126. package/tests/serve.test.js +120 -50
  127. package/tests/server-resource-pool.test.js +106 -0
  128. package/tests/small-controls-bundle-size.test.js +252 -0
  129. package/tests/test-runner.js +13 -1
  130. package/tests/window-examples.puppeteer.test.js +204 -1
  131. package/tests/window-resource-integration.puppeteer.test.js +585 -0
  132. package/tests/temp_invalid.js +0 -7
  133. package/tests/temp_invalid_utf8.js +0 -1
  134. package/tests/temp_malformed.js +0 -10
@@ -0,0 +1,950 @@
1
+ const { Resource } = require('jsgui3-html');
2
+ const { spawn, execFile } = require('child_process');
3
+ const fs = require('fs');
4
+ const http = require('http');
5
+ const https = require('https');
6
+ const lib_net = require('net');
7
+ const lib_path = require('path');
8
+
9
+ const default_health_interval_ms = 30000;
10
+ const default_health_timeout_ms = 5000;
11
+ const default_health_failures_before_unhealthy = 3;
12
+ const default_stop_timeout_ms = 5000;
13
+ const default_kill_timeout_ms = 2000;
14
+ const default_restart_backoff_base_ms = 1000;
15
+ const max_restart_backoff_ms = 60000;
16
+ const default_startup_health_retry_interval_ms = 500;
17
+ const default_startup_health_timeout_ms = 15000;
18
+
19
+ const pm2_online_statuses = new Set(['online', 'launching', 'waiting restart']);
20
+
21
+ const to_error_message = (error_value) => {
22
+ if (!error_value) return null;
23
+ if (typeof error_value === 'string') return error_value;
24
+ if (error_value instanceof Error) return error_value.message;
25
+ return String(error_value);
26
+ };
27
+
28
+ class Process_Resource extends Resource {
29
+ constructor(spec = {}) {
30
+ super(spec);
31
+
32
+ this.command = spec.command;
33
+ this.args = Array.isArray(spec.args) ? spec.args : [];
34
+ this.cwd = spec.cwd;
35
+ this.env = spec.env && typeof spec.env === 'object' ? { ...spec.env } : {};
36
+
37
+ this.auto_restart = spec.autoRestart === true;
38
+ this.max_restarts = Number.isInteger(spec.maxRestarts) ? spec.maxRestarts : 5;
39
+
40
+ this.health_check = spec.healthCheck || null;
41
+ this.health_interval_ms = Number.isFinite(spec.healthIntervalMs)
42
+ ? Number(spec.healthIntervalMs)
43
+ : Number(this.health_check?.intervalMs) || default_health_interval_ms;
44
+ this.health_timeout_ms = Number.isFinite(spec.healthTimeoutMs)
45
+ ? Number(spec.healthTimeoutMs)
46
+ : Number(this.health_check?.timeoutMs) || default_health_timeout_ms;
47
+ this.health_failures_before_unhealthy = Number.isInteger(spec.healthFailuresBeforeUnhealthy)
48
+ ? spec.healthFailuresBeforeUnhealthy
49
+ : Number(this.health_check?.failuresBeforeUnhealthy) || default_health_failures_before_unhealthy;
50
+
51
+ this.stop_timeout_ms = Number.isFinite(spec.stopTimeoutMs)
52
+ ? Number(spec.stopTimeoutMs)
53
+ : default_stop_timeout_ms;
54
+ this.kill_timeout_ms = Number.isFinite(spec.killTimeoutMs)
55
+ ? Number(spec.killTimeoutMs)
56
+ : default_kill_timeout_ms;
57
+
58
+ this.restart_backoff_base_ms = Number.isFinite(spec.restartBackoffBaseMs)
59
+ ? Number(spec.restartBackoffBaseMs)
60
+ : default_restart_backoff_base_ms;
61
+
62
+ this.startup_health_retry_interval_ms = Number.isFinite(spec.startupHealthRetryIntervalMs)
63
+ ? Number(spec.startupHealthRetryIntervalMs)
64
+ : default_startup_health_retry_interval_ms;
65
+ this.startup_health_timeout_ms = Number.isFinite(spec.startupHealthTimeoutMs)
66
+ ? Number(spec.startupHealthTimeoutMs)
67
+ : default_startup_health_timeout_ms;
68
+
69
+ this.process_manager = this._normalize_process_manager(spec.processManager);
70
+
71
+ this.state = 'stopped';
72
+ this.pid = null;
73
+ this.child_process = null;
74
+ this.start_timestamp = null;
75
+ this.restart_count = 0;
76
+ this.last_health_check = null;
77
+ this.memory_usage = null;
78
+
79
+ this.consecutive_health_failures = 0;
80
+ this._unhealthy_raised = false;
81
+
82
+ this._manual_stop_requested = false;
83
+ this._pending_restart_timer = null;
84
+ this._health_timer = null;
85
+ this._memory_timer = null;
86
+ this._stdout_remainder = '';
87
+ this._stderr_remainder = '';
88
+
89
+ this._current_exit_promise = null;
90
+ this._resolve_current_exit = null;
91
+
92
+ this._operation_promise = Promise.resolve();
93
+ }
94
+
95
+ _normalize_process_manager(process_manager_spec) {
96
+ if (!process_manager_spec) {
97
+ return { type: 'direct' };
98
+ }
99
+
100
+ if (typeof process_manager_spec === 'string') {
101
+ return { type: process_manager_spec.toLowerCase() };
102
+ }
103
+
104
+ if (typeof process_manager_spec === 'object') {
105
+ const normalized_manager = {
106
+ ...process_manager_spec,
107
+ type: String(process_manager_spec.type || 'direct').toLowerCase()
108
+ };
109
+ return normalized_manager;
110
+ }
111
+
112
+ return { type: 'direct' };
113
+ }
114
+
115
+ _resolve_with_optional_callback(operation_promise, callback) {
116
+ if (typeof callback === 'function') {
117
+ operation_promise.then(
118
+ (result) => callback(null, result),
119
+ (error) => callback(error)
120
+ );
121
+ return;
122
+ }
123
+ return operation_promise;
124
+ }
125
+
126
+ _enqueue_operation(operation_fn) {
127
+ const next_promise = this._operation_promise.then(() => operation_fn());
128
+ this._operation_promise = next_promise.catch(() => {
129
+ // Keep the chain alive for future operations.
130
+ });
131
+ return next_promise;
132
+ }
133
+
134
+ _set_state(next_state) {
135
+ const previous_state = this.state;
136
+ if (previous_state === next_state) {
137
+ return;
138
+ }
139
+
140
+ this.state = next_state;
141
+ this.raise('state_change', {
142
+ from: previous_state,
143
+ to: next_state,
144
+ timestamp: Date.now()
145
+ });
146
+ }
147
+
148
+ _clear_pending_restart_timer() {
149
+ if (this._pending_restart_timer) {
150
+ clearTimeout(this._pending_restart_timer);
151
+ this._pending_restart_timer = null;
152
+ }
153
+ }
154
+
155
+ _sleep(duration_ms) {
156
+ return new Promise((resolve) => setTimeout(resolve, duration_ms));
157
+ }
158
+
159
+ _resolve_pm2_command() {
160
+ if (this.process_manager.pm2Path) {
161
+ return this.process_manager.pm2Path;
162
+ }
163
+
164
+ if (process.env.PM2_PATH) {
165
+ return process.env.PM2_PATH;
166
+ }
167
+
168
+ const pm2_binary_name = process.platform === 'win32' ? 'pm2.cmd' : 'pm2';
169
+ const local_pm2_binary = lib_path.join(process.cwd(), 'node_modules', '.bin', pm2_binary_name);
170
+ if (fs.existsSync(local_pm2_binary)) {
171
+ return local_pm2_binary;
172
+ }
173
+
174
+ return pm2_binary_name;
175
+ }
176
+
177
+ _run_exec_file(command, args = [], options = {}) {
178
+ return new Promise((resolve, reject) => {
179
+ execFile(command, args, {
180
+ maxBuffer: 8 * 1024 * 1024,
181
+ ...options
182
+ }, (error, stdout, stderr) => {
183
+ if (error) {
184
+ error.stdout = stdout;
185
+ error.stderr = stderr;
186
+ reject(error);
187
+ return;
188
+ }
189
+ resolve({ stdout, stderr });
190
+ });
191
+ });
192
+ }
193
+
194
+ _normalize_sse_line(line) {
195
+ if (line.endsWith('\r')) {
196
+ return line.slice(0, -1);
197
+ }
198
+ return line;
199
+ }
200
+
201
+ _bind_stream_line_events(stream, event_name, remainder_key) {
202
+ if (!stream || typeof stream.on !== 'function') {
203
+ return;
204
+ }
205
+
206
+ stream.on('data', (chunk) => {
207
+ this[remainder_key] = `${this[remainder_key] || ''}${chunk.toString('utf8')}`;
208
+ let newline_index = this[remainder_key].indexOf('\n');
209
+ while (newline_index >= 0) {
210
+ const raw_line = this[remainder_key].slice(0, newline_index);
211
+ this[remainder_key] = this[remainder_key].slice(newline_index + 1);
212
+ this.raise(event_name, {
213
+ line: this._normalize_sse_line(raw_line),
214
+ pid: this.pid,
215
+ timestamp: Date.now()
216
+ });
217
+ newline_index = this[remainder_key].indexOf('\n');
218
+ }
219
+ });
220
+
221
+ stream.on('end', () => {
222
+ if (this[remainder_key]) {
223
+ this.raise(event_name, {
224
+ line: this._normalize_sse_line(this[remainder_key]),
225
+ pid: this.pid,
226
+ timestamp: Date.now()
227
+ });
228
+ this[remainder_key] = '';
229
+ }
230
+ });
231
+ }
232
+
233
+ _create_exit_waiter() {
234
+ this._current_exit_promise = new Promise((resolve) => {
235
+ this._resolve_current_exit = resolve;
236
+ });
237
+ }
238
+
239
+ _resolve_exit_waiter(exit_info) {
240
+ if (this._resolve_current_exit) {
241
+ this._resolve_current_exit(exit_info);
242
+ }
243
+ this._resolve_current_exit = null;
244
+ this._current_exit_promise = null;
245
+ }
246
+
247
+ async _wait_for_exit(exit_promise, timeout_ms) {
248
+ if (!exit_promise) {
249
+ return true;
250
+ }
251
+
252
+ return new Promise((resolve) => {
253
+ let settled = false;
254
+ const complete = (result) => {
255
+ if (settled) return;
256
+ settled = true;
257
+ clearTimeout(timeout_handle);
258
+ resolve(result);
259
+ };
260
+
261
+ const timeout_handle = setTimeout(() => complete(false), timeout_ms);
262
+ exit_promise.then(() => complete(true), () => complete(true));
263
+ });
264
+ }
265
+
266
+ async _start_internal({ manual_start = false } = {}) {
267
+ if (manual_start) {
268
+ this.restart_count = 0;
269
+ }
270
+
271
+ if (this.state === 'running' || this.state === 'starting') {
272
+ return this.status;
273
+ }
274
+
275
+ this._clear_pending_restart_timer();
276
+ this._manual_stop_requested = false;
277
+
278
+ this._set_state('starting');
279
+
280
+ if (this.process_manager.type === 'pm2') {
281
+ await this._start_pm2_process();
282
+ } else {
283
+ await this._start_direct_process();
284
+ }
285
+
286
+ const initial_health_ok = await this._wait_for_initial_health();
287
+ if (!initial_health_ok && this.health_check) {
288
+ const startup_health_error = new Error(`Startup health check failed for resource "${this.name || 'unnamed'}".`);
289
+ this.raise('unhealthy', {
290
+ timestamp: Date.now(),
291
+ consecutiveFailures: this.consecutive_health_failures,
292
+ state: this.state,
293
+ error: startup_health_error.message
294
+ });
295
+
296
+ try {
297
+ await this._stop_internal();
298
+ } catch {
299
+ // Best effort shutdown after failed startup health checks.
300
+ }
301
+
302
+ this._set_state('crashed');
303
+ this.raise('crashed', {
304
+ timestamp: Date.now(),
305
+ restartCount: this.restart_count,
306
+ error: startup_health_error.message
307
+ });
308
+ throw startup_health_error;
309
+ }
310
+
311
+ this.start_timestamp = Date.now();
312
+ this._set_state('running');
313
+
314
+ this._start_runtime_timers();
315
+
316
+ return this.status;
317
+ }
318
+
319
+ _start_runtime_timers() {
320
+ this._stop_runtime_timers();
321
+
322
+ const runtime_interval_ms = this.health_check
323
+ ? this.health_interval_ms
324
+ : default_health_interval_ms;
325
+
326
+ if (this.health_check) {
327
+ this._health_timer = setInterval(() => {
328
+ this._run_health_check().catch((error) => {
329
+ this.raise('health_check', {
330
+ healthy: false,
331
+ latencyMs: null,
332
+ error: to_error_message(error),
333
+ timestamp: Date.now()
334
+ });
335
+ });
336
+ }, runtime_interval_ms);
337
+ this._health_timer.unref?.();
338
+ }
339
+
340
+ this._memory_timer = setInterval(() => {
341
+ this._update_memory_usage().catch(() => {
342
+ // Ignore periodic metric errors.
343
+ });
344
+ }, runtime_interval_ms);
345
+ this._memory_timer.unref?.();
346
+ }
347
+
348
+ _stop_runtime_timers() {
349
+ if (this._health_timer) {
350
+ clearInterval(this._health_timer);
351
+ this._health_timer = null;
352
+ }
353
+ if (this._memory_timer) {
354
+ clearInterval(this._memory_timer);
355
+ this._memory_timer = null;
356
+ }
357
+ }
358
+
359
+ async _start_direct_process() {
360
+ if (!this.command) {
361
+ throw new Error('Process_Resource requires `command` when processManager is direct.');
362
+ }
363
+
364
+ const merged_env = {
365
+ ...process.env,
366
+ ...this.env
367
+ };
368
+
369
+ const child_process = spawn(this.command, this.args, {
370
+ cwd: this.cwd || process.cwd(),
371
+ env: merged_env,
372
+ stdio: ['ignore', 'pipe', 'pipe']
373
+ });
374
+
375
+ this.child_process = child_process;
376
+ this.pid = child_process.pid || null;
377
+ this._stdout_remainder = '';
378
+ this._stderr_remainder = '';
379
+
380
+ this._create_exit_waiter();
381
+
382
+ this._bind_stream_line_events(child_process.stdout, 'stdout', '_stdout_remainder');
383
+ this._bind_stream_line_events(child_process.stderr, 'stderr', '_stderr_remainder');
384
+
385
+ child_process.once('error', (error) => {
386
+ this.raise('stderr', {
387
+ line: `spawn_error: ${to_error_message(error)}`,
388
+ pid: this.pid,
389
+ timestamp: Date.now()
390
+ });
391
+ });
392
+
393
+ child_process.once('exit', (code, signal) => {
394
+ this.raise('exit', {
395
+ code,
396
+ signal,
397
+ timestamp: Date.now()
398
+ });
399
+ this._resolve_exit_waiter({ code, signal });
400
+ this._handle_process_exit(code, signal).catch(() => {
401
+ // Exit handling errors should not crash the server.
402
+ });
403
+ });
404
+
405
+ await this._sleep(20);
406
+ }
407
+
408
+ async _start_pm2_process() {
409
+ const pm2_command = this._resolve_pm2_command();
410
+
411
+ if (!this.name) {
412
+ throw new Error('Process_Resource using PM2 requires a resource name.');
413
+ }
414
+
415
+ let pm2_args;
416
+ if (this.process_manager.ecosystem) {
417
+ pm2_args = ['start', this.process_manager.ecosystem, '--only', this.name];
418
+ } else {
419
+ if (!this.command) {
420
+ throw new Error('Process_Resource PM2 mode requires `command` or `ecosystem`.');
421
+ }
422
+ pm2_args = ['start', this.command, '--name', this.name];
423
+ if (this.cwd) {
424
+ pm2_args.push('--cwd', this.cwd);
425
+ }
426
+ if (this.args.length > 0) {
427
+ pm2_args.push('--', ...this.args);
428
+ }
429
+ }
430
+
431
+ await this._run_exec_file(pm2_command, pm2_args, {
432
+ env: {
433
+ ...process.env,
434
+ ...this.env
435
+ },
436
+ cwd: this.cwd || process.cwd()
437
+ });
438
+
439
+ await this._refresh_pm2_status();
440
+ }
441
+
442
+ async _wait_for_initial_health() {
443
+ if (!this.health_check) {
444
+ return true;
445
+ }
446
+
447
+ const deadline_timestamp = Date.now() + this.startup_health_timeout_ms;
448
+ while (Date.now() < deadline_timestamp) {
449
+ const health_result = await this._run_health_check();
450
+ if (health_result.healthy) {
451
+ return true;
452
+ }
453
+ await this._sleep(this.startup_health_retry_interval_ms);
454
+ }
455
+
456
+ return false;
457
+ }
458
+
459
+ async _handle_process_exit(exit_code, signal) {
460
+ this._stop_runtime_timers();
461
+
462
+ const exit_was_expected = this._manual_stop_requested || this.state === 'stopping';
463
+
464
+ this.child_process = null;
465
+ this.pid = null;
466
+ this.start_timestamp = null;
467
+
468
+ if (exit_was_expected) {
469
+ this._manual_stop_requested = false;
470
+ this._set_state('stopped');
471
+ return;
472
+ }
473
+
474
+ if (exit_code === 0) {
475
+ this._set_state('stopped');
476
+ return;
477
+ }
478
+
479
+ if (this.auto_restart) {
480
+ this._schedule_auto_restart();
481
+ return;
482
+ }
483
+
484
+ this._set_state('crashed');
485
+ this.raise('crashed', {
486
+ code: exit_code,
487
+ signal,
488
+ restartCount: this.restart_count,
489
+ timestamp: Date.now()
490
+ });
491
+ }
492
+
493
+ _schedule_auto_restart() {
494
+ this.restart_count += 1;
495
+
496
+ if (this.restart_count > this.max_restarts) {
497
+ this._set_state('crashed');
498
+ this.raise('crashed', {
499
+ restartCount: this.restart_count,
500
+ maxRestarts: this.max_restarts,
501
+ timestamp: Date.now()
502
+ });
503
+ return;
504
+ }
505
+
506
+ const delay_ms = Math.min(
507
+ this.restart_backoff_base_ms * (2 ** Math.max(0, this.restart_count - 1)),
508
+ max_restart_backoff_ms
509
+ );
510
+
511
+ this._set_state('restarting');
512
+ this._pending_restart_timer = setTimeout(() => {
513
+ this._pending_restart_timer = null;
514
+ this._enqueue_operation(() => this._start_internal({ manual_start: false })).catch((error) => {
515
+ this._set_state('crashed');
516
+ this.raise('crashed', {
517
+ restartCount: this.restart_count,
518
+ error: to_error_message(error),
519
+ timestamp: Date.now()
520
+ });
521
+ });
522
+ }, delay_ms);
523
+ this._pending_restart_timer.unref?.();
524
+ }
525
+
526
+ async _run_health_check() {
527
+ const started_at = Date.now();
528
+
529
+ let health_result;
530
+ try {
531
+ health_result = await this._perform_health_probe();
532
+ } catch (error) {
533
+ health_result = {
534
+ healthy: false,
535
+ error: to_error_message(error)
536
+ };
537
+ }
538
+
539
+ const latency_ms = Date.now() - started_at;
540
+ const normalized_result = {
541
+ healthy: !!health_result.healthy,
542
+ latencyMs: latency_ms,
543
+ timestamp: Date.now()
544
+ };
545
+
546
+ if (health_result.error) {
547
+ normalized_result.error = to_error_message(health_result.error);
548
+ }
549
+ if (health_result.statusCode !== undefined) {
550
+ normalized_result.statusCode = health_result.statusCode;
551
+ }
552
+
553
+ this.last_health_check = normalized_result;
554
+ this.raise('health_check', normalized_result);
555
+
556
+ if (normalized_result.healthy) {
557
+ this.consecutive_health_failures = 0;
558
+ this._unhealthy_raised = false;
559
+ } else {
560
+ this.consecutive_health_failures += 1;
561
+ if (!this._unhealthy_raised && this.consecutive_health_failures >= this.health_failures_before_unhealthy) {
562
+ this._unhealthy_raised = true;
563
+ this.raise('unhealthy', {
564
+ consecutiveFailures: this.consecutive_health_failures,
565
+ lastHealthCheck: normalized_result,
566
+ timestamp: Date.now()
567
+ });
568
+ }
569
+ }
570
+
571
+ return normalized_result;
572
+ }
573
+
574
+ async _perform_health_probe() {
575
+ if (!this.health_check) {
576
+ return { healthy: true };
577
+ }
578
+
579
+ const health_type = String(this.health_check.type || 'http').toLowerCase();
580
+ if (health_type === 'http') {
581
+ return this._perform_http_health_probe();
582
+ }
583
+ if (health_type === 'tcp') {
584
+ return this._perform_tcp_health_probe();
585
+ }
586
+ if (health_type === 'custom') {
587
+ return this._perform_custom_health_probe();
588
+ }
589
+
590
+ throw new Error(`Unsupported healthCheck.type: ${this.health_check.type}`);
591
+ }
592
+
593
+ _perform_http_health_probe() {
594
+ return new Promise((resolve) => {
595
+ if (!this.health_check?.url) {
596
+ resolve({ healthy: false, error: 'healthCheck.url is required for HTTP health checks.' });
597
+ return;
598
+ }
599
+
600
+ let parsed_url;
601
+ try {
602
+ parsed_url = new URL(this.health_check.url);
603
+ } catch (error) {
604
+ resolve({ healthy: false, error: to_error_message(error) });
605
+ return;
606
+ }
607
+
608
+ const request_module = parsed_url.protocol === 'https:' ? https : http;
609
+ const request = request_module.request({
610
+ method: 'GET',
611
+ protocol: parsed_url.protocol,
612
+ hostname: parsed_url.hostname,
613
+ port: parsed_url.port || (parsed_url.protocol === 'https:' ? 443 : 80),
614
+ path: `${parsed_url.pathname || '/'}${parsed_url.search || ''}`,
615
+ timeout: this.health_timeout_ms
616
+ }, (response) => {
617
+ const healthy = response.statusCode === 200;
618
+ response.resume();
619
+ resolve({
620
+ healthy,
621
+ statusCode: response.statusCode
622
+ });
623
+ });
624
+
625
+ request.once('timeout', () => {
626
+ request.destroy(new Error('HTTP health check timeout'));
627
+ });
628
+
629
+ request.once('error', (error) => {
630
+ resolve({ healthy: false, error: to_error_message(error) });
631
+ });
632
+
633
+ request.end();
634
+ });
635
+ }
636
+
637
+ _perform_tcp_health_probe() {
638
+ return new Promise((resolve) => {
639
+ const tcp_host = this.health_check.host || '127.0.0.1';
640
+ const tcp_port = Number(this.health_check.port);
641
+ if (!Number.isFinite(tcp_port)) {
642
+ resolve({ healthy: false, error: 'healthCheck.port is required for TCP health checks.' });
643
+ return;
644
+ }
645
+
646
+ const socket = lib_net.createConnection({
647
+ host: tcp_host,
648
+ port: tcp_port
649
+ });
650
+
651
+ const cleanup = () => {
652
+ socket.removeAllListeners('connect');
653
+ socket.removeAllListeners('timeout');
654
+ socket.removeAllListeners('error');
655
+ socket.end();
656
+ socket.destroy();
657
+ };
658
+
659
+ socket.setTimeout(this.health_timeout_ms);
660
+ socket.once('connect', () => {
661
+ cleanup();
662
+ resolve({ healthy: true });
663
+ });
664
+ socket.once('timeout', () => {
665
+ cleanup();
666
+ resolve({ healthy: false, error: 'TCP health check timeout' });
667
+ });
668
+ socket.once('error', (error) => {
669
+ cleanup();
670
+ resolve({ healthy: false, error: to_error_message(error) });
671
+ });
672
+ });
673
+ }
674
+
675
+ async _perform_custom_health_probe() {
676
+ const custom_fn = this.health_check.fn || this.health_check.custom;
677
+ if (typeof custom_fn !== 'function') {
678
+ throw new Error('healthCheck.fn (or healthCheck.custom) must be a function for custom checks.');
679
+ }
680
+
681
+ const probe_result = await custom_fn(this);
682
+ if (typeof probe_result === 'boolean') {
683
+ return { healthy: probe_result };
684
+ }
685
+
686
+ if (probe_result && typeof probe_result === 'object') {
687
+ return {
688
+ healthy: !!probe_result.healthy,
689
+ ...probe_result
690
+ };
691
+ }
692
+
693
+ return { healthy: !!probe_result };
694
+ }
695
+
696
+ async _update_memory_usage() {
697
+ if (this.process_manager.type === 'pm2') {
698
+ await this._refresh_pm2_status();
699
+ return;
700
+ }
701
+
702
+ if (!this.pid) {
703
+ this.memory_usage = null;
704
+ return;
705
+ }
706
+
707
+ this.memory_usage = await this._read_direct_process_memory_usage(this.pid);
708
+ }
709
+
710
+ async _read_direct_process_memory_usage(pid) {
711
+ if (process.platform === 'win32') {
712
+ return this._read_windows_memory_usage(pid);
713
+ }
714
+ return this._read_unix_memory_usage(pid);
715
+ }
716
+
717
+ async _read_unix_memory_usage(pid) {
718
+ try {
719
+ const { stdout } = await this._run_exec_file('ps', ['-o', 'rss=', '-p', String(pid)]);
720
+ const rss_kb = Number.parseInt(String(stdout || '').trim(), 10);
721
+ if (!Number.isFinite(rss_kb)) {
722
+ return null;
723
+ }
724
+ return {
725
+ rssBytes: rss_kb * 1024,
726
+ source: 'ps',
727
+ timestamp: Date.now()
728
+ };
729
+ } catch {
730
+ return null;
731
+ }
732
+ }
733
+
734
+ async _read_windows_memory_usage(pid) {
735
+ try {
736
+ const { stdout } = await this._run_exec_file('tasklist', ['/FI', `PID eq ${pid}`, '/FO', 'CSV', '/NH']);
737
+ const first_line = String(stdout || '').trim().split(/\r?\n/)[0] || '';
738
+ if (!first_line || first_line.startsWith('INFO:')) {
739
+ return null;
740
+ }
741
+ const csv_columns = first_line.replace(/^"|"$/g, '').split('","');
742
+ const memory_column = csv_columns[4] || '';
743
+ const normalized_memory = Number.parseInt(memory_column.replace(/[^\d]/g, ''), 10);
744
+ if (!Number.isFinite(normalized_memory)) {
745
+ return null;
746
+ }
747
+ return {
748
+ rssBytes: normalized_memory * 1024,
749
+ source: 'tasklist',
750
+ timestamp: Date.now()
751
+ };
752
+ } catch {
753
+ return null;
754
+ }
755
+ }
756
+
757
+ _map_pm2_status_to_resource_state(pm2_status) {
758
+ const normalized_pm2_status = String(pm2_status || '').toLowerCase();
759
+ if (pm2_online_statuses.has(normalized_pm2_status)) {
760
+ return 'running';
761
+ }
762
+ if (normalized_pm2_status === 'stopped') {
763
+ return 'stopped';
764
+ }
765
+ if (normalized_pm2_status === 'errored') {
766
+ return 'crashed';
767
+ }
768
+ if (normalized_pm2_status === 'stopping') {
769
+ return 'stopping';
770
+ }
771
+ if (normalized_pm2_status === 'launching') {
772
+ return 'starting';
773
+ }
774
+ return this.state;
775
+ }
776
+
777
+ async _read_pm2_process_info() {
778
+ const pm2_command = this._resolve_pm2_command();
779
+ const { stdout } = await this._run_exec_file(pm2_command, ['jlist'], {
780
+ env: {
781
+ ...process.env,
782
+ ...this.env
783
+ },
784
+ cwd: this.cwd || process.cwd()
785
+ });
786
+
787
+ const parsed = JSON.parse(String(stdout || '[]'));
788
+ if (!Array.isArray(parsed)) {
789
+ return null;
790
+ }
791
+
792
+ const found = parsed.find((item) => item && item.name === this.name);
793
+ return found || null;
794
+ }
795
+
796
+ async _refresh_pm2_status() {
797
+ try {
798
+ const pm2_process_info = await this._read_pm2_process_info();
799
+ if (!pm2_process_info) {
800
+ this.pid = null;
801
+ this.memory_usage = null;
802
+ return;
803
+ }
804
+
805
+ const next_pid = Number.isFinite(pm2_process_info.pid) ? pm2_process_info.pid : null;
806
+ this.pid = next_pid;
807
+
808
+ if (pm2_process_info.monit && typeof pm2_process_info.monit === 'object') {
809
+ this.memory_usage = {
810
+ rssBytes: Number(pm2_process_info.monit.memory) || null,
811
+ cpuPercent: Number(pm2_process_info.monit.cpu) || null,
812
+ source: 'pm2',
813
+ timestamp: Date.now()
814
+ };
815
+ }
816
+
817
+ const pm2_status = pm2_process_info.pm2_env && pm2_process_info.pm2_env.status;
818
+ const mapped_state = this._map_pm2_status_to_resource_state(pm2_status);
819
+ if (mapped_state && mapped_state !== this.state && this.state !== 'stopping' && this.state !== 'restarting') {
820
+ this._set_state(mapped_state);
821
+ }
822
+ } catch {
823
+ // Ignore PM2 status refresh errors during periodic updates.
824
+ }
825
+ }
826
+
827
+ async _stop_direct_process() {
828
+ if (!this.child_process) {
829
+ this.pid = null;
830
+ this.start_timestamp = null;
831
+ return;
832
+ }
833
+
834
+ const child_process = this.child_process;
835
+ const exit_promise = this._current_exit_promise;
836
+
837
+ try {
838
+ child_process.kill('SIGTERM');
839
+ } catch {
840
+ // Ignore signal delivery errors.
841
+ }
842
+
843
+ let did_exit = await this._wait_for_exit(exit_promise, this.stop_timeout_ms);
844
+ if (!did_exit) {
845
+ try {
846
+ child_process.kill('SIGKILL');
847
+ } catch {
848
+ // Ignore signal delivery errors.
849
+ }
850
+ did_exit = await this._wait_for_exit(this._current_exit_promise, this.kill_timeout_ms);
851
+ }
852
+
853
+ if (!did_exit) {
854
+ this.child_process = null;
855
+ this.pid = null;
856
+ this.start_timestamp = null;
857
+ }
858
+ }
859
+
860
+ async _stop_pm2_process() {
861
+ const pm2_command = this._resolve_pm2_command();
862
+ await this._run_exec_file(pm2_command, ['stop', this.name], {
863
+ env: {
864
+ ...process.env,
865
+ ...this.env
866
+ },
867
+ cwd: this.cwd || process.cwd()
868
+ });
869
+
870
+ this.pid = null;
871
+ this.start_timestamp = null;
872
+ }
873
+
874
+ async _stop_internal() {
875
+ this._clear_pending_restart_timer();
876
+ this._stop_runtime_timers();
877
+
878
+ if (this.state === 'stopped') {
879
+ return this.status;
880
+ }
881
+
882
+ this._manual_stop_requested = true;
883
+ this._set_state('stopping');
884
+
885
+ if (this.process_manager.type === 'pm2') {
886
+ await this._stop_pm2_process();
887
+ this._manual_stop_requested = false;
888
+ } else {
889
+ await this._stop_direct_process();
890
+ if (!this.child_process && !this._current_exit_promise) {
891
+ this._manual_stop_requested = false;
892
+ }
893
+ }
894
+
895
+ this.start_timestamp = null;
896
+ this.memory_usage = null;
897
+ this._set_state('stopped');
898
+ return this.status;
899
+ }
900
+
901
+ start(callback) {
902
+ const operation_promise = this._enqueue_operation(() => this._start_internal({ manual_start: true }));
903
+ return this._resolve_with_optional_callback(operation_promise, callback);
904
+ }
905
+
906
+ stop(callback) {
907
+ const operation_promise = this._enqueue_operation(() => this._stop_internal());
908
+
909
+ return this._resolve_with_optional_callback(operation_promise, callback);
910
+ }
911
+
912
+ restart(callback) {
913
+ const operation_promise = this._enqueue_operation(async () => {
914
+ this._set_state('restarting');
915
+ await this._stop_internal();
916
+ return this._start_internal({ manual_start: true });
917
+ });
918
+
919
+ return this._resolve_with_optional_callback(operation_promise, callback);
920
+ }
921
+
922
+ get status() {
923
+ const uptime = this.start_timestamp ? Math.max(0, Date.now() - this.start_timestamp) : 0;
924
+
925
+ return {
926
+ state: this.state,
927
+ pid: this.pid,
928
+ uptime,
929
+ restartCount: this.restart_count,
930
+ lastHealthCheck: this.last_health_check,
931
+ memoryUsage: this.memory_usage,
932
+ processManager: {
933
+ type: this.process_manager.type
934
+ }
935
+ };
936
+ }
937
+
938
+ get_abstract() {
939
+ const status_snapshot = this.status;
940
+ return {
941
+ name: this.name,
942
+ state: status_snapshot.state,
943
+ pid: status_snapshot.pid,
944
+ uptime: status_snapshot.uptime,
945
+ restartCount: status_snapshot.restartCount
946
+ };
947
+ }
948
+ }
949
+
950
+ module.exports = Process_Resource;