tardis-machine 18.6.2 → 18.8.0

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.
Files changed (104) hide show
  1. package/dist/ws/subscriptionsmappers.d.ts.map +1 -1
  2. package/dist/ws/subscriptionsmappers.js +5 -0
  3. package/dist/ws/subscriptionsmappers.js.map +1 -1
  4. package/node_modules/tardis-dev/dist/consts.d.ts +2 -1
  5. package/node_modules/tardis-dev/dist/consts.d.ts.map +1 -1
  6. package/node_modules/tardis-dev/dist/consts.js +13 -0
  7. package/node_modules/tardis-dev/dist/consts.js.map +1 -1
  8. package/node_modules/tardis-dev/dist/downloaddatasets.d.ts +2 -0
  9. package/node_modules/tardis-dev/dist/downloaddatasets.d.ts.map +1 -1
  10. package/node_modules/tardis-dev/dist/downloaddatasets.js +21 -11
  11. package/node_modules/tardis-dev/dist/downloaddatasets.js.map +1 -1
  12. package/node_modules/tardis-dev/dist/handy.d.ts +16 -0
  13. package/node_modules/tardis-dev/dist/handy.d.ts.map +1 -1
  14. package/node_modules/tardis-dev/dist/handy.js +211 -15
  15. package/node_modules/tardis-dev/dist/handy.js.map +1 -1
  16. package/node_modules/tardis-dev/dist/instrumentinfo.d.ts +1 -1
  17. package/node_modules/tardis-dev/dist/instrumentinfo.d.ts.map +1 -1
  18. package/node_modules/tardis-dev/dist/mappers/ascendex.d.ts +4 -4
  19. package/node_modules/tardis-dev/dist/mappers/aster.d.ts +8 -8
  20. package/node_modules/tardis-dev/dist/mappers/binance.d.ts +18 -18
  21. package/node_modules/tardis-dev/dist/mappers/binanceeuropeanoptions.d.ts +1 -1
  22. package/node_modules/tardis-dev/dist/mappers/bitfinex.d.ts +8 -8
  23. package/node_modules/tardis-dev/dist/mappers/bitflyer.d.ts +1 -1
  24. package/node_modules/tardis-dev/dist/mappers/bitget.d.ts +1 -1
  25. package/node_modules/tardis-dev/dist/mappers/bithumb.d.ts +2 -2
  26. package/node_modules/tardis-dev/dist/mappers/bitmex.d.ts +2 -2
  27. package/node_modules/tardis-dev/dist/mappers/bitnomial.d.ts +1 -1
  28. package/node_modules/tardis-dev/dist/mappers/bitstamp.d.ts +2 -2
  29. package/node_modules/tardis-dev/dist/mappers/bitvavo.d.ts +3 -3
  30. package/node_modules/tardis-dev/dist/mappers/blockchaincom.d.ts +2 -2
  31. package/node_modules/tardis-dev/dist/mappers/bullish.d.ts +5 -5
  32. package/node_modules/tardis-dev/dist/mappers/bybit.d.ts +10 -10
  33. package/node_modules/tardis-dev/dist/mappers/bybitspot.d.ts +1 -1
  34. package/node_modules/tardis-dev/dist/mappers/coinbase.d.ts +1 -1
  35. package/node_modules/tardis-dev/dist/mappers/coinbasederivatives.d.ts +4 -4
  36. package/node_modules/tardis-dev/dist/mappers/coinbaseinternational.d.ts +2 -2
  37. package/node_modules/tardis-dev/dist/mappers/coinflex.d.ts +1 -1
  38. package/node_modules/tardis-dev/dist/mappers/cryptocom.d.ts +5 -5
  39. package/node_modules/tardis-dev/dist/mappers/cryptofacilities.d.ts +2 -2
  40. package/node_modules/tardis-dev/dist/mappers/delta.d.ts +1 -1
  41. package/node_modules/tardis-dev/dist/mappers/delta_india.d.ts +11 -0
  42. package/node_modules/tardis-dev/dist/mappers/delta_india.d.ts.map +1 -0
  43. package/node_modules/tardis-dev/dist/mappers/delta_india.js +197 -0
  44. package/node_modules/tardis-dev/dist/mappers/delta_india.js.map +1 -0
  45. package/node_modules/tardis-dev/dist/mappers/deribit.d.ts +3 -3
  46. package/node_modules/tardis-dev/dist/mappers/dydx.d.ts +3 -3
  47. package/node_modules/tardis-dev/dist/mappers/dydxv4.d.ts +4 -4
  48. package/node_modules/tardis-dev/dist/mappers/ftx.d.ts +8 -8
  49. package/node_modules/tardis-dev/dist/mappers/gateio.d.ts +1 -1
  50. package/node_modules/tardis-dev/dist/mappers/gateiofutures.d.ts +4 -4
  51. package/node_modules/tardis-dev/dist/mappers/gemini.d.ts +1 -1
  52. package/node_modules/tardis-dev/dist/mappers/huobi.d.ts +26 -26
  53. package/node_modules/tardis-dev/dist/mappers/hyperliquid.d.ts +3 -3
  54. package/node_modules/tardis-dev/dist/mappers/index.d.ts +202 -190
  55. package/node_modules/tardis-dev/dist/mappers/index.d.ts.map +1 -1
  56. package/node_modules/tardis-dev/dist/mappers/index.js +2 -1
  57. package/node_modules/tardis-dev/dist/mappers/index.js.map +1 -1
  58. package/node_modules/tardis-dev/dist/mappers/kucoin.d.ts +3 -3
  59. package/node_modules/tardis-dev/dist/mappers/kucoinfutures.d.ts +4 -4
  60. package/node_modules/tardis-dev/dist/mappers/lighter.d.ts +10 -10
  61. package/node_modules/tardis-dev/dist/mappers/mexc.d.ts +3 -3
  62. package/node_modules/tardis-dev/dist/mappers/mexcfutures.d.ts +3 -3
  63. package/node_modules/tardis-dev/dist/mappers/okex.d.ts +15 -15
  64. package/node_modules/tardis-dev/dist/mappers/phemex.d.ts +1 -1
  65. package/node_modules/tardis-dev/dist/mappers/polymarket.d.ts +3 -3
  66. package/node_modules/tardis-dev/dist/mappers/serum.d.ts +9 -9
  67. package/node_modules/tardis-dev/dist/mappers/upbit.d.ts +2 -2
  68. package/node_modules/tardis-dev/dist/realtimefeeds/delta_india.d.ts +32 -0
  69. package/node_modules/tardis-dev/dist/realtimefeeds/delta_india.d.ts.map +1 -0
  70. package/node_modules/tardis-dev/dist/realtimefeeds/delta_india.js +117 -0
  71. package/node_modules/tardis-dev/dist/realtimefeeds/delta_india.js.map +1 -0
  72. package/node_modules/tardis-dev/dist/realtimefeeds/dydx_v4.d.ts +1 -0
  73. package/node_modules/tardis-dev/dist/realtimefeeds/dydx_v4.d.ts.map +1 -1
  74. package/node_modules/tardis-dev/dist/realtimefeeds/dydx_v4.js +1 -0
  75. package/node_modules/tardis-dev/dist/realtimefeeds/dydx_v4.js.map +1 -1
  76. package/node_modules/tardis-dev/dist/realtimefeeds/index.d.ts.map +1 -1
  77. package/node_modules/tardis-dev/dist/realtimefeeds/index.js +2 -0
  78. package/node_modules/tardis-dev/dist/realtimefeeds/index.js.map +1 -1
  79. package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.d.ts +1 -0
  80. package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.d.ts.map +1 -1
  81. package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.js +3 -1
  82. package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.js.map +1 -1
  83. package/node_modules/tardis-dev/dist/worker.d.ts.map +1 -1
  84. package/node_modules/tardis-dev/dist/worker.js +1 -2
  85. package/node_modules/tardis-dev/dist/worker.js.map +1 -1
  86. package/node_modules/tardis-dev/dist/zstd.d.ts +1 -2
  87. package/node_modules/tardis-dev/dist/zstd.d.ts.map +1 -1
  88. package/node_modules/tardis-dev/dist/zstd.js +4 -118
  89. package/node_modules/tardis-dev/dist/zstd.js.map +1 -1
  90. package/node_modules/tardis-dev/package.json +1 -1
  91. package/node_modules/tardis-dev/src/consts.ts +14 -0
  92. package/node_modules/tardis-dev/src/downloaddatasets.ts +31 -12
  93. package/node_modules/tardis-dev/src/handy.ts +261 -14
  94. package/node_modules/tardis-dev/src/instrumentinfo.ts +1 -1
  95. package/node_modules/tardis-dev/src/mappers/delta_india.ts +290 -0
  96. package/node_modules/tardis-dev/src/mappers/index.ts +2 -0
  97. package/node_modules/tardis-dev/src/realtimefeeds/delta_india.ts +131 -0
  98. package/node_modules/tardis-dev/src/realtimefeeds/dydx_v4.ts +1 -0
  99. package/node_modules/tardis-dev/src/realtimefeeds/index.ts +2 -0
  100. package/node_modules/tardis-dev/src/realtimefeeds/realtimefeed.ts +2 -1
  101. package/node_modules/tardis-dev/src/worker.ts +1 -2
  102. package/node_modules/tardis-dev/src/zstd.ts +4 -135
  103. package/package.json +2 -2
  104. package/src/ws/subscriptionsmappers.ts +6 -0
@@ -1,122 +1,8 @@
1
- import { once } from 'node:events';
2
- import { open } from 'node:fs/promises';
3
- import { Readable } from 'node:stream';
1
+ import { createReadStream } from 'node:fs';
4
2
  import { createZstdDecompress } from 'node:zlib';
5
- const ZSTD_FRAME_MAGIC = 0xfd2fb528;
6
- const DICTIONARY_ID_SIZES = [0, 1, 2, 4];
7
3
  export function createZstdDecompressionStream(path, chunkSize) {
8
- return Readable.from(decompressZstdFrames(path, chunkSize), { objectMode: false });
9
- }
10
- async function* decompressZstdFrames(path, chunkSize) {
11
- const file = await open(path, 'r');
12
- try {
13
- const fileSize = (await file.stat()).size;
14
- const frames = await findZstdFrames(file, path, fileSize);
15
- for (const frame of frames) {
16
- const source = Readable.from(readFileRange(file, frame.offset, frame.length, chunkSize), { objectMode: false });
17
- const decompressor = createZstdDecompress({ chunkSize });
18
- source.on('error', (error) => decompressor.destroy(error));
19
- source.pipe(decompressor);
20
- try {
21
- for await (const chunk of decompressor) {
22
- yield chunk;
23
- }
24
- if (decompressor.bytesWritten !== frame.length) {
25
- throw new Error(`Zstd decoder consumed ${decompressor.bytesWritten} of ${frame.length} bytes at offset ${frame.offset} in ${path}`);
26
- }
27
- }
28
- finally {
29
- source.unpipe(decompressor);
30
- source.destroy();
31
- decompressor.destroy();
32
- if (!source.closed) {
33
- await once(source, 'close');
34
- }
35
- }
36
- }
37
- }
38
- finally {
39
- await file.close();
40
- }
41
- }
42
- async function findZstdFrames(file, path, fileSize) {
43
- if (fileSize === 0) {
44
- throw new Error(`Empty zstd file: ${path}`);
45
- }
46
- const bytes = Buffer.allocUnsafe(4);
47
- const frames = [];
48
- let offset = 0;
49
- while (offset < fileSize) {
50
- const frameStart = offset;
51
- await readExactly(file, bytes, 4, offset, fileSize, path);
52
- if (bytes.readUInt32LE(0) !== ZSTD_FRAME_MAGIC) {
53
- throw new Error(`Invalid zstd frame magic at offset ${offset} in ${path}`);
54
- }
55
- offset += 4;
56
- await readExactly(file, bytes, 1, offset, fileSize, path);
57
- const descriptor = bytes[0];
58
- if ((descriptor & 0x08) !== 0) {
59
- throw new Error(`Unsupported reserved zstd frame descriptor bit at offset ${offset} in ${path}`);
60
- }
61
- offset++;
62
- const singleSegment = (descriptor & 0x20) !== 0;
63
- const contentChecksum = (descriptor & 0x04) !== 0;
64
- const dictionaryIdSize = DICTIONARY_ID_SIZES[descriptor & 0x03];
65
- const contentSizeFlag = descriptor >>> 6;
66
- const contentSizeSize = contentSizeFlag === 0 ? (singleSegment ? 1 : 0) : 1 << contentSizeFlag;
67
- const windowDescriptorSize = singleSegment ? 0 : 1;
68
- offset += windowDescriptorSize + dictionaryIdSize + contentSizeSize;
69
- ensureAvailable(offset, 0, fileSize, path);
70
- let lastBlock = false;
71
- while (!lastBlock) {
72
- await readExactly(file, bytes, 3, offset, fileSize, path);
73
- const blockHeader = bytes.readUIntLE(0, 3);
74
- offset += 3;
75
- lastBlock = (blockHeader & 1) !== 0;
76
- const blockType = (blockHeader >>> 1) & 3;
77
- if (blockType === 3) {
78
- throw new Error(`Invalid reserved zstd block type at offset ${offset - 3} in ${path}`);
79
- }
80
- const blockSize = blockHeader >>> 3;
81
- // RLE blocks store one repeated byte; other block types store blockSize bytes.
82
- offset += blockType === 1 ? 1 : blockSize;
83
- ensureAvailable(offset, 0, fileSize, path);
84
- }
85
- if (contentChecksum) {
86
- offset += 4;
87
- ensureAvailable(offset, 0, fileSize, path);
88
- }
89
- frames.push({ offset: frameStart, length: offset - frameStart });
90
- }
91
- return frames;
92
- }
93
- async function* readFileRange(file, position, length, chunkSize) {
94
- let bytesRemaining = length;
95
- while (bytesRemaining > 0) {
96
- const buffer = Buffer.allocUnsafe(Math.min(bytesRemaining, chunkSize));
97
- const { bytesRead } = await file.read(buffer, 0, buffer.length, position);
98
- if (bytesRead === 0) {
99
- throw new Error(`Expected ${length} compressed bytes, read ${length - bytesRemaining}`);
100
- }
101
- yield bytesRead === buffer.length ? buffer : buffer.subarray(0, bytesRead);
102
- position += bytesRead;
103
- bytesRemaining -= bytesRead;
104
- }
105
- }
106
- async function readExactly(file, buffer, length, position, fileSize, path) {
107
- ensureAvailable(position, length, fileSize, path);
108
- let bytesRead = 0;
109
- while (bytesRead < length) {
110
- const result = await file.read(buffer, bytesRead, length - bytesRead, position + bytesRead);
111
- if (result.bytesRead === 0) {
112
- throw new Error(`Truncated zstd frame at offset ${position + bytesRead} in ${path}`);
113
- }
114
- bytesRead += result.bytesRead;
115
- }
116
- }
117
- function ensureAvailable(position, length, fileSize, path) {
118
- if (position < 0 || length < 0 || position > fileSize - length) {
119
- throw new Error(`Truncated zstd frame at offset ${position} in ${path}`);
120
- }
4
+ // Node supports rejectGarbageAfterEnd for Zstd; @types/node does not declare it yet.
5
+ const decoderOptions = { chunkSize, rejectGarbageAfterEnd: true };
6
+ return createReadStream(path, { highWaterMark: chunkSize }).compose(createZstdDecompress(decoderOptions));
121
7
  }
122
8
  //# sourceMappingURL=zstd.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"zstd.js","sourceRoot":"","sources":["../src/zstd.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAClC,OAAO,EAAE,IAAI,EAAmB,MAAM,kBAAkB,CAAA;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AACtC,OAAO,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAA;AAEhD,MAAM,gBAAgB,GAAG,UAAU,CAAA;AACnC,MAAM,mBAAmB,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;AAExC,MAAM,UAAU,6BAA6B,CAAC,IAAY,EAAE,SAAiB;IAC3E,OAAO,QAAQ,CAAC,IAAI,CAAC,oBAAoB,CAAC,IAAI,EAAE,SAAS,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAA;AACpF,CAAC;AAED,KAAK,SAAS,CAAC,CAAC,oBAAoB,CAAC,IAAY,EAAE,SAAiB;IAClE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;IAClC,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAA;QACzC,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAA;QAEzD,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,SAAS,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAA;YAC/G,MAAM,YAAY,GAAG,oBAAoB,CAAC,EAAE,SAAS,EAAE,CAAC,CAAA;YACxD,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,YAAY,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAA;YAC1D,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;YAEzB,IAAI,CAAC;gBACH,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,YAAY,EAAE,CAAC;oBACvC,MAAM,KAAK,CAAA;gBACb,CAAC;gBAED,IAAI,YAAY,CAAC,YAAY,KAAK,KAAK,CAAC,MAAM,EAAE,CAAC;oBAC/C,MAAM,IAAI,KAAK,CACb,yBAAyB,YAAY,CAAC,YAAY,OAAO,KAAK,CAAC,MAAM,oBAAoB,KAAK,CAAC,MAAM,OAAO,IAAI,EAAE,CACnH,CAAA;gBACH,CAAC;YACH,CAAC;oBAAS,CAAC;gBACT,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAA;gBAC3B,MAAM,CAAC,OAAO,EAAE,CAAA;gBAChB,YAAY,CAAC,OAAO,EAAE,CAAA;gBACtB,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;oBACnB,MAAM,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;gBAC7B,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;YAAS,CAAC;QACT,MAAM,IAAI,CAAC,KAAK,EAAE,CAAA;IACpB,CAAC;AACH,CAAC;AAED,KAAK,UAAU,cAAc,CAAC,IAAgB,EAAE,IAAY,EAAE,QAAgB;IAC5E,IAAI,QAAQ,KAAK,CAAC,EAAE,CAAC;QACnB,MAAM,IAAI,KAAK,CAAC,oBAAoB,IAAI,EAAE,CAAC,CAAA;IAC7C,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,CAAA;IACnC,MAAM,MAAM,GAAyC,EAAE,CAAA;IACvD,IAAI,MAAM,GAAG,CAAC,CAAA;IAEd,OAAO,MAAM,GAAG,QAAQ,EAAE,CAAC;QACzB,MAAM,UAAU,GAAG,MAAM,CAAA;QACzB,MAAM,WAAW,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAA;QACzD,IAAI,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC,KAAK,gBAAgB,EAAE,CAAC;YAC/C,MAAM,IAAI,KAAK,CAAC,sCAAsC,MAAM,OAAO,IAAI,EAAE,CAAC,CAAA;QAC5E,CAAC;QACD,MAAM,IAAI,CAAC,CAAA;QAEX,MAAM,WAAW,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAA;QACzD,MAAM,UAAU,GAAG,KAAK,CAAC,CAAC,CAAC,CAAA;QAC3B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CAAC,4DAA4D,MAAM,OAAO,IAAI,EAAE,CAAC,CAAA;QAClG,CAAC;QACD,MAAM,EAAE,CAAA;QAER,MAAM,aAAa,GAAG,CAAC,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,CAAA;QAC/C,MAAM,eAAe,GAAG,CAAC,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,CAAA;QACjD,MAAM,gBAAgB,GAAG,mBAAmB,CAAC,UAAU,GAAG,IAAI,CAAC,CAAA;QAC/D,MAAM,eAAe,GAAG,UAAU,KAAK,CAAC,CAAA;QACxC,MAAM,eAAe,GAAG,eAAe,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,eAAe,CAAA;QAC9F,MAAM,oBAAoB,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QAClD,MAAM,IAAI,oBAAoB,GAAG,gBAAgB,GAAG,eAAe,CAAA;QACnE,eAAe,CAAC,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAA;QAE1C,IAAI,SAAS,GAAG,KAAK,CAAA;QACrB,OAAO,CAAC,SAAS,EAAE,CAAC;YAClB,MAAM,WAAW,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAA;YACzD,MAAM,WAAW,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;YAC1C,MAAM,IAAI,CAAC,CAAA;YACX,SAAS,GAAG,CAAC,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,CAAA;YACnC,MAAM,SAAS,GAAG,CAAC,WAAW,KAAK,CAAC,CAAC,GAAG,CAAC,CAAA;YACzC,IAAI,SAAS,KAAK,CAAC,EAAE,CAAC;gBACpB,MAAM,IAAI,KAAK,CAAC,8CAA8C,MAAM,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC,CAAA;YACxF,CAAC;YAED,MAAM,SAAS,GAAG,WAAW,KAAK,CAAC,CAAA;YACnC,+EAA+E;YAC/E,MAAM,IAAI,SAAS,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;YACzC,eAAe,CAAC,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAA;QAC5C,CAAC;QAED,IAAI,eAAe,EAAE,CAAC;YACpB,MAAM,IAAI,CAAC,CAAA;YACX,eAAe,CAAC,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAA;QAC5C,CAAC;QACD,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,EAAE,CAAC,CAAA;IAClE,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC;AAED,KAAK,SAAS,CAAC,CAAC,aAAa,CAAC,IAAgB,EAAE,QAAgB,EAAE,MAAc,EAAE,SAAiB;IACjG,IAAI,cAAc,GAAG,MAAM,CAAA;IAC3B,OAAO,cAAc,GAAG,CAAC,EAAE,CAAC;QAC1B,MAAM,MAAM,GAAG,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,cAAc,EAAE,SAAS,CAAC,CAAC,CAAA;QACtE,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;QACzE,IAAI,SAAS,KAAK,CAAC,EAAE,CAAC;YACpB,MAAM,IAAI,KAAK,CAAC,YAAY,MAAM,2BAA2B,MAAM,GAAG,cAAc,EAAE,CAAC,CAAA;QACzF,CAAC;QACD,MAAM,SAAS,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,CAAA;QAC1E,QAAQ,IAAI,SAAS,CAAA;QACrB,cAAc,IAAI,SAAS,CAAA;IAC7B,CAAC;AACH,CAAC;AAED,KAAK,UAAU,WAAW,CAAC,IAAgB,EAAE,MAAc,EAAE,MAAc,EAAE,QAAgB,EAAE,QAAgB,EAAE,IAAY;IAC3H,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAA;IACjD,IAAI,SAAS,GAAG,CAAC,CAAA;IACjB,OAAO,SAAS,GAAG,MAAM,EAAE,CAAC;QAC1B,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,GAAG,SAAS,CAAC,CAAA;QAC3F,IAAI,MAAM,CAAC,SAAS,KAAK,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,KAAK,CAAC,kCAAkC,QAAQ,GAAG,SAAS,OAAO,IAAI,EAAE,CAAC,CAAA;QACtF,CAAC;QACD,SAAS,IAAI,MAAM,CAAC,SAAS,CAAA;IAC/B,CAAC;AACH,CAAC;AAED,SAAS,eAAe,CAAC,QAAgB,EAAE,MAAc,EAAE,QAAgB,EAAE,IAAY;IACvF,IAAI,QAAQ,GAAG,CAAC,IAAI,MAAM,GAAG,CAAC,IAAI,QAAQ,GAAG,QAAQ,GAAG,MAAM,EAAE,CAAC;QAC/D,MAAM,IAAI,KAAK,CAAC,kCAAkC,QAAQ,OAAO,IAAI,EAAE,CAAC,CAAA;IAC1E,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"zstd.js","sourceRoot":"","sources":["../src/zstd.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAA;AAC1C,OAAO,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAA;AAEhD,MAAM,UAAU,6BAA6B,CAAC,IAAY,EAAE,SAAiB;IAC3E,qFAAqF;IACrF,MAAM,cAAc,GAAG,EAAE,SAAS,EAAE,qBAAqB,EAAE,IAAI,EAAE,CAAA;IACjE,OAAO,gBAAgB,CAAC,IAAI,EAAE,EAAE,aAAa,EAAE,SAAS,EAAE,CAAC,CAAC,OAAO,CAAC,oBAAoB,CAAC,cAAc,CAAC,CAAC,CAAA;AAC3G,CAAC"}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tardis-dev",
3
- "version": "18.6.2",
3
+ "version": "18.8.0",
4
4
  "engines": {
5
5
  "node": "^24.22.1 || >=26.12.1"
6
6
  },
@@ -35,6 +35,7 @@ export const EXCHANGES = [
35
35
  'bybit-spread',
36
36
  'phemex',
37
37
  'delta',
38
+ 'delta-india',
38
39
  'ftx-us',
39
40
  'binance-us',
40
41
  'gate-io-futures',
@@ -475,6 +476,18 @@ const DELTA_CHANNELS = [
475
476
  'spot_30mtwap_price'
476
477
  ] as const
477
478
 
479
+ const DELTA_INDIA_CHANNELS = [
480
+ 'trades',
481
+ 'ob_updates',
482
+ 'ob_l1',
483
+ 'ticker',
484
+ 'mark_price',
485
+ 'funding_rate',
486
+ 'spot_price',
487
+ 'product_updates',
488
+ 'system_status'
489
+ ] as const
490
+
478
491
  const GATE_IO_CHANNELS = ['trades', 'depth', 'ticker', 'book_ticker', 'order_book_update', 'obu'] as const
479
492
  const GATE_IO_FUTURES_CHANNELS = ['trades', 'order_book', 'tickers', 'book_ticker', 'obu', 'public_liquidates'] as const
480
493
  const MEXC_FUTURES_CHANNELS = [
@@ -654,6 +667,7 @@ export const EXCHANGE_CHANNELS_INFO = {
654
667
  coinflex: COINFLEX_CHANNELS,
655
668
  phemex: PHEMEX_CHANNELS,
656
669
  delta: DELTA_CHANNELS,
670
+ 'delta-india': DELTA_INDIA_CHANNELS,
657
671
  'gate-io': GATE_IO_CHANNELS,
658
672
  'gate-io-futures': GATE_IO_FUTURES_CHANNELS,
659
673
  'mexc-futures': MEXC_FUTURES_CHANNELS,
@@ -2,11 +2,11 @@ import { existsSync } from 'node:fs'
2
2
  import { debug } from './debug.ts'
3
3
  import { DatasetType } from './exchangedetails.ts'
4
4
  import { forEachConcurrent } from './foreachconcurrent.ts'
5
- import { addDays, doubleDigit, download, parseAsUTCDate, sequence } from './handy.ts'
5
+ import { addDays, doubleDigit, downloadDatasetFile, parseAsUTCDate, RequestSlots, sequence } from './handy.ts'
6
6
  import { getOptions } from './options.ts'
7
7
  import { Exchange } from './types.ts'
8
8
 
9
- const CONCURRENCY_LIMIT = 20
9
+ const DEFAULT_CONCURRENCY = 20
10
10
  const MILLISECONDS_IN_SINGLE_DAY = 24 * 60 * 60 * 1000
11
11
  const DEFAULT_DOWNLOAD_DIR = './datasets'
12
12
  const DATASET_DOWNLOAD_ATTEMPT_TIMEOUT_MS = 120_000
@@ -19,6 +19,14 @@ export async function downloadDatasets(downloadDatasetsOptions: DownloadDatasets
19
19
  const format = downloadDatasetsOptions.format !== undefined ? downloadDatasetsOptions.format : 'csv'
20
20
  const getFilename = downloadDatasetsOptions.getFilename !== undefined ? downloadDatasetsOptions.getFilename : getFilenameDefault
21
21
  const skipIfExists = downloadDatasetsOptions.skipIfExists === undefined ? true : downloadDatasetsOptions.skipIfExists
22
+ const verifyMd5 = downloadDatasetsOptions.verifyMd5 === true
23
+ const concurrency = downloadDatasetsOptions.concurrency !== undefined ? downloadDatasetsOptions.concurrency : DEFAULT_CONCURRENCY
24
+
25
+ if (concurrency < 1) {
26
+ throw new Error('Invalid "concurrency" argument. Please provide value greater than 0.')
27
+ }
28
+
29
+ const requestSlots = new RequestSlots(concurrency)
22
30
 
23
31
  // in case someone provided 'api/exchange' symbol, transform it to symbol that is accepted by datasets API
24
32
  const datasetsSymbols = symbols.map((s) => s.replace(/\/|:/g, '-').toUpperCase())
@@ -42,9 +50,11 @@ export async function downloadDatasets(downloadDatasetsOptions: DownloadDatasets
42
50
  dataType,
43
51
  format,
44
52
  getFilename,
53
+ verifyMd5,
45
54
  date: addDays(startDate, daysCountToFetch - 1)
46
55
  }),
47
- skipIfExists
56
+ skipIfExists,
57
+ requestSlots
48
58
  )
49
59
  }
50
60
 
@@ -60,15 +70,17 @@ export async function downloadDatasets(downloadDatasetsOptions: DownloadDatasets
60
70
  dataType,
61
71
  format,
62
72
  getFilename,
73
+ verifyMd5,
63
74
  date: startDate
64
75
  }),
65
- skipIfExists
76
+ skipIfExists,
77
+ requestSlots
66
78
  )
67
79
 
68
- // download the rest concurrently up to the CONCURRENCY_LIMIT
80
+ // download the rest concurrently up to the concurrency limit
69
81
  await forEachConcurrent(
70
82
  sequence(daysCountToFetch - 1, 1), // this will produce Iterable sequence from 1 to daysCountToFetch - 1 (as we already downloaded data for the first and last day)
71
- CONCURRENCY_LIMIT,
83
+ concurrency,
72
84
  (offset) =>
73
85
  downloadDataSet(
74
86
  getDownloadOptions({
@@ -81,9 +93,11 @@ export async function downloadDatasets(downloadDatasetsOptions: DownloadDatasets
81
93
  dataType,
82
94
  format,
83
95
  getFilename,
96
+ verifyMd5,
84
97
  date: addDays(startDate, offset)
85
98
  }),
86
- skipIfExists
99
+ skipIfExists,
100
+ requestSlots
87
101
  )
88
102
  )
89
103
  const elapsedSeconds = (new Date().valueOf() - startTimestamp) / 1000
@@ -93,12 +107,12 @@ export async function downloadDatasets(downloadDatasetsOptions: DownloadDatasets
93
107
  }
94
108
  }
95
109
 
96
- async function downloadDataSet(downloadOptions: DownloadOptions, skipIfExists: boolean) {
110
+ async function downloadDataSet(downloadOptions: DownloadOptions, skipIfExists: boolean, requestSlots: RequestSlots) {
97
111
  if (skipIfExists && existsSync(downloadOptions.downloadPath)) {
98
112
  debug('dataset %s already exists, skipping download', downloadOptions.downloadPath)
99
113
  return
100
114
  } else {
101
- return await download(downloadOptions)
115
+ return await downloadDatasetFile({ ...downloadOptions, requestSlots })
102
116
  }
103
117
  }
104
118
 
@@ -112,7 +126,8 @@ function getDownloadOptions({
112
126
  symbol,
113
127
  format,
114
128
  downloadDir,
115
- getFilename
129
+ getFilename,
130
+ verifyMd5
116
131
  }: {
117
132
  exchange: Exchange
118
133
  dataType: DatasetType
@@ -124,6 +139,7 @@ function getDownloadOptions({
124
139
  userAgent: string
125
140
  downloadDir: string
126
141
  getFilename: (options: GetFilenameOptions) => string
142
+ verifyMd5: boolean
127
143
  }): DownloadOptions {
128
144
  const year = date.getUTCFullYear()
129
145
  const month = doubleDigit(date.getUTCMonth() + 1)
@@ -145,11 +161,12 @@ function getDownloadOptions({
145
161
  downloadPath,
146
162
  userAgent,
147
163
  apiKey,
148
- attemptTimeoutMS: DATASET_DOWNLOAD_ATTEMPT_TIMEOUT_MS
164
+ attemptTimeoutMS: DATASET_DOWNLOAD_ATTEMPT_TIMEOUT_MS,
165
+ verifyMd5
149
166
  }
150
167
  }
151
168
 
152
- type DownloadOptions = Parameters<typeof download>[0]
169
+ type DownloadOptions = Omit<Parameters<typeof downloadDatasetFile>[0], 'requestSlots'>
153
170
 
154
171
  export function sanitizeForFilename(s: string) {
155
172
  return s.replace(/[:\\/?*<>|"]/g, '-')
@@ -201,4 +218,6 @@ type DownloadDatasetsOptions = {
201
218
  downloadDir?: string
202
219
  getFilename?: (options: GetFilenameOptions) => string
203
220
  skipIfExists?: boolean
221
+ verifyMd5?: boolean
222
+ concurrency?: number
204
223
  }
@@ -1,11 +1,12 @@
1
1
  import crypto, { createHash } from 'crypto'
2
- import { createWriteStream, mkdirSync, rmSync } from 'node:fs'
2
+ import { createReadStream, createWriteStream, mkdirSync, rmSync } from 'node:fs'
3
3
  import { rename } from 'node:fs/promises'
4
4
  import { Agent as HttpAgent, request as httpRequest, type IncomingMessage } from 'node:http'
5
5
  import { Agent as HttpsAgent, request as httpsRequest, type RequestOptions } from 'node:https'
6
6
  import { pipeline } from 'node:stream/promises'
7
7
  import path from 'path'
8
8
  import { debug } from './debug.ts'
9
+ import { forEachConcurrent } from './foreachconcurrent.ts'
9
10
  import { Mapper } from './mappers/index.ts'
10
11
  import { Disconnect, Exchange, Filter, FilterForExchange } from './types.ts'
11
12
 
@@ -500,7 +501,7 @@ async function openDownloadResponse(url: string, options: DownloadRequestOptions
500
501
  const statusCode = response.statusCode ?? 0
501
502
  const location = response.headers.location
502
503
  if (location === undefined || statusCode < 300 || statusCode >= 400) {
503
- return response
504
+ return { response }
504
505
  }
505
506
 
506
507
  response.destroy()
@@ -512,10 +513,13 @@ async function openDownloadResponse(url: string, options: DownloadRequestOptions
512
513
  delete headers.authorization
513
514
  }
514
515
 
515
- return sendHttpRequest(redirectUrl, {
516
- ...options,
517
- headers
518
- })
516
+ return {
517
+ response: await sendHttpRequest(redirectUrl, {
518
+ ...options,
519
+ headers
520
+ }),
521
+ redirectUrl: redirectUrl.toString()
522
+ }
519
523
  }
520
524
 
521
525
  async function readResponseText(response: IncomingMessage) {
@@ -619,6 +623,12 @@ export async function download({
619
623
  }
620
624
  }
621
625
 
626
+ return withDownloadRetries(url, downloadPath, (attemptUrl) =>
627
+ _downloadFile(httpRequestOptions, attemptUrl, downloadPath, appendContentEncodingExtension)
628
+ )
629
+ }
630
+
631
+ async function withDownloadRetries<T>(url: string, downloadPath: string, attempt: (attemptUrl: string) => Promise<T>) {
622
632
  const MAX_ATTEMPTS = 30
623
633
  const MAX_RETRY_AFTER_MS = 300 * ONE_SEC_IN_MS
624
634
  let attempts = 0
@@ -628,11 +638,7 @@ export async function download({
628
638
  attempts++
629
639
  try {
630
640
  const addRetryAttempt = attempts - 1 > 0 && url.endsWith('gz')
631
- if (addRetryAttempt) {
632
- return await _downloadFile(httpRequestOptions, `${url}?retryAttempt=${attempts - 1}`, downloadPath, appendContentEncodingExtension)
633
- } else {
634
- return await _downloadFile(httpRequestOptions, url, downloadPath, appendContentEncodingExtension)
635
- }
641
+ return await attempt(addRetryAttempt ? `${url}?retryAttempt=${attempts - 1}` : url)
636
642
  } catch (error) {
637
643
  const unsupportedDataFeedEncoding = error instanceof Error && error.message.startsWith('Unsupported data feed content encoding')
638
644
  const nonRetryableHttpError =
@@ -640,13 +646,20 @@ export async function download({
640
646
  ((error.status === 400 && error.message.includes('ISO 8601 format') === false) ||
641
647
  error.status === 401 ||
642
648
  error.status === 403 ||
643
- error.status === 404)
649
+ error.status === 404 ||
650
+ error.status === 416)
644
651
  const tooManyRequests = error instanceof HttpError && error.status === 429
645
652
  const retryAfterMS = tooManyRequests ? error.retryAfterMS : undefined
646
653
  const retryAfterTooLong = retryAfterMS !== undefined && retryAfterMS > MAX_RETRY_AFTER_MS
647
654
  const internalServiceError = error instanceof HttpError && error.status === 500
648
655
  // do not retry when we've got bad or unauthorized request, too long Retry-After or enough attempts
649
- if (unsupportedDataFeedEncoding || nonRetryableHttpError || retryAfterTooLong || attempts === MAX_ATTEMPTS) {
656
+ if (
657
+ unsupportedDataFeedEncoding ||
658
+ error instanceof DatasetMismatchError ||
659
+ nonRetryableHttpError ||
660
+ retryAfterTooLong ||
661
+ attempts === MAX_ATTEMPTS
662
+ ) {
650
663
  throw error
651
664
  }
652
665
 
@@ -703,7 +716,7 @@ async function _downloadFile(
703
716
 
704
717
  try {
705
718
  // based on https://github.com/nodejs/node/issues/28172 - only reliable way to consume response stream and avoiding all the 'gotchas'
706
- const response = await openDownloadResponse(url, requestOptions)
719
+ const { response } = await openDownloadResponse(url, requestOptions)
707
720
  const { statusCode } = response
708
721
  const responseHeaders = parseNodeResponseHeaders(response.headers)
709
722
  if (statusCode !== 200) {
@@ -741,6 +754,240 @@ async function _downloadFile(
741
754
  }
742
755
  }
743
756
 
757
+ const DATASET_RANGE_SIZE = 64 * 1024 * 1024
758
+ // A dataset is rarely exported again while it downloads, so repeated mismatches mean another restart won't help.
759
+ const MAX_DATASET_RESTARTS = 3
760
+
761
+ // The downloaded ranges don't make up one dataset file, because it was exported again during the download or doesn't
762
+ // match its announced MD5, so the download starts over.
763
+ class DatasetMismatchError extends Error {}
764
+
765
+ // One pool for all files and ranges of a download, so splitting files into ranges does not add connections.
766
+ export class RequestSlots {
767
+ private active = 0
768
+ private readonly waiting: (() => void)[] = []
769
+
770
+ constructor(readonly limit: number) {}
771
+
772
+ async run<T>(request: () => Promise<T>) {
773
+ if (this.active < this.limit) {
774
+ this.active++
775
+ } else {
776
+ await new Promise<void>((resolve) => this.waiting.push(resolve))
777
+ }
778
+
779
+ try {
780
+ return await request()
781
+ } finally {
782
+ const next = this.waiting.shift()
783
+ if (next === undefined) {
784
+ this.active--
785
+ } else {
786
+ next()
787
+ }
788
+ }
789
+ }
790
+ }
791
+
792
+ // Downloads byte ranges in parallel into one temp file, and a failed range retries alone.
793
+ export async function downloadDatasetFile({
794
+ apiKey,
795
+ downloadPath,
796
+ url,
797
+ userAgent,
798
+ attemptTimeoutMS,
799
+ verifyMd5,
800
+ requestSlots
801
+ }: {
802
+ url: string
803
+ downloadPath: string
804
+ userAgent: string
805
+ apiKey: string
806
+ attemptTimeoutMS: number
807
+ verifyMd5: boolean
808
+ requestSlots: RequestSlots
809
+ }) {
810
+ // The API key goes only to the API, never to the storage the API redirects to.
811
+ const storageRequestOptions = {
812
+ timeout: attemptTimeoutMS,
813
+ headers: {
814
+ 'Accept-Encoding': 'gzip',
815
+ 'User-Agent': userAgent
816
+ }
817
+ }
818
+ const apiRequestOptions = {
819
+ ...storageRequestOptions,
820
+ headers: { ...storageRequestOptions.headers, Authorization: apiKey ? `Bearer ${apiKey}` : '' }
821
+ }
822
+
823
+ mkdirSync(path.dirname(downloadPath), { recursive: true })
824
+ const tmpFilePath = `${downloadPath}${crypto.randomBytes(8).toString('hex')}.unconfirmed`
825
+ const cleanup = () => {
826
+ try {
827
+ rmSync(tmpFilePath, { force: true })
828
+ } catch {}
829
+ }
830
+ tmpFileCleanups.set(tmpFilePath, cleanup)
831
+
832
+ try {
833
+ for (let restarts = 0; ; restarts++) {
834
+ try {
835
+ const announcedMd5 = await downloadDatasetRanges(url, apiRequestOptions, storageRequestOptions, tmpFilePath, requestSlots)
836
+ if (verifyMd5) {
837
+ await checkDatasetMd5(tmpFilePath, announcedMd5, url)
838
+ }
839
+ break
840
+ } catch (error) {
841
+ if (!(error instanceof DatasetMismatchError) || restarts === MAX_DATASET_RESTARTS) {
842
+ throw error
843
+ }
844
+ }
845
+ }
846
+
847
+ await rename(tmpFilePath, downloadPath)
848
+ } finally {
849
+ tmpFileCleanups.delete(tmpFilePath)
850
+ cleanup()
851
+ }
852
+ }
853
+
854
+ async function downloadDatasetRanges(
855
+ url: string,
856
+ apiRequestOptions: DownloadRequestOptions,
857
+ storageRequestOptions: DownloadRequestOptions,
858
+ tmpFilePath: string,
859
+ requestSlots: RequestSlots
860
+ ) {
861
+ const first = await withDownloadRetries(url, tmpFilePath, (attemptUrl) =>
862
+ requestSlots.run(() => downloadDatasetRange(attemptUrl, apiRequestOptions, tmpFilePath, 0))
863
+ )
864
+ const datasetSize = first.datasetSize
865
+ if (datasetSize === undefined || datasetSize <= DATASET_RANGE_SIZE) {
866
+ return first.md5
867
+ }
868
+
869
+ // A low-cost account gets a storage redirect that stays valid for two hours, and every redirect counts the whole
870
+ // dataset toward its transfer limit, so the remaining ranges reuse it and only an expired one is renewed.
871
+ let storageUrl = first.redirectUrl
872
+ let renewal: ReturnType<typeof downloadDatasetRange> | undefined
873
+
874
+ const downloadRemainingRange = async (attemptUrl: string, start: number) => {
875
+ // Read the URL only once the range has a slot, so a range that waited long uses the newest URL.
876
+ const rangeUrl = storageUrl
877
+ if (rangeUrl === undefined) {
878
+ return downloadDatasetRange(attemptUrl, apiRequestOptions, tmpFilePath, start, datasetSize, first.md5)
879
+ }
880
+
881
+ try {
882
+ return await downloadDatasetRange(rangeUrl, storageRequestOptions, tmpFilePath, start, datasetSize, first.md5)
883
+ } catch (error) {
884
+ if (!(error instanceof HttpError && error.status === 403)) {
885
+ throw error
886
+ }
887
+ // The first range to find the URL expired downloads through the API again, like the first range, and the
888
+ // redirect it gets replaces the URL. The others wait for it and retry with the new URL, or fail this attempt
889
+ // with its error.
890
+ if (renewal === undefined && storageUrl === rangeUrl) {
891
+ renewal = downloadDatasetRange(attemptUrl, apiRequestOptions, tmpFilePath, start, datasetSize, first.md5)
892
+ .then((renewedRange) => {
893
+ storageUrl = renewedRange.redirectUrl
894
+ return renewedRange
895
+ })
896
+ .finally(() => {
897
+ renewal = undefined
898
+ })
899
+ return await renewal
900
+ }
901
+ await renewal
902
+ throw new Error('Dataset redirect expired')
903
+ }
904
+ }
905
+
906
+ const rangeStarts = [...sequence(Math.ceil(datasetSize / DATASET_RANGE_SIZE), 1)].map((index) => index * DATASET_RANGE_SIZE)
907
+ // Unlike Promise.all, forEachConcurrent rejects only after the ranges already started have finished, retries included,
908
+ // so a restart or cleanup never overlaps a range still writing.
909
+ await forEachConcurrent(rangeStarts, requestSlots.limit, (start) =>
910
+ withDownloadRetries(url, tmpFilePath, (attemptUrl) => requestSlots.run(() => downloadRemainingRange(attemptUrl, start)))
911
+ )
912
+
913
+ return first.md5
914
+ }
915
+
916
+ async function downloadDatasetRange(
917
+ url: string,
918
+ requestOptions: DownloadRequestOptions,
919
+ tmpFilePath: string,
920
+ start: number,
921
+ expectedDatasetSize?: number,
922
+ expectedMd5?: string
923
+ ) {
924
+ const end = start + DATASET_RANGE_SIZE - 1
925
+ const { response, redirectUrl } = await openDownloadResponse(url, {
926
+ ...requestOptions,
927
+ headers: { ...requestOptions.headers, Range: `bytes=${start}-${end}` }
928
+ })
929
+ const statusCode = response.statusCode ?? 0
930
+ // A later range that comes back whole or out of bounds belongs to a dataset exported again with another size.
931
+ if (start > 0 && (statusCode === 200 || statusCode === 416)) {
932
+ response.destroy()
933
+ throw new DatasetMismatchError(`Dataset changed during download: bytes=${start}-${end} returned ${statusCode}`)
934
+ }
935
+ // 200 means the storage ignored the range and sends the whole dataset.
936
+ if (statusCode !== 206 && statusCode !== 200) {
937
+ throw new HttpError(statusCode, await readResponseText(response), url, parseNodeResponseHeaders(response.headers))
938
+ }
939
+
940
+ let datasetSize: number | undefined
941
+ if (statusCode === 206) {
942
+ const contentRange = /^bytes (\d+)-(\d+)\/(\d+)$/.exec(asSingleHeaderValue(response.headers['content-range']) ?? '')
943
+ datasetSize = Number(contentRange?.[3])
944
+ if (contentRange === null || Number(contentRange[1]) !== start || Number(contentRange[2]) !== Math.min(end, datasetSize - 1)) {
945
+ response.destroy()
946
+ throw new Error(`Unexpected Content-Range for bytes=${start}-${end}: ${response.headers['content-range']}`)
947
+ }
948
+ if (expectedDatasetSize !== undefined && datasetSize !== expectedDatasetSize) {
949
+ response.destroy()
950
+ throw new DatasetMismatchError(`Dataset size changed from ${expectedDatasetSize} to ${datasetSize} bytes during download`)
951
+ }
952
+ }
953
+ // A dataset exported again with the same size announces another MD5.
954
+ const md5 = getAnnouncedMd5(parseNodeResponseHeaders(response.headers))
955
+ if (expectedMd5 !== undefined && md5 !== undefined && md5 !== expectedMd5) {
956
+ response.destroy()
957
+ throw new DatasetMismatchError(`Dataset MD5 changed from ${expectedMd5} to ${md5} during download`)
958
+ }
959
+
960
+ await pipeline(response, createWriteStream(tmpFilePath, { flags: start === 0 ? 'w' : 'r+', start }))
961
+ if (!response.complete) {
962
+ throw new Error('The connection was terminated while the message was still being sent')
963
+ }
964
+
965
+ return { datasetSize, redirectUrl, md5 }
966
+ }
967
+
968
+ // Ranges are written in parallel, so the MD5 is computed from the finished file.
969
+ async function checkDatasetMd5(tmpFilePath: string, announcedMd5: string | undefined, url: string) {
970
+ if (announcedMd5 === undefined) {
971
+ return
972
+ }
973
+
974
+ const md5 = createHash('md5')
975
+ for await (const chunk of createReadStream(tmpFilePath)) {
976
+ md5.update(chunk)
977
+ }
978
+ const actualMd5 = md5.digest('hex')
979
+ if (actualMd5 !== announcedMd5) {
980
+ throw new DatasetMismatchError(`MD5 mismatch for ${url}: expected ${announcedMd5}, got ${actualMd5}`)
981
+ }
982
+ }
983
+
984
+ // The API sends x-md5, while a redirect to the storage returns the storage's x-amz-meta-md5 and ETag.
985
+ // A multipart ETag ("<hash>-<parts>") is not the file MD5, so such a file is saved without a check.
986
+ function getAnnouncedMd5(headers: Record<string, string>) {
987
+ const md5 = (headers['x-md5'] ?? headers['x-amz-meta-md5'] ?? headers['etag'] ?? '').replace(/"/g, '').toLowerCase()
988
+ return /^[0-9a-f]{32}$/.test(md5) ? md5 : undefined
989
+ }
990
+
744
991
  function asSingleHeaderValue(headerValue: string | string[] | undefined) {
745
992
  if (Array.isArray(headerValue)) {
746
993
  return headerValue[0]
@@ -162,7 +162,7 @@ export interface InstrumentInfo {
162
162
  contractMultiplier?: number
163
163
  /** only for quanto instruments */
164
164
  quanto?: boolean
165
- /** only for quanto instruments as settlement currency is different base/quote currency */
165
+ /** currency in which the derivative contract settles, when provided */
166
166
  settlementCurrency?: string
167
167
  /** strike price, only for options */
168
168
  strikePrice?: number