@tdengine/websocket 3.2.3 → 3.3.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 (224) hide show
  1. package/lib/package.json +1 -1
  2. package/lib/src/client/wsClient.d.ts +23 -7
  3. package/lib/src/client/wsClient.d.ts.map +1 -1
  4. package/lib/src/client/wsClient.js +154 -139
  5. package/lib/src/client/wsConnector.d.ts +55 -9
  6. package/lib/src/client/wsConnector.d.ts.map +1 -1
  7. package/lib/src/client/wsConnector.js +519 -100
  8. package/lib/src/client/wsConnectorPool.d.ts +5 -1
  9. package/lib/src/client/wsConnectorPool.d.ts.map +1 -1
  10. package/lib/src/client/wsConnectorPool.js +61 -43
  11. package/lib/src/client/wsEventCallback.d.ts +3 -0
  12. package/lib/src/client/wsEventCallback.d.ts.map +1 -1
  13. package/lib/src/client/wsEventCallback.js +67 -8
  14. package/lib/src/common/addressConnectionTracker.d.ts +11 -0
  15. package/lib/src/common/addressConnectionTracker.d.ts.map +1 -0
  16. package/lib/src/common/addressConnectionTracker.js +53 -0
  17. package/lib/src/common/dsn.d.ts +14 -2
  18. package/lib/src/common/dsn.d.ts.map +1 -1
  19. package/lib/src/common/dsn.js +91 -22
  20. package/lib/src/common/taosResult.d.ts.map +1 -1
  21. package/lib/src/common/taosResult.js +0 -5
  22. package/lib/src/common/urlParser.d.ts +32 -0
  23. package/lib/src/common/urlParser.d.ts.map +1 -0
  24. package/lib/src/common/urlParser.js +201 -0
  25. package/lib/src/common/utils.d.ts +2 -1
  26. package/lib/src/common/utils.d.ts.map +1 -1
  27. package/lib/src/common/utils.js +35 -34
  28. package/lib/src/sql/wsSql.js +2 -2
  29. package/lib/src/stmt/FieldBindParams.d.ts.map +1 -1
  30. package/lib/src/stmt/wsColumnInfo.d.ts.map +1 -1
  31. package/lib/src/stmt/wsParams1.d.ts.map +1 -1
  32. package/lib/src/stmt/wsParams1.js +26 -26
  33. package/lib/src/stmt/wsParams2.d.ts.map +1 -1
  34. package/lib/src/stmt/wsParams2.js +0 -3
  35. package/lib/src/stmt/wsParamsBase.d.ts.map +1 -1
  36. package/lib/src/stmt/wsProto.d.ts.map +1 -1
  37. package/lib/src/stmt/wsProto.js +16 -16
  38. package/lib/src/stmt/wsStmt1.d.ts.map +1 -1
  39. package/lib/src/stmt/wsStmt2.d.ts +12 -4
  40. package/lib/src/stmt/wsStmt2.d.ts.map +1 -1
  41. package/lib/src/stmt/wsStmt2.js +182 -64
  42. package/lib/src/stmt/wsTableInfo.d.ts.map +1 -1
  43. package/lib/src/tmq/config.d.ts +3 -2
  44. package/lib/src/tmq/config.d.ts.map +1 -1
  45. package/lib/src/tmq/config.js +15 -15
  46. package/lib/src/tmq/wsTmq.d.ts +4 -1
  47. package/lib/src/tmq/wsTmq.d.ts.map +1 -1
  48. package/lib/src/tmq/wsTmq.js +50 -27
  49. package/lib/test/bulkPulling/a.test.d.ts +2 -0
  50. package/lib/test/bulkPulling/a.test.d.ts.map +1 -0
  51. package/lib/test/bulkPulling/a.test.js +166 -0
  52. package/lib/test/bulkPulling/dsn.test.js +19 -0
  53. package/lib/test/bulkPulling/retryConfig.test.js +11 -11
  54. package/lib/test/bulkPulling/sql.failover.test.d.ts +2 -0
  55. package/lib/test/bulkPulling/sql.failover.test.d.ts.map +1 -0
  56. package/lib/test/bulkPulling/sql.failover.test.js +338 -0
  57. package/lib/test/bulkPulling/stmt2.failover.test.d.ts +2 -0
  58. package/lib/test/bulkPulling/stmt2.failover.test.d.ts.map +1 -0
  59. package/lib/test/bulkPulling/stmt2.failover.test.js +313 -0
  60. package/lib/test/bulkPulling/stmt2.init.failover.test.d.ts +2 -0
  61. package/lib/test/bulkPulling/stmt2.init.failover.test.d.ts.map +1 -0
  62. package/lib/test/bulkPulling/stmt2.init.failover.test.js +50 -0
  63. package/lib/test/bulkPulling/tmq.failover.test.d.ts +2 -0
  64. package/lib/test/bulkPulling/tmq.failover.test.d.ts.map +1 -0
  65. package/lib/test/bulkPulling/tmq.failover.test.js +404 -0
  66. package/lib/test/bulkPulling/urlParser.test.d.ts +2 -0
  67. package/lib/test/bulkPulling/urlParser.test.d.ts.map +1 -0
  68. package/lib/test/bulkPulling/urlParser.test.js +452 -0
  69. package/lib/test/bulkPulling/wsClient.reconnect.integration.test.js +2 -2
  70. package/lib/test/bulkPulling/wsClient.recovery.test.d.ts +2 -0
  71. package/lib/test/bulkPulling/wsClient.recovery.test.d.ts.map +1 -0
  72. package/lib/test/bulkPulling/wsClient.recovery.test.js +104 -0
  73. package/lib/test/bulkPulling/wsConfig.dsn.test.js +2 -0
  74. package/lib/test/bulkPulling/wsConnector.failover.test.js +396 -27
  75. package/lib/test/bulkPulling/wsConnectorPool.key.test.js +12 -10
  76. package/lib/test/bulkPulling/wsConnectorPool.keyAuth.test.d.ts +2 -0
  77. package/lib/test/bulkPulling/wsConnectorPool.keyAuth.test.d.ts.map +1 -0
  78. package/lib/test/bulkPulling/wsConnectorPool.keyAuth.test.js +28 -0
  79. package/lib/test/bulkPulling/wsProxy.failover.integration.test.d.ts +2 -0
  80. package/lib/test/bulkPulling/wsProxy.failover.integration.test.d.ts.map +1 -0
  81. package/lib/test/bulkPulling/wsProxy.failover.integration.test.js +120 -0
  82. package/lib/test/bulkPulling/wsProxy.failover.test.d.ts +2 -0
  83. package/lib/test/bulkPulling/wsProxy.failover.test.d.ts.map +1 -0
  84. package/lib/test/bulkPulling/wsProxy.failover.test.js +465 -0
  85. package/lib/test/client/wsClient.recovery.test.d.ts +2 -0
  86. package/lib/test/client/wsClient.recovery.test.d.ts.map +1 -0
  87. package/lib/test/client/wsClient.recovery.test.js +122 -0
  88. package/lib/test/client/wsConnectPool.test.d.ts +2 -0
  89. package/lib/test/client/wsConnectPool.test.d.ts.map +1 -0
  90. package/lib/test/client/wsConnectPool.test.js +147 -0
  91. package/lib/test/client/wsConnector.failover.test.d.ts +2 -0
  92. package/lib/test/client/wsConnector.failover.test.d.ts.map +1 -0
  93. package/lib/test/client/wsConnector.failover.test.js +681 -0
  94. package/lib/test/client/wsConnector.leastConnections.test.d.ts +2 -0
  95. package/lib/test/client/wsConnector.leastConnections.test.d.ts.map +1 -0
  96. package/lib/test/client/wsConnector.leastConnections.test.js +71 -0
  97. package/lib/test/client/wsConnectorPool.key.test.d.ts +2 -0
  98. package/lib/test/client/wsConnectorPool.key.test.d.ts.map +1 -0
  99. package/lib/test/client/wsConnectorPool.key.test.js +127 -0
  100. package/lib/test/client/wsEventCallback.test.d.ts +2 -0
  101. package/lib/test/client/wsEventCallback.test.d.ts.map +1 -0
  102. package/lib/test/client/wsEventCallback.test.js +98 -0
  103. package/lib/test/common/addressConnectionTracker.test.d.ts +2 -0
  104. package/lib/test/common/addressConnectionTracker.test.d.ts.map +1 -0
  105. package/lib/test/common/addressConnectionTracker.test.js +74 -0
  106. package/lib/test/common/dsn.test.d.ts +2 -0
  107. package/lib/test/common/dsn.test.d.ts.map +1 -0
  108. package/lib/test/common/dsn.test.js +406 -0
  109. package/lib/test/common/log.test.d.ts +2 -0
  110. package/lib/test/common/log.test.d.ts.map +1 -0
  111. package/lib/test/common/log.test.js +54 -0
  112. package/lib/test/common/utils.test.d.ts +2 -0
  113. package/lib/test/common/utils.test.d.ts.map +1 -0
  114. package/lib/test/common/utils.test.js +13 -0
  115. package/lib/test/common/wsConfig.dsn.test.d.ts +2 -0
  116. package/lib/test/common/wsConfig.dsn.test.d.ts.map +1 -0
  117. package/lib/test/common/wsConfig.dsn.test.js +39 -0
  118. package/lib/test/helpers/utils.d.ts +27 -0
  119. package/lib/test/helpers/utils.d.ts.map +1 -0
  120. package/lib/test/helpers/utils.js +341 -0
  121. package/lib/test/helpers/wsFailoverProxy.d.ts +109 -0
  122. package/lib/test/helpers/wsFailoverProxy.d.ts.map +1 -0
  123. package/lib/test/helpers/wsFailoverProxy.js +420 -0
  124. package/lib/test/helpers/wsProxy.d.ts +110 -0
  125. package/lib/test/helpers/wsProxy.d.ts.map +1 -0
  126. package/lib/test/helpers/wsProxy.js +429 -0
  127. package/lib/test/sql/core/decimal.test.d.ts +2 -0
  128. package/lib/test/sql/core/decimal.test.d.ts.map +1 -0
  129. package/lib/test/sql/core/decimal.test.js +153 -0
  130. package/lib/test/sql/core/queryTables.test.d.ts +2 -0
  131. package/lib/test/sql/core/queryTables.test.d.ts.map +1 -0
  132. package/lib/test/sql/core/queryTables.test.js +506 -0
  133. package/lib/test/sql/core/schemaless.test.d.ts +2 -0
  134. package/lib/test/sql/core/schemaless.test.d.ts.map +1 -0
  135. package/lib/test/sql/core/schemaless.test.js +102 -0
  136. package/lib/test/sql/core/sql.test.d.ts +2 -0
  137. package/lib/test/sql/core/sql.test.d.ts.map +1 -0
  138. package/lib/test/sql/core/sql.test.js +324 -0
  139. package/lib/test/sql/decimal.test.d.ts +2 -0
  140. package/lib/test/sql/decimal.test.d.ts.map +1 -0
  141. package/lib/test/sql/decimal.test.js +153 -0
  142. package/lib/test/sql/failover/sql.failover.test.d.ts +2 -0
  143. package/lib/test/sql/failover/sql.failover.test.d.ts.map +1 -0
  144. package/lib/test/sql/failover/sql.failover.test.js +341 -0
  145. package/lib/test/sql/queryTables.test.d.ts +2 -0
  146. package/lib/test/sql/queryTables.test.d.ts.map +1 -0
  147. package/lib/test/sql/queryTables.test.js +506 -0
  148. package/lib/test/sql/schemaless.test.d.ts +2 -0
  149. package/lib/test/sql/schemaless.test.d.ts.map +1 -0
  150. package/lib/test/sql/schemaless.test.js +102 -0
  151. package/lib/test/sql/sql.failover.test.d.ts +2 -0
  152. package/lib/test/sql/sql.failover.test.d.ts.map +1 -0
  153. package/lib/test/sql/sql.failover.test.js +341 -0
  154. package/lib/test/sql/sql.test.d.ts +2 -0
  155. package/lib/test/sql/sql.test.d.ts.map +1 -0
  156. package/lib/test/sql/sql.test.js +324 -0
  157. package/lib/test/stmt/failover/stmt2.failover.mock.test.d.ts +2 -0
  158. package/lib/test/stmt/failover/stmt2.failover.mock.test.d.ts.map +1 -0
  159. package/lib/test/stmt/failover/stmt2.failover.mock.test.js +341 -0
  160. package/lib/test/stmt/failover/stmt2.failover.test.d.ts +2 -0
  161. package/lib/test/stmt/failover/stmt2.failover.test.d.ts.map +1 -0
  162. package/lib/test/stmt/failover/stmt2.failover.test.js +384 -0
  163. package/lib/test/stmt/stmt1.func.test.d.ts +2 -0
  164. package/lib/test/stmt/stmt1.func.test.d.ts.map +1 -0
  165. package/lib/test/stmt/stmt1.func.test.js +418 -0
  166. package/lib/test/stmt/stmt1.type.test.d.ts +2 -0
  167. package/lib/test/stmt/stmt1.type.test.d.ts.map +1 -0
  168. package/lib/test/stmt/stmt1.type.test.js +399 -0
  169. package/lib/test/stmt/stmt2.failover.mock.test.d.ts +2 -0
  170. package/lib/test/stmt/stmt2.failover.mock.test.d.ts.map +1 -0
  171. package/lib/test/stmt/stmt2.failover.mock.test.js +341 -0
  172. package/lib/test/stmt/stmt2.failover.test.d.ts +2 -0
  173. package/lib/test/stmt/stmt2.failover.test.d.ts.map +1 -0
  174. package/lib/test/stmt/stmt2.failover.test.js +384 -0
  175. package/lib/test/stmt/stmt2.func.test.d.ts +2 -0
  176. package/lib/test/stmt/stmt2.func.test.d.ts.map +1 -0
  177. package/lib/test/stmt/stmt2.func.test.js +537 -0
  178. package/lib/test/stmt/stmt2.type.test.d.ts +2 -0
  179. package/lib/test/stmt/stmt2.type.test.d.ts.map +1 -0
  180. package/lib/test/stmt/stmt2.type.test.js +401 -0
  181. package/lib/test/stmt/v1/stmt1.func.test.d.ts +2 -0
  182. package/lib/test/stmt/v1/stmt1.func.test.d.ts.map +1 -0
  183. package/lib/test/stmt/v1/stmt1.func.test.js +418 -0
  184. package/lib/test/stmt/v1/stmt1.type.test.d.ts +2 -0
  185. package/lib/test/stmt/v1/stmt1.type.test.d.ts.map +1 -0
  186. package/lib/test/stmt/v1/stmt1.type.test.js +399 -0
  187. package/lib/test/stmt/v2/stmt2.func.test.d.ts +2 -0
  188. package/lib/test/stmt/v2/stmt2.func.test.d.ts.map +1 -0
  189. package/lib/test/stmt/v2/stmt2.func.test.js +537 -0
  190. package/lib/test/stmt/v2/stmt2.type.test.d.ts +2 -0
  191. package/lib/test/stmt/v2/stmt2.type.test.d.ts.map +1 -0
  192. package/lib/test/stmt/v2/stmt2.type.test.js +401 -0
  193. package/lib/test/tmq/cloud/cloud.tmq.test.d.ts +2 -0
  194. package/lib/test/tmq/cloud/cloud.tmq.test.d.ts.map +1 -0
  195. package/lib/test/tmq/cloud/cloud.tmq.test.js +84 -0
  196. package/lib/test/tmq/cloud/tmq.cloud.test.d.ts +2 -0
  197. package/lib/test/tmq/cloud/tmq.cloud.test.d.ts.map +1 -0
  198. package/lib/test/tmq/cloud/tmq.cloud.test.js +82 -0
  199. package/lib/test/tmq/core/tmq.config.test.d.ts +2 -0
  200. package/lib/test/tmq/core/tmq.config.test.d.ts.map +1 -0
  201. package/lib/test/tmq/core/tmq.config.test.js +83 -0
  202. package/lib/test/tmq/core/tmq.test.d.ts +2 -0
  203. package/lib/test/tmq/core/tmq.test.d.ts.map +1 -0
  204. package/lib/test/tmq/core/tmq.test.js +513 -0
  205. package/lib/test/tmq/failover/tmq.failover.test.d.ts +2 -0
  206. package/lib/test/tmq/failover/tmq.failover.test.d.ts.map +1 -0
  207. package/lib/test/tmq/failover/tmq.failover.test.js +404 -0
  208. package/lib/test/tmq/tmq.cloud.test.d.ts +2 -0
  209. package/lib/test/tmq/tmq.cloud.test.d.ts.map +1 -0
  210. package/lib/test/tmq/tmq.cloud.test.js +82 -0
  211. package/lib/test/tmq/tmq.config.test.d.ts +2 -0
  212. package/lib/test/tmq/tmq.config.test.d.ts.map +1 -0
  213. package/lib/test/tmq/tmq.config.test.js +94 -0
  214. package/lib/test/tmq/tmq.failover.test.d.ts +2 -0
  215. package/lib/test/tmq/tmq.failover.test.d.ts.map +1 -0
  216. package/lib/test/tmq/tmq.failover.test.js +404 -0
  217. package/lib/test/tmq/tmq.test.d.ts +2 -0
  218. package/lib/test/tmq/tmq.test.d.ts.map +1 -0
  219. package/lib/test/tmq/tmq.test.js +513 -0
  220. package/lib/test/unit/connectionManager.test.d.ts +2 -0
  221. package/lib/test/unit/connectionManager.test.d.ts.map +1 -0
  222. package/lib/test/unit/connectionManager.test.js +91 -0
  223. package/package.json +1 -1
  224. package/readme.md +2 -2
@@ -3,60 +3,193 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.WebSocketConnector = void 0;
6
+ exports.WebSocketConnector = exports.RetryConfig = void 0;
7
7
  const websocket_1 = require("websocket");
8
+ const addressConnectionTracker_1 = require("../common/addressConnectionTracker");
8
9
  const wsError_1 = require("../common/wsError");
9
10
  const wsEventCallback_1 = require("./wsEventCallback");
10
11
  const log_1 = __importDefault(require("../common/log"));
11
- const reqid_1 = require("../common/reqid");
12
12
  const utils_1 = require("../common/utils");
13
- class WebSocketConnector {
14
- constructor(url, timeout) {
15
- this._timeout = 5000;
16
- if (url) {
17
- this._wsURL = url;
18
- let origin = url.origin;
19
- let pathname = url.pathname;
20
- let search = url.search;
21
- if (timeout) {
22
- this._timeout = timeout;
23
- }
24
- this._wsConn = new websocket_1.w3cwebsocket(origin.concat(pathname).concat(search), undefined, undefined, undefined, undefined, {
25
- maxReceivedFrameSize: 0x60000000,
26
- maxReceivedMessageSize: 0x60000000,
27
- });
28
- this._wsConn.onerror = function (err) {
29
- log_1.default.error(`webSocket connection failed, url: ${(0, utils_1.maskUrlForLog)(new URL(this.url))}, error: ${err.message}`);
30
- };
31
- this._wsConn.onclose = this._onclose;
32
- this._wsConn.onmessage = this._onmessage;
33
- this._wsConn._binaryType = "arraybuffer";
13
+ class InflightRequestStore {
14
+ constructor() {
15
+ this.nextMsgId = 1n;
16
+ this.reqIdToMsgId = new Map();
17
+ this.msgIdToRequest = new Map();
18
+ }
19
+ insert(req) {
20
+ const msgId = this.nextMsgId;
21
+ this.nextMsgId += 1n;
22
+ this.reqIdToMsgId.set(req.reqId, msgId);
23
+ this.msgIdToRequest.set(msgId, req);
24
+ }
25
+ remove(reqId) {
26
+ const msgId = this.reqIdToMsgId.get(reqId);
27
+ if (msgId === undefined) {
28
+ return;
34
29
  }
35
- else {
30
+ this.reqIdToMsgId.delete(reqId);
31
+ this.msgIdToRequest.delete(msgId);
32
+ }
33
+ getRequests() {
34
+ return Array.from(this.msgIdToRequest.entries())
35
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
36
+ .map(([, req]) => req);
37
+ }
38
+ clear() {
39
+ this.nextMsgId = 1n;
40
+ this.reqIdToMsgId.clear();
41
+ this.msgIdToRequest.clear();
42
+ }
43
+ }
44
+ const RETRIABLE_ACTIONS = new Set(["insert", "options_connection", "poll", "subscribe"]);
45
+ // TDengine websocket binary op codes that are safe to replay after reconnect.
46
+ const BINARY_RETRIABLE_ACTIONS = new Set([4n, 5n, 6n, 10n]);
47
+ const DEFAULT_RETRIES = 5;
48
+ const DEFAULT_BACKOFF_MS = 200;
49
+ const DEFAULT_BACKOFF_MAX_MS = 2000;
50
+ const NETWORK_ERROR_CODES = new Set([
51
+ "econnreset",
52
+ "econnrefused",
53
+ "ehostunreach",
54
+ "enotfound",
55
+ "eai_again",
56
+ "epipe",
57
+ "etimedout",
58
+ "err_socket_closed",
59
+ "err_stream_write_after_end",
60
+ ]);
61
+ const NETWORK_ERROR_MESSAGE_PATTERNS = [
62
+ "not connected",
63
+ "socket",
64
+ "network",
65
+ "econnreset",
66
+ "econnrefused",
67
+ "epipe",
68
+ "etimedout",
69
+ "write after end",
70
+ "broken pipe",
71
+ "connection reset",
72
+ "connection closed",
73
+ ];
74
+ function parseNonNegativeInt(value, fallback) {
75
+ return parseIntWithValidation(value, fallback, (parsed) => parsed >= 0);
76
+ }
77
+ function parsePositiveInt(value, fallback) {
78
+ return parseIntWithValidation(value, fallback, (parsed) => parsed > 0);
79
+ }
80
+ function parseIntWithValidation(value, fallback, isValid) {
81
+ if (value === undefined || value === null || value.length === 0) {
82
+ return fallback;
83
+ }
84
+ const parsed = Number.parseInt(value, 10);
85
+ if (Number.isNaN(parsed) || !isValid(parsed)) {
86
+ return fallback;
87
+ }
88
+ return parsed;
89
+ }
90
+ class RetryConfig {
91
+ constructor(retries, retryBackoffMs, retryBackoffMaxMs) {
92
+ this.retries = retries;
93
+ this.retryBackoffMs = retryBackoffMs;
94
+ this.retryBackoffMaxMs = Math.max(retryBackoffMs, retryBackoffMaxMs);
95
+ }
96
+ getBackoffDelay(attempt) {
97
+ const safeAttempt = Math.max(0, attempt);
98
+ const rawDelay = this.retryBackoffMs * Math.pow(2, safeAttempt);
99
+ const finiteDelay = Number.isFinite(rawDelay) ? rawDelay : this.retryBackoffMaxMs;
100
+ return Math.min(finiteDelay, this.retryBackoffMaxMs);
101
+ }
102
+ static fromDsn(dsn) {
103
+ const retries = parseNonNegativeInt(dsn.params.get("retries"), DEFAULT_RETRIES);
104
+ const retryBackoffMs = parsePositiveInt(dsn.params.get("retry_backoff_ms"), DEFAULT_BACKOFF_MS);
105
+ const retryBackoffMaxMs = parsePositiveInt(dsn.params.get("retry_backoff_max_ms"), DEFAULT_BACKOFF_MAX_MS);
106
+ return new RetryConfig(retries, retryBackoffMs, retryBackoffMaxMs);
107
+ }
108
+ }
109
+ exports.RetryConfig = RetryConfig;
110
+ class WebSocketConnector {
111
+ constructor(dsn, poolKey, timeout) {
112
+ this._suppressedSockets = new WeakSet();
113
+ this._reconnectLock = null;
114
+ this._isReconnecting = false;
115
+ this._allowReconnect = true;
116
+ this._connectionReady = Promise.resolve();
117
+ this._sessionRecoveryHook = null;
118
+ this._timeout = 60000;
119
+ if (!dsn || dsn.addresses.length === 0) {
36
120
  throw new wsError_1.WebSocketQueryError(wsError_1.ErrorCode.ERR_INVALID_URL, "websocket URL must be defined");
37
121
  }
122
+ this._poolKey = poolKey;
123
+ this._dsn = dsn;
124
+ this._currentAddress = this.selectLeastConnectedAddress();
125
+ this._retryConfig = RetryConfig.fromDsn(dsn);
126
+ this._inflightStore = new InflightRequestStore();
127
+ if (timeout) {
128
+ this._timeout = timeout;
129
+ }
130
+ log_1.default.info(`Initial websocket address selected: ${this.getCurrentAddress()}`);
131
+ this.createConnection();
38
132
  }
39
- async ready() {
40
- return new Promise((resolve, reject) => {
41
- let reqId = reqid_1.ReqId.getReqID();
42
- wsEventCallback_1.WsEventCallback.instance().registerCallback({
43
- action: "websocket_connection",
44
- req_id: BigInt(reqId),
45
- timeout: this._timeout,
46
- id: BigInt(reqId),
47
- }, resolve, reject);
48
- this._wsConn.onopen = () => {
133
+ refreshRetryConfig(dsn) {
134
+ this._retryConfig = RetryConfig.fromDsn(dsn);
135
+ }
136
+ buildUrl(addr) {
137
+ const path = this._dsn.path();
138
+ const url = new URL(`${this._dsn.scheme}://${addr.host}:${addr.port}/${path}`);
139
+ const forwardedParams = ["token", "bearer_token"];
140
+ for (const key of forwardedParams) {
141
+ const value = this._dsn.params.get(key);
142
+ if (value !== undefined) {
143
+ url.searchParams.set(key, value);
144
+ }
145
+ }
146
+ return url.toString();
147
+ }
148
+ createConnection() {
149
+ const conn = new websocket_1.w3cwebsocket(this.buildUrl(this._currentAddress), undefined, undefined, undefined, undefined, {
150
+ maxReceivedFrameSize: 0x60000000,
151
+ maxReceivedMessageSize: 0x60000000,
152
+ });
153
+ conn._binaryType = "arraybuffer";
154
+ conn.onmessage = this._onmessage;
155
+ this._connectionReady = new Promise((resolve, reject) => {
156
+ let settled = false;
157
+ const settle = (handler) => {
158
+ if (settled) {
159
+ return;
160
+ }
161
+ settled = true;
162
+ clearTimeout(timeoutId);
163
+ handler();
164
+ };
165
+ const timeoutId = setTimeout(() => {
166
+ settle(() => {
167
+ reject(new wsError_1.WebSocketQueryError(wsError_1.ErrorCode.ERR_WEBSOCKET_QUERY_TIMEOUT, `websocket connection timeout with ${this._timeout} milliseconds`));
168
+ });
169
+ }, this._timeout);
170
+ conn.onopen = () => {
49
171
  log_1.default.debug("websocket connection opened");
50
- wsEventCallback_1.WsEventCallback.instance().handleEventCallback({
51
- id: BigInt(reqId),
52
- action: "websocket_connection",
53
- req_id: BigInt(reqId),
54
- }, wsEventCallback_1.OnMessageType.MESSAGE_TYPE_CONNECTION, this);
172
+ addressConnectionTracker_1.AddressConnectionTracker.instance().increment(this.getCurrentAddress());
173
+ settle(resolve);
174
+ };
175
+ conn.onerror = (err) => {
176
+ log_1.default.error(`webSocket connection failed, url: ${(0, utils_1.maskUrlForLog)(new URL(conn.url))}, error: ${err.message}`);
177
+ if (conn.readyState !== websocket_1.w3cwebsocket.OPEN) {
178
+ settle(() => reject(err));
179
+ }
180
+ void this.handleDisconnect(conn);
181
+ };
182
+ conn.onclose = (e) => {
183
+ log_1.default.info(`websocket connection closed, code: ${e.code}, reason: ${e.reason}`);
184
+ if (conn.readyState !== websocket_1.w3cwebsocket.OPEN) {
185
+ settle(() => {
186
+ reject(new wsError_1.WebSocketQueryError(wsError_1.ErrorCode.ERR_WEBSOCKET_CONNECTION_FAIL, `websocket connection closed: ${e.code} ${e.reason}`));
187
+ });
188
+ }
189
+ void this.handleDisconnect(conn, e);
55
190
  };
56
191
  });
57
- }
58
- async _onclose(e) {
59
- log_1.default.info("websocket connection closed");
192
+ this._conn = conn;
60
193
  }
61
194
  _onmessage(event) {
62
195
  let data = event.data;
@@ -74,85 +207,371 @@ class WebSocketConnector {
74
207
  throw new wsError_1.TDWebSocketClientError(wsError_1.ErrorCode.ERR_INVALID_MESSAGE_TYPE, `invalid message type ${Object.prototype.toString.call(data)}`);
75
208
  }
76
209
  }
77
- close() {
78
- if (this._wsConn) {
79
- this._wsConn.close();
210
+ shouldSkipReconnect(conn) {
211
+ if (!this.isReconnectAllowed()) {
212
+ return true;
80
213
  }
81
- else {
82
- throw new wsError_1.TDWebSocketClientError(wsError_1.ErrorCode.ERR_WEBSOCKET_CONNECTION_FAIL, "WebSocket connection is undefined.");
214
+ return this._suppressedSockets.has(conn);
215
+ }
216
+ async failNonRetriableCallbacksOnDisconnect() {
217
+ const keepReqIds = new Set(this._inflightStore.getRequests().map((req) => req.reqId));
218
+ await wsEventCallback_1.WsEventCallback.instance().rejectCallbacksExceptReqIds(keepReqIds, new wsError_1.TDWebSocketClientError(wsError_1.ErrorCode.ERR_CONNECTION_CLOSED, "websocket connection closed before response was received"), this._poolKey);
219
+ }
220
+ async handleDisconnect(conn, event) {
221
+ if (this.shouldSkipReconnect(conn) || this._isReconnecting) {
222
+ return;
223
+ }
224
+ if (event && event.code === 1000) {
225
+ log_1.default.info("Websocket closed normally, skipping reconnect.");
226
+ return;
227
+ }
228
+ await this.failNonRetriableCallbacksOnDisconnect();
229
+ try {
230
+ await this.triggerReconnect();
231
+ }
232
+ catch (err) {
233
+ const type = event ? 'close' : 'error';
234
+ const message = err instanceof Error ? err.message : String(err);
235
+ log_1.default.error(`Reconnect failed after websocket ${type}: ${message}`);
83
236
  }
84
237
  }
85
- readyState() {
86
- return this._wsConn.readyState;
238
+ extractReqId(reqId) {
239
+ if (reqId === undefined || reqId === null) {
240
+ return null;
241
+ }
242
+ try {
243
+ return BigInt(reqId);
244
+ }
245
+ catch (err) {
246
+ return null;
247
+ }
87
248
  }
88
- async sendMsgNoResp(message) {
89
- log_1.default.debug("[wsClient.sendMsgNoResp()]===>" + message);
90
- let msg = JSON.parse(message);
91
- if (msg.args.id !== undefined) {
92
- msg.args.id = BigInt(msg.args.id);
249
+ isRetriableAction(action) {
250
+ return RETRIABLE_ACTIONS.has(action);
251
+ }
252
+ extractBinaryAction(message) {
253
+ if (message.byteLength < 24) {
254
+ return -1n;
93
255
  }
94
- return new Promise((resolve, reject) => {
95
- if (this._wsConn && this._wsConn.readyState === websocket_1.w3cwebsocket.OPEN) {
96
- this._wsConn.send(message);
97
- resolve();
256
+ return new DataView(message, 16, 8).getBigInt64(0, true);
257
+ }
258
+ isRetriableBinaryAction(action) {
259
+ return BINARY_RETRIABLE_ACTIONS.has(action);
260
+ }
261
+ isNetworkError(err) {
262
+ if (!this._conn || this._conn.readyState !== websocket_1.w3cwebsocket.OPEN) {
263
+ return true;
264
+ }
265
+ const errObj = err;
266
+ const code = errObj?.code;
267
+ if (typeof code === "number" && code === wsError_1.ErrorCode.ERR_CONNECTION_CLOSED) {
268
+ return true;
269
+ }
270
+ if (typeof code === "string") {
271
+ const loweredCode = code.toLowerCase();
272
+ if (loweredCode.length > 0 && NETWORK_ERROR_CODES.has(loweredCode)) {
273
+ return true;
274
+ }
275
+ }
276
+ const message = err instanceof Error
277
+ ? err.message
278
+ : typeof errObj?.message === "string"
279
+ ? errObj.message
280
+ : String(err);
281
+ const lowered = message.toLowerCase();
282
+ return NETWORK_ERROR_MESSAGE_PATTERNS.some((pattern) => lowered.includes(pattern));
283
+ }
284
+ send(message, triggerReconnect = true) {
285
+ try {
286
+ this._conn.send(message);
287
+ }
288
+ catch (err) {
289
+ if (triggerReconnect && this.isNetworkError(err)) {
290
+ void this.triggerReconnect().catch((err) => {
291
+ const message = err instanceof Error ? err.message : String(err);
292
+ log_1.default.error(`Reconnect trigger failed: ${message}`);
293
+ });
98
294
  }
99
- else {
100
- reject(new wsError_1.WebSocketQueryError(wsError_1.ErrorCode.ERR_WEBSOCKET_CONNECTION_FAIL, `WebSocket connection is not ready, status: ${this._wsConn?.readyState}`));
295
+ throw err;
296
+ }
297
+ }
298
+ async ready() {
299
+ if (this._conn && this._conn.readyState === websocket_1.w3cwebsocket.OPEN) {
300
+ return;
301
+ }
302
+ if (this._reconnectLock) {
303
+ await this._reconnectLock;
304
+ return;
305
+ }
306
+ try {
307
+ await this._connectionReady;
308
+ }
309
+ catch (err) {
310
+ if (this._reconnectLock) {
311
+ await this._reconnectLock;
312
+ return;
101
313
  }
314
+ throw err;
315
+ }
316
+ }
317
+ getCurrentAddress() {
318
+ return `${this._currentAddress.host}:${this._currentAddress.port}`;
319
+ }
320
+ selectLeastConnectedAddress(excludedAddresses = new Set()) {
321
+ const candidates = this._dsn.addresses.filter((address) => {
322
+ const addressKey = `${address.host}:${address.port}`;
323
+ return !excludedAddresses.has(addressKey);
102
324
  });
325
+ const selectableAddresses = candidates.length > 0 ? candidates : this._dsn.addresses;
326
+ const selectedIndex = addressConnectionTracker_1.AddressConnectionTracker.instance()
327
+ .selectLeastConnected(selectableAddresses);
328
+ return selectableAddresses[selectedIndex];
103
329
  }
104
- async sendMsg(message, register = true) {
105
- if (log_1.default.isDebugEnabled()) {
106
- log_1.default.debug("[wsClient.sendMessage()]===>" + (0, utils_1.maskSensitiveForLog)(message));
330
+ async sleep(ms) {
331
+ await new Promise((resolve) => setTimeout(resolve, ms));
332
+ }
333
+ isReconnectAllowed() {
334
+ return this._allowReconnect;
335
+ }
336
+ async triggerReconnect() {
337
+ if (!this.isReconnectAllowed()) {
338
+ return;
107
339
  }
108
- let msg = JSON.parse(message);
109
- if (msg.args.id !== undefined) {
110
- msg.args.id = BigInt(msg.args.id);
340
+ if (!this._reconnectLock) {
341
+ this._reconnectLock = this._doReconnect();
111
342
  }
112
- return new Promise((resolve, reject) => {
113
- if (this._wsConn && this._wsConn.readyState === websocket_1.w3cwebsocket.OPEN) {
114
- if (register) {
115
- wsEventCallback_1.WsEventCallback.instance().registerCallback({
116
- action: msg.action,
117
- req_id: msg.args.req_id,
118
- timeout: this._timeout,
119
- id: msg.args.id === undefined ? msg.args.id : BigInt(msg.args.id),
120
- }, resolve, reject);
343
+ const lock = this._reconnectLock;
344
+ try {
345
+ await lock;
346
+ }
347
+ finally {
348
+ if (this._reconnectLock === lock) {
349
+ this._reconnectLock = null;
350
+ }
351
+ }
352
+ }
353
+ async _doReconnect() {
354
+ if (!this.isReconnectAllowed()) {
355
+ return;
356
+ }
357
+ this._isReconnecting = true;
358
+ try {
359
+ await this.attemptReconnect();
360
+ }
361
+ catch (err) {
362
+ const reconnectError = err instanceof Error
363
+ ? err
364
+ : new Error("unknown reconnect error");
365
+ this.failAllInflightRequests(reconnectError);
366
+ throw reconnectError;
367
+ }
368
+ finally {
369
+ this._isReconnecting = false;
370
+ }
371
+ }
372
+ async reconnect() {
373
+ if (this._conn) {
374
+ addressConnectionTracker_1.AddressConnectionTracker.instance().decrement(this.getCurrentAddress());
375
+ this._suppressedSockets.add(this._conn);
376
+ this._conn.close();
377
+ }
378
+ this.createConnection();
379
+ await this._connectionReady;
380
+ }
381
+ async attemptReconnect() {
382
+ const totalAddresses = this._dsn.addresses.length;
383
+ const failedAddresses = new Set();
384
+ for (let i = 0; i < totalAddresses; i++) {
385
+ for (let retry = 0; retry < this._retryConfig.retries; retry++) {
386
+ if (!this.isReconnectAllowed()) {
387
+ return;
121
388
  }
122
- if (log_1.default.isDebugEnabled()) {
123
- log_1.default.debug("[wsClient.sendMessage.msg]===>" + (0, utils_1.maskSensitiveForLog)(message));
389
+ try {
390
+ log_1.default.info(`Reconnecting to ${this.getCurrentAddress()}, attempt ${retry + 1}`);
391
+ await this.reconnect();
392
+ await this.recoverSessionContext();
393
+ await this.replayRequests();
394
+ log_1.default.info(`Reconnection successful to ${this.getCurrentAddress()}`);
395
+ return;
124
396
  }
125
- this._wsConn.send(message);
397
+ catch (err) {
398
+ log_1.default.warn(`Reconnect failed: ${err.message}`);
399
+ if (retry < this._retryConfig.retries - 1) {
400
+ const delay = this._retryConfig.getBackoffDelay(retry);
401
+ await this.sleep(delay);
402
+ }
403
+ }
404
+ }
405
+ if (i < totalAddresses - 1) {
406
+ failedAddresses.add(this.getCurrentAddress());
407
+ this._currentAddress = this.selectLeastConnectedAddress(failedAddresses);
408
+ log_1.default.info(`Switching to least-connected address: ${this.getCurrentAddress()}`);
126
409
  }
127
- else {
128
- reject(new wsError_1.WebSocketQueryError(wsError_1.ErrorCode.ERR_WEBSOCKET_CONNECTION_FAIL, `WebSocket connection is not ready, status: ${this._wsConn?.readyState}`));
410
+ }
411
+ throw new wsError_1.TDWebSocketClientError(wsError_1.ErrorCode.ERR_WEBSOCKET_CONNECTION_FAIL, "Failed to reconnect to any available address");
412
+ }
413
+ async replayRequests() {
414
+ log_1.default.info("Replaying requests after reconnection");
415
+ for (const req of this._inflightStore.getRequests()) {
416
+ try {
417
+ this.send(req.message, false);
418
+ }
419
+ catch (err) {
420
+ const message = err instanceof Error ? err.message : String(err);
421
+ if (this.isNetworkError(err)) {
422
+ log_1.default.warn(`Network error while replaying inflight request, stopping current replay round: ${message}`);
423
+ throw err;
424
+ }
425
+ log_1.default.error(`Failed to replay inflight request: ${req}, error: ${message}`);
426
+ req.reject(err);
129
427
  }
428
+ }
429
+ }
430
+ failAllInflightRequests(error) {
431
+ for (const req of this._inflightStore.getRequests()) {
432
+ req.reject(error);
433
+ }
434
+ this._inflightStore.clear();
435
+ }
436
+ close() {
437
+ this._allowReconnect = false;
438
+ this.failAllInflightRequests(new wsError_1.TDWebSocketClientError(wsError_1.ErrorCode.ERR_CONNECTION_CLOSED, "websocket connection closed"));
439
+ if (this._conn) {
440
+ addressConnectionTracker_1.AddressConnectionTracker.instance().decrement(this.getCurrentAddress());
441
+ this._suppressedSockets.add(this._conn);
442
+ this._conn.close();
443
+ }
444
+ else {
445
+ log_1.default.warn("close() called but websocket connection is undefined");
446
+ }
447
+ }
448
+ readyState() {
449
+ return this._conn.readyState;
450
+ }
451
+ setSessionRecoveryHook(hook) {
452
+ this._sessionRecoveryHook = hook || null;
453
+ }
454
+ async recoverSessionContext() {
455
+ if (!this._sessionRecoveryHook) {
456
+ return;
457
+ }
458
+ await this._sessionRecoveryHook();
459
+ }
460
+ async sendMsgDirect(message) {
461
+ if (log_1.default.isDebugEnabled()) {
462
+ log_1.default.debug("[wsClient.sendMsgDirect]===>" + (0, utils_1.maskSensitiveForLog)(message));
463
+ }
464
+ const msg = JSON.parse(message);
465
+ const reqId = this.extractReqId(msg?.args?.req_id) ?? BigInt(0);
466
+ let resolveResp = () => { };
467
+ let rejectResp = () => { };
468
+ const responsePromise = new Promise((resolve, reject) => {
469
+ resolveResp = resolve;
470
+ rejectResp = reject;
130
471
  });
472
+ await wsEventCallback_1.WsEventCallback.instance().registerCallback({
473
+ action: msg.action,
474
+ req_id: reqId,
475
+ timeout: this._timeout,
476
+ poolKey: this._poolKey,
477
+ }, resolveResp, rejectResp);
478
+ try {
479
+ this.send(message, false);
480
+ return await responsePromise;
481
+ }
482
+ catch (err) {
483
+ await wsEventCallback_1.WsEventCallback.instance().unregisterCallback(reqId);
484
+ throw err;
485
+ }
486
+ }
487
+ async sendMsgNoResp(message) {
488
+ log_1.default.debug("[wsClient.sendMsgNoResp]===>" + message);
489
+ if (this._reconnectLock) {
490
+ await this._reconnectLock;
491
+ }
492
+ this.send(message);
493
+ }
494
+ async sendMsg(message) {
495
+ if (log_1.default.isDebugEnabled()) {
496
+ log_1.default.debug("[wsConnector.sendMsg]===>" + (0, utils_1.maskSensitiveForLog)(message));
497
+ }
498
+ const msg = JSON.parse(message);
499
+ const id = msg?.args?.id !== undefined ? BigInt(msg.args.id) : undefined;
500
+ const reqId = this.extractReqId(msg?.args?.req_id) ?? BigInt(0);
501
+ return this.sendAndTrackResponse(reqId, msg.action, message, this.isRetriableAction(msg.action), id);
131
502
  }
132
- async sendBinaryMsg(reqId, action, message, register = true) {
503
+ async sendBinaryMsg(reqId, action, message) {
504
+ return this.sendAndTrackResponse(reqId, action, message, this.isRetriableBinaryAction(this.extractBinaryAction(message)), reqId);
505
+ }
506
+ async sendAndTrackResponse(reqId, action, message, retriable, callbackId) {
507
+ if (this._reconnectLock) {
508
+ await this._reconnectLock;
509
+ }
133
510
  return new Promise((resolve, reject) => {
134
- if (this._wsConn && this._wsConn.readyState === websocket_1.w3cwebsocket.OPEN) {
135
- if (register) {
136
- wsEventCallback_1.WsEventCallback.instance().registerCallback({
137
- action: action,
138
- req_id: reqId,
139
- timeout: this._timeout,
140
- id: reqId,
141
- }, resolve, reject);
511
+ let settled = false;
512
+ const safeResolve = (result) => {
513
+ if (settled) {
514
+ return;
142
515
  }
143
- log_1.default.debug("[wsClient.sendBinaryMsg()]===>" +
144
- reqId +
145
- action +
146
- message.byteLength);
147
- this._wsConn.send(message);
148
- }
149
- else {
150
- reject(new wsError_1.WebSocketQueryError(wsError_1.ErrorCode.ERR_WEBSOCKET_CONNECTION_FAIL, `WebSocket connection is not ready, status: ${this._wsConn?.readyState}`));
151
- }
516
+ settled = true;
517
+ if (retriable) {
518
+ this._inflightStore.remove(reqId);
519
+ }
520
+ resolve(result);
521
+ };
522
+ const safeReject = (error) => {
523
+ if (settled) {
524
+ return;
525
+ }
526
+ settled = true;
527
+ if (retriable) {
528
+ this._inflightStore.remove(reqId);
529
+ }
530
+ void wsEventCallback_1.WsEventCallback.instance().unregisterCallback(reqId);
531
+ reject(error);
532
+ };
533
+ const registerAndSend = async () => {
534
+ if (retriable) {
535
+ this._inflightStore.insert({
536
+ reqId,
537
+ action,
538
+ id: callbackId,
539
+ message,
540
+ resolve: safeResolve,
541
+ reject: safeReject,
542
+ });
543
+ }
544
+ await wsEventCallback_1.WsEventCallback.instance().registerCallback({
545
+ action,
546
+ req_id: reqId,
547
+ timeout: this._timeout,
548
+ id: callbackId,
549
+ poolKey: this._poolKey,
550
+ }, safeResolve, safeReject);
551
+ if (settled) {
552
+ await wsEventCallback_1.WsEventCallback.instance().unregisterCallback(reqId);
553
+ return;
554
+ }
555
+ try {
556
+ this.send(message);
557
+ }
558
+ catch (err) {
559
+ if (retriable && this.isNetworkError(err)) {
560
+ return;
561
+ }
562
+ safeReject(err);
563
+ }
564
+ };
565
+ void registerAndSend().catch((error) => {
566
+ safeReject(error);
567
+ });
152
568
  });
153
569
  }
154
- getWsURL() {
155
- return this._wsURL;
570
+ getPoolKey() {
571
+ return this._poolKey;
572
+ }
573
+ getReconnectRetries() {
574
+ return this._retryConfig.retries;
156
575
  }
157
576
  }
158
577
  exports.WebSocketConnector = WebSocketConnector;
@@ -1,3 +1,4 @@
1
+ import { Dsn } from "../common/dsn";
1
2
  import { WebSocketConnector } from "./wsConnector";
2
3
  export declare class WebSocketConnectionPool {
3
4
  private static _instance?;
@@ -7,7 +8,10 @@ export declare class WebSocketConnectionPool {
7
8
  private static sharedArray;
8
9
  private constructor();
9
10
  static instance(maxConnections?: number): WebSocketConnectionPool;
10
- getConnection(url: URL, timeout: number | undefined | null): Promise<WebSocketConnector>;
11
+ private maskPoolKeyForLog;
12
+ private buildAuthScope;
13
+ private getPoolKey;
14
+ getConnection(dsn: Dsn, timeout: number | undefined | null): Promise<WebSocketConnector>;
11
15
  releaseConnection(connector: WebSocketConnector): Promise<void>;
12
16
  destroyed(): void;
13
17
  }