jsgui3-server 0.0.140 → 0.0.141
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/.github/agents/jsgui3-server.agent.md +699 -0
- package/.github/instructions/copilot.instructions.md +180 -0
- package/.playwright-mcp/page-2025-11-29T23-39-18-629Z.png +0 -0
- package/.playwright-mcp/page-2025-11-29T23-39-31-903Z.png +0 -0
- package/.playwright-mcp/page-2025-11-30T00-33-56-265Z.png +0 -0
- package/.playwright-mcp/page-2025-11-30T00-34-06-619Z.png +0 -0
- package/docs/agent-development-guide.md +108 -4
- package/docs/api-reference.md +116 -0
- package/docs/controls-development.md +127 -0
- package/docs/css/luxuryObsidianCss.js +1203 -0
- package/docs/css/obsidian-scrollbars.css +370 -0
- package/docs/diagrams/jsgui3-stack.svg +568 -0
- package/docs/guides/JSGUI3_UI_ARCHITECTURE_GUIDE.md +2527 -0
- package/docs/guides/OBSIDIAN_LUXURY_DESIGN_GUIDE.md +847 -0
- package/docs/jsgui3-vs-express-comparison.svg +542 -0
- package/docs/jsgui3-vs-nestjs-comparison.svg +550 -0
- package/docs/publishers-guide.md +76 -0
- package/docs/troubleshooting.md +51 -0
- package/examples/controls/15) window, observable SSE/README.md +125 -0
- package/examples/controls/15) window, observable SSE/check.js +144 -0
- package/examples/controls/15) window, observable SSE/client.js +395 -0
- package/examples/controls/15) window, observable SSE/server.js +111 -0
- package/http/responders/static/Static_Route_HTTP_Responder.js +16 -16
- package/module.js +7 -0
- package/package.json +7 -6
- package/port-utils.js +112 -0
- package/serve-factory.js +27 -5
- package/tests/README.md +40 -26
- package/tests/examples-controls.e2e.test.js +164 -0
- package/tests/observable-sse.test.js +363 -0
- package/tests/port-utils.test.js +114 -0
- package/tests/test-runner.js +13 -12
package/port-utils.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port utilities for jsgui3-server
|
|
3
|
+
* Provides automatic free port detection and port management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const net = require('net');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Find a free port on the specified host
|
|
10
|
+
* @param {Object} options - Options for port selection
|
|
11
|
+
* @param {string} [options.host='127.0.0.1'] - Host to check
|
|
12
|
+
* @param {number} [options.startPort=8080] - Starting port to try (0 = let OS choose)
|
|
13
|
+
* @param {number} [options.endPort=65535] - Maximum port to try
|
|
14
|
+
* @returns {Promise<number>} - A free port number
|
|
15
|
+
*/
|
|
16
|
+
function get_free_port(options = {}) {
|
|
17
|
+
const host = options.host || '127.0.0.1';
|
|
18
|
+
const start_port = options.startPort || options.start_port || 0;
|
|
19
|
+
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const server = net.createServer();
|
|
22
|
+
|
|
23
|
+
server.on('error', (err) => {
|
|
24
|
+
if (err.code === 'EADDRINUSE' && start_port > 0) {
|
|
25
|
+
// Try next port if specific port was requested
|
|
26
|
+
get_free_port({ ...options, startPort: start_port + 1 })
|
|
27
|
+
.then(resolve)
|
|
28
|
+
.catch(reject);
|
|
29
|
+
} else {
|
|
30
|
+
reject(err);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
server.listen(start_port, host, () => {
|
|
35
|
+
const { port } = server.address();
|
|
36
|
+
server.close((err) => {
|
|
37
|
+
if (err) {
|
|
38
|
+
reject(err);
|
|
39
|
+
} else {
|
|
40
|
+
resolve(port);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if a specific port is available
|
|
49
|
+
* @param {number} port - Port to check
|
|
50
|
+
* @param {string} [host='127.0.0.1'] - Host to check
|
|
51
|
+
* @returns {Promise<boolean>} - True if port is available
|
|
52
|
+
*/
|
|
53
|
+
function is_port_available(port, host = '127.0.0.1') {
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
const server = net.createServer();
|
|
56
|
+
|
|
57
|
+
server.on('error', () => {
|
|
58
|
+
resolve(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
server.listen(port, host, () => {
|
|
62
|
+
server.close(() => {
|
|
63
|
+
resolve(true);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Find multiple free ports
|
|
71
|
+
* @param {number} count - Number of ports to find
|
|
72
|
+
* @param {Object} options - Options for port selection
|
|
73
|
+
* @returns {Promise<number[]>} - Array of free port numbers
|
|
74
|
+
*/
|
|
75
|
+
async function get_free_ports(count, options = {}) {
|
|
76
|
+
const ports = [];
|
|
77
|
+
for (let i = 0; i < count; i++) {
|
|
78
|
+
const port = await get_free_port(options);
|
|
79
|
+
ports.push(port);
|
|
80
|
+
}
|
|
81
|
+
return ports;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get a free port, preferring a specific port if available
|
|
86
|
+
* @param {number} preferred_port - Preferred port (0 = auto-select)
|
|
87
|
+
* @param {string} [host='127.0.0.1'] - Host to check
|
|
88
|
+
* @returns {Promise<number>} - The preferred port if available, or a free port
|
|
89
|
+
*/
|
|
90
|
+
async function get_port_or_free(preferred_port, host = '127.0.0.1') {
|
|
91
|
+
// If 0 or undefined, always auto-select
|
|
92
|
+
if (!preferred_port || preferred_port === 0) {
|
|
93
|
+
return get_free_port({ host });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check if preferred port is available
|
|
97
|
+
const available = await is_port_available(preferred_port, host);
|
|
98
|
+
if (available) {
|
|
99
|
+
return preferred_port;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Fall back to auto-select
|
|
103
|
+
console.log(`Port ${preferred_port} is in use, selecting a free port...`);
|
|
104
|
+
return get_free_port({ host });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
get_free_port,
|
|
109
|
+
is_port_available,
|
|
110
|
+
get_free_ports,
|
|
111
|
+
get_port_or_free
|
|
112
|
+
};
|
package/serve-factory.js
CHANGED
|
@@ -9,6 +9,7 @@ const lib_path = require('path');
|
|
|
9
9
|
const Webpage = require('./website/webpage');
|
|
10
10
|
const HTTP_Webpage_Publisher = require('./publishers/http-webpage-publisher');
|
|
11
11
|
const Static_Route_HTTP_Responder = require('./http/responders/static/Static_Route_HTTP_Responder');
|
|
12
|
+
const { get_port_or_free } = require('./port-utils');
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
const prepare_webpage_route = (server, route, page_options = {}, defaults = {}) => {
|
|
@@ -130,8 +131,10 @@ module.exports = (Server) => {
|
|
|
130
131
|
throw new Error('`pages` option requires at least one control constructor.');
|
|
131
132
|
}
|
|
132
133
|
|
|
133
|
-
const port = Number.isFinite(serve_options.port) ? Number(serve_options.port) : (process.env.PORT ? Number(process.env.PORT) : 8080);
|
|
134
|
-
|
|
134
|
+
const port = Number.isFinite(serve_options.port) ? Number(serve_options.port) : (serve_options.port === 'auto' ? 0 : (process.env.PORT ? Number(process.env.PORT) : 8080));
|
|
135
|
+
const auto_port = serve_options.autoPort !== false && (port === 0 || serve_options.port === 'auto' || serve_options.autoPort === true);
|
|
136
|
+
|
|
137
|
+
if (!Number.isFinite(port) && serve_options.port !== 'auto') {
|
|
135
138
|
throw new Error('Invalid port specified for Server.serve');
|
|
136
139
|
}
|
|
137
140
|
|
|
@@ -193,17 +196,36 @@ module.exports = (Server) => {
|
|
|
193
196
|
}
|
|
194
197
|
|
|
195
198
|
return new Promise((resolve, reject) => {
|
|
196
|
-
const start_server = () => {
|
|
199
|
+
const start_server = async () => {
|
|
197
200
|
if (has_started) return;
|
|
198
201
|
has_started = true;
|
|
202
|
+
|
|
203
|
+
// Determine the actual port to use
|
|
204
|
+
let actual_port = port;
|
|
205
|
+
if (auto_port) {
|
|
206
|
+
try {
|
|
207
|
+
const check_host = host || '127.0.0.1';
|
|
208
|
+
actual_port = await get_port_or_free(port, Array.isArray(check_host) ? check_host[0] : check_host);
|
|
209
|
+
if (actual_port !== port && port !== 0) {
|
|
210
|
+
console.log(`Port ${port} was in use, using port ${actual_port} instead`);
|
|
211
|
+
}
|
|
212
|
+
} catch (err) {
|
|
213
|
+
console.error('Failed to find free port:', err);
|
|
214
|
+
return settle(reject, err);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Store the actual port on the server instance for reference
|
|
219
|
+
server_instance.port = actual_port;
|
|
220
|
+
|
|
199
221
|
console.log('🔍 DEBUG: Calling server_instance.start()');
|
|
200
|
-
server_instance.start(
|
|
222
|
+
server_instance.start(actual_port, (err) => {
|
|
201
223
|
if (err) {
|
|
202
224
|
console.log('🔍 DEBUG: server_instance.start() failed:', err);
|
|
203
225
|
return settle(reject, err);
|
|
204
226
|
}
|
|
205
227
|
console.log('🔍 DEBUG: server_instance.start() succeeded');
|
|
206
|
-
const message = host ? `Serving on http://${Array.isArray(host) ? host[0] : host}:${
|
|
228
|
+
const message = host ? `Serving on http://${Array.isArray(host) ? host[0] : host}:${actual_port}/` : `Serving on port ${actual_port} (all IPv4 interfaces)`;
|
|
207
229
|
console.log(message);
|
|
208
230
|
console.log('Server ready');
|
|
209
231
|
settle(resolve, server_instance);
|
package/tests/README.md
CHANGED
|
@@ -22,12 +22,13 @@ tests/
|
|
|
22
22
|
├── publishers.test.js # Component isolation tests for publishers
|
|
23
23
|
├── configuration-validation.test.js # Configuration validation tests
|
|
24
24
|
├── end-to-end.test.js # Full integration tests
|
|
25
|
-
├── content-analysis.test.js # Content analysis and verification
|
|
26
|
-
├── performance.test.js # Performance benchmarks
|
|
27
|
-
├── error-handling.test.js # Error handling and edge cases
|
|
28
|
-
├──
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
├── content-analysis.test.js # Content analysis and verification
|
|
26
|
+
├── performance.test.js # Performance benchmarks
|
|
27
|
+
├── error-handling.test.js # Error handling and edge cases
|
|
28
|
+
├── examples-controls.e2e.test.js # Example apps regression (controls)
|
|
29
|
+
├── test-runner.js # Custom test runner with reporting
|
|
30
|
+
└── README.md # This file
|
|
31
|
+
```
|
|
31
32
|
|
|
32
33
|
## Running Tests
|
|
33
34
|
|
|
@@ -36,19 +37,24 @@ tests/
|
|
|
36
37
|
npm test
|
|
37
38
|
```
|
|
38
39
|
|
|
39
|
-
### Run Specific Test Suite
|
|
40
|
-
```bash
|
|
41
|
-
# Using the custom test runner
|
|
42
|
-
node tests/test-runner.js --test=bundlers.test.js
|
|
43
|
-
|
|
44
|
-
# Using mocha directly
|
|
45
|
-
npx mocha tests/bundlers.test.js
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
### Run
|
|
49
|
-
```bash
|
|
50
|
-
|
|
51
|
-
|
|
40
|
+
### Run Specific Test Suite
|
|
41
|
+
```bash
|
|
42
|
+
# Using the custom test runner
|
|
43
|
+
node tests/test-runner.js --test=bundlers.test.js
|
|
44
|
+
|
|
45
|
+
# Using mocha directly
|
|
46
|
+
npx mocha tests/bundlers.test.js
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Run Example Apps Regression Suite
|
|
50
|
+
```bash
|
|
51
|
+
npm run test:examples:controls
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Run Tests with Options
|
|
55
|
+
```bash
|
|
56
|
+
# Debug mode (enables sourcemaps)
|
|
57
|
+
node tests/test-runner.js --debug
|
|
52
58
|
|
|
53
59
|
# Verbose output
|
|
54
60
|
node tests/test-runner.js --verbose
|
|
@@ -113,12 +119,20 @@ Comprehensive error scenario testing:
|
|
|
113
119
|
|
|
114
120
|
- Invalid JavaScript syntax
|
|
115
121
|
- File system errors (permissions, missing files)
|
|
116
|
-
- Configuration validation errors
|
|
117
|
-
- Network and HTTP errors
|
|
118
|
-
- Memory and performance limits
|
|
119
|
-
- Encoding issues
|
|
120
|
-
|
|
121
|
-
|
|
122
|
+
- Configuration validation errors
|
|
123
|
+
- Network and HTTP errors
|
|
124
|
+
- Memory and performance limits
|
|
125
|
+
- Encoding issues
|
|
126
|
+
|
|
127
|
+
### 7. Examples/Controls E2E Tests (`examples-controls.e2e.test.js`)
|
|
128
|
+
|
|
129
|
+
Regression coverage for a representative set of `examples/controls/*` apps:
|
|
130
|
+
|
|
131
|
+
- Boots a server per example control
|
|
132
|
+
- Verifies `/`, `/js/js.js`, and `/css/css.css` routes
|
|
133
|
+
- Ensures HTML rendering works without `Accept-Encoding`
|
|
134
|
+
|
|
135
|
+
## Configuration Examples
|
|
122
136
|
|
|
123
137
|
### Basic Minification
|
|
124
138
|
```javascript
|
|
@@ -247,4 +261,4 @@ The test suite is designed to work with continuous integration:
|
|
|
247
261
|
path: test-report.json
|
|
248
262
|
```
|
|
249
263
|
|
|
250
|
-
This ensures the minification, compression, and sourcemap features maintain their functionality across code changes.
|
|
264
|
+
This ensures the minification, compression, and sourcemap features maintain their functionality across code changes.
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E regression tests for example control apps under examples/controls/.
|
|
3
|
+
*
|
|
4
|
+
* These tests boot a server for a selected set of examples and validate:
|
|
5
|
+
* - The HTML route renders successfully (even without Accept-Encoding)
|
|
6
|
+
* - Bundled JS and CSS routes are served
|
|
7
|
+
* - The server-rendered HTML includes the expected number of Window controls
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const assert = require('assert');
|
|
11
|
+
const http = require('http');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { describe, it } = require('mocha');
|
|
14
|
+
|
|
15
|
+
const Server = require('../server');
|
|
16
|
+
const { get_free_port } = require('../port-utils');
|
|
17
|
+
|
|
18
|
+
const repo_root_path = path.join(__dirname, '..');
|
|
19
|
+
const examples_controls_root_path = path.join(repo_root_path, 'examples', 'controls');
|
|
20
|
+
|
|
21
|
+
const examples = [
|
|
22
|
+
{ dir_name: '1) window', ctrl_name: 'Demo_UI', expected_window_count: 1 },
|
|
23
|
+
{ dir_name: '2) two windows', ctrl_name: 'Demo_UI', expected_window_count: 2 },
|
|
24
|
+
{ dir_name: '3) five windows', ctrl_name: 'Demo_UI', expected_window_count: 5 },
|
|
25
|
+
{ dir_name: '4) window, tabbed panel', ctrl_name: 'Demo_UI', expected_window_count: 1 },
|
|
26
|
+
{ dir_name: '5) window, grid', ctrl_name: 'Demo_UI', expected_window_count: 1 },
|
|
27
|
+
{ dir_name: '8) window, checkbox/a)', ctrl_name: 'Demo_UI', expected_window_count: 1 },
|
|
28
|
+
{ dir_name: '9) window, date picker', ctrl_name: 'Demo_UI', expected_window_count: 1 },
|
|
29
|
+
{ dir_name: '12) window, Select_Options control', ctrl_name: 'Demo_UI', expected_window_count: 1 },
|
|
30
|
+
{ dir_name: '13) window, Dropdown_Menu control', ctrl_name: 'Demo_UI', expected_window_count: 1 }
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
function make_request(url, { headers = {} } = {}) {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const parsed_url = new URL(url);
|
|
36
|
+
const options = {
|
|
37
|
+
hostname: parsed_url.hostname,
|
|
38
|
+
port: parsed_url.port,
|
|
39
|
+
path: parsed_url.pathname,
|
|
40
|
+
method: 'GET',
|
|
41
|
+
headers: {
|
|
42
|
+
'User-Agent': 'JSGUI3-Examples-E2E/1.0',
|
|
43
|
+
...headers
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const req = http.request(options, (res) => {
|
|
48
|
+
const chunks = [];
|
|
49
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
50
|
+
res.on('end', () => {
|
|
51
|
+
resolve({
|
|
52
|
+
status_code: res.statusCode,
|
|
53
|
+
headers: res.headers,
|
|
54
|
+
body: Buffer.concat(chunks).toString('utf8')
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
req.on('error', reject);
|
|
60
|
+
req.setTimeout(15000, () => {
|
|
61
|
+
req.destroy();
|
|
62
|
+
reject(new Error('Request timeout'));
|
|
63
|
+
});
|
|
64
|
+
req.end();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function count_occurrences(haystack, needle) {
|
|
69
|
+
let idx = 0;
|
|
70
|
+
let count = 0;
|
|
71
|
+
while (true) {
|
|
72
|
+
idx = haystack.indexOf(needle, idx);
|
|
73
|
+
if (idx === -1) return count;
|
|
74
|
+
count++;
|
|
75
|
+
idx += needle.length;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function start_example_server({ dir_name, ctrl_name }) {
|
|
80
|
+
const example_dir_path = path.join(examples_controls_root_path, dir_name);
|
|
81
|
+
const example_client_path = path.join(example_dir_path, 'client.js');
|
|
82
|
+
|
|
83
|
+
const jsgui = require(example_client_path);
|
|
84
|
+
const ctrl = jsgui.controls && jsgui.controls[ctrl_name];
|
|
85
|
+
assert(ctrl, `Missing exported control jsgui.controls.${ctrl_name} in ${example_client_path}`);
|
|
86
|
+
|
|
87
|
+
const server = new Server({
|
|
88
|
+
Ctrl: ctrl,
|
|
89
|
+
src_path_client_js: example_client_path,
|
|
90
|
+
name: `examples/controls/${dir_name}`
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
server.allowed_addresses = ['127.0.0.1'];
|
|
94
|
+
|
|
95
|
+
await new Promise((resolve, reject) => {
|
|
96
|
+
const timeout = setTimeout(() => reject(new Error('Publisher ready timeout')), 60000);
|
|
97
|
+
server.on('ready', () => {
|
|
98
|
+
clearTimeout(timeout);
|
|
99
|
+
resolve();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const port = await get_free_port();
|
|
104
|
+
|
|
105
|
+
await new Promise((resolve, reject) => {
|
|
106
|
+
server.start(port, (err) => (err ? reject(err) : resolve()));
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return { server, port };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function close_server(server) {
|
|
113
|
+
await new Promise((resolve) => server.close(resolve));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
describe('Examples/Controls E2E Regression Tests', function() {
|
|
117
|
+
this.timeout(180000);
|
|
118
|
+
|
|
119
|
+
for (const example of examples) {
|
|
120
|
+
it(`should render and serve bundles for "${example.dir_name}"`, async function() {
|
|
121
|
+
const { server, port } = await start_example_server(example);
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const base_url = `http://127.0.0.1:${port}`;
|
|
125
|
+
|
|
126
|
+
const html_response = await make_request(`${base_url}/`, {
|
|
127
|
+
headers: {
|
|
128
|
+
// Deliberately omit Accept-Encoding to ensure identity works.
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
assert.strictEqual(html_response.status_code, 200);
|
|
133
|
+
assert(
|
|
134
|
+
(html_response.headers['content-type'] || '').includes('text/html'),
|
|
135
|
+
'Expected HTML content-type'
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const window_count = count_occurrences(html_response.body, 'data-jsgui-type="window"');
|
|
139
|
+
assert.strictEqual(
|
|
140
|
+
window_count,
|
|
141
|
+
example.expected_window_count,
|
|
142
|
+
`Expected ${example.expected_window_count} windows, got ${window_count}`
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const js_response = await make_request(`${base_url}/js/js.js`);
|
|
146
|
+
assert.strictEqual(js_response.status_code, 200);
|
|
147
|
+
assert(
|
|
148
|
+
(js_response.headers['content-type'] || '').includes('javascript'),
|
|
149
|
+
'Expected JavaScript content-type'
|
|
150
|
+
);
|
|
151
|
+
assert(js_response.body.length > 1000, 'Expected non-trivial JS bundle');
|
|
152
|
+
|
|
153
|
+
const css_response = await make_request(`${base_url}/css/css.css`);
|
|
154
|
+
assert.strictEqual(css_response.status_code, 200);
|
|
155
|
+
assert(
|
|
156
|
+
(css_response.headers['content-type'] || '').includes('css'),
|
|
157
|
+
'Expected CSS content-type'
|
|
158
|
+
);
|
|
159
|
+
} finally {
|
|
160
|
+
await close_server(server);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
});
|