@travetto/web-http 8.0.0-alpha.23 → 8.0.0-alpha.25
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 +12 -14
- package/__index__.ts +1 -1
- package/package.json +4 -4
- package/src/config.ts +8 -8
- package/src/http.ts +8 -9
- package/src/node.ts +3 -4
- package/src/tls.ts +3 -4
- package/src/types.ts +2 -2
- package/support/cli.web_http.ts +4 -5
- package/support/test/dispatcher.ts +10 -11
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ npm install @travetto/web-http
|
|
|
13
13
|
yarn add @travetto/web-http
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
This module provides basic for running [http](https://nodejs.org/api/http.html). [https](https://nodejs.org/api/https.html)
|
|
16
|
+
This module provides basic for running [http](https://nodejs.org/api/http.html). [https](https://nodejs.org/api/https.html) and [http2](https://nodejs.org/api/http2.html) servers, along with support for tls key generation during development.
|
|
17
17
|
|
|
18
18
|
## CLI - web:http
|
|
19
19
|
By default, the framework provides a default [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#L20) for [WebHttpServer](https://github.com/travetto/travetto/tree/main/module/web-http/src/types.ts#L19) that will follow default behaviors, and spin up the server.
|
|
@@ -117,7 +117,6 @@ Listening on port { port: 3000 }
|
|
|
117
117
|
**Code: Standard Web Http Config**
|
|
118
118
|
```typescript
|
|
119
119
|
export class WebHttpConfig {
|
|
120
|
-
|
|
121
120
|
/**
|
|
122
121
|
* What version of HTTP to use
|
|
123
122
|
* Version 2 requires SSL for direct browser access
|
|
@@ -153,8 +152,8 @@ export class WebHttpConfig {
|
|
|
153
152
|
|
|
154
153
|
@PostConstruct()
|
|
155
154
|
async finalizeConfig(): Promise<void> {
|
|
156
|
-
this.tls ??=
|
|
157
|
-
this.port =
|
|
155
|
+
this.tls ??= this.httpVersion === '2' || !!this.tlsKeys;
|
|
156
|
+
this.port = this.port < 0 ? await NetUtil.getFreePort() : this.port;
|
|
158
157
|
this.bindAddress ||= NetUtil.getLocalAddress();
|
|
159
158
|
|
|
160
159
|
if (!this.tls) {
|
|
@@ -166,7 +165,8 @@ export class WebHttpConfig {
|
|
|
166
165
|
}
|
|
167
166
|
this.tlsKeys = await WebTlsUtil.generateKeyPair();
|
|
168
167
|
} else {
|
|
169
|
-
if (this.tlsKeys.key.length < 100) {
|
|
168
|
+
if (this.tlsKeys.key.length < 100) {
|
|
169
|
+
// We have files or resources
|
|
170
170
|
this.tlsKeys.key = await RuntimeResources.readUTF8(this.tlsKeys.key);
|
|
171
171
|
this.tlsKeys.cert = await RuntimeResources.readUTF8(this.tlsKeys.cert);
|
|
172
172
|
}
|
|
@@ -182,17 +182,16 @@ To customize a Web server, you may need to construct an entry point using the [@
|
|
|
182
182
|
|
|
183
183
|
**Code: Application entry point for Web Applications**
|
|
184
184
|
```typescript
|
|
185
|
-
import { Env, toConcrete } from '@travetto/runtime';
|
|
186
185
|
import { CliCommand } from '@travetto/cli';
|
|
187
186
|
import { DependencyRegistryIndex } from '@travetto/di';
|
|
188
187
|
import { Registry } from '@travetto/registry';
|
|
189
|
-
import {
|
|
188
|
+
import { Env, toConcrete } from '@travetto/runtime';
|
|
189
|
+
import { WebHttpConfig, type WebHttpServer } from '@travetto/web-http';
|
|
190
190
|
|
|
191
191
|
import './config-override.ts';
|
|
192
192
|
|
|
193
193
|
@CliCommand({ runTarget: true })
|
|
194
194
|
export class SampleApp {
|
|
195
|
-
|
|
196
195
|
preMain(): void {
|
|
197
196
|
Env.NODE_ENV.set('production');
|
|
198
197
|
}
|
|
@@ -288,7 +287,6 @@ Listening on port { port: 3000 }
|
|
|
288
287
|
**Code: Implementation**
|
|
289
288
|
```typescript
|
|
290
289
|
export class NodeWebHttpServer implements WebHttpServer {
|
|
291
|
-
|
|
292
290
|
@Inject()
|
|
293
291
|
serverConfig: WebHttpConfig;
|
|
294
292
|
|
|
@@ -299,7 +297,7 @@ export class NodeWebHttpServer implements WebHttpServer {
|
|
|
299
297
|
configService: ConfigurationService;
|
|
300
298
|
|
|
301
299
|
async serve(): Promise<WebServerHandle> {
|
|
302
|
-
const handle = await WebHttpUtil.startHttpServer({ ...this.serverConfig, dispatcher: this.router
|
|
300
|
+
const handle = await WebHttpUtil.startHttpServer({ ...this.serverConfig, dispatcher: this.router });
|
|
303
301
|
console.log('Initialized', await this.configService.initBanner());
|
|
304
302
|
console.log('Listening', { port: this.serverConfig.port });
|
|
305
303
|
return handle;
|
|
@@ -307,7 +305,7 @@ export class NodeWebHttpServer implements WebHttpServer {
|
|
|
307
305
|
}
|
|
308
306
|
```
|
|
309
307
|
|
|
310
|
-
Current the [NodeWebHttpServer](https://github.com/travetto/travetto/tree/main/module/web-http/src/node.ts#L13) is the only provided [WebHttpServer](https://github.com/travetto/travetto/tree/main/module/web-http/src/types.ts#L19) implementation.
|
|
308
|
+
Current the [NodeWebHttpServer](https://github.com/travetto/travetto/tree/main/module/web-http/src/node.ts#L13) is the only provided [WebHttpServer](https://github.com/travetto/travetto/tree/main/module/web-http/src/types.ts#L19) implementation. It supports http/1.1, http/2, and tls, and is the same foundation as used by express, koa, and other popular frameworks.
|
|
311
309
|
|
|
312
310
|
## Standard Utilities
|
|
313
311
|
The module also provides standard utilities for starting http servers programmatically:
|
|
@@ -349,12 +347,12 @@ static buildHandler(dispatcher: WebDispatcher): (request: HttpRequest, response:
|
|
|
349
347
|
```
|
|
350
348
|
|
|
351
349
|
we can see the structure for integrating the server behavior with the [Web API](https://github.com/travetto/travetto/tree/main/module/web#readme "Declarative support for creating Web Applications") module dispatcher:
|
|
352
|
-
* Converting the node primitive request to a
|
|
350
|
+
* Converting the node primitive request to a [WebRequest](https://github.com/travetto/travetto/tree/main/module/web/src/types/request.ts#L11)
|
|
353
351
|
* Dispatching the request through the framework
|
|
354
352
|
* Receiving the [WebResponse](https://github.com/travetto/travetto/tree/main/module/web/src/types/response.ts#L3) and sending that back over the primitive response.
|
|
355
353
|
|
|
356
354
|
## TLS Support
|
|
357
|
-
Additionally the framework supports TLS out of the box, by allowing you to specify your public and private keys for the cert.
|
|
355
|
+
Additionally the framework supports TLS out of the box, by allowing you to specify your public and private keys for the cert. In dev mode, the framework will also automatically generate a self-signed cert if:
|
|
358
356
|
* TLS support is configured
|
|
359
357
|
* [node-forge](https://www.npmjs.com/package/node-forge) is installed
|
|
360
358
|
* Not running in production
|
|
@@ -362,4 +360,4 @@ Additionally the framework supports TLS out of the box, by allowing you to speci
|
|
|
362
360
|
|
|
363
361
|
This is useful for local development where you implicitly trust the cert.
|
|
364
362
|
|
|
365
|
-
TLS support can be enabled by setting `web.http.tls: true` in your config. The key/cert can be specified as string directly in the config file/environment variables.
|
|
363
|
+
TLS support can be enabled by setting `web.http.tls: true` in your config. The key/cert can be specified as string directly in the config file/environment variables. The key/cert can also be specified as a path to be picked up by [RuntimeResources](https://github.com/travetto/travetto/tree/main/module/runtime/src/resources.ts#L8).
|
package/__index__.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/web-http",
|
|
3
|
-
"version": "8.0.0-alpha.
|
|
3
|
+
"version": "8.0.0-alpha.25",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Web HTTP Server Support",
|
|
6
6
|
"keywords": [
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
"directory": "module/web-http"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@travetto/web": "^8.0.0-alpha.
|
|
30
|
+
"@travetto/web": "^8.0.0-alpha.24"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@travetto/cli": "^8.0.0-alpha.
|
|
34
|
-
"@travetto/test": "^8.0.0-alpha.
|
|
33
|
+
"@travetto/cli": "^8.0.0-alpha.29",
|
|
34
|
+
"@travetto/test": "^8.0.0-alpha.22"
|
|
35
35
|
},
|
|
36
36
|
"peerDependenciesMeta": {
|
|
37
37
|
"@travetto/test": {
|
package/src/config.ts
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { Config, EnvVar } from '@travetto/config';
|
|
2
|
+
import { PostConstruct } from '@travetto/di';
|
|
3
|
+
import { Runtime, RuntimeError, RuntimeResources } from '@travetto/runtime';
|
|
2
4
|
import { Ignore, Secret } from '@travetto/schema';
|
|
3
|
-
import { RuntimeError, Runtime, RuntimeResources } from '@travetto/runtime';
|
|
4
5
|
import { NetUtil } from '@travetto/web';
|
|
5
|
-
import { PostConstruct } from '@travetto/di';
|
|
6
6
|
|
|
7
|
-
import type { WebSecureKeyPair } from './types.ts';
|
|
8
7
|
import { WebTlsUtil } from './tls.ts';
|
|
8
|
+
import type { WebSecureKeyPair } from './types.ts';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Web HTTP configuration
|
|
12
12
|
*/
|
|
13
13
|
@Config('web.http')
|
|
14
14
|
export class WebHttpConfig {
|
|
15
|
-
|
|
16
15
|
/**
|
|
17
16
|
* What version of HTTP to use
|
|
18
17
|
* Version 2 requires SSL for direct browser access
|
|
@@ -48,8 +47,8 @@ export class WebHttpConfig {
|
|
|
48
47
|
|
|
49
48
|
@PostConstruct()
|
|
50
49
|
async finalizeConfig(): Promise<void> {
|
|
51
|
-
this.tls ??=
|
|
52
|
-
this.port =
|
|
50
|
+
this.tls ??= this.httpVersion === '2' || !!this.tlsKeys;
|
|
51
|
+
this.port = this.port < 0 ? await NetUtil.getFreePort() : this.port;
|
|
53
52
|
this.bindAddress ||= NetUtil.getLocalAddress();
|
|
54
53
|
|
|
55
54
|
if (!this.tls) {
|
|
@@ -61,7 +60,8 @@ export class WebHttpConfig {
|
|
|
61
60
|
}
|
|
62
61
|
this.tlsKeys = await WebTlsUtil.generateKeyPair();
|
|
63
62
|
} else {
|
|
64
|
-
if (this.tlsKeys.key.length < 100) {
|
|
63
|
+
if (this.tlsKeys.key.length < 100) {
|
|
64
|
+
// We have files or resources
|
|
65
65
|
this.tlsKeys.key = await RuntimeResources.readUTF8(this.tlsKeys.key);
|
|
66
66
|
this.tlsKeys.cert = await RuntimeResources.readUTF8(this.tlsKeys.cert);
|
|
67
67
|
}
|
|
@@ -69,4 +69,4 @@ export class WebHttpConfig {
|
|
|
69
69
|
|
|
70
70
|
this.fetchUrl = `${this.tls ? 'https' : 'http'}://${this.bindAddress}:${this.port}`;
|
|
71
71
|
}
|
|
72
|
-
}
|
|
72
|
+
}
|
package/src/http.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type net from 'node:net';
|
|
2
1
|
import http from 'node:http';
|
|
3
2
|
import http2 from 'node:http2';
|
|
4
3
|
import https from 'node:https';
|
|
4
|
+
import type net from 'node:net';
|
|
5
5
|
import { TLSSocket } from 'node:tls';
|
|
6
6
|
|
|
7
|
-
import { WebBodyUtil, WebCommonUtil, type WebDispatcher, WebRequest, WebResponse } from '@travetto/web';
|
|
8
7
|
import { type BinaryType, BinaryUtil, castTo, ShutdownManager } from '@travetto/runtime';
|
|
8
|
+
import { WebBodyUtil, WebCommonUtil, type WebDispatcher, WebRequest, WebResponse } from '@travetto/web';
|
|
9
9
|
|
|
10
10
|
import type { WebSecureKeyPair, WebServerHandle } from './types.ts';
|
|
11
11
|
|
|
@@ -24,7 +24,6 @@ type WebHttpServerConfig = {
|
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
export class WebHttpUtil {
|
|
27
|
-
|
|
28
27
|
/**
|
|
29
28
|
* Build a simple request handler
|
|
30
29
|
* @param dispatcher
|
|
@@ -69,9 +68,7 @@ export class WebHttpUtil {
|
|
|
69
68
|
socket.on('close', () => activeConnections.delete(socket));
|
|
70
69
|
});
|
|
71
70
|
|
|
72
|
-
target.listen(config.port, config.bindAddress)
|
|
73
|
-
.on('error', reject)
|
|
74
|
-
.on('listening', resolve);
|
|
71
|
+
target.listen(config.port, config.bindAddress).on('error', reject).on('listening', resolve);
|
|
75
72
|
|
|
76
73
|
await promise;
|
|
77
74
|
|
|
@@ -115,7 +112,7 @@ export class WebHttpUtil {
|
|
|
115
112
|
},
|
|
116
113
|
httpMethod: castTo(request.method?.toUpperCase()),
|
|
117
114
|
path,
|
|
118
|
-
httpQuery: Object.fromEntries(new URLSearchParams(query))
|
|
115
|
+
httpQuery: Object.fromEntries(new URLSearchParams(query))
|
|
119
116
|
},
|
|
120
117
|
headers: request.headers,
|
|
121
118
|
body: WebBodyUtil.markRawBinary(request)
|
|
@@ -127,7 +124,9 @@ export class WebHttpUtil {
|
|
|
127
124
|
*/
|
|
128
125
|
static async respondToServerResponse(webResponse: WebResponse, response: HttpResponse): Promise<void> {
|
|
129
126
|
const binaryResponse = new WebResponse<BinaryType>({ context: webResponse.context, ...WebBodyUtil.toBinaryMessage(webResponse) });
|
|
130
|
-
binaryResponse.headers.forEach((value, key) =>
|
|
127
|
+
binaryResponse.headers.forEach((value, key) => {
|
|
128
|
+
response.setHeader(key, value);
|
|
129
|
+
});
|
|
131
130
|
response.statusCode = WebCommonUtil.getStatusCode(binaryResponse);
|
|
132
131
|
|
|
133
132
|
if (binaryResponse.body) {
|
|
@@ -137,4 +136,4 @@ export class WebHttpUtil {
|
|
|
137
136
|
response.end();
|
|
138
137
|
}
|
|
139
138
|
}
|
|
140
|
-
}
|
|
139
|
+
}
|
package/src/node.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import type { ConfigurationService } from '@travetto/config';
|
|
1
2
|
import { Inject, Injectable } from '@travetto/di';
|
|
2
3
|
import type { StandardWebRouter } from '@travetto/web';
|
|
3
|
-
import type { ConfigurationService } from '@travetto/config';
|
|
4
4
|
|
|
5
5
|
import type { WebHttpConfig } from './config.ts';
|
|
6
6
|
import { WebHttpUtil } from './http.ts';
|
|
@@ -11,7 +11,6 @@ import type { WebHttpServer, WebServerHandle } from './types.ts';
|
|
|
11
11
|
*/
|
|
12
12
|
@Injectable()
|
|
13
13
|
export class NodeWebHttpServer implements WebHttpServer {
|
|
14
|
-
|
|
15
14
|
@Inject()
|
|
16
15
|
serverConfig: WebHttpConfig;
|
|
17
16
|
|
|
@@ -22,9 +21,9 @@ export class NodeWebHttpServer implements WebHttpServer {
|
|
|
22
21
|
configService: ConfigurationService;
|
|
23
22
|
|
|
24
23
|
async serve(): Promise<WebServerHandle> {
|
|
25
|
-
const handle = await WebHttpUtil.startHttpServer({ ...this.serverConfig, dispatcher: this.router
|
|
24
|
+
const handle = await WebHttpUtil.startHttpServer({ ...this.serverConfig, dispatcher: this.router });
|
|
26
25
|
console.log('Initialized', await this.configService.initBanner());
|
|
27
26
|
console.log('Listening', { port: this.serverConfig.port });
|
|
28
27
|
return handle;
|
|
29
28
|
}
|
|
30
|
-
}
|
|
29
|
+
}
|
package/src/tls.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Runtime } from '@travetto/runtime';
|
|
1
|
+
import { type Any, Runtime } from '@travetto/runtime';
|
|
2
2
|
|
|
3
3
|
import type { WebSecureKeyPair } from './types.ts';
|
|
4
4
|
|
|
@@ -6,13 +6,12 @@ import type { WebSecureKeyPair } from './types.ts';
|
|
|
6
6
|
* Utils for generating key pairs
|
|
7
7
|
*/
|
|
8
8
|
export class WebTlsUtil {
|
|
9
|
-
|
|
10
9
|
/**
|
|
11
10
|
* Generate TLS key pair on demand
|
|
12
11
|
* @param subj The subject for the app
|
|
13
12
|
*/
|
|
14
13
|
static async generateKeyPair(subj = { C: 'US', ST: 'CA', O: 'TRAVETTO', OU: 'WEB', CN: 'DEV' }): Promise<WebSecureKeyPair> {
|
|
15
|
-
let forge;
|
|
14
|
+
let forge: Any;
|
|
16
15
|
|
|
17
16
|
try {
|
|
18
17
|
forge = (await import('node-forge')).default;
|
|
@@ -47,4 +46,4 @@ export class WebTlsUtil {
|
|
|
47
46
|
key: pki.privateKeyToPem(keys.privateKey)
|
|
48
47
|
};
|
|
49
48
|
}
|
|
50
|
-
}
|
|
49
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Any } from '@travetto/runtime';
|
|
2
2
|
|
|
3
|
-
export type WebSecureKeyPair = { cert: string
|
|
3
|
+
export type WebSecureKeyPair = { cert: string; key: string };
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Handle for a web server
|
|
@@ -18,4 +18,4 @@ export type WebServerHandle<T = Any> = {
|
|
|
18
18
|
*/
|
|
19
19
|
export interface WebHttpServer {
|
|
20
20
|
serve(): Promise<WebServerHandle>;
|
|
21
|
-
}
|
|
21
|
+
}
|
package/support/cli.web_http.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CliCommand, type CliCommandShape, CliDebugIpcFlag, CliModuleFlag, CliProfilesFlag, CliRestartOnChangeFlag } from '@travetto/cli';
|
|
2
2
|
import { DependencyRegistryIndex } from '@travetto/di';
|
|
3
|
-
import { CliCommand, CliDebugIpcFlag, CliModuleFlag, CliProfilesFlag, CliRestartOnChangeFlag, type CliCommandShape } from '@travetto/cli';
|
|
4
|
-
import { NetUtil } from '@travetto/web';
|
|
5
3
|
import { Registry } from '@travetto/registry';
|
|
4
|
+
import { Runtime, toConcrete } from '@travetto/runtime';
|
|
5
|
+
import { NetUtil } from '@travetto/web';
|
|
6
6
|
|
|
7
7
|
import type { WebHttpServer } from '../src/types.ts';
|
|
8
8
|
|
|
@@ -18,7 +18,6 @@ import type { WebHttpServer } from '../src/types.ts';
|
|
|
18
18
|
*/
|
|
19
19
|
@CliCommand()
|
|
20
20
|
export class WebHttpCommand implements CliCommandShape {
|
|
21
|
-
|
|
22
21
|
/** Port to run on */
|
|
23
22
|
port?: number;
|
|
24
23
|
|
|
@@ -60,4 +59,4 @@ export class WebHttpCommand implements CliCommandShape {
|
|
|
60
59
|
throw err;
|
|
61
60
|
}
|
|
62
61
|
}
|
|
63
|
-
}
|
|
62
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Inject, Injectable } from '@travetto/di';
|
|
2
|
-
import { type WebFilterContext, WebResponse, type WebDispatcher, WebBodyUtil } from '@travetto/web';
|
|
3
2
|
import { BinaryUtil, castTo } from '@travetto/runtime';
|
|
3
|
+
import { WebBodyUtil, type WebDispatcher, type WebFilterContext, WebResponse } from '@travetto/web';
|
|
4
4
|
|
|
5
5
|
import { WebTestDispatchUtil } from '@travetto/web/support/test/dispatch-util.ts';
|
|
6
6
|
|
|
@@ -11,22 +11,21 @@ import type { WebHttpConfig } from '../../src/config.ts';
|
|
|
11
11
|
*/
|
|
12
12
|
@Injectable()
|
|
13
13
|
export class FetchWebDispatcher implements WebDispatcher {
|
|
14
|
-
|
|
15
14
|
@Inject()
|
|
16
15
|
config: WebHttpConfig;
|
|
17
16
|
|
|
18
17
|
async dispatch({ request }: WebFilterContext): Promise<WebResponse> {
|
|
19
18
|
const baseRequest = await WebTestDispatchUtil.applyRequestBody(request);
|
|
20
19
|
const finalPath = WebTestDispatchUtil.buildPath(baseRequest);
|
|
21
|
-
const body: RequestInit['body'] = WebBodyUtil.isRawBinary(request.body)
|
|
22
|
-
await BinaryUtil.toBinaryArray(request.body)
|
|
23
|
-
castTo(request.body);
|
|
24
|
-
const {
|
|
20
|
+
const body: RequestInit['body'] = WebBodyUtil.isRawBinary(request.body)
|
|
21
|
+
? await BinaryUtil.toBinaryArray(request.body)
|
|
22
|
+
: castTo(request.body);
|
|
23
|
+
const {
|
|
24
|
+
context: { httpMethod: method },
|
|
25
|
+
headers
|
|
26
|
+
} = request;
|
|
25
27
|
|
|
26
|
-
const response = await fetch(
|
|
27
|
-
`${this.config.fetchUrl}${finalPath}`,
|
|
28
|
-
{ method, headers, body }
|
|
29
|
-
);
|
|
28
|
+
const response = await fetch(`${this.config.fetchUrl}${finalPath}`, { method, headers, body });
|
|
30
29
|
|
|
31
30
|
return await WebTestDispatchUtil.finalizeResponseBody(
|
|
32
31
|
new WebResponse({
|
|
@@ -36,4 +35,4 @@ export class FetchWebDispatcher implements WebDispatcher {
|
|
|
36
35
|
})
|
|
37
36
|
);
|
|
38
37
|
}
|
|
39
|
-
}
|
|
38
|
+
}
|