jsgui3-server 0.0.151 → 0.0.155
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.
- package/README.md +21 -0
- package/admin-ui/v1/controls/admin_shell.js +33 -0
- package/admin-ui/v1/server.js +14 -1
- package/docs/agi/skills/README.md +23 -0
- package/docs/agi/skills/agent-output-control/SKILL.md +56 -0
- package/docs/agi/skills/ai-deep-research/SKILL.md +52 -0
- package/docs/agi/skills/autonomous-ui-inspection/SKILL.md +102 -0
- package/docs/agi/skills/deep-research/SKILL.md +156 -0
- package/docs/agi/skills/endurance/SKILL.md +53 -0
- package/docs/agi/skills/exploring-other-codebases/SKILL.md +56 -0
- package/docs/agi/skills/instruction-adherence/SKILL.md +73 -0
- package/docs/agi/skills/jsgui3-activation-debug/SKILL.md +94 -0
- package/docs/agi/skills/jsgui3-context-menu-patterns/SKILL.md +94 -0
- package/docs/agi/skills/puppeteer-efficient-ui-verification/SKILL.md +65 -0
- package/docs/agi/skills/runaway-process-guard/SKILL.md +49 -0
- package/docs/agi/skills/session-discipline/SKILL.md +40 -0
- package/docs/agi/skills/skill-writing/SKILL.md +211 -0
- package/docs/agi/skills/static-analysis/SKILL.md +58 -0
- package/docs/agi/skills/targeted-testing/SKILL.md +63 -0
- package/docs/agi/skills/understanding-jsgui3/SKILL.md +85 -0
- package/docs/api-reference.md +120 -2
- package/docs/books/jsgui3-bundling-research-book/06-unused-module-elimination-strategy.md +1 -0
- package/docs/books/jsgui3-bundling-research-book/07-jsgui3-html-control-and-mixin-pruning.md +33 -0
- package/docs/books/website-design/01-introduction.md +73 -0
- package/docs/books/website-design/02-current-state.md +195 -0
- package/docs/books/website-design/03-base-class.md +181 -0
- package/docs/books/website-design/04-webpage.md +307 -0
- package/docs/books/website-design/05-website.md +456 -0
- package/docs/books/website-design/06-pages-storage.md +170 -0
- package/docs/books/website-design/07-api-layer.md +285 -0
- package/docs/books/website-design/08-server-integration.md +271 -0
- package/docs/books/website-design/09-cross-agent-review.md +190 -0
- package/docs/books/website-design/10-open-questions.md +196 -0
- package/docs/books/website-design/11-converged-recommendation.md +205 -0
- package/docs/books/website-design/12-content-model.md +395 -0
- package/docs/books/website-design/13-webpage-module-spec.md +404 -0
- package/docs/books/website-design/14-website-module-spec.md +541 -0
- package/docs/books/website-design/15-multi-repo-plan.md +275 -0
- package/docs/books/website-design/16-minimal-first.md +203 -0
- package/docs/books/website-design/17-implementation-report-codex.md +81 -0
- package/docs/books/website-design/README.md +43 -0
- package/docs/bundling-system-deep-dive.md +112 -3
- package/docs/configuration-reference.md +84 -0
- package/docs/proposals/jsgui3-website-and-webpage-design-jsgui3-server-support.md +257 -0
- package/docs/proposals/jsgui3-website-and-webpage-design-review.md +73 -0
- package/docs/proposals/jsgui3-website-and-webpage-design.md +732 -0
- package/docs/swagger.md +316 -0
- package/examples/controls/1) window/server.js +6 -1
- package/examples/controls/21) mvvm and declarative api/check.js +94 -0
- package/examples/controls/21) mvvm and declarative api/check_output.txt +25 -0
- package/examples/controls/21) mvvm and declarative api/check_output_2.txt +27 -0
- package/examples/controls/21) mvvm and declarative api/client.js +241 -0
- declarative api/e2e-screenshot-1-name-change.png +0 -0
- declarative api/e2e-screenshot-2-toggled.png +0 -0
- declarative api/e2e-screenshot-3-final.png +0 -0
- declarative api/e2e-screenshot-final.png +0 -0
- package/examples/controls/21) mvvm and declarative api/e2e-test.js +175 -0
- package/examples/controls/21) mvvm and declarative api/out.html +1 -0
- package/examples/controls/21) mvvm and declarative api/page_out.html +1 -0
- package/examples/controls/21) mvvm and declarative api/server.js +18 -0
- package/examples/data-views/01) query-endpoint/server.js +61 -0
- package/labs/website-design/001-base-class-overhead/check.js +162 -0
- package/labs/website-design/002-pages-storage/check.js +244 -0
- package/labs/website-design/002-pages-storage/results.txt +0 -0
- package/labs/website-design/003-type-detection/check.js +193 -0
- package/labs/website-design/003-type-detection/results.txt +0 -0
- package/labs/website-design/004-two-stage-validation/check.js +314 -0
- package/labs/website-design/004-two-stage-validation/results.txt +0 -0
- package/labs/website-design/005-normalize-input/check.js +303 -0
- package/labs/website-design/006-serve-website-spike/check.js +290 -0
- package/labs/website-design/README.md +34 -0
- package/labs/website-design/manifest.json +68 -0
- package/labs/website-design/run-all.js +60 -0
- package/middleware/json-body.js +126 -0
- package/openapi.js +474 -0
- package/package.json +13 -7
- package/publishers/Publishers.js +6 -5
- package/publishers/http-function-publisher.js +135 -126
- package/publishers/http-webpage-publisher.js +89 -11
- package/publishers/query-publisher.js +116 -0
- package/publishers/swagger-publisher.js +203 -0
- package/publishers/swagger-ui.js +578 -0
- package/resources/adapters/array-adapter.js +143 -0
- package/resources/processors/bundlers/js/esbuild/Advanced_JS_Bundler_Using_ESBuild.js +90 -22
- package/resources/processors/bundlers/js/esbuild/Core_JS_Non_Minifying_Bundler_Using_ESBuild.js +50 -14
- package/resources/processors/bundlers/js/esbuild/Core_JS_Single_File_Minifying_Bundler_Using_ESBuild.js +48 -14
- package/resources/processors/bundlers/js/esbuild/JSGUI3_HTML_Control_Optimizer.js +396 -44
- package/resources/query-resource.js +131 -0
- package/serve-factory.js +677 -18
- package/server.js +585 -167
- package/tests/README.md +86 -2
- package/tests/admin-ui-jsgui-controls.test.js +16 -1
- package/tests/bundling-default-control-elimination.puppeteer.test.js +32 -1
- package/tests/control-elimination-root-feature-pruning.test.js +440 -0
- package/tests/control-elimination-static-bracket-access.test.js +245 -0
- package/tests/control-scan-manifest-regression.test.js +2 -0
- package/tests/end-to-end.test.js +22 -21
- package/tests/fixtures/control_scan_manifest_expectations.json +4 -2
- package/tests/helpers/playwright-e2e-harness.js +326 -0
- package/tests/helpers/puppeteer-e2e-harness.js +62 -1
- package/tests/openapi.test.js +319 -0
- package/tests/playwright-smoke.test.js +134 -0
- package/tests/project-local-controls-bundling.puppeteer.test.js +462 -0
- package/tests/publish-enhancements.test.js +673 -0
- package/tests/query-publisher.test.js +430 -0
- package/tests/quick-json-body-test.js +169 -0
- package/tests/serve.test.js +425 -122
- package/tests/swagger-publisher.test.js +1076 -0
- package/tests/test-runner.js +4 -0
package/server.js
CHANGED
|
@@ -30,6 +30,7 @@ const Process_Resource = require('./resources/process-resource');
|
|
|
30
30
|
const Remote_Process_Resource = require('./resources/remote-process-resource');
|
|
31
31
|
|
|
32
32
|
const Static_Route_HTTP_Responder = require('./http/responders/static/Static_Route_HTTP_Responder');
|
|
33
|
+
const { get_port_or_free } = require('./port-utils');
|
|
33
34
|
|
|
34
35
|
const Publishers = require('./publishers/Publishers');
|
|
35
36
|
|
|
@@ -39,6 +40,8 @@ class JSGUI_Single_Process_Server extends Evented_Class {
|
|
|
39
40
|
}, __type_name) {
|
|
40
41
|
super();
|
|
41
42
|
this.http_servers = [];
|
|
43
|
+
this.listening_endpoints = [];
|
|
44
|
+
this._api_registry = [];
|
|
42
45
|
let disk_path_client_js;
|
|
43
46
|
if (spec.disk_path_client_js) {
|
|
44
47
|
disk_path_client_js = spec.disk_path_client_js;
|
|
@@ -151,35 +154,35 @@ class JSGUI_Single_Process_Server extends Evented_Class {
|
|
|
151
154
|
|
|
152
155
|
const Admin_Module_V1 = require('./admin-ui/v1/server');
|
|
153
156
|
if (admin_enabled) {
|
|
154
|
-
|
|
155
|
-
|
|
157
|
+
this.admin_v1 = new Admin_Module_V1(typeof admin_config === 'object' ? admin_config : {});
|
|
158
|
+
this.admin_v1.init(this);
|
|
156
159
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
160
|
+
// Register Admin V1 Page Route
|
|
161
|
+
let Admin_Shell_Control;
|
|
162
|
+
try {
|
|
163
|
+
Admin_Shell_Control = require('./admin-ui/v1/client').controls.Admin_Shell;
|
|
164
|
+
} catch (e) {
|
|
165
|
+
console.warn('Failed to load Admin_Shell control:', e.message || e);
|
|
166
|
+
}
|
|
164
167
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
168
|
+
if (Admin_Shell_Control) {
|
|
169
|
+
const admin_v1_app = new Webpage({
|
|
170
|
+
content: Admin_Shell_Control,
|
|
171
|
+
title: 'jsgui3 Admin Dashboard'
|
|
172
|
+
});
|
|
170
173
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
174
|
+
const admin_v1_publisher = new HTTP_Webpage_Publisher({
|
|
175
|
+
name: 'Admin_V1_Publisher',
|
|
176
|
+
webpage: admin_v1_app,
|
|
177
|
+
src_path_client_js: lib_path.join(__dirname, 'admin-ui/v1/client.js')
|
|
178
|
+
});
|
|
179
|
+
admin_v1_publisher.name = 'Admin_V1_Publisher';
|
|
177
180
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
const is_dev_defaults = !process.env.ADMIN_V1_PASSWORD && process.env.NODE_ENV !== 'production';
|
|
182
|
+
const login_hint = is_dev_defaults
|
|
183
|
+
? '<div class="hint dev-creds">Dev defaults active — username: <code>admin</code> password: <code>admin</code></div><div class="hint">Set ADMIN_V1_USER / ADMIN_V1_PASSWORD env vars for production.</div>'
|
|
184
|
+
: '<div class="hint">Sign in with your configured credentials.</div>';
|
|
185
|
+
const login_html = `<!doctype html>
|
|
183
186
|
<html>
|
|
184
187
|
<head>
|
|
185
188
|
<meta charset="utf-8" />
|
|
@@ -250,82 +253,82 @@ class JSGUI_Single_Process_Server extends Evented_Class {
|
|
|
250
253
|
</body>
|
|
251
254
|
</html>`;
|
|
252
255
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if (!this.admin_v1 || !this.admin_v1.is_admin_read_request(req)) {
|
|
261
|
-
res.writeHead(302, { Location: '/admin/v1/login' });
|
|
262
|
-
res.end();
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
if (admin_v1_cached_html) {
|
|
266
|
-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
267
|
-
res.end(admin_v1_cached_html);
|
|
268
|
-
} else {
|
|
269
|
-
res.writeHead(503, { 'Content-Type': 'text/plain' });
|
|
270
|
-
res.end('Admin UI v1 is loading, please retry...');
|
|
271
|
-
}
|
|
272
|
-
};
|
|
256
|
+
const serve_admin_v1_page = (req, res) => {
|
|
257
|
+
if (req && typeof req.url === 'string' && req.url.startsWith('/admin/v1/login')) {
|
|
258
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
259
|
+
res.end(login_html);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
273
262
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
263
|
+
if (!this.admin_v1 || !this.admin_v1.is_admin_read_request(req)) {
|
|
264
|
+
res.writeHead(302, { Location: '/admin/v1/login' });
|
|
265
|
+
res.end();
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (admin_v1_cached_html) {
|
|
269
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
270
|
+
res.end(admin_v1_cached_html);
|
|
271
|
+
} else {
|
|
272
|
+
res.writeHead(503, { 'Content-Type': 'text/plain' });
|
|
273
|
+
res.end('Admin UI v1 is loading, please retry...');
|
|
274
|
+
}
|
|
275
|
+
};
|
|
278
276
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
277
|
+
server_router.set_route('/admin/v1/login', null, (req, res) => {
|
|
278
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
279
|
+
res.end(login_html);
|
|
280
|
+
});
|
|
282
281
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
// Inject viewport meta for mobile rendering
|
|
300
|
-
if (!html.includes('name="viewport"')) {
|
|
282
|
+
// Namespace admin v1 assets under /admin/v1/ to avoid
|
|
283
|
+
// colliding with the main app's /js/js.js and /css/css.css.
|
|
284
|
+
let admin_v1_cached_html = null;
|
|
285
|
+
|
|
286
|
+
admin_v1_publisher.on('ready', (res_ready) => {
|
|
287
|
+
if (res_ready && res_ready._arr) {
|
|
288
|
+
for (const bundle_item of res_ready._arr) {
|
|
289
|
+
const { type } = bundle_item;
|
|
290
|
+
if (type === 'HTML') {
|
|
291
|
+
// Rewrite asset references so browser fetches
|
|
292
|
+
// the admin-specific JS/CSS, not the main app's.
|
|
293
|
+
let html = bundle_item.text || '';
|
|
294
|
+
html = html.replace(
|
|
295
|
+
/href="\/css\/css\.css"/g,
|
|
296
|
+
'href="/admin/v1/css/css.css"'
|
|
297
|
+
);
|
|
301
298
|
html = html.replace(
|
|
302
|
-
|
|
303
|
-
'
|
|
299
|
+
/src="\/js\/js\.js"/g,
|
|
300
|
+
'src="/admin/v1/js/js.js"'
|
|
304
301
|
);
|
|
302
|
+
// Inject viewport meta for mobile rendering
|
|
303
|
+
if (!html.includes('name="viewport"')) {
|
|
304
|
+
html = html.replace(
|
|
305
|
+
/<head([^>]*)>/i,
|
|
306
|
+
'<head$1><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">'
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
admin_v1_cached_html = html;
|
|
310
|
+
} else {
|
|
311
|
+
// JS → /admin/v1/js/js.js, CSS → /admin/v1/css/css.css
|
|
312
|
+
const namespaced_route =
|
|
313
|
+
type === 'JavaScript' ? '/admin/v1/js/js.js' :
|
|
314
|
+
type === 'CSS' ? '/admin/v1/css/css.css' :
|
|
315
|
+
'/admin/v1' + bundle_item.route;
|
|
316
|
+
const responder = new Static_Route_HTTP_Responder(bundle_item);
|
|
317
|
+
server_router.set_route(namespaced_route, responder, responder.handle_http);
|
|
305
318
|
}
|
|
306
|
-
admin_v1_cached_html = html;
|
|
307
|
-
} else {
|
|
308
|
-
// JS → /admin/v1/js/js.js, CSS → /admin/v1/css/css.css
|
|
309
|
-
const namespaced_route =
|
|
310
|
-
type === 'JavaScript' ? '/admin/v1/js/js.js' :
|
|
311
|
-
type === 'CSS' ? '/admin/v1/css/css.css' :
|
|
312
|
-
'/admin/v1' + bundle_item.route;
|
|
313
|
-
const responder = new Static_Route_HTTP_Responder(bundle_item);
|
|
314
|
-
server_router.set_route(namespaced_route, responder, responder.handle_http);
|
|
315
319
|
}
|
|
316
|
-
}
|
|
317
320
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
321
|
+
// Serve authenticated admin page at /admin/v1
|
|
322
|
+
server_router.set_route('/admin/v1', null, serve_admin_v1_page);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
322
325
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
326
|
+
// Temporary handler until the publisher finishes bundling
|
|
327
|
+
server_router.set_route('/admin/v1', null, serve_admin_v1_page);
|
|
328
|
+
resource_pool.add(admin_v1_publisher);
|
|
329
|
+
} else {
|
|
330
|
+
console.warn('Skipping /admin/v1 route registration due to missing Admin_Shell control.');
|
|
331
|
+
}
|
|
329
332
|
} else {
|
|
330
333
|
// admin_enabled === false
|
|
331
334
|
this.admin_v1 = null;
|
|
@@ -371,10 +374,11 @@ class JSGUI_Single_Process_Server extends Evented_Class {
|
|
|
371
374
|
// Specific options for when that publisher is in debug mode.
|
|
372
375
|
|
|
373
376
|
|
|
374
|
-
console.log('waiting for wp_publisher ready');
|
|
375
|
-
wp_publisher.on('ready', (wp_ready_res) => {
|
|
376
|
-
|
|
377
|
-
|
|
377
|
+
console.log('waiting for wp_publisher ready');
|
|
378
|
+
wp_publisher.on('ready', (wp_ready_res) => {
|
|
379
|
+
this.latest_wp_bundle = wp_ready_res;
|
|
380
|
+
//console.log('wp publisher is ready');
|
|
381
|
+
if (wp_ready_res._arr) {
|
|
378
382
|
|
|
379
383
|
|
|
380
384
|
for (const bundle_item of wp_ready_res._arr) {
|
|
@@ -456,20 +460,196 @@ class JSGUI_Single_Process_Server extends Evented_Class {
|
|
|
456
460
|
Object.defineProperty(this, 'router', { get: () => server_router })
|
|
457
461
|
}
|
|
458
462
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
463
|
+
/**
|
|
464
|
+
* Publish a JavaScript function as an HTTP API endpoint.
|
|
465
|
+
*
|
|
466
|
+
* The function is wrapped in a {@link Function_Publisher} and
|
|
467
|
+
* registered with the server's router. An entry is also added to
|
|
468
|
+
* `this._api_registry` so the OpenAPI spec generator can discover it.
|
|
469
|
+
*
|
|
470
|
+
* ### Route resolution
|
|
471
|
+
*
|
|
472
|
+
* - If `name` starts with `'/'`, it is used as-is (e.g. `'/health'`).
|
|
473
|
+
* - Otherwise it is auto-prefixed with `'/api/'` (e.g. `'users/list'` → `'/api/users/list'`).
|
|
474
|
+
*
|
|
475
|
+
* ### Metadata for Swagger
|
|
476
|
+
*
|
|
477
|
+
* The `meta` argument feeds the OpenAPI spec generator. All fields
|
|
478
|
+
* are optional — endpoints without metadata still work but produce
|
|
479
|
+
* a minimal Swagger entry.
|
|
480
|
+
*
|
|
481
|
+
* | Field | Type | Purpose |
|
|
482
|
+
* |---------------------|----------|----------------------------------------------|
|
|
483
|
+
* | `meta.method` | string | HTTP method (`'GET'`, `'POST'`, etc.) |
|
|
484
|
+
* | `meta.summary` | string | One-line summary displayed in Swagger UI |
|
|
485
|
+
* | `meta.description` | string | Multi-line Markdown description |
|
|
486
|
+
* | `meta.tags` | string[] | Grouping tags in Swagger UI |
|
|
487
|
+
* | `meta.params` | Object | Request body schema (`{key: {type, ...}}`) |
|
|
488
|
+
* | `meta.returns` | Object | Response body schema (`{key: {type, ...}}`) |
|
|
489
|
+
* | `meta.deprecated` | boolean | Mark endpoint as deprecated |
|
|
490
|
+
* | `meta.operationId` | string | Custom OpenAPI operationId |
|
|
491
|
+
* | `meta.raw` | boolean | Raw `(req, res)` handler — skip Function_Publisher |
|
|
492
|
+
*
|
|
493
|
+
* @param {string} name - Endpoint name or route path.
|
|
494
|
+
* @param {Function} fn - Handler function `(input) => result`, or `(req, res)` when `meta.raw` is `true`.
|
|
495
|
+
* @param {Object} [meta={}] - API metadata for routing and Swagger.
|
|
496
|
+
*
|
|
497
|
+
* @example
|
|
498
|
+
* // Minimal — just a name and function:
|
|
499
|
+
* server.publish('ping', () => ({ pong: true }));
|
|
500
|
+
* // → POST /api/ping
|
|
501
|
+
*
|
|
502
|
+
* @example
|
|
503
|
+
* // With full metadata for Swagger:
|
|
504
|
+
* server.publish('users/list', listUsers, {
|
|
505
|
+
* method: 'POST',
|
|
506
|
+
* summary: 'List all users',
|
|
507
|
+
* description: 'Returns paginated user list with filtering.',
|
|
508
|
+
* tags: ['Users'],
|
|
509
|
+
* params: {
|
|
510
|
+
* page: { type: 'integer', description: 'Page number', default: 1 },
|
|
511
|
+
* page_size: { type: 'integer', description: 'Items per page', default: 25 }
|
|
512
|
+
* },
|
|
513
|
+
* returns: {
|
|
514
|
+
* rows: { type: 'array', items: { type: 'object' } },
|
|
515
|
+
* total_count: { type: 'integer' }
|
|
516
|
+
* }
|
|
517
|
+
* });
|
|
518
|
+
* // → POST /api/users/list (documented in Swagger UI)
|
|
519
|
+
*
|
|
520
|
+
* @example
|
|
521
|
+
* // Raw handler for streaming NDJSON:
|
|
522
|
+
* server.publish('events/stream', (req, res) => {
|
|
523
|
+
* res.writeHead(200, { 'Content-Type': 'application/x-ndjson' });
|
|
524
|
+
* res.write(JSON.stringify({ event: 'start' }) + '\n');
|
|
525
|
+
* res.end();
|
|
526
|
+
* }, { method: 'GET', raw: true, summary: 'Stream events' });
|
|
527
|
+
*/
|
|
528
|
+
publish(name, fn, meta = {}) {
|
|
529
|
+
// Auto-prefix /api/ for simple names.
|
|
530
|
+
// If name already starts with '/', use as-is for full route control.
|
|
531
|
+
const full_route = name.startsWith('/') ? name : '/api/' + name;
|
|
532
|
+
|
|
533
|
+
// ── Raw handler passthrough ──
|
|
534
|
+
// When meta.raw is true, fn receives (req, res) directly.
|
|
535
|
+
// Useful for streaming, SSE, gzip, or custom response control.
|
|
536
|
+
if (meta.raw) {
|
|
537
|
+
this._api_registry = this._api_registry || [];
|
|
538
|
+
this._api_registry.push({
|
|
539
|
+
path: full_route,
|
|
540
|
+
method: (meta.method && meta.method !== 'ANY') ? meta.method.toUpperCase() : 'GET',
|
|
541
|
+
meta,
|
|
542
|
+
schema: {}
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
if (meta.method && meta.method !== 'ANY') {
|
|
546
|
+
const method = meta.method.toUpperCase();
|
|
547
|
+
this.server_router.set_route(full_route, null, (req, res) => {
|
|
548
|
+
if (req.method.toUpperCase() !== method && req.method.toUpperCase() !== 'HEAD') {
|
|
549
|
+
res.writeHead(405, { 'Allow': method });
|
|
550
|
+
res.end('Method Not Allowed');
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
return fn(req, res);
|
|
554
|
+
});
|
|
555
|
+
} else {
|
|
556
|
+
this.server_router.set_route(full_route, null, fn);
|
|
557
|
+
}
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// ── Standard function publisher ──
|
|
562
|
+
const fpub = new HTTP_Function_Publisher({ name, fn, meta });
|
|
465
563
|
|
|
466
564
|
this.function_publishers = this.function_publishers || [];
|
|
467
565
|
this.function_publishers.push(fpub);
|
|
468
566
|
|
|
469
|
-
//
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
567
|
+
// Register in API registry for OpenAPI spec generation.
|
|
568
|
+
this._api_registry = this._api_registry || [];
|
|
569
|
+
this._api_registry.push({
|
|
570
|
+
path: full_route,
|
|
571
|
+
method: (meta.method && meta.method !== 'ANY') ? meta.method.toUpperCase() : 'POST',
|
|
572
|
+
meta,
|
|
573
|
+
schema: fpub.schema
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
if (meta.method && meta.method !== 'ANY') {
|
|
577
|
+
const method = meta.method.toUpperCase();
|
|
578
|
+
this.server_router.set_route(full_route, fpub, (req, res) => {
|
|
579
|
+
if (req.method.toUpperCase() !== method) {
|
|
580
|
+
res.writeHead(405, { 'Allow': method });
|
|
581
|
+
res.end('Method Not Allowed');
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
return fpub.handle_http(req, res);
|
|
585
|
+
});
|
|
586
|
+
} else {
|
|
587
|
+
this.server_router.set_route(full_route, fpub, fpub.handle_http);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Register the built-in Swagger / OpenAPI routes on this server.
|
|
593
|
+
*
|
|
594
|
+
* Creates two endpoints:
|
|
595
|
+
*
|
|
596
|
+
* | Route | Method | Content-Type | Purpose |
|
|
597
|
+
* |----------------------|--------|--------------------|----------------------------------|
|
|
598
|
+
* | `/api/openapi.json` | GET | `application/json` | OpenAPI 3.0.3 spec (JSON) |
|
|
599
|
+
* | `/api/docs` | GET | `text/html` | Interactive Swagger UI page |
|
|
600
|
+
*
|
|
601
|
+
* The Swagger UI page loads its JS and CSS from the unpkg CDN at
|
|
602
|
+
* runtime, so zero npm dependencies are required. The page is
|
|
603
|
+
* styled to match jsgui3's dark aesthetic.
|
|
604
|
+
*
|
|
605
|
+
* ### Automatic registration
|
|
606
|
+
*
|
|
607
|
+
* When using `Server.serve()`, this method is called automatically
|
|
608
|
+
* after all endpoints are registered — unless `swagger: false` is
|
|
609
|
+
* set in the options. The default is:
|
|
610
|
+
*
|
|
611
|
+
* - **Development** (`NODE_ENV !== 'production'`): Swagger enabled.
|
|
612
|
+
* - **Production** (`NODE_ENV === 'production'`): Swagger disabled.
|
|
613
|
+
*
|
|
614
|
+
* ### Manual registration
|
|
615
|
+
*
|
|
616
|
+
* For servers created directly via `new Server(...)`, call this
|
|
617
|
+
* method after all `publish()` calls:
|
|
618
|
+
*
|
|
619
|
+
* ```js
|
|
620
|
+
* server._register_swagger_routes({ title: 'My API', version: '2.0.0' });
|
|
621
|
+
* ```
|
|
622
|
+
*
|
|
623
|
+
* This method is idempotent — calling it multiple times has no
|
|
624
|
+
* effect after the first call.
|
|
625
|
+
*
|
|
626
|
+
* @param {Object} [options] - Override options passed to the spec generator.
|
|
627
|
+
* @param {string} [options.title] - Override API title (default: server.name).
|
|
628
|
+
* @param {string} [options.version] - Override API version (default: '1.0.0').
|
|
629
|
+
* @param {string} [options.description] - Override API description.
|
|
630
|
+
*/
|
|
631
|
+
_register_swagger_routes(options = {}) {
|
|
632
|
+
if (this._swagger_registered) return;
|
|
633
|
+
this._swagger_registered = true;
|
|
634
|
+
|
|
635
|
+
const Swagger_Publisher = require('./publishers/swagger-publisher');
|
|
636
|
+
|
|
637
|
+
const swagger_pub = new Swagger_Publisher({
|
|
638
|
+
server: this,
|
|
639
|
+
title: options.title || this.name || 'API Documentation',
|
|
640
|
+
version: options.version,
|
|
641
|
+
description: options.description
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* The Swagger_Publisher instance, stored for introspection.
|
|
646
|
+
* @type {Swagger_Publisher}
|
|
647
|
+
*/
|
|
648
|
+
this._swagger_publisher = swagger_pub;
|
|
649
|
+
|
|
650
|
+
// Register both routes pointing to the same publisher.
|
|
651
|
+
this.server_router.set_route('/api/openapi.json', swagger_pub, swagger_pub.handle_http.bind(swagger_pub));
|
|
652
|
+
this.server_router.set_route('/api/docs', swagger_pub, swagger_pub.handle_http.bind(swagger_pub));
|
|
473
653
|
}
|
|
474
654
|
|
|
475
655
|
publish_observable(route, obs, options = {}) {
|
|
@@ -513,7 +693,71 @@ class JSGUI_Single_Process_Server extends Evented_Class {
|
|
|
513
693
|
get resource_names() {
|
|
514
694
|
return this.resource_pool.resource_names;
|
|
515
695
|
}
|
|
696
|
+
|
|
697
|
+
get_listening_endpoints() {
|
|
698
|
+
if (!this.listening_endpoints || !this.listening_endpoints.length) {
|
|
699
|
+
return [];
|
|
700
|
+
}
|
|
701
|
+
return this.listening_endpoints.map(endpoint => ({ ...endpoint }));
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
get_primary_endpoint() {
|
|
705
|
+
const endpoints = this.get_listening_endpoints();
|
|
706
|
+
if (!endpoints.length) return null;
|
|
707
|
+
return endpoints[0].url;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
get_startup_diagnostics() {
|
|
711
|
+
if (!this.startup_diagnostics) {
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
return {
|
|
715
|
+
...this.startup_diagnostics,
|
|
716
|
+
addresses_attempted: Array.isArray(this.startup_diagnostics.addresses_attempted)
|
|
717
|
+
? [...this.startup_diagnostics.addresses_attempted]
|
|
718
|
+
: [],
|
|
719
|
+
errors_by_address: this.startup_diagnostics.errors_by_address
|
|
720
|
+
? { ...this.startup_diagnostics.errors_by_address }
|
|
721
|
+
: {}
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
print_endpoints(options = {}) {
|
|
726
|
+
const endpoints = this.get_listening_endpoints();
|
|
727
|
+
const logger = typeof options.logger === 'function' ? options.logger : console.log;
|
|
728
|
+
const include_index = !!options.include_index;
|
|
729
|
+
const prefix = typeof options.prefix === 'string' ? options.prefix : 'listening endpoint';
|
|
730
|
+
|
|
731
|
+
if (!endpoints.length) {
|
|
732
|
+
logger('no listening endpoints');
|
|
733
|
+
return [];
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const lines = endpoints.map((endpoint, index) => {
|
|
737
|
+
if (include_index) {
|
|
738
|
+
return `${prefix} [${index}]: ${endpoint.url}`;
|
|
739
|
+
}
|
|
740
|
+
return `${prefix}: ${endpoint.url}`;
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
lines.forEach((line) => logger(line));
|
|
744
|
+
return lines;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
_record_listening_endpoint(protocol, host, port) {
|
|
748
|
+
this.listening_endpoints = this.listening_endpoints || [];
|
|
749
|
+
this.listening_endpoints.push({
|
|
750
|
+
protocol,
|
|
751
|
+
host,
|
|
752
|
+
port,
|
|
753
|
+
url: `${protocol}://${host}:${port}/`
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
|
|
516
757
|
'start'(port, callback, fnProcessRequest) {
|
|
758
|
+
const start_options = (fnProcessRequest && typeof fnProcessRequest === 'object') ? fnProcessRequest : {};
|
|
759
|
+
const fallback_on_port_conflict = start_options.on_port_conflict === 'auto-loopback';
|
|
760
|
+
|
|
517
761
|
// Guard against double-start which causes EADDRINUSE
|
|
518
762
|
if (this._started) {
|
|
519
763
|
console.warn('Server.start() called but server already started. Ignoring duplicate call.');
|
|
@@ -521,6 +765,7 @@ class JSGUI_Single_Process_Server extends Evented_Class {
|
|
|
521
765
|
return;
|
|
522
766
|
}
|
|
523
767
|
this._started = true;
|
|
768
|
+
this.listening_endpoints = [];
|
|
524
769
|
|
|
525
770
|
if (tof(port) !== 'number') {
|
|
526
771
|
console.log('Invalid port:', port);
|
|
@@ -534,12 +779,14 @@ class JSGUI_Single_Process_Server extends Evented_Class {
|
|
|
534
779
|
this.raise('starting');
|
|
535
780
|
rp.start(err => {
|
|
536
781
|
if (err) {
|
|
782
|
+
this._started = false;
|
|
537
783
|
throw err;
|
|
538
784
|
} else {
|
|
539
785
|
const lsi = rp.get_resource('Local Server Info');
|
|
540
786
|
const server_router = rp.get_resource('Server Router');
|
|
541
787
|
lsi.getters.net((err, net) => {
|
|
542
788
|
if (err) {
|
|
789
|
+
this._started = false;
|
|
543
790
|
callback(err);
|
|
544
791
|
} else {
|
|
545
792
|
// NEW: Filter addresses by allowed_addresses if specified.
|
|
@@ -559,11 +806,47 @@ class JSGUI_Single_Process_Server extends Evented_Class {
|
|
|
559
806
|
let num_to_start = arr_ipv4_addresses.length;
|
|
560
807
|
let started_count = 0;
|
|
561
808
|
let last_error = null;
|
|
809
|
+
const errors_by_address = {};
|
|
562
810
|
let ready_raised = false;
|
|
811
|
+
let fallback_attempted = false;
|
|
563
812
|
if (num_to_start === 0) {
|
|
564
|
-
|
|
813
|
+
const no_interface_error = new Error('No allowed network interfaces found.');
|
|
814
|
+
no_interface_error.code = 'ENOINTERFACES';
|
|
815
|
+
this._started = false;
|
|
816
|
+
if (callback) callback(no_interface_error);
|
|
565
817
|
return;
|
|
566
818
|
}
|
|
819
|
+
|
|
820
|
+
const start_loopback_fallback = async (process_request) => {
|
|
821
|
+
const fallback_host = '127.0.0.1';
|
|
822
|
+
const fallback_port = await get_port_or_free(0, fallback_host);
|
|
823
|
+
const fallback_protocol = this.https_options ? 'https' : 'http';
|
|
824
|
+
const fallback_server = this.https_options
|
|
825
|
+
? https.createServer(this.https_options, (req, res) => process_request(req, res))
|
|
826
|
+
: http.createServer((req, res) => process_request(req, res));
|
|
827
|
+
this.http_servers.push(fallback_server);
|
|
828
|
+
fallback_server.timeout = 10800000;
|
|
829
|
+
await new Promise((resolve, reject) => {
|
|
830
|
+
fallback_server.once('error', reject);
|
|
831
|
+
fallback_server.listen(fallback_port, fallback_host, resolve);
|
|
832
|
+
});
|
|
833
|
+
this._record_listening_endpoint(fallback_protocol, fallback_host, fallback_port);
|
|
834
|
+
if (!ready_raised) {
|
|
835
|
+
console.warn(`[server.start] Port conflict fallback engaged. Listening on ${fallback_host}:${fallback_port}`);
|
|
836
|
+
this.raise('listening');
|
|
837
|
+
ready_raised = true;
|
|
838
|
+
}
|
|
839
|
+
this.port = fallback_port;
|
|
840
|
+
this.startup_diagnostics = {
|
|
841
|
+
requested_port: port,
|
|
842
|
+
fallback_port,
|
|
843
|
+
fallback_host,
|
|
844
|
+
addresses_attempted: arr_ipv4_addresses,
|
|
845
|
+
errors_by_address
|
|
846
|
+
};
|
|
847
|
+
if (callback) callback(null, true);
|
|
848
|
+
};
|
|
849
|
+
|
|
567
850
|
const finalize_start = (err) => {
|
|
568
851
|
if (num_to_start !== 0) return;
|
|
569
852
|
if (started_count > 0) {
|
|
@@ -572,10 +855,32 @@ class JSGUI_Single_Process_Server extends Evented_Class {
|
|
|
572
855
|
this.raise('listening'); // Changed from 'ready' to avoid double-fire
|
|
573
856
|
ready_raised = true;
|
|
574
857
|
}
|
|
858
|
+
this.port = port;
|
|
859
|
+
this.startup_diagnostics = {
|
|
860
|
+
requested_port: port,
|
|
861
|
+
addresses_attempted: arr_ipv4_addresses,
|
|
862
|
+
errors_by_address
|
|
863
|
+
};
|
|
575
864
|
if (callback) callback(null, true);
|
|
576
865
|
return;
|
|
577
866
|
}
|
|
578
867
|
const final_error = err || last_error || new Error('No servers started.');
|
|
868
|
+
final_error.startup_diagnostics = {
|
|
869
|
+
requested_port: port,
|
|
870
|
+
addresses_attempted: arr_ipv4_addresses,
|
|
871
|
+
errors_by_address
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
if (fallback_on_port_conflict && !fallback_attempted && final_error && final_error.code === 'EADDRINUSE') {
|
|
875
|
+
fallback_attempted = true;
|
|
876
|
+
start_loopback_fallback(process_request).catch((fallback_err) => {
|
|
877
|
+
this._started = false;
|
|
878
|
+
if (callback) callback(fallback_err);
|
|
879
|
+
});
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
this._started = false;
|
|
579
884
|
if (callback) callback(final_error);
|
|
580
885
|
};
|
|
581
886
|
const respond_not_found = (res) => {
|
|
@@ -662,15 +967,27 @@ class JSGUI_Single_Process_Server extends Evented_Class {
|
|
|
662
967
|
next();
|
|
663
968
|
};
|
|
664
969
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
970
|
+
if (this.https_options) {
|
|
971
|
+
each(arr_ipv4_addresses, (ipv4_address) => {
|
|
972
|
+
try {
|
|
973
|
+
var https_server = https.createServer(this.https_options, function (req, res) {
|
|
974
|
+
process_request(req, res);
|
|
975
|
+
});
|
|
976
|
+
const open_sockets = new Set();
|
|
977
|
+
https_server._open_sockets = open_sockets;
|
|
978
|
+
https_server.on('connection', (socket) => {
|
|
979
|
+
open_sockets.add(socket);
|
|
980
|
+
socket.on('close', () => {
|
|
981
|
+
open_sockets.delete(socket);
|
|
982
|
+
});
|
|
983
|
+
});
|
|
984
|
+
this.http_servers.push(https_server);
|
|
985
|
+
https_server.on('error', (err) => {
|
|
986
|
+
last_error = err;
|
|
987
|
+
errors_by_address[ipv4_address] = {
|
|
988
|
+
code: err.code,
|
|
989
|
+
message: err.message
|
|
990
|
+
};
|
|
674
991
|
if (err.code === 'EACCES') {
|
|
675
992
|
console.error('Permission denied:', err.message);
|
|
676
993
|
} else if (err.code === 'EADDRINUSE') {
|
|
@@ -683,6 +1000,7 @@ class JSGUI_Single_Process_Server extends Evented_Class {
|
|
|
683
1000
|
});
|
|
684
1001
|
https_server.timeout = 10800000;
|
|
685
1002
|
https_server.listen(port, ipv4_address, () => {
|
|
1003
|
+
this._record_listening_endpoint('https', ipv4_address, port);
|
|
686
1004
|
console.log('* Server running at https://' + ipv4_address + ':' + port + '/');
|
|
687
1005
|
started_count++;
|
|
688
1006
|
num_to_start--;
|
|
@@ -694,15 +1012,27 @@ class JSGUI_Single_Process_Server extends Evented_Class {
|
|
|
694
1012
|
finalize_start(err);
|
|
695
1013
|
}
|
|
696
1014
|
});
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
1015
|
+
} else {
|
|
1016
|
+
each(arr_ipv4_addresses, (ipv4_address) => {
|
|
1017
|
+
try {
|
|
1018
|
+
var http_server = http.createServer(function (req, res) {
|
|
1019
|
+
process_request(req, res);
|
|
1020
|
+
});
|
|
1021
|
+
const open_sockets = new Set();
|
|
1022
|
+
http_server._open_sockets = open_sockets;
|
|
1023
|
+
http_server.on('connection', (socket) => {
|
|
1024
|
+
open_sockets.add(socket);
|
|
1025
|
+
socket.on('close', () => {
|
|
1026
|
+
open_sockets.delete(socket);
|
|
1027
|
+
});
|
|
1028
|
+
});
|
|
1029
|
+
this.http_servers.push(http_server);
|
|
1030
|
+
http_server.on('error', (err) => {
|
|
1031
|
+
last_error = err;
|
|
1032
|
+
errors_by_address[ipv4_address] = {
|
|
1033
|
+
code: err.code,
|
|
1034
|
+
message: err.message
|
|
1035
|
+
};
|
|
706
1036
|
if (err.code === 'EACCES') {
|
|
707
1037
|
console.error('Permission denied:', err.message);
|
|
708
1038
|
} else if (err.code === 'EADDRINUSE') {
|
|
@@ -715,6 +1045,7 @@ class JSGUI_Single_Process_Server extends Evented_Class {
|
|
|
715
1045
|
});
|
|
716
1046
|
http_server.timeout = 10800000;
|
|
717
1047
|
http_server.listen(port, ipv4_address, () => {
|
|
1048
|
+
this._record_listening_endpoint('http', ipv4_address, port);
|
|
718
1049
|
console.log('* Server running at http://' + ipv4_address + ':' + port + '/');
|
|
719
1050
|
started_count++;
|
|
720
1051
|
num_to_start--;
|
|
@@ -733,48 +1064,135 @@ class JSGUI_Single_Process_Server extends Evented_Class {
|
|
|
733
1064
|
});
|
|
734
1065
|
}
|
|
735
1066
|
|
|
736
|
-
close(callback) {
|
|
737
|
-
const invoke_stop = (target, done) => {
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
done(null);
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
if (
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
1067
|
+
close(callback) {
|
|
1068
|
+
const invoke_stop = (target, done) => {
|
|
1069
|
+
const stop_timeout_ms = 5000;
|
|
1070
|
+
let did_finish = false;
|
|
1071
|
+
const stop_timeout_handle = setTimeout(() => {
|
|
1072
|
+
const target_name = (target && (target.name || target.__type_name || (target.constructor && target.constructor.name)))
|
|
1073
|
+
|| 'unknown_target';
|
|
1074
|
+
console.warn(`Timed out waiting for stop() on ${target_name}; continuing shutdown.`);
|
|
1075
|
+
finish_stop(null);
|
|
1076
|
+
}, stop_timeout_ms);
|
|
1077
|
+
stop_timeout_handle.unref?.();
|
|
1078
|
+
|
|
1079
|
+
const finish_stop = (error) => {
|
|
1080
|
+
if (did_finish) {
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
did_finish = true;
|
|
1084
|
+
clearTimeout(stop_timeout_handle);
|
|
1085
|
+
done(error || null);
|
|
1086
|
+
};
|
|
1087
|
+
|
|
1088
|
+
if (!target || typeof target.stop !== 'function') {
|
|
1089
|
+
finish_stop(null);
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
if (target.stop.length >= 1) {
|
|
1094
|
+
try {
|
|
1095
|
+
target.stop((error) => finish_stop(error || null));
|
|
1096
|
+
} catch (error) {
|
|
1097
|
+
finish_stop(error);
|
|
1098
|
+
}
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
try {
|
|
1103
|
+
const stop_result = target.stop();
|
|
1104
|
+
if (stop_result && typeof stop_result.then === 'function') {
|
|
1105
|
+
stop_result.then(() => finish_stop(null), (error) => finish_stop(error || null));
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
finish_stop(null);
|
|
1109
|
+
} catch (error) {
|
|
1110
|
+
finish_stop(error);
|
|
1111
|
+
}
|
|
1112
|
+
};
|
|
1113
|
+
|
|
1114
|
+
const force_close_server_connections = (server_instance) => {
|
|
1115
|
+
if (!server_instance) {
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
if (typeof server_instance.closeIdleConnections === 'function') {
|
|
1119
|
+
try {
|
|
1120
|
+
server_instance.closeIdleConnections();
|
|
1121
|
+
} catch (error) {
|
|
1122
|
+
// Ignore best-effort close idle connection errors.
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
if (typeof server_instance.closeAllConnections === 'function') {
|
|
1126
|
+
try {
|
|
1127
|
+
server_instance.closeAllConnections();
|
|
1128
|
+
} catch (error) {
|
|
1129
|
+
// Ignore best-effort close all connection errors.
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
if (server_instance._open_sockets && typeof server_instance._open_sockets.forEach === 'function') {
|
|
1133
|
+
server_instance._open_sockets.forEach((socket) => {
|
|
1134
|
+
try {
|
|
1135
|
+
socket.destroy();
|
|
1136
|
+
} catch (error) {
|
|
1137
|
+
// Ignore socket destroy errors during shutdown.
|
|
1138
|
+
}
|
|
1139
|
+
});
|
|
1140
|
+
server_instance._open_sockets.clear?.();
|
|
1141
|
+
}
|
|
1142
|
+
};
|
|
1143
|
+
|
|
1144
|
+
const close_http_servers = (done) => {
|
|
1145
|
+
const http_server_list = Array.isArray(this.http_servers) ? [...this.http_servers] : [];
|
|
1146
|
+
let pending_server_count = http_server_list.length;
|
|
1147
|
+
const complete_http_server_close = () => {
|
|
1148
|
+
this.http_servers = [];
|
|
1149
|
+
this.listening_endpoints = [];
|
|
1150
|
+
this.startup_diagnostics = null;
|
|
1151
|
+
this._started = false;
|
|
1152
|
+
done();
|
|
1153
|
+
};
|
|
1154
|
+
if (pending_server_count === 0) {
|
|
1155
|
+
complete_http_server_close();
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
http_server_list.forEach((server_instance) => {
|
|
1160
|
+
let did_close_server = false;
|
|
1161
|
+
const mark_server_closed = () => {
|
|
1162
|
+
if (did_close_server) {
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
did_close_server = true;
|
|
1166
|
+
pending_server_count--;
|
|
1167
|
+
if (pending_server_count === 0) {
|
|
1168
|
+
complete_http_server_close();
|
|
1169
|
+
}
|
|
1170
|
+
};
|
|
1171
|
+
|
|
1172
|
+
const close_timeout_ms = 5000;
|
|
1173
|
+
const close_timeout_handle = setTimeout(() => {
|
|
1174
|
+
force_close_server_connections(server_instance);
|
|
1175
|
+
mark_server_closed();
|
|
1176
|
+
}, close_timeout_ms);
|
|
1177
|
+
close_timeout_handle.unref?.();
|
|
1178
|
+
|
|
1179
|
+
try {
|
|
1180
|
+
force_close_server_connections(server_instance);
|
|
1181
|
+
server_instance.close((error) => {
|
|
1182
|
+
clearTimeout(close_timeout_handle);
|
|
1183
|
+
if (error && error.code !== 'ERR_SERVER_NOT_RUNNING') {
|
|
1184
|
+
console.error('Error while closing HTTP server:', error);
|
|
1185
|
+
}
|
|
1186
|
+
force_close_server_connections(server_instance);
|
|
1187
|
+
mark_server_closed();
|
|
1188
|
+
});
|
|
1189
|
+
} catch (error) {
|
|
1190
|
+
clearTimeout(close_timeout_handle);
|
|
1191
|
+
force_close_server_connections(server_instance);
|
|
1192
|
+
mark_server_closed();
|
|
1193
|
+
}
|
|
1194
|
+
});
|
|
1195
|
+
};
|
|
778
1196
|
|
|
779
1197
|
const finalize_close = (error) => {
|
|
780
1198
|
if (callback) callback(error || null);
|