tardis-machine 18.0.0 → 18.0.1
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/benchmark.js +35 -14
- package/bin/tardis-machine.js +0 -1
- package/dist/tardismachine.d.ts.map +1 -1
- package/dist/tardismachine.js +20 -55
- package/dist/tardismachine.js.map +1 -1
- package/dist/ws/replay.d.ts +2 -2
- package/dist/ws/replay.d.ts.map +1 -1
- package/dist/ws/replay.js +11 -5
- package/dist/ws/replay.js.map +1 -1
- package/dist/ws/replaynormalized.d.ts +2 -2
- package/dist/ws/replaynormalized.d.ts.map +1 -1
- package/dist/ws/replaynormalized.js +7 -5
- package/dist/ws/replaynormalized.js.map +1 -1
- package/dist/ws/server.d.ts +20 -0
- package/dist/ws/server.d.ts.map +1 -0
- package/dist/ws/server.js +154 -0
- package/dist/ws/server.js.map +1 -0
- package/dist/ws/streamnormalized.d.ts +2 -2
- package/dist/ws/streamnormalized.d.ts.map +1 -1
- package/dist/ws/streamnormalized.js +7 -5
- package/dist/ws/streamnormalized.js.map +1 -1
- package/dist/ws/subscriptionsmappers.d.ts +1 -1
- package/dist/ws/subscriptionsmappers.d.ts.map +1 -1
- package/node_modules/tardis-dev/README.md +1 -1
- package/node_modules/tardis-dev/dist/debug.d.ts +12 -2
- package/node_modules/tardis-dev/dist/debug.d.ts.map +1 -1
- package/node_modules/tardis-dev/dist/debug.js +2 -1
- package/node_modules/tardis-dev/dist/debug.js.map +1 -1
- package/node_modules/tardis-dev/dist/handy.d.ts +3 -1
- package/node_modules/tardis-dev/dist/handy.d.ts.map +1 -1
- package/node_modules/tardis-dev/dist/handy.js +72 -86
- package/node_modules/tardis-dev/dist/handy.js.map +1 -1
- package/node_modules/tardis-dev/dist/realtimefeeds/mexc.d.ts +0 -7
- package/node_modules/tardis-dev/dist/realtimefeeds/mexc.d.ts.map +1 -1
- package/node_modules/tardis-dev/dist/realtimefeeds/mexc.js +2 -70
- package/node_modules/tardis-dev/dist/realtimefeeds/mexc.js.map +1 -1
- package/node_modules/tardis-dev/dist/realtimefeeds/mexcprotobuf.d.ts +43 -0
- package/node_modules/tardis-dev/dist/realtimefeeds/mexcprotobuf.d.ts.map +1 -0
- package/node_modules/tardis-dev/dist/realtimefeeds/mexcprotobuf.js +294 -0
- package/node_modules/tardis-dev/dist/realtimefeeds/mexcprotobuf.js.map +1 -0
- package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.d.ts +3 -3
- package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.d.ts.map +1 -1
- package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.js +3 -3
- package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.js.map +1 -1
- package/node_modules/tardis-dev/dist/worker.js +2 -2
- package/node_modules/tardis-dev/dist/worker.js.map +1 -1
- package/node_modules/tardis-dev/package.json +3 -7
- package/node_modules/tardis-dev/src/debug.ts +16 -1
- package/node_modules/tardis-dev/src/external-modules.d.ts +5 -0
- package/node_modules/tardis-dev/src/handy.ts +93 -89
- package/node_modules/tardis-dev/src/realtimefeeds/mexc.ts +2 -72
- package/node_modules/tardis-dev/src/realtimefeeds/mexcprotobuf.ts +364 -0
- package/node_modules/tardis-dev/src/realtimefeeds/realtimefeed.ts +5 -5
- package/node_modules/tardis-dev/src/worker.ts +2 -2
- package/package.json +8 -34
- package/src/tardismachine.ts +25 -66
- package/src/ws/replay.ts +13 -8
- package/src/ws/replaynormalized.ts +8 -6
- package/src/ws/server.ts +173 -0
- package/src/ws/streamnormalized.ts +8 -6
- package/src/ws/subscriptionsmappers.ts +1 -1
- package/node_modules/follow-redirects/LICENSE +0 -18
- package/node_modules/follow-redirects/README.md +0 -157
- package/node_modules/follow-redirects/debug.js +0 -15
- package/node_modules/follow-redirects/http.js +0 -1
- package/node_modules/follow-redirects/https.js +0 -1
- package/node_modules/follow-redirects/index.js +0 -709
- package/node_modules/follow-redirects/package.json +0 -58
- package/node_modules/long/LICENSE +0 -202
- package/node_modules/long/README.md +0 -286
- package/node_modules/long/index.d.ts +0 -2
- package/node_modules/long/index.js +0 -1581
- package/node_modules/long/package.json +0 -58
- package/node_modules/long/types.d.ts +0 -474
- package/node_modules/long/umd/index.d.ts +0 -3
- package/node_modules/long/umd/index.js +0 -1622
- package/node_modules/long/umd/package.json +0 -3
- package/node_modules/long/umd/types.d.ts +0 -474
- package/node_modules/protobufjs/LICENSE +0 -39
- package/node_modules/protobufjs/README.md +0 -440
- package/node_modules/protobufjs/dist/light/protobuf.js +0 -9276
- package/node_modules/protobufjs/dist/light/protobuf.js.map +0 -1
- package/node_modules/protobufjs/dist/light/protobuf.min.js +0 -8
- package/node_modules/protobufjs/dist/light/protobuf.min.js.map +0 -1
- package/node_modules/protobufjs/dist/minimal/protobuf.js +0 -3556
- package/node_modules/protobufjs/dist/minimal/protobuf.js.map +0 -1
- package/node_modules/protobufjs/dist/minimal/protobuf.min.js +0 -8
- package/node_modules/protobufjs/dist/minimal/protobuf.min.js.map +0 -1
- package/node_modules/protobufjs/dist/protobuf.js +0 -11233
- package/node_modules/protobufjs/dist/protobuf.js.map +0 -1
- package/node_modules/protobufjs/dist/protobuf.min.js +0 -8
- package/node_modules/protobufjs/dist/protobuf.min.js.map +0 -1
- package/node_modules/protobufjs/ext/README.md +0 -70
- package/node_modules/protobufjs/ext/debug/README.md +0 -4
- package/node_modules/protobufjs/ext/debug/index.js +0 -71
- package/node_modules/protobufjs/ext/descriptor/README.md +0 -5
- package/node_modules/protobufjs/ext/descriptor/index.d.ts +0 -2
- package/node_modules/protobufjs/ext/descriptor/index.js +0 -2
- package/node_modules/protobufjs/ext/descriptor.d.ts +0 -87
- package/node_modules/protobufjs/ext/descriptor.generated.d.ts +0 -417
- package/node_modules/protobufjs/ext/descriptor.js +0 -1356
- package/node_modules/protobufjs/ext/protojson.LICENSE +0 -201
- package/node_modules/protobufjs/ext/protojson.d.ts +0 -20
- package/node_modules/protobufjs/ext/protojson.generated.d.ts +0 -49
- package/node_modules/protobufjs/ext/protojson.js +0 -951
- package/node_modules/protobufjs/ext/textformat.d.ts +0 -19
- package/node_modules/protobufjs/ext/textformat.generated.d.ts +0 -33
- package/node_modules/protobufjs/ext/textformat.js +0 -1276
- package/node_modules/protobufjs/google/LICENSE +0 -27
- package/node_modules/protobufjs/google/README.md +0 -1
- package/node_modules/protobufjs/google/api/annotations.json +0 -85
- package/node_modules/protobufjs/google/api/annotations.proto +0 -11
- package/node_modules/protobufjs/google/api/http.json +0 -87
- package/node_modules/protobufjs/google/api/http.proto +0 -31
- package/node_modules/protobufjs/google/protobuf/api.json +0 -124
- package/node_modules/protobufjs/google/protobuf/api.proto +0 -34
- package/node_modules/protobufjs/google/protobuf/compiler/plugin.json +0 -126
- package/node_modules/protobufjs/google/protobuf/compiler/plugin.proto +0 -47
- package/node_modules/protobufjs/google/protobuf/descriptor.json +0 -1465
- package/node_modules/protobufjs/google/protobuf/descriptor.proto +0 -527
- package/node_modules/protobufjs/google/protobuf/source_context.json +0 -21
- package/node_modules/protobufjs/google/protobuf/source_context.proto +0 -7
- package/node_modules/protobufjs/google/protobuf/type.json +0 -209
- package/node_modules/protobufjs/google/protobuf/type.proto +0 -89
- package/node_modules/protobufjs/index.d.ts +0 -3203
- package/node_modules/protobufjs/index.js +0 -4
- package/node_modules/protobufjs/light.d.ts +0 -2
- package/node_modules/protobufjs/light.js +0 -4
- package/node_modules/protobufjs/minimal.d.ts +0 -2
- package/node_modules/protobufjs/minimal.js +0 -4
- package/node_modules/protobufjs/package.json +0 -93
- package/node_modules/protobufjs/src/common.js +0 -405
- package/node_modules/protobufjs/src/converter.js +0 -352
- package/node_modules/protobufjs/src/decoder.js +0 -279
- package/node_modules/protobufjs/src/encoder.js +0 -123
- package/node_modules/protobufjs/src/enum.js +0 -239
- package/node_modules/protobufjs/src/field.js +0 -505
- package/node_modules/protobufjs/src/index-light.js +0 -104
- package/node_modules/protobufjs/src/index-minimal.js +0 -35
- package/node_modules/protobufjs/src/index.js +0 -12
- package/node_modules/protobufjs/src/mapfield.js +0 -144
- package/node_modules/protobufjs/src/message.js +0 -137
- package/node_modules/protobufjs/src/method.js +0 -183
- package/node_modules/protobufjs/src/namespace.js +0 -574
- package/node_modules/protobufjs/src/object.js +0 -392
- package/node_modules/protobufjs/src/oneof.js +0 -233
- package/node_modules/protobufjs/src/parse.js +0 -1111
- package/node_modules/protobufjs/src/reader.js +0 -788
- package/node_modules/protobufjs/src/reader_buffer.js +0 -77
- package/node_modules/protobufjs/src/root.js +0 -424
- package/node_modules/protobufjs/src/roots.js +0 -18
- package/node_modules/protobufjs/src/rpc/service.js +0 -155
- package/node_modules/protobufjs/src/rpc.js +0 -36
- package/node_modules/protobufjs/src/service.js +0 -206
- package/node_modules/protobufjs/src/tokenize.js +0 -421
- package/node_modules/protobufjs/src/type.js +0 -676
- package/node_modules/protobufjs/src/types.js +0 -196
- package/node_modules/protobufjs/src/typescript.js +0 -25
- package/node_modules/protobufjs/src/util/aspromise.d.ts +0 -13
- package/node_modules/protobufjs/src/util/aspromise.js +0 -52
- package/node_modules/protobufjs/src/util/base64.d.ts +0 -32
- package/node_modules/protobufjs/src/util/base64.js +0 -146
- package/node_modules/protobufjs/src/util/codegen.d.ts +0 -31
- package/node_modules/protobufjs/src/util/codegen.js +0 -118
- package/node_modules/protobufjs/src/util/eventemitter.d.ts +0 -45
- package/node_modules/protobufjs/src/util/eventemitter.js +0 -86
- package/node_modules/protobufjs/src/util/fetch.d.ts +0 -56
- package/node_modules/protobufjs/src/util/fetch.js +0 -112
- package/node_modules/protobufjs/src/util/float.d.ts +0 -83
- package/node_modules/protobufjs/src/util/float.js +0 -335
- package/node_modules/protobufjs/src/util/fs.js +0 -11
- package/node_modules/protobufjs/src/util/longbits.js +0 -204
- package/node_modules/protobufjs/src/util/minimal.js +0 -511
- package/node_modules/protobufjs/src/util/path.d.ts +0 -22
- package/node_modules/protobufjs/src/util/path.js +0 -100
- package/node_modules/protobufjs/src/util/patterns.js +0 -7
- package/node_modules/protobufjs/src/util/pool.d.ts +0 -32
- package/node_modules/protobufjs/src/util/pool.js +0 -48
- package/node_modules/protobufjs/src/util/utf8.d.ts +0 -24
- package/node_modules/protobufjs/src/util/utf8.js +0 -190
- package/node_modules/protobufjs/src/util.js +0 -264
- package/node_modules/protobufjs/src/verifier.js +0 -185
- package/node_modules/protobufjs/src/wrappers.js +0 -106
- package/node_modules/protobufjs/src/writer.js +0 -700
- package/node_modules/protobufjs/src/writer_buffer.js +0 -100
- package/node_modules/protobufjs/tsconfig.json +0 -6
- package/node_modules/uWebSockets.js/ESM_wrapper.mjs +0 -53
- package/node_modules/uWebSockets.js/LICENSE +0 -201
- package/node_modules/uWebSockets.js/README.md +0 -6
- package/node_modules/uWebSockets.js/index.d.ts +0 -468
- package/node_modules/uWebSockets.js/package.json +0 -12
- package/node_modules/uWebSockets.js/source_commit +0 -1
- package/node_modules/uWebSockets.js/uws.js +0 -76
- package/node_modules/uWebSockets.js/uws_darwin_arm64_127.node +0 -0
- package/node_modules/uWebSockets.js/uws_darwin_arm64_137.node +0 -0
- package/node_modules/uWebSockets.js/uws_darwin_arm64_147.node +0 -0
- package/node_modules/uWebSockets.js/uws_darwin_x64_127.node +0 -0
- package/node_modules/uWebSockets.js/uws_darwin_x64_137.node +0 -0
- package/node_modules/uWebSockets.js/uws_darwin_x64_147.node +0 -0
- package/node_modules/uWebSockets.js/uws_linux_arm64_127.node +0 -0
- package/node_modules/uWebSockets.js/uws_linux_arm64_137.node +0 -0
- package/node_modules/uWebSockets.js/uws_linux_arm64_147.node +0 -0
- package/node_modules/uWebSockets.js/uws_linux_x64_127.node +0 -0
- package/node_modules/uWebSockets.js/uws_linux_x64_137.node +0 -0
- package/node_modules/uWebSockets.js/uws_linux_x64_147.node +0 -0
- package/node_modules/uWebSockets.js/uws_win32_x64_127.node +0 -0
- package/node_modules/uWebSockets.js/uws_win32_x64_137.node +0 -0
- package/node_modules/uWebSockets.js/uws_win32_x64_147.node +0 -0
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import crypto, { createHash } from 'crypto'
|
|
2
2
|
import { createWriteStream, mkdirSync, rmSync } from 'node:fs'
|
|
3
3
|
import { rename } from 'node:fs/promises'
|
|
4
|
-
import { Agent as HttpAgent } from 'node:http'
|
|
5
|
-
import { Agent as HttpsAgent, type RequestOptions } from 'node:https'
|
|
6
|
-
import
|
|
4
|
+
import { Agent as HttpAgent, request as httpRequest, type IncomingMessage } from 'node:http'
|
|
5
|
+
import { Agent as HttpsAgent, request as httpsRequest, type RequestOptions } from 'node:https'
|
|
6
|
+
import { pipeline } from 'node:stream/promises'
|
|
7
7
|
import path from 'path'
|
|
8
8
|
import { debug } from './debug.ts'
|
|
9
9
|
import { Mapper } from './mappers/index.ts'
|
|
10
10
|
import { Disconnect, Exchange, Filter, FilterForExchange } from './types.ts'
|
|
11
|
-
const { http: followRedirectsHttp, https: followRedirectsHttps } = followRedirects
|
|
12
11
|
|
|
13
12
|
export function parseAsUTCDate(val: string) {
|
|
14
13
|
// Treat date-only and minute-level strings as UTC instead of local time.
|
|
@@ -456,44 +455,75 @@ async function requestViaFetch(method: string, url: string, options: HttpRequest
|
|
|
456
455
|
}
|
|
457
456
|
}
|
|
458
457
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
458
|
+
type DownloadRequestOptions = {
|
|
459
|
+
headers: Record<string, string>
|
|
460
|
+
timeout: number
|
|
461
|
+
}
|
|
462
462
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
headers: preparedRequest.headers,
|
|
471
|
-
timeout: options.timeout
|
|
472
|
-
},
|
|
473
|
-
(response) => {
|
|
474
|
-
response.setEncoding('utf8')
|
|
475
|
-
let body = ''
|
|
476
|
-
response.on('error', reject)
|
|
477
|
-
response.on('data', (chunk) => (body += chunk))
|
|
478
|
-
response.on('end', () => {
|
|
479
|
-
resolve(createHttpResponse(response.statusCode ?? 0, parseNodeResponseHeaders(response.headers), body))
|
|
480
|
-
})
|
|
481
|
-
}
|
|
482
|
-
)
|
|
483
|
-
.on('error', reject)
|
|
484
|
-
.on('timeout', () => {
|
|
485
|
-
reject(new Error('Request timed out'))
|
|
486
|
-
request.destroy()
|
|
487
|
-
})
|
|
488
|
-
|
|
489
|
-
if (preparedRequest.body !== undefined) {
|
|
490
|
-
request.write(preparedRequest.body)
|
|
491
|
-
}
|
|
463
|
+
function sendHttpRequest(url: string | URL, options: RequestOptions, body?: string): Promise<IncomingMessage> {
|
|
464
|
+
const requestUrl = new URL(url)
|
|
465
|
+
const requestClient = requestUrl.protocol === 'http:' ? httpRequest : httpsRequest
|
|
466
|
+
const agent = getProxyAgent(requestUrl) ?? (requestUrl.protocol === 'https:' ? httpsAgent : undefined)
|
|
467
|
+
|
|
468
|
+
return new Promise((resolve, reject) => {
|
|
469
|
+
const request = requestClient(requestUrl, { ...options, agent }, resolve)
|
|
492
470
|
|
|
471
|
+
request.once('error', reject)
|
|
472
|
+
request.once('timeout', () => request.destroy(new Error('Request timed out')))
|
|
473
|
+
if (body !== undefined) {
|
|
474
|
+
request.write(body)
|
|
475
|
+
}
|
|
493
476
|
request.end()
|
|
494
477
|
})
|
|
495
478
|
}
|
|
496
479
|
|
|
480
|
+
async function openDownloadResponse(url: string, options: DownloadRequestOptions) {
|
|
481
|
+
const response = await sendHttpRequest(url, options)
|
|
482
|
+
const statusCode = response.statusCode ?? 0
|
|
483
|
+
const location = response.headers.location
|
|
484
|
+
if (location === undefined || statusCode < 300 || statusCode >= 400) {
|
|
485
|
+
return response
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
response.destroy()
|
|
489
|
+
const sourceUrl = new URL(url)
|
|
490
|
+
const redirectUrl = new URL(location, sourceUrl)
|
|
491
|
+
const headers: Record<string, string> = { ...options.headers }
|
|
492
|
+
if (sourceUrl.origin !== redirectUrl.origin) {
|
|
493
|
+
delete headers.Authorization
|
|
494
|
+
delete headers.authorization
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return sendHttpRequest(redirectUrl, {
|
|
498
|
+
...options,
|
|
499
|
+
headers
|
|
500
|
+
})
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function readResponseText(response: IncomingMessage) {
|
|
504
|
+
response.setEncoding('utf8')
|
|
505
|
+
let body = ''
|
|
506
|
+
for await (const chunk of response) {
|
|
507
|
+
body += chunk
|
|
508
|
+
}
|
|
509
|
+
return body
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async function requestViaProxy(method: string, url: string, options: HttpRequestOptions): Promise<HttpResponse> {
|
|
513
|
+
const preparedRequest = prepareRequest(method, options)
|
|
514
|
+
const response = await sendHttpRequest(
|
|
515
|
+
url,
|
|
516
|
+
{
|
|
517
|
+
method,
|
|
518
|
+
headers: preparedRequest.headers,
|
|
519
|
+
timeout: options.timeout
|
|
520
|
+
},
|
|
521
|
+
preparedRequest.body
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
return createHttpResponse(response.statusCode ?? 0, parseNodeResponseHeaders(response.headers), await readResponseText(response))
|
|
525
|
+
}
|
|
526
|
+
|
|
497
527
|
async function request(method: string, url: string, options: HttpRequestOptions = {}) {
|
|
498
528
|
const retrySettings = getRetrySettings(method, options.retry)
|
|
499
529
|
|
|
@@ -622,7 +652,12 @@ export function cleanTempFiles() {
|
|
|
622
652
|
tmpFileCleanups.forEach((cleanup) => cleanup())
|
|
623
653
|
}
|
|
624
654
|
|
|
625
|
-
async function _downloadFile(
|
|
655
|
+
async function _downloadFile(
|
|
656
|
+
requestOptions: DownloadRequestOptions,
|
|
657
|
+
url: string,
|
|
658
|
+
downloadPath: string,
|
|
659
|
+
appendContentEncodingExtension: boolean
|
|
660
|
+
) {
|
|
626
661
|
// first ensure that directory where we want to download file exists
|
|
627
662
|
mkdirSync(path.dirname(downloadPath), { recursive: true })
|
|
628
663
|
|
|
@@ -642,59 +677,28 @@ async function _downloadFile(requestOptions: RequestOptions, url: string, downlo
|
|
|
642
677
|
|
|
643
678
|
try {
|
|
644
679
|
// based on https://github.com/nodejs/node/issues/28172 - only reliable way to consume response stream and avoiding all the 'gotchas'
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
const req = requestClient
|
|
651
|
-
.get(url, { ...requestOptions, agent }, (res) => {
|
|
652
|
-
const { statusCode } = res
|
|
653
|
-
if (statusCode !== 200) {
|
|
654
|
-
// read the error response text and throw it as an HttpError
|
|
655
|
-
res.setEncoding('utf8')
|
|
656
|
-
let body = ''
|
|
657
|
-
res.on('error', reject)
|
|
658
|
-
res.on('data', (chunk) => (body += chunk))
|
|
659
|
-
res.on('end', () => {
|
|
660
|
-
reject(new HttpError(statusCode!, body, url))
|
|
661
|
-
})
|
|
662
|
-
} else {
|
|
663
|
-
responseHeaders = parseNodeResponseHeaders(res.headers)
|
|
664
|
-
if (appendContentEncodingExtension) {
|
|
665
|
-
const contentEncoding = asSingleHeaderValue(res.headers['content-encoding'])
|
|
666
|
-
if (contentEncoding === 'zstd') {
|
|
667
|
-
finalDownloadPath = `${downloadPath}.zst`
|
|
668
|
-
} else if (contentEncoding === undefined || contentEncoding === 'gzip') {
|
|
669
|
-
finalDownloadPath = `${downloadPath}.gz`
|
|
670
|
-
} else {
|
|
671
|
-
reject(new Error(`Unsupported data feed content encoding: ${contentEncoding}`))
|
|
672
|
-
return
|
|
673
|
-
}
|
|
674
|
-
}
|
|
680
|
+
const response = await openDownloadResponse(url, requestOptions)
|
|
681
|
+
const { statusCode } = response
|
|
682
|
+
if (statusCode !== 200) {
|
|
683
|
+
throw new HttpError(statusCode ?? 0, await readResponseText(response), url)
|
|
684
|
+
}
|
|
675
685
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
.on('timeout', () => {
|
|
693
|
-
debug('download file request timeout, %s', url)
|
|
694
|
-
reject(new Error('Request timed out'))
|
|
695
|
-
req.destroy()
|
|
696
|
-
})
|
|
697
|
-
})
|
|
686
|
+
const responseHeaders = parseNodeResponseHeaders(response.headers)
|
|
687
|
+
if (appendContentEncodingExtension) {
|
|
688
|
+
const contentEncoding = asSingleHeaderValue(response.headers['content-encoding'])
|
|
689
|
+
if (contentEncoding === 'zstd') {
|
|
690
|
+
finalDownloadPath = `${downloadPath}.zst`
|
|
691
|
+
} else if (contentEncoding === undefined || contentEncoding === 'gzip') {
|
|
692
|
+
finalDownloadPath = `${downloadPath}.gz`
|
|
693
|
+
} else {
|
|
694
|
+
throw new Error(`Unsupported data feed content encoding: ${contentEncoding}`)
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
await pipeline(response, fileWriteStream)
|
|
699
|
+
if (!response.complete) {
|
|
700
|
+
throw new Error('The connection was terminated while the message was still being sent')
|
|
701
|
+
}
|
|
698
702
|
|
|
699
703
|
// finally when saving from the network to file has succeded, rename tmp file to normal name
|
|
700
704
|
// then we're sure that responses is 100% saved and also even if different process was doing the same we're good
|
|
@@ -1,71 +1,13 @@
|
|
|
1
|
-
import protobuf from 'protobufjs'
|
|
2
1
|
import { CircularBuffer, getJSON, wait } from '../handy.ts'
|
|
3
2
|
import type { MexcDepthSnapshotMessage } from '../mappers/mexc.ts'
|
|
4
3
|
import { Filter } from '../types.ts'
|
|
5
4
|
import { RealTimeFeedBase } from './realtimefeed.ts'
|
|
5
|
+
import { decodeMexcProtobufMessage } from './mexcprotobuf.ts'
|
|
6
6
|
|
|
7
7
|
export class MexcRealTimeFeed extends RealTimeFeedBase {
|
|
8
8
|
private static readonly depthChannel = 'spot@public.aggre.depth.v3.api.pb@10ms'
|
|
9
9
|
private static readonly depthSnapshotChannel = 'spot@public.aggre.depth.snapshot.v3.api.pb@10ms'
|
|
10
10
|
private static readonly jsonObjectStart = '{'.charCodeAt(0)
|
|
11
|
-
private static pushDataV3ApiWrapper: protobuf.Type | undefined
|
|
12
|
-
/**
|
|
13
|
-
* MEXC protobuf docs at @see https://www.mexc.com/api-docs/spot-v3/websocket-market-streams/protocol-buffers-integration
|
|
14
|
-
* and schemas from @see https://github.com/mexcdevelop/websocket-proto, mirrored in tardis-recorder/datafeeds/src/mexc/proto.
|
|
15
|
-
*/
|
|
16
|
-
private static readonly pushDataV3ApiWrapperSchema = `
|
|
17
|
-
syntax = "proto3";
|
|
18
|
-
|
|
19
|
-
message PushDataV3ApiWrapper {
|
|
20
|
-
string channel = 1;
|
|
21
|
-
oneof body {
|
|
22
|
-
PublicAggreDepthsV3Api publicAggreDepths = 313;
|
|
23
|
-
PublicAggreDealsV3Api publicAggreDeals = 314;
|
|
24
|
-
PublicAggreBookTickerV3Api publicAggreBookTicker = 315;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
optional string symbol = 3;
|
|
28
|
-
optional string symbolId = 4;
|
|
29
|
-
optional int64 createTime = 5;
|
|
30
|
-
optional int64 sendTime = 6;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
message PublicAggreDealsV3Api {
|
|
34
|
-
repeated PublicAggreDealsV3ApiItem deals = 1;
|
|
35
|
-
string eventType = 2;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
message PublicAggreDealsV3ApiItem {
|
|
39
|
-
string price = 1;
|
|
40
|
-
string quantity = 2;
|
|
41
|
-
int32 tradeType = 3;
|
|
42
|
-
int64 time = 4;
|
|
43
|
-
string tradeId = 5;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
message PublicAggreDepthsV3Api {
|
|
47
|
-
repeated PublicAggreDepthV3ApiItem asks = 1;
|
|
48
|
-
repeated PublicAggreDepthV3ApiItem bids = 2;
|
|
49
|
-
string eventType = 3;
|
|
50
|
-
string fromVersion = 4;
|
|
51
|
-
string toVersion = 5;
|
|
52
|
-
int64 lastOrderCreateTime = 6;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
message PublicAggreDepthV3ApiItem {
|
|
56
|
-
string price = 1;
|
|
57
|
-
string quantity = 2;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
message PublicAggreBookTickerV3Api {
|
|
61
|
-
string bidPrice = 1;
|
|
62
|
-
string bidQuantity = 2;
|
|
63
|
-
string askPrice = 3;
|
|
64
|
-
string askQuantity = 4;
|
|
65
|
-
string version = 5;
|
|
66
|
-
int64 lastOrderCreateTime = 6;
|
|
67
|
-
}
|
|
68
|
-
`
|
|
69
11
|
|
|
70
12
|
private readonly pendingDepthSnapshotSymbols = new Set<string>()
|
|
71
13
|
private readonly bufferedDepthUpdates = new Map<string, CircularBuffer<MexcDepthUpdateData>>()
|
|
@@ -114,11 +56,7 @@ export class MexcRealTimeFeed extends RealTimeFeedBase {
|
|
|
114
56
|
return JSON.parse(buffer.toString())
|
|
115
57
|
}
|
|
116
58
|
|
|
117
|
-
|
|
118
|
-
return pushDataV3ApiWrapper.toObject(pushDataV3ApiWrapper.decode(buffer), {
|
|
119
|
-
longs: String,
|
|
120
|
-
arrays: true
|
|
121
|
-
})
|
|
59
|
+
return decodeMexcProtobufMessage(buffer)
|
|
122
60
|
}
|
|
123
61
|
|
|
124
62
|
protected override messageIsError(message: any): boolean {
|
|
@@ -168,14 +106,6 @@ export class MexcRealTimeFeed extends RealTimeFeedBase {
|
|
|
168
106
|
this.send({ method: 'PING' })
|
|
169
107
|
}
|
|
170
108
|
|
|
171
|
-
private getPushDataV3ApiWrapper() {
|
|
172
|
-
MexcRealTimeFeed.pushDataV3ApiWrapper ??= protobuf
|
|
173
|
-
.parse(MexcRealTimeFeed.pushDataV3ApiWrapperSchema)
|
|
174
|
-
.root.lookupType('PushDataV3ApiWrapper')
|
|
175
|
-
|
|
176
|
-
return MexcRealTimeFeed.pushDataV3ApiWrapper
|
|
177
|
-
}
|
|
178
|
-
|
|
179
109
|
private resetDepthSnapshotTracking(filters: Required<Filter<string>>[]) {
|
|
180
110
|
this.pendingDepthSnapshotSymbols.clear()
|
|
181
111
|
this.bufferedDepthUpdates.clear()
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
const textDecoder = new TextDecoder()
|
|
2
|
+
|
|
3
|
+
// Decodes only the current MEXC real-time channels supported by MexcRealTimeFeed.
|
|
4
|
+
// Field numbers mirror the official schemas at commit 7b8ac7a6681f28551612a5a7cefbb7e09b56bb85:
|
|
5
|
+
// https://github.com/mexcdevelop/websocket-proto/tree/7b8ac7a6681f28551612a5a7cefbb7e09b56bb85
|
|
6
|
+
// int64 values remain decimal strings and repeated fields remain empty arrays when absent,
|
|
7
|
+
// preserving the output shape previously produced by protobufjs.
|
|
8
|
+
export function decodeMexcProtobufMessage(data: Uint8Array) {
|
|
9
|
+
const reader = new ProtobufReader(data)
|
|
10
|
+
const message: {
|
|
11
|
+
channel?: string
|
|
12
|
+
symbol?: string
|
|
13
|
+
symbolId?: string
|
|
14
|
+
createTime?: string
|
|
15
|
+
sendTime?: string
|
|
16
|
+
publicAggreDepths?: ReturnType<typeof decodeDepths>
|
|
17
|
+
publicAggreDeals?: ReturnType<typeof decodeDeals>
|
|
18
|
+
publicAggreBookTicker?: ReturnType<typeof decodeBookTicker>
|
|
19
|
+
} = {}
|
|
20
|
+
|
|
21
|
+
while (reader.done === false) {
|
|
22
|
+
const { fieldNumber, wireType } = reader.readTag()
|
|
23
|
+
|
|
24
|
+
switch (fieldNumber) {
|
|
25
|
+
case 1:
|
|
26
|
+
message.channel = reader.readString(wireType)
|
|
27
|
+
break
|
|
28
|
+
case 3:
|
|
29
|
+
message.symbol = reader.readString(wireType)
|
|
30
|
+
break
|
|
31
|
+
case 4:
|
|
32
|
+
message.symbolId = reader.readString(wireType)
|
|
33
|
+
break
|
|
34
|
+
case 5:
|
|
35
|
+
message.createTime = reader.readInt64String(wireType)
|
|
36
|
+
break
|
|
37
|
+
case 6:
|
|
38
|
+
message.sendTime = reader.readInt64String(wireType)
|
|
39
|
+
break
|
|
40
|
+
case 313:
|
|
41
|
+
message.publicAggreDepths = decodeDepths(reader.readBytes(wireType))
|
|
42
|
+
break
|
|
43
|
+
case 314:
|
|
44
|
+
message.publicAggreDeals = decodeDeals(reader.readBytes(wireType))
|
|
45
|
+
break
|
|
46
|
+
case 315:
|
|
47
|
+
message.publicAggreBookTicker = decodeBookTicker(reader.readBytes(wireType))
|
|
48
|
+
break
|
|
49
|
+
default:
|
|
50
|
+
reader.skipField(wireType)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return message
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function decodeDeals(data: Uint8Array) {
|
|
58
|
+
const reader = new ProtobufReader(data)
|
|
59
|
+
const message: { deals: ReturnType<typeof decodeTrade>[]; eventType?: string } = { deals: [] }
|
|
60
|
+
|
|
61
|
+
while (reader.done === false) {
|
|
62
|
+
const { fieldNumber, wireType } = reader.readTag()
|
|
63
|
+
|
|
64
|
+
switch (fieldNumber) {
|
|
65
|
+
case 1:
|
|
66
|
+
message.deals.push(decodeTrade(reader.readBytes(wireType)))
|
|
67
|
+
break
|
|
68
|
+
case 2:
|
|
69
|
+
message.eventType = reader.readString(wireType)
|
|
70
|
+
break
|
|
71
|
+
default:
|
|
72
|
+
reader.skipField(wireType)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return message
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function decodeTrade(data: Uint8Array) {
|
|
80
|
+
const reader = new ProtobufReader(data)
|
|
81
|
+
const message: { price?: string; quantity?: string; tradeType?: number; time?: string; tradeId?: string } = {}
|
|
82
|
+
|
|
83
|
+
while (reader.done === false) {
|
|
84
|
+
const { fieldNumber, wireType } = reader.readTag()
|
|
85
|
+
|
|
86
|
+
switch (fieldNumber) {
|
|
87
|
+
case 1:
|
|
88
|
+
message.price = reader.readString(wireType)
|
|
89
|
+
break
|
|
90
|
+
case 2:
|
|
91
|
+
message.quantity = reader.readString(wireType)
|
|
92
|
+
break
|
|
93
|
+
case 3:
|
|
94
|
+
message.tradeType = reader.readInt32(wireType)
|
|
95
|
+
break
|
|
96
|
+
case 4:
|
|
97
|
+
message.time = reader.readInt64String(wireType)
|
|
98
|
+
break
|
|
99
|
+
case 5:
|
|
100
|
+
message.tradeId = reader.readString(wireType)
|
|
101
|
+
break
|
|
102
|
+
default:
|
|
103
|
+
reader.skipField(wireType)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return message
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function decodeDepths(data: Uint8Array) {
|
|
111
|
+
const reader = new ProtobufReader(data)
|
|
112
|
+
const message: {
|
|
113
|
+
asks: ReturnType<typeof decodePriceLevel>[]
|
|
114
|
+
bids: ReturnType<typeof decodePriceLevel>[]
|
|
115
|
+
eventType?: string
|
|
116
|
+
fromVersion?: string
|
|
117
|
+
toVersion?: string
|
|
118
|
+
lastOrderCreateTime?: string
|
|
119
|
+
} = { asks: [], bids: [] }
|
|
120
|
+
|
|
121
|
+
while (reader.done === false) {
|
|
122
|
+
const { fieldNumber, wireType } = reader.readTag()
|
|
123
|
+
|
|
124
|
+
switch (fieldNumber) {
|
|
125
|
+
case 1:
|
|
126
|
+
message.asks.push(decodePriceLevel(reader.readBytes(wireType)))
|
|
127
|
+
break
|
|
128
|
+
case 2:
|
|
129
|
+
message.bids.push(decodePriceLevel(reader.readBytes(wireType)))
|
|
130
|
+
break
|
|
131
|
+
case 3:
|
|
132
|
+
message.eventType = reader.readString(wireType)
|
|
133
|
+
break
|
|
134
|
+
case 4:
|
|
135
|
+
message.fromVersion = reader.readString(wireType)
|
|
136
|
+
break
|
|
137
|
+
case 5:
|
|
138
|
+
message.toVersion = reader.readString(wireType)
|
|
139
|
+
break
|
|
140
|
+
case 6:
|
|
141
|
+
message.lastOrderCreateTime = reader.readInt64String(wireType)
|
|
142
|
+
break
|
|
143
|
+
default:
|
|
144
|
+
reader.skipField(wireType)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return message
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function decodePriceLevel(data: Uint8Array) {
|
|
152
|
+
const reader = new ProtobufReader(data)
|
|
153
|
+
const message: { price?: string; quantity?: string } = {}
|
|
154
|
+
|
|
155
|
+
while (reader.done === false) {
|
|
156
|
+
const { fieldNumber, wireType } = reader.readTag()
|
|
157
|
+
|
|
158
|
+
switch (fieldNumber) {
|
|
159
|
+
case 1:
|
|
160
|
+
message.price = reader.readString(wireType)
|
|
161
|
+
break
|
|
162
|
+
case 2:
|
|
163
|
+
message.quantity = reader.readString(wireType)
|
|
164
|
+
break
|
|
165
|
+
default:
|
|
166
|
+
reader.skipField(wireType)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return message
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function decodeBookTicker(data: Uint8Array) {
|
|
174
|
+
const reader = new ProtobufReader(data)
|
|
175
|
+
const message: {
|
|
176
|
+
bidPrice?: string
|
|
177
|
+
bidQuantity?: string
|
|
178
|
+
askPrice?: string
|
|
179
|
+
askQuantity?: string
|
|
180
|
+
version?: string
|
|
181
|
+
lastOrderCreateTime?: string
|
|
182
|
+
} = {}
|
|
183
|
+
|
|
184
|
+
while (reader.done === false) {
|
|
185
|
+
const { fieldNumber, wireType } = reader.readTag()
|
|
186
|
+
|
|
187
|
+
switch (fieldNumber) {
|
|
188
|
+
case 1:
|
|
189
|
+
message.bidPrice = reader.readString(wireType)
|
|
190
|
+
break
|
|
191
|
+
case 2:
|
|
192
|
+
message.bidQuantity = reader.readString(wireType)
|
|
193
|
+
break
|
|
194
|
+
case 3:
|
|
195
|
+
message.askPrice = reader.readString(wireType)
|
|
196
|
+
break
|
|
197
|
+
case 4:
|
|
198
|
+
message.askQuantity = reader.readString(wireType)
|
|
199
|
+
break
|
|
200
|
+
case 5:
|
|
201
|
+
message.version = reader.readString(wireType)
|
|
202
|
+
break
|
|
203
|
+
case 6:
|
|
204
|
+
message.lastOrderCreateTime = reader.readInt64String(wireType)
|
|
205
|
+
break
|
|
206
|
+
default:
|
|
207
|
+
reader.skipField(wireType)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return message
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
class ProtobufReader {
|
|
215
|
+
private offset = 0
|
|
216
|
+
|
|
217
|
+
constructor(private readonly data: Uint8Array) {}
|
|
218
|
+
|
|
219
|
+
get done() {
|
|
220
|
+
return this.offset === this.data.length
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
readTag() {
|
|
224
|
+
const tag = this.readVarint()
|
|
225
|
+
if (typeof tag === 'bigint' || tag > 0xffffffff) {
|
|
226
|
+
throw new Error('Invalid protobuf tag')
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const fieldNumber = tag >>> 3
|
|
230
|
+
const wireType = tag & 7
|
|
231
|
+
|
|
232
|
+
if (fieldNumber === 0) {
|
|
233
|
+
throw new Error('Invalid protobuf field number 0')
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return { fieldNumber, wireType }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
readString(wireType: number) {
|
|
240
|
+
this.expectWireType(wireType, 2)
|
|
241
|
+
const length = this.readLength()
|
|
242
|
+
const start = this.offset
|
|
243
|
+
const end = start + length
|
|
244
|
+
|
|
245
|
+
if (end > this.data.length) {
|
|
246
|
+
throw new Error('Invalid protobuf message length')
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
this.offset = end
|
|
250
|
+
return Buffer.isBuffer(this.data) ? this.data.toString('utf8', start, end) : textDecoder.decode(this.data.subarray(start, end))
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
readBytes(wireType: number) {
|
|
254
|
+
this.expectWireType(wireType, 2)
|
|
255
|
+
const length = this.readLength()
|
|
256
|
+
const end = this.offset + length
|
|
257
|
+
|
|
258
|
+
if (end > this.data.length) {
|
|
259
|
+
throw new Error('Invalid protobuf message length')
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const value = this.data.subarray(this.offset, end)
|
|
263
|
+
this.offset = end
|
|
264
|
+
return value
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
readInt32(wireType: number) {
|
|
268
|
+
this.expectWireType(wireType, 0)
|
|
269
|
+
const value = this.readVarint()
|
|
270
|
+
return typeof value === 'number' ? value | 0 : Number(BigInt.asIntN(32, value))
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
readInt64String(wireType: number) {
|
|
274
|
+
this.expectWireType(wireType, 0)
|
|
275
|
+
const value = this.readVarint()
|
|
276
|
+
return typeof value === 'number' ? value.toString() : BigInt.asIntN(64, value).toString()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
skipField(wireType: number): void {
|
|
280
|
+
switch (wireType) {
|
|
281
|
+
case 0:
|
|
282
|
+
this.skipVarint()
|
|
283
|
+
return
|
|
284
|
+
case 1:
|
|
285
|
+
this.skipBytes(8)
|
|
286
|
+
return
|
|
287
|
+
case 2:
|
|
288
|
+
this.skipBytes(this.readLength())
|
|
289
|
+
return
|
|
290
|
+
case 5:
|
|
291
|
+
this.skipBytes(4)
|
|
292
|
+
return
|
|
293
|
+
default:
|
|
294
|
+
throw new Error(`Unsupported protobuf wire type ${wireType}`)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private readVarint() {
|
|
299
|
+
let value = 0
|
|
300
|
+
let multiplier = 1
|
|
301
|
+
|
|
302
|
+
// Seven base-128 digits use at most 49 bits, so they remain exact and fast as a number.
|
|
303
|
+
for (let index = 0; index < 7; index++) {
|
|
304
|
+
const byte = this.readByte()
|
|
305
|
+
value += (byte & 0x7f) * multiplier
|
|
306
|
+
|
|
307
|
+
if ((byte & 0x80) === 0) {
|
|
308
|
+
return value
|
|
309
|
+
}
|
|
310
|
+
multiplier *= 0x80
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Larger int64 values need BigInt to preserve every bit before converting them to strings.
|
|
314
|
+
let bigValue = BigInt(value)
|
|
315
|
+
for (let index = 7; index < 10; index++) {
|
|
316
|
+
const byte = this.readByte()
|
|
317
|
+
bigValue |= BigInt(byte & 0x7f) << BigInt(index * 7)
|
|
318
|
+
|
|
319
|
+
if ((byte & 0x80) === 0) {
|
|
320
|
+
return bigValue
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
throw new Error('Invalid protobuf varint')
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private readLength() {
|
|
328
|
+
const length = this.readVarint()
|
|
329
|
+
if (typeof length === 'bigint' || Number.isSafeInteger(length) === false) {
|
|
330
|
+
throw new Error('Invalid protobuf message length')
|
|
331
|
+
}
|
|
332
|
+
return length
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private skipVarint() {
|
|
336
|
+
for (let index = 0; index < 10; index++) {
|
|
337
|
+
if ((this.readByte() & 0x80) === 0) {
|
|
338
|
+
return
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
throw new Error('Invalid protobuf varint')
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private readByte() {
|
|
345
|
+
if (this.offset >= this.data.length) {
|
|
346
|
+
throw new Error('Unexpected end of protobuf message')
|
|
347
|
+
}
|
|
348
|
+
return this.data[this.offset++]!
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private skipBytes(length: number) {
|
|
352
|
+
const end = this.offset + length
|
|
353
|
+
if (Number.isSafeInteger(length) === false || end > this.data.length) {
|
|
354
|
+
throw new Error('Invalid protobuf message length')
|
|
355
|
+
}
|
|
356
|
+
this.offset = end
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private expectWireType(actual: number, expected: number) {
|
|
360
|
+
if (actual !== expected) {
|
|
361
|
+
throw new Error(`Invalid protobuf wire type ${actual}, expected ${expected}`)
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|