@libp2p/tcp 10.0.5 → 10.0.6-24fa1d5af
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/dist/index.min.js +1 -1
- package/dist/src/constants.js +1 -1
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/listener.d.ts.map +1 -1
- package/dist/src/listener.js +8 -6
- package/dist/src/listener.js.map +1 -1
- package/dist/src/socket-to-conn.d.ts +1 -0
- package/dist/src/socket-to-conn.d.ts.map +1 -1
- package/dist/src/socket-to-conn.js +79 -74
- package/dist/src/socket-to-conn.js.map +1 -1
- package/dist/src/tcp.d.ts.map +1 -1
- package/dist/src/tcp.js +34 -19
- package/dist/src/tcp.js.map +1 -1
- package/package.json +7 -6
- package/src/constants.ts +1 -1
- package/src/index.ts +2 -1
- package/src/listener.ts +10 -7
- package/src/socket-to-conn.ts +89 -80
- package/src/tcp.ts +35 -21
- package/dist/typedoc-urls.json +0 -19
package/src/socket-to-conn.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { InvalidParametersError, TimeoutError } from '@libp2p/interface'
|
|
2
2
|
import { ipPortToMultiaddr as toMultiaddr } from '@libp2p/utils/ip-port-to-multiaddr'
|
|
3
|
+
import pDefer from 'p-defer'
|
|
4
|
+
import { raceEvent } from 'race-event'
|
|
3
5
|
import { duplex } from 'stream-to-it'
|
|
4
6
|
import { CLOSE_TIMEOUT, SOCKET_TIMEOUT } from './constants.js'
|
|
5
7
|
import { multiaddrToNetConfig } from './utils.js'
|
|
6
8
|
import type { ComponentLogger, MultiaddrConnection, CounterGroup } from '@libp2p/interface'
|
|
7
9
|
import type { AbortOptions, Multiaddr } from '@multiformats/multiaddr'
|
|
8
10
|
import type { Socket } from 'net'
|
|
11
|
+
import type { DeferredPromise } from 'p-defer'
|
|
9
12
|
|
|
10
13
|
interface ToConnectionOptions {
|
|
11
14
|
listeningAddr?: Multiaddr
|
|
@@ -16,6 +19,7 @@ interface ToConnectionOptions {
|
|
|
16
19
|
metrics?: CounterGroup
|
|
17
20
|
metricPrefix?: string
|
|
18
21
|
logger: ComponentLogger
|
|
22
|
+
direction: 'inbound' | 'outbound'
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
/**
|
|
@@ -23,12 +27,15 @@ interface ToConnectionOptions {
|
|
|
23
27
|
* https://github.com/libp2p/interface-transport#multiaddrconnection
|
|
24
28
|
*/
|
|
25
29
|
export const toMultiaddrConnection = (socket: Socket, options: ToConnectionOptions): MultiaddrConnection => {
|
|
26
|
-
let closePromise:
|
|
30
|
+
let closePromise: DeferredPromise<void>
|
|
27
31
|
const log = options.logger.forComponent('libp2p:tcp:socket')
|
|
32
|
+
const direction = options.direction
|
|
28
33
|
const metrics = options.metrics
|
|
29
34
|
const metricPrefix = options.metricPrefix ?? ''
|
|
30
35
|
const inactivityTimeout = options.socketInactivityTimeout ?? SOCKET_TIMEOUT
|
|
31
36
|
const closeTimeout = options.socketCloseTimeout ?? CLOSE_TIMEOUT
|
|
37
|
+
let timedout = false
|
|
38
|
+
let errored = false
|
|
32
39
|
|
|
33
40
|
// Check if we are connected on a unix path
|
|
34
41
|
if (options.listeningAddr?.getPath() != null) {
|
|
@@ -39,6 +46,19 @@ export const toMultiaddrConnection = (socket: Socket, options: ToConnectionOptio
|
|
|
39
46
|
options.localAddr = options.remoteAddr
|
|
40
47
|
}
|
|
41
48
|
|
|
49
|
+
// handle socket errors
|
|
50
|
+
socket.on('error', err => {
|
|
51
|
+
errored = true
|
|
52
|
+
|
|
53
|
+
if (!timedout) {
|
|
54
|
+
log.error('%s socket error - %e', direction, err)
|
|
55
|
+
metrics?.increment({ [`${metricPrefix}error`]: true })
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
socket.destroy()
|
|
59
|
+
maConn.timeline.close = Date.now()
|
|
60
|
+
})
|
|
61
|
+
|
|
42
62
|
let remoteAddr: Multiaddr
|
|
43
63
|
|
|
44
64
|
if (options.remoteAddr != null) {
|
|
@@ -59,37 +79,37 @@ export const toMultiaddrConnection = (socket: Socket, options: ToConnectionOptio
|
|
|
59
79
|
|
|
60
80
|
// by default there is no timeout
|
|
61
81
|
// https://nodejs.org/dist/latest-v16.x/docs/api/net.html#socketsettimeouttimeout-callback
|
|
62
|
-
socket.setTimeout(inactivityTimeout
|
|
63
|
-
log('%s socket read timeout', lOptsStr)
|
|
64
|
-
metrics?.increment({ [`${metricPrefix}timeout`]: true })
|
|
82
|
+
socket.setTimeout(inactivityTimeout)
|
|
65
83
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
84
|
+
socket.once('timeout', () => {
|
|
85
|
+
timedout = true
|
|
86
|
+
log('%s %s socket read timeout', direction, lOptsStr)
|
|
87
|
+
metrics?.increment({ [`${metricPrefix}timeout`]: true })
|
|
71
88
|
|
|
72
89
|
// if the socket times out due to inactivity we must manually close the connection
|
|
73
90
|
// https://nodejs.org/dist/latest-v16.x/docs/api/net.html#event-timeout
|
|
74
|
-
socket.destroy(
|
|
91
|
+
socket.destroy(new TimeoutError())
|
|
92
|
+
maConn.timeline.close = Date.now()
|
|
75
93
|
})
|
|
76
94
|
|
|
77
95
|
socket.once('close', () => {
|
|
78
|
-
|
|
79
|
-
|
|
96
|
+
// record metric for clean exit
|
|
97
|
+
if (!timedout && !errored) {
|
|
98
|
+
log('%s %s socket close', direction, lOptsStr)
|
|
99
|
+
metrics?.increment({ [`${metricPrefix}close`]: true })
|
|
100
|
+
}
|
|
80
101
|
|
|
81
102
|
// In instances where `close` was not explicitly called,
|
|
82
103
|
// such as an iterable stream ending, ensure we have set the close
|
|
83
104
|
// timeline
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
105
|
+
socket.destroy()
|
|
106
|
+
maConn.timeline.close = Date.now()
|
|
87
107
|
})
|
|
88
108
|
|
|
89
109
|
socket.once('end', () => {
|
|
90
110
|
// the remote sent a FIN packet which means no more data will be sent
|
|
91
111
|
// https://nodejs.org/dist/latest-v16.x/docs/api/net.html#event-end
|
|
92
|
-
log('%s socket end', lOptsStr)
|
|
112
|
+
log('%s %s socket end', direction, lOptsStr)
|
|
93
113
|
metrics?.increment({ [`${metricPrefix}end`]: true })
|
|
94
114
|
})
|
|
95
115
|
|
|
@@ -111,7 +131,7 @@ export const toMultiaddrConnection = (socket: Socket, options: ToConnectionOptio
|
|
|
111
131
|
// If the source errored the socket will already have been destroyed by
|
|
112
132
|
// duplex(). If the socket errored it will already be
|
|
113
133
|
// destroyed. There's nothing to do here except log the error & return.
|
|
114
|
-
log.error('%s error in sink', lOptsStr, err)
|
|
134
|
+
log.error('%s %s error in sink - %e', direction, lOptsStr, err)
|
|
115
135
|
}
|
|
116
136
|
}
|
|
117
137
|
|
|
@@ -127,92 +147,67 @@ export const toMultiaddrConnection = (socket: Socket, options: ToConnectionOptio
|
|
|
127
147
|
timeline: { open: Date.now() },
|
|
128
148
|
|
|
129
149
|
async close (options: AbortOptions = {}) {
|
|
130
|
-
if (socket.
|
|
131
|
-
log('
|
|
150
|
+
if (socket.closed) {
|
|
151
|
+
log('the %s %s socket is already closed', direction, lOptsStr)
|
|
132
152
|
return
|
|
133
153
|
}
|
|
134
154
|
|
|
135
|
-
if (
|
|
136
|
-
log('
|
|
137
|
-
return
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (options.signal == null) {
|
|
141
|
-
const signal = AbortSignal.timeout(closeTimeout)
|
|
142
|
-
|
|
143
|
-
options = {
|
|
144
|
-
...options,
|
|
145
|
-
signal
|
|
146
|
-
}
|
|
155
|
+
if (socket.destroyed) {
|
|
156
|
+
log('the %s %s socket is already destroyed', direction, lOptsStr)
|
|
157
|
+
return
|
|
147
158
|
}
|
|
148
159
|
|
|
149
|
-
|
|
150
|
-
|
|
160
|
+
if (closePromise != null) {
|
|
161
|
+
return closePromise.promise
|
|
151
162
|
}
|
|
152
163
|
|
|
153
|
-
options.signal?.addEventListener('abort', abortSignalListener)
|
|
154
|
-
|
|
155
164
|
try {
|
|
156
|
-
|
|
157
|
-
closePromise = new Promise<void>((resolve, reject) => {
|
|
158
|
-
socket.once('close', () => {
|
|
159
|
-
// socket completely closed
|
|
160
|
-
log('%s socket closed', lOptsStr)
|
|
165
|
+
closePromise = pDefer()
|
|
161
166
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
socket.once('error', (err: Error) => {
|
|
165
|
-
log('%s socket error', lOptsStr, err)
|
|
166
|
-
|
|
167
|
-
// error closing socket
|
|
168
|
-
if (maConn.timeline.close == null) {
|
|
169
|
-
maConn.timeline.close = Date.now()
|
|
170
|
-
}
|
|
171
|
-
if (!socket.destroyed) {
|
|
172
|
-
reject(err)
|
|
173
|
-
}
|
|
174
|
-
// if socket is destroyed, 'closed' event will be emitted later to resolve the promise
|
|
175
|
-
})
|
|
167
|
+
// close writable end of socket
|
|
168
|
+
socket.end()
|
|
176
169
|
|
|
177
|
-
|
|
178
|
-
|
|
170
|
+
// convert EventEmitter to EventTarget
|
|
171
|
+
const eventTarget = socketToEventTarget(socket)
|
|
179
172
|
|
|
180
|
-
|
|
181
|
-
|
|
173
|
+
// don't wait forever to close
|
|
174
|
+
const signal = options.signal ?? AbortSignal.timeout(closeTimeout)
|
|
182
175
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
176
|
+
// wait for any unsent data to be sent
|
|
177
|
+
if (socket.writableLength > 0) {
|
|
178
|
+
log('%s %s draining socket', direction, lOptsStr)
|
|
179
|
+
await raceEvent(eventTarget, 'drain', signal, {
|
|
180
|
+
errorEvent: 'error'
|
|
181
|
+
})
|
|
182
|
+
log('%s %s socket drained', direction, lOptsStr)
|
|
183
|
+
}
|
|
187
184
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
// nothing to send, destroy immediately, no need for the timeout
|
|
193
|
-
socket.destroy()
|
|
194
|
-
}
|
|
195
|
-
})
|
|
185
|
+
await Promise.all([
|
|
186
|
+
raceEvent(eventTarget, 'close', signal, {
|
|
187
|
+
errorEvent: 'error'
|
|
188
|
+
}),
|
|
196
189
|
|
|
197
|
-
|
|
190
|
+
// all bytes have been sent we can destroy the socket
|
|
191
|
+
socket.destroy()
|
|
192
|
+
])
|
|
198
193
|
} catch (err: any) {
|
|
199
194
|
this.abort(err)
|
|
200
195
|
} finally {
|
|
201
|
-
|
|
196
|
+
closePromise.resolve()
|
|
202
197
|
}
|
|
203
198
|
},
|
|
204
199
|
|
|
205
200
|
abort: (err: Error) => {
|
|
206
|
-
log('%s socket abort due to error', lOptsStr, err)
|
|
201
|
+
log('%s %s socket abort due to error - %e', direction, lOptsStr, err)
|
|
207
202
|
|
|
208
203
|
// the abortSignalListener may already destroyed the socket with an error
|
|
209
|
-
|
|
210
|
-
socket.destroy(err)
|
|
211
|
-
}
|
|
204
|
+
socket.destroy()
|
|
212
205
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
206
|
+
// closing a socket is always asynchronous (must wait for "close" event)
|
|
207
|
+
// but the tests expect this to be a synchronous operation so we have to
|
|
208
|
+
// set the close time here. the tests should be refactored to reflect
|
|
209
|
+
// reality.
|
|
210
|
+
maConn.timeline.close = Date.now()
|
|
216
211
|
},
|
|
217
212
|
|
|
218
213
|
log
|
|
@@ -220,3 +215,17 @@ export const toMultiaddrConnection = (socket: Socket, options: ToConnectionOptio
|
|
|
220
215
|
|
|
221
216
|
return maConn
|
|
222
217
|
}
|
|
218
|
+
|
|
219
|
+
function socketToEventTarget (obj?: any): EventTarget {
|
|
220
|
+
const eventTarget = {
|
|
221
|
+
addEventListener: (type: any, cb: any) => {
|
|
222
|
+
obj.addListener(type, cb)
|
|
223
|
+
},
|
|
224
|
+
removeEventListener: (type: any, cb: any) => {
|
|
225
|
+
obj.removeListener(type, cb)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// @ts-expect-error partial implementation
|
|
230
|
+
return eventTarget
|
|
231
|
+
}
|
package/src/tcp.ts
CHANGED
|
@@ -36,7 +36,7 @@ import { TCPListener } from './listener.js'
|
|
|
36
36
|
import { toMultiaddrConnection } from './socket-to-conn.js'
|
|
37
37
|
import { multiaddrToNetConfig } from './utils.js'
|
|
38
38
|
import type { TCPComponents, TCPCreateListenerOptions, TCPDialEvents, TCPDialOptions, TCPMetrics, TCPOptions } from './index.js'
|
|
39
|
-
import type { Logger, Connection, Transport, Listener } from '@libp2p/interface'
|
|
39
|
+
import type { Logger, Connection, Transport, Listener, MultiaddrConnection } from '@libp2p/interface'
|
|
40
40
|
import type { Multiaddr } from '@multiformats/multiaddr'
|
|
41
41
|
import type { Socket, IpcSocketConnectOpts, TcpSocketConnectOpts } from 'net'
|
|
42
42
|
|
|
@@ -53,7 +53,11 @@ export class TCP implements Transport<TCPDialEvents> {
|
|
|
53
53
|
|
|
54
54
|
if (components.metrics != null) {
|
|
55
55
|
this.metrics = {
|
|
56
|
-
|
|
56
|
+
events: components.metrics.registerCounterGroup('libp2p_tcp_dialer_events_total', {
|
|
57
|
+
label: 'event',
|
|
58
|
+
help: 'Total count of TCP dialer events by type'
|
|
59
|
+
}),
|
|
60
|
+
errors: components.metrics.registerCounterGroup('libp2p_tcp_dialer_errors_total', {
|
|
57
61
|
label: 'event',
|
|
58
62
|
help: 'Total count of TCP dialer events by type'
|
|
59
63
|
})
|
|
@@ -76,23 +80,28 @@ export class TCP implements Transport<TCPDialEvents> {
|
|
|
76
80
|
// options.signal destroys the socket before 'connect' event
|
|
77
81
|
const socket = await this._connect(ma, options)
|
|
78
82
|
|
|
79
|
-
|
|
80
|
-
socket.on('error', err => {
|
|
81
|
-
this.log('socket error', err)
|
|
82
|
-
})
|
|
83
|
+
let maConn: MultiaddrConnection
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
85
|
+
try {
|
|
86
|
+
maConn = toMultiaddrConnection(socket, {
|
|
87
|
+
remoteAddr: ma,
|
|
88
|
+
socketInactivityTimeout: this.opts.outboundSocketInactivityTimeout,
|
|
89
|
+
socketCloseTimeout: this.opts.socketCloseTimeout,
|
|
90
|
+
metrics: this.metrics?.events,
|
|
91
|
+
logger: this.components.logger,
|
|
92
|
+
direction: 'outbound'
|
|
93
|
+
})
|
|
94
|
+
} catch (err: any) {
|
|
95
|
+
this.metrics?.errors.increment({ outbound_to_connection: true })
|
|
96
|
+
socket.destroy(err)
|
|
97
|
+
throw err
|
|
98
|
+
}
|
|
91
99
|
|
|
92
100
|
try {
|
|
93
101
|
this.log('new outbound connection %s', maConn.remoteAddr)
|
|
94
102
|
return await options.upgrader.upgradeOutbound(maConn, options)
|
|
95
103
|
} catch (err: any) {
|
|
104
|
+
this.metrics?.errors.increment({ outbound_upgrade: true })
|
|
96
105
|
this.log.error('error upgrading outbound connection', err)
|
|
97
106
|
maConn.abort(err)
|
|
98
107
|
throw err
|
|
@@ -103,6 +112,8 @@ export class TCP implements Transport<TCPDialEvents> {
|
|
|
103
112
|
options.signal?.throwIfAborted()
|
|
104
113
|
options.onProgress?.(new CustomProgressEvent('tcp:open-connection'))
|
|
105
114
|
|
|
115
|
+
let rawSocket: Socket
|
|
116
|
+
|
|
106
117
|
return new Promise<Socket>((resolve, reject) => {
|
|
107
118
|
const start = Date.now()
|
|
108
119
|
const cOpts = multiaddrToNetConfig(ma, {
|
|
@@ -111,35 +122,34 @@ export class TCP implements Transport<TCPDialEvents> {
|
|
|
111
122
|
}) as (IpcSocketConnectOpts & TcpSocketConnectOpts)
|
|
112
123
|
|
|
113
124
|
this.log('dialing %a', ma)
|
|
114
|
-
|
|
125
|
+
rawSocket = net.connect(cOpts)
|
|
115
126
|
|
|
116
127
|
const onError = (err: Error): void => {
|
|
128
|
+
this.log.error('dial to %a errored - %e', ma, err)
|
|
117
129
|
const cOptsStr = cOpts.path ?? `${cOpts.host ?? ''}:${cOpts.port}`
|
|
118
130
|
err.message = `connection error ${cOptsStr}: ${err.message}`
|
|
119
|
-
this.metrics?.
|
|
120
|
-
|
|
131
|
+
this.metrics?.events.increment({ error: true })
|
|
121
132
|
done(err)
|
|
122
133
|
}
|
|
123
134
|
|
|
124
135
|
const onTimeout = (): void => {
|
|
125
136
|
this.log('connection timeout %a', ma)
|
|
126
|
-
this.metrics?.
|
|
137
|
+
this.metrics?.events.increment({ timeout: true })
|
|
127
138
|
|
|
128
|
-
const err = new TimeoutError(`
|
|
139
|
+
const err = new TimeoutError(`Connection timeout after ${Date.now() - start}ms`)
|
|
129
140
|
// Note: this will result in onError() being called
|
|
130
141
|
rawSocket.emit('error', err)
|
|
131
142
|
}
|
|
132
143
|
|
|
133
144
|
const onConnect = (): void => {
|
|
134
145
|
this.log('connection opened %a', ma)
|
|
135
|
-
this.metrics?.
|
|
146
|
+
this.metrics?.events.increment({ connect: true })
|
|
136
147
|
done()
|
|
137
148
|
}
|
|
138
149
|
|
|
139
150
|
const onAbort = (): void => {
|
|
140
151
|
this.log('connection aborted %a', ma)
|
|
141
|
-
this.metrics?.
|
|
142
|
-
rawSocket.destroy()
|
|
152
|
+
this.metrics?.events.increment({ abort: true })
|
|
143
153
|
done(new AbortError())
|
|
144
154
|
}
|
|
145
155
|
|
|
@@ -167,6 +177,10 @@ export class TCP implements Transport<TCPDialEvents> {
|
|
|
167
177
|
options.signal.addEventListener('abort', onAbort)
|
|
168
178
|
}
|
|
169
179
|
})
|
|
180
|
+
.catch(err => {
|
|
181
|
+
rawSocket?.destroy()
|
|
182
|
+
throw err
|
|
183
|
+
})
|
|
170
184
|
}
|
|
171
185
|
|
|
172
186
|
/**
|
package/dist/typedoc-urls.json
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"CloseServerOnMaxConnectionsOpts": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_tcp.CloseServerOnMaxConnectionsOpts.html",
|
|
3
|
-
"TCPComponents": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_tcp.TCPComponents.html",
|
|
4
|
-
".:TCPComponents": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_tcp.TCPComponents.html",
|
|
5
|
-
"TCPCreateListenerOptions": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_tcp.TCPCreateListenerOptions.html",
|
|
6
|
-
".:TCPCreateListenerOptions": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_tcp.TCPCreateListenerOptions.html",
|
|
7
|
-
"TCPDialOptions": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_tcp.TCPDialOptions.html",
|
|
8
|
-
".:TCPDialOptions": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_tcp.TCPDialOptions.html",
|
|
9
|
-
"TCPMetrics": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_tcp.TCPMetrics.html",
|
|
10
|
-
".:TCPMetrics": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_tcp.TCPMetrics.html",
|
|
11
|
-
"TCPOptions": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_tcp.TCPOptions.html",
|
|
12
|
-
".:TCPOptions": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_tcp.TCPOptions.html",
|
|
13
|
-
"TCPSocketOptions": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_tcp.TCPSocketOptions.html",
|
|
14
|
-
".:TCPSocketOptions": "https://libp2p.github.io/js-libp2p/interfaces/_libp2p_tcp.TCPSocketOptions.html",
|
|
15
|
-
"TCPDialEvents": "https://libp2p.github.io/js-libp2p/types/_libp2p_tcp.TCPDialEvents.html",
|
|
16
|
-
".:TCPDialEvents": "https://libp2p.github.io/js-libp2p/types/_libp2p_tcp.TCPDialEvents.html",
|
|
17
|
-
"tcp": "https://libp2p.github.io/js-libp2p/functions/_libp2p_tcp.tcp.html",
|
|
18
|
-
".:tcp": "https://libp2p.github.io/js-libp2p/functions/_libp2p_tcp.tcp.html"
|
|
19
|
-
}
|