@clickhouse/client 0.3.1 → 0.100.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.
Files changed (57) hide show
  1. package/LICENSE +2 -2
  2. package/README.md +14 -0
  3. package/dist/client.d.ts +11 -26
  4. package/dist/client.js +11 -53
  5. package/dist/client.js.map +1 -1
  6. package/dist/config.d.ts +52 -0
  7. package/dist/config.js +80 -0
  8. package/dist/config.js.map +1 -0
  9. package/dist/connection/compression.d.ts +4 -3
  10. package/dist/connection/compression.js +7 -5
  11. package/dist/connection/compression.js.map +1 -1
  12. package/dist/connection/create_connection.d.ts +16 -0
  13. package/dist/connection/create_connection.js +42 -0
  14. package/dist/connection/create_connection.js.map +1 -0
  15. package/dist/connection/index.d.ts +1 -0
  16. package/dist/connection/index.js +1 -0
  17. package/dist/connection/index.js.map +1 -1
  18. package/dist/connection/node_base_connection.d.ts +23 -12
  19. package/dist/connection/node_base_connection.js +428 -227
  20. package/dist/connection/node_base_connection.js.map +1 -1
  21. package/dist/connection/node_custom_agent_connection.d.ts +8 -0
  22. package/dist/connection/node_custom_agent_connection.js +47 -0
  23. package/dist/connection/node_custom_agent_connection.js.map +1 -0
  24. package/dist/connection/node_http_connection.d.ts +1 -5
  25. package/dist/connection/node_http_connection.js +6 -5
  26. package/dist/connection/node_http_connection.js.map +1 -1
  27. package/dist/connection/node_https_connection.d.ts +3 -6
  28. package/dist/connection/node_https_connection.js +39 -19
  29. package/dist/connection/node_https_connection.js.map +1 -1
  30. package/dist/connection/stream.d.ts +3 -2
  31. package/dist/connection/stream.js +4 -3
  32. package/dist/connection/stream.js.map +1 -1
  33. package/dist/index.d.ts +6 -3
  34. package/dist/index.js +13 -4
  35. package/dist/index.js.map +1 -1
  36. package/dist/result_set.d.ts +41 -6
  37. package/dist/result_set.js +133 -51
  38. package/dist/result_set.js.map +1 -1
  39. package/dist/utils/encoder.d.ts +3 -2
  40. package/dist/utils/encoder.js +16 -3
  41. package/dist/utils/encoder.js.map +1 -1
  42. package/dist/utils/index.js.map +1 -1
  43. package/dist/utils/process.js +1 -2
  44. package/dist/utils/process.js.map +1 -1
  45. package/dist/utils/runtime.d.ts +6 -0
  46. package/dist/utils/runtime.js +65 -0
  47. package/dist/utils/runtime.js.map +1 -0
  48. package/dist/utils/stream.d.ts +1 -2
  49. package/dist/utils/stream.js +23 -9
  50. package/dist/utils/stream.js.map +1 -1
  51. package/dist/utils/user_agent.d.ts +4 -0
  52. package/dist/utils/user_agent.js +7 -31
  53. package/dist/utils/user_agent.js.map +1 -1
  54. package/dist/version.d.ts +1 -1
  55. package/dist/version.js +1 -1
  56. package/dist/version.js.map +1 -1
  57. package/package.json +10 -3
@@ -25,7 +25,19 @@ class NodeBaseConnection {
25
25
  writable: true,
26
26
  value: agent
27
27
  });
28
- Object.defineProperty(this, "headers", {
28
+ Object.defineProperty(this, "defaultAuthHeader", {
29
+ enumerable: true,
30
+ configurable: true,
31
+ writable: true,
32
+ value: void 0
33
+ });
34
+ Object.defineProperty(this, "defaultHeaders", {
35
+ enumerable: true,
36
+ configurable: true,
37
+ writable: true,
38
+ value: void 0
39
+ });
40
+ Object.defineProperty(this, "jsonHandling", {
29
41
  enumerable: true,
30
42
  configurable: true,
31
43
  writable: true,
@@ -49,225 +61,106 @@ class NodeBaseConnection {
49
61
  writable: true,
50
62
  value: void 0
51
63
  });
52
- this.logger = params.log_writer;
53
- this.idleSocketTTL = params.keep_alive.idle_socket_ttl;
54
- this.headers = this.buildDefaultHeaders(params.username, params.password, params.additional_headers);
55
- }
56
- buildDefaultHeaders(username, password, additional_headers) {
57
- return {
58
- // KeepAlive agent for some reason does not set this on its own
64
+ if (params.auth.type === 'Credentials') {
65
+ this.defaultAuthHeader = `Basic ${Buffer.from(`${params.auth.username}:${params.auth.password}`).toString('base64')}`;
66
+ }
67
+ else if (params.auth.type === 'JWT') {
68
+ this.defaultAuthHeader = `Bearer ${params.auth.access_token}`;
69
+ }
70
+ else {
71
+ throw new Error(`Unknown auth type: ${params.auth.type}`);
72
+ }
73
+ this.defaultHeaders = {
74
+ // Node.js HTTP agent, for some reason, does not set this on its own when KeepAlive is enabled
59
75
  Connection: this.params.keep_alive.enabled ? 'keep-alive' : 'close',
60
- Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`,
61
76
  'User-Agent': (0, utils_1.getUserAgent)(this.params.application_id),
62
- ...additional_headers,
77
+ };
78
+ this.logger = params.log_writer;
79
+ this.idleSocketTTL = params.keep_alive.idle_socket_ttl;
80
+ this.jsonHandling = params.json ?? {
81
+ parse: JSON.parse,
82
+ stringify: JSON.stringify,
63
83
  };
64
84
  }
65
- async request(params, op) {
66
- return new Promise((resolve, reject) => {
67
- const start = Date.now();
68
- const request = this.createClientRequest(params);
69
- function onError(err) {
70
- removeRequestListeners();
71
- reject(err);
72
- }
73
- const onResponse = async (_response) => {
74
- this.logResponse(op, request, params, _response, start);
75
- const decompressionResult = (0, compression_1.decompressResponse)(_response);
76
- if ((0, compression_1.isDecompressionError)(decompressionResult)) {
77
- return reject(decompressionResult.error);
78
- }
79
- if ((0, client_common_1.isSuccessfulResponse)(_response.statusCode)) {
80
- return resolve({
81
- stream: decompressionResult.response,
82
- summary: params.parse_summary
83
- ? this.parseSummary(op, _response)
84
- : undefined,
85
- });
86
- }
87
- else {
88
- reject((0, client_common_1.parseError)(await (0, utils_1.getAsText)(decompressionResult.response)));
89
- }
90
- };
91
- function onAbort() {
92
- // Prefer 'abort' event since it always triggered unlike 'error' and 'close'
93
- // see the full sequence of events https://nodejs.org/api/http.html#httprequesturl-options-callback
94
- removeRequestListeners();
95
- request.once('error', function () {
96
- /**
97
- * catch "Error: ECONNRESET" error which shouldn't be reported to users.
98
- * see the full sequence of events https://nodejs.org/api/http.html#httprequesturl-options-callback
99
- * */
85
+ async ping(params) {
86
+ const query_id = this.getQueryId(params.query_id);
87
+ const { controller, controllerCleanup } = this.getAbortController(params);
88
+ let result;
89
+ try {
90
+ if (params.select) {
91
+ const searchParams = (0, client_common_1.toSearchParams)({
92
+ database: undefined,
93
+ query: PingQuery,
94
+ query_id,
100
95
  });
101
- reject(new Error('The user aborted a request.'));
96
+ result = await this.request({
97
+ method: 'GET',
98
+ url: (0, client_common_1.transformUrl)({ url: this.params.url, searchParams }),
99
+ query: PingQuery,
100
+ abort_signal: controller.signal,
101
+ headers: this.buildRequestHeaders(),
102
+ }, 'Ping');
102
103
  }
103
- function onClose() {
104
- // Adapter uses 'close' event to clean up listeners after the successful response.
105
- // It's necessary in order to handle 'abort' and 'timeout' events while response is streamed.
106
- // It's always the last event, according to https://nodejs.org/docs/latest-v14.x/api/http.html#http_http_request_url_options_callback
107
- removeRequestListeners();
108
- }
109
- function pipeStream() {
110
- // if request.end() was called due to no data to send
111
- if (request.writableEnded) {
112
- return;
113
- }
114
- const bodyStream = (0, utils_1.isStream)(params.body)
115
- ? params.body
116
- : stream_1.default.Readable.from([params.body]);
117
- const callback = (err) => {
118
- if (err) {
119
- removeRequestListeners();
120
- reject(err);
121
- }
122
- };
123
- if (params.compress_request) {
124
- stream_1.default.pipeline(bodyStream, zlib_1.default.createGzip(), request, callback);
125
- }
126
- else {
127
- stream_1.default.pipeline(bodyStream, request, callback);
128
- }
104
+ else {
105
+ result = await this.request({
106
+ method: 'GET',
107
+ url: (0, client_common_1.transformUrl)({ url: this.params.url, pathname: '/ping' }),
108
+ abort_signal: controller.signal,
109
+ headers: this.buildRequestHeaders(),
110
+ query: 'ping',
111
+ }, 'Ping');
129
112
  }
130
- const onSocket = (socket) => {
131
- if (this.params.keep_alive.enabled) {
132
- const socketInfo = this.knownSockets.get(socket);
133
- // It is the first time we encounter this socket,
134
- // so it doesn't have the idle timeout handler attached to it
135
- if (socketInfo === undefined) {
136
- const socketId = crypto_1.default.randomUUID();
137
- this.logger.trace({
138
- message: `Using a fresh socket ${socketId}, setting up a new 'free' listener`,
139
- });
140
- this.knownSockets.set(socket, {
141
- id: socketId,
142
- idle_timeout_handle: undefined,
143
- });
144
- // When the request is complete and the socket is released,
145
- // make sure that the socket is removed after `idleSocketTTL`.
146
- socket.on('free', () => {
147
- this.logger.trace({
148
- message: `Socket ${socketId} was released`,
149
- });
150
- // Avoiding the built-in socket.timeout() method usage here,
151
- // as we don't want to clash with the actual request timeout.
152
- const idleTimeoutHandle = setTimeout(() => {
153
- this.logger.trace({
154
- message: `Removing socket ${socketId} after ${this.idleSocketTTL} ms of idle`,
155
- });
156
- this.knownSockets.delete(socket);
157
- socket.destroy();
158
- }, this.idleSocketTTL).unref();
159
- this.knownSockets.set(socket, {
160
- id: socketId,
161
- idle_timeout_handle: idleTimeoutHandle,
162
- });
163
- });
164
- const cleanup = () => {
165
- const maybeSocketInfo = this.knownSockets.get(socket);
166
- // clean up a possibly dangling idle timeout handle (preventing leaks)
167
- if (maybeSocketInfo?.idle_timeout_handle) {
168
- clearTimeout(maybeSocketInfo.idle_timeout_handle);
169
- }
170
- this.logger.trace({
171
- message: `Socket ${socketId} was closed or ended, 'free' listener removed`,
172
- });
173
- };
174
- socket.once('end', cleanup);
175
- socket.once('close', cleanup);
176
- }
177
- else {
178
- clearTimeout(socketInfo.idle_timeout_handle);
179
- this.logger.trace({
180
- message: `Reusing socket ${socketInfo.id}`,
181
- });
182
- this.knownSockets.set(socket, {
183
- ...socketInfo,
184
- idle_timeout_handle: undefined,
185
- });
186
- }
187
- }
188
- // Socket is "prepared" with idle handlers, continue with our request
189
- pipeStream();
190
- // This is for request timeout only. Surprisingly, it is not always enough to set in the HTTP request.
191
- // The socket won't be actually destroyed, and it will be returned to the pool.
192
- socket.setTimeout(this.params.request_timeout, onTimeout);
193
- };
194
- function onTimeout() {
195
- removeRequestListeners();
196
- request.destroy();
197
- reject(new Error('Timeout error.'));
198
- }
199
- function removeRequestListeners() {
200
- if (request.socket !== null) {
201
- request.socket.setTimeout(0); // reset previously set timeout
202
- request.socket.removeListener('timeout', onTimeout);
203
- }
204
- request.removeListener('socket', onSocket);
205
- request.removeListener('response', onResponse);
206
- request.removeListener('error', onError);
207
- request.removeListener('close', onClose);
208
- if (params.abort_signal !== undefined) {
209
- request.removeListener('abort', onAbort);
210
- }
211
- }
212
- request.on('socket', onSocket);
213
- request.on('response', onResponse);
214
- request.on('error', onError);
215
- request.on('close', onClose);
216
- if (params.abort_signal !== undefined) {
217
- params.abort_signal.addEventListener('abort', onAbort, { once: true });
218
- }
219
- if (!params.body)
220
- return request.end();
221
- });
222
- }
223
- async ping() {
224
- const abortController = new AbortController();
225
- try {
226
- const { stream } = await this.request({
227
- method: 'GET',
228
- url: (0, client_common_1.transformUrl)({ url: this.params.url, pathname: '/ping' }),
229
- abort_signal: abortController.signal,
230
- }, 'Ping');
231
- await (0, stream_2.drainStream)(stream);
113
+ await (0, stream_2.drainStream)(result.stream);
232
114
  return { success: true };
233
115
  }
234
116
  catch (error) {
235
117
  // it is used to ensure that the outgoing request is terminated,
236
- // and we don't get an unhandled error propagation later
237
- abortController.abort('Ping failed');
118
+ // and we don't get unhandled error propagation later
119
+ controller.abort('Ping failed');
238
120
  // not an error, as this might be semi-expected
239
121
  this.logger.warn({
240
122
  message: this.httpRequestErrorMessage('Ping'),
241
123
  err: error,
124
+ args: {
125
+ query_id,
126
+ },
242
127
  });
243
128
  return {
244
129
  success: false,
245
130
  error: error, // should NOT be propagated to the user
246
131
  };
247
132
  }
133
+ finally {
134
+ controllerCleanup();
135
+ }
248
136
  }
249
137
  async query(params) {
250
138
  const query_id = this.getQueryId(params.query_id);
251
139
  const clickhouse_settings = (0, client_common_1.withHttpSettings)(params.clickhouse_settings, this.params.compression.decompress_response);
252
140
  const searchParams = (0, client_common_1.toSearchParams)({
253
141
  database: this.params.database,
254
- clickhouse_settings,
255
142
  query_params: params.query_params,
256
143
  session_id: params.session_id,
144
+ clickhouse_settings,
257
145
  query_id,
146
+ role: params.role,
258
147
  });
259
- const decompressResponse = clickhouse_settings.enable_http_compression === 1;
260
148
  const { controller, controllerCleanup } = this.getAbortController(params);
149
+ // allows enforcing the compression via the settings even if the client instance has it disabled
150
+ const enableResponseCompression = clickhouse_settings.enable_http_compression === 1;
261
151
  try {
262
- const { stream } = await this.request({
152
+ const { response_headers, stream } = await this.request({
263
153
  method: 'POST',
264
154
  url: (0, client_common_1.transformUrl)({ url: this.params.url, searchParams }),
265
155
  body: params.query,
266
156
  abort_signal: controller.signal,
267
- decompress_response: decompressResponse,
157
+ enable_response_compression: enableResponseCompression,
158
+ headers: this.buildRequestHeaders(params),
159
+ query: params.query,
268
160
  }, 'Query');
269
161
  return {
270
162
  stream,
163
+ response_headers,
271
164
  query_id,
272
165
  };
273
166
  }
@@ -280,7 +173,7 @@ class NodeBaseConnection {
280
173
  search_params: searchParams,
281
174
  err: err,
282
175
  extra_args: {
283
- decompress_response: decompressResponse,
176
+ decompress_response: enableResponseCompression,
284
177
  clickhouse_settings,
285
178
  },
286
179
  });
@@ -290,48 +183,6 @@ class NodeBaseConnection {
290
183
  controllerCleanup();
291
184
  }
292
185
  }
293
- async exec(params) {
294
- const query_id = this.getQueryId(params.query_id);
295
- const searchParams = (0, client_common_1.toSearchParams)({
296
- database: this.params.database,
297
- clickhouse_settings: params.clickhouse_settings,
298
- query_params: params.query_params,
299
- session_id: params.session_id,
300
- query_id,
301
- });
302
- const { controller, controllerCleanup } = this.getAbortController(params);
303
- try {
304
- const { stream, summary } = await this.request({
305
- method: 'POST',
306
- url: (0, client_common_1.transformUrl)({ url: this.params.url, searchParams }),
307
- body: params.query,
308
- abort_signal: controller.signal,
309
- parse_summary: true,
310
- }, 'Exec');
311
- return {
312
- stream,
313
- query_id,
314
- summary,
315
- };
316
- }
317
- catch (err) {
318
- controller.abort('Exec HTTP request failed');
319
- this.logRequestError({
320
- op: 'Exec',
321
- query_id: query_id,
322
- query_params: params,
323
- search_params: searchParams,
324
- err: err,
325
- extra_args: {
326
- clickhouse_settings: params.clickhouse_settings ?? {},
327
- },
328
- });
329
- throw err; // should be propagated to the user
330
- }
331
- finally {
332
- controllerCleanup();
333
- }
334
- }
335
186
  async insert(params) {
336
187
  const query_id = this.getQueryId(params.query_id);
337
188
  const searchParams = (0, client_common_1.toSearchParams)({
@@ -340,20 +191,23 @@ class NodeBaseConnection {
340
191
  query_params: params.query_params,
341
192
  query: params.query,
342
193
  session_id: params.session_id,
194
+ role: params.role,
343
195
  query_id,
344
196
  });
345
197
  const { controller, controllerCleanup } = this.getAbortController(params);
346
198
  try {
347
- const { stream, summary } = await this.request({
199
+ const { stream, summary, response_headers } = await this.request({
348
200
  method: 'POST',
349
201
  url: (0, client_common_1.transformUrl)({ url: this.params.url, searchParams }),
350
202
  body: params.values,
351
203
  abort_signal: controller.signal,
352
- compress_request: this.params.compression.compress_request,
204
+ enable_request_compression: this.params.compression.compress_request,
353
205
  parse_summary: true,
206
+ headers: this.buildRequestHeaders(params),
207
+ query: params.query,
354
208
  }, 'Insert');
355
209
  await (0, stream_2.drainStream)(stream);
356
- return { query_id, summary };
210
+ return { query_id, summary, response_headers };
357
211
  }
358
212
  catch (err) {
359
213
  controller.abort('Insert HTTP request failed');
@@ -373,11 +227,64 @@ class NodeBaseConnection {
373
227
  controllerCleanup();
374
228
  }
375
229
  }
230
+ async exec(params) {
231
+ return this.runExec({
232
+ ...params,
233
+ op: 'Exec',
234
+ });
235
+ }
236
+ async command(params) {
237
+ const { stream, query_id, summary, response_headers } = await this.runExec({
238
+ ...params,
239
+ op: 'Command',
240
+ });
241
+ // ignore the response stream and release the socket immediately
242
+ await (0, stream_2.drainStream)(stream);
243
+ return { query_id, summary, response_headers };
244
+ }
376
245
  async close() {
377
246
  if (this.agent !== undefined && this.agent.destroy !== undefined) {
378
247
  this.agent.destroy();
379
248
  }
380
249
  }
250
+ defaultHeadersWithOverride(params) {
251
+ return {
252
+ // Custom HTTP headers from the client configuration
253
+ ...(this.params.http_headers ?? {}),
254
+ // Custom HTTP headers for this particular request; it will override the client configuration with the same keys
255
+ ...(params?.http_headers ?? {}),
256
+ // Includes the `Connection` + `User-Agent` headers which we do not allow to override
257
+ // An appropriate `Authorization` header might be added later
258
+ // It is not always required - see the TLS headers in `node_https_connection.ts`
259
+ ...this.defaultHeaders,
260
+ };
261
+ }
262
+ buildRequestHeaders(params) {
263
+ const headers = this.defaultHeadersWithOverride(params);
264
+ if ((0, client_common_1.isJWTAuth)(params?.auth)) {
265
+ return {
266
+ ...headers,
267
+ Authorization: `Bearer ${params.auth.access_token}`,
268
+ };
269
+ }
270
+ if (this.params.set_basic_auth_header) {
271
+ if ((0, client_common_1.isCredentialsAuth)(params?.auth)) {
272
+ return {
273
+ ...headers,
274
+ Authorization: `Basic ${Buffer.from(`${params.auth.username}:${params.auth.password}`).toString('base64')}`,
275
+ };
276
+ }
277
+ else {
278
+ return {
279
+ ...headers,
280
+ Authorization: this.defaultAuthHeader,
281
+ };
282
+ }
283
+ }
284
+ return {
285
+ ...headers,
286
+ };
287
+ }
381
288
  getQueryId(query_id) {
382
289
  return query_id || crypto_1.default.randomUUID();
383
290
  }
@@ -434,7 +341,7 @@ class NodeBaseConnection {
434
341
  const summaryHeader = response.headers['x-clickhouse-summary'];
435
342
  if (typeof summaryHeader === 'string') {
436
343
  try {
437
- return JSON.parse(summaryHeader);
344
+ return this.jsonHandling.parse(summaryHeader);
438
345
  }
439
346
  catch (err) {
440
347
  this.logger.error({
@@ -447,6 +354,300 @@ class NodeBaseConnection {
447
354
  }
448
355
  }
449
356
  }
357
+ async runExec(params) {
358
+ const query_id = this.getQueryId(params.query_id);
359
+ const sendQueryInParams = params.values !== undefined;
360
+ const clickhouse_settings = (0, client_common_1.withHttpSettings)(params.clickhouse_settings, this.params.compression.decompress_response);
361
+ const toSearchParamsOptions = {
362
+ query: sendQueryInParams ? params.query : undefined,
363
+ database: this.params.database,
364
+ query_params: params.query_params,
365
+ session_id: params.session_id,
366
+ role: params.role,
367
+ clickhouse_settings,
368
+ query_id,
369
+ };
370
+ const searchParams = (0, client_common_1.toSearchParams)(toSearchParamsOptions);
371
+ const { controller, controllerCleanup } = this.getAbortController(params);
372
+ const tryDecompressResponseStream = params.op === 'Exec'
373
+ ? // allows disabling stream decompression for the `Exec` operation only
374
+ (params.decompress_response_stream ??
375
+ this.params.compression.decompress_response)
376
+ : // there is nothing useful in the response stream for the `Command` operation,
377
+ // and it is immediately destroyed; never decompress it
378
+ false;
379
+ const ignoreErrorResponse = params.ignore_error_response ?? false;
380
+ try {
381
+ const { stream, summary, response_headers } = await this.request({
382
+ method: 'POST',
383
+ url: (0, client_common_1.transformUrl)({ url: this.params.url, searchParams }),
384
+ body: sendQueryInParams ? params.values : params.query,
385
+ abort_signal: controller.signal,
386
+ parse_summary: true,
387
+ enable_request_compression: this.params.compression.compress_request,
388
+ enable_response_compression: this.params.compression.decompress_response,
389
+ try_decompress_response_stream: tryDecompressResponseStream,
390
+ ignore_error_response: ignoreErrorResponse,
391
+ headers: this.buildRequestHeaders(params),
392
+ query: params.query,
393
+ }, params.op);
394
+ return {
395
+ stream,
396
+ query_id,
397
+ summary,
398
+ response_headers,
399
+ };
400
+ }
401
+ catch (err) {
402
+ controller.abort(`${params.op} HTTP request failed`);
403
+ this.logRequestError({
404
+ op: params.op,
405
+ query_id: query_id,
406
+ query_params: params,
407
+ search_params: searchParams,
408
+ err: err,
409
+ extra_args: {
410
+ clickhouse_settings: params.clickhouse_settings ?? {},
411
+ },
412
+ });
413
+ throw err; // should be propagated to the user
414
+ }
415
+ finally {
416
+ controllerCleanup();
417
+ }
418
+ }
419
+ async request(params, op) {
420
+ // allows the event loop to process the idle socket timers, if the CPU load is high
421
+ // otherwise, we can occasionally get an expired socket, see https://github.com/ClickHouse/clickhouse-js/issues/294
422
+ await (0, client_common_1.sleep)(0);
423
+ const currentStackTrace = this.params.capture_enhanced_stack_trace
424
+ ? (0, client_common_1.getCurrentStackTrace)()
425
+ : undefined;
426
+ const logger = this.logger;
427
+ return new Promise((resolve, reject) => {
428
+ const start = Date.now();
429
+ const request = this.createClientRequest(params);
430
+ function onError(e) {
431
+ removeRequestListeners();
432
+ const err = (0, client_common_1.enhanceStackTrace)(e, currentStackTrace);
433
+ reject(err);
434
+ }
435
+ let responseStream;
436
+ const onResponse = async (_response) => {
437
+ this.logResponse(op, request, params, _response, start);
438
+ const tryDecompressResponseStream = params.try_decompress_response_stream ?? true;
439
+ const ignoreErrorResponse = params.ignore_error_response ?? false;
440
+ // even if the stream decompression is disabled, we have to decompress it in case of an error
441
+ const isFailedResponse = !(0, client_common_1.isSuccessfulResponse)(_response.statusCode);
442
+ if (tryDecompressResponseStream ||
443
+ (isFailedResponse && !ignoreErrorResponse)) {
444
+ const decompressionResult = (0, compression_1.decompressResponse)(_response, this.logger);
445
+ if ((0, compression_1.isDecompressionError)(decompressionResult)) {
446
+ const err = (0, client_common_1.enhanceStackTrace)(decompressionResult.error, currentStackTrace);
447
+ return reject(err);
448
+ }
449
+ responseStream = decompressionResult.response;
450
+ }
451
+ else {
452
+ responseStream = _response;
453
+ }
454
+ if (isFailedResponse && !ignoreErrorResponse) {
455
+ try {
456
+ const errorMessage = await (0, utils_1.getAsText)(responseStream);
457
+ const err = (0, client_common_1.enhanceStackTrace)((0, client_common_1.parseError)(errorMessage), currentStackTrace);
458
+ reject(err);
459
+ }
460
+ catch (e) {
461
+ // If the ClickHouse response is malformed
462
+ const err = (0, client_common_1.enhanceStackTrace)(e, currentStackTrace);
463
+ reject(err);
464
+ }
465
+ }
466
+ else {
467
+ return resolve({
468
+ stream: responseStream,
469
+ summary: params.parse_summary
470
+ ? this.parseSummary(op, _response)
471
+ : undefined,
472
+ response_headers: { ..._response.headers },
473
+ });
474
+ }
475
+ };
476
+ function onAbort() {
477
+ // Prefer 'abort' event since it always triggered unlike 'error' and 'close'
478
+ // see the full sequence of events https://nodejs.org/api/http.html#httprequesturl-options-callback
479
+ removeRequestListeners();
480
+ request.once('error', function () {
481
+ /**
482
+ * catch "Error: ECONNRESET" error which shouldn't be reported to users.
483
+ * see the full sequence of events https://nodejs.org/api/http.html#httprequesturl-options-callback
484
+ * */
485
+ });
486
+ const err = (0, client_common_1.enhanceStackTrace)(new Error('The user aborted a request.'), currentStackTrace);
487
+ reject(err);
488
+ }
489
+ function onClose() {
490
+ // Adapter uses 'close' event to clean up listeners after the successful response.
491
+ // It's necessary in order to handle 'abort' and 'timeout' events while response is streamed.
492
+ // It's always the last event, according to https://nodejs.org/docs/latest-v14.x/api/http.html#http_http_request_url_options_callback
493
+ removeRequestListeners();
494
+ }
495
+ function pipeStream() {
496
+ // if request.end() was called due to no data to send
497
+ if (request.writableEnded) {
498
+ return;
499
+ }
500
+ const bodyStream = (0, utils_1.isStream)(params.body)
501
+ ? params.body
502
+ : stream_1.default.Readable.from([params.body]);
503
+ const callback = (e) => {
504
+ if (e) {
505
+ removeRequestListeners();
506
+ const err = (0, client_common_1.enhanceStackTrace)(e, currentStackTrace);
507
+ reject(err);
508
+ }
509
+ };
510
+ if (params.enable_request_compression) {
511
+ stream_1.default.pipeline(bodyStream, zlib_1.default.createGzip(), request, callback);
512
+ }
513
+ else {
514
+ stream_1.default.pipeline(bodyStream, request, callback);
515
+ }
516
+ }
517
+ const onSocket = (socket) => {
518
+ try {
519
+ if (this.params.keep_alive.enabled &&
520
+ this.params.keep_alive.idle_socket_ttl > 0) {
521
+ const socketInfo = this.knownSockets.get(socket);
522
+ // It is the first time we've encountered this socket,
523
+ // so it doesn't have the idle timeout handler attached to it
524
+ if (socketInfo === undefined) {
525
+ const socketId = crypto_1.default.randomUUID();
526
+ this.logger.trace({
527
+ message: `Using a fresh socket ${socketId}, setting up a new 'free' listener`,
528
+ });
529
+ this.knownSockets.set(socket, {
530
+ id: socketId,
531
+ idle_timeout_handle: undefined,
532
+ });
533
+ // When the request is complete and the socket is released,
534
+ // make sure that the socket is removed after `idleSocketTTL`.
535
+ socket.on('free', () => {
536
+ this.logger.trace({
537
+ message: `Socket ${socketId} was released`,
538
+ });
539
+ // Avoiding the built-in socket.timeout() method usage here,
540
+ // as we don't want to clash with the actual request timeout.
541
+ const idleTimeoutHandle = setTimeout(() => {
542
+ this.logger.trace({
543
+ message: `Removing socket ${socketId} after ${this.idleSocketTTL} ms of idle`,
544
+ });
545
+ this.knownSockets.delete(socket);
546
+ socket.destroy();
547
+ }, this.idleSocketTTL).unref();
548
+ this.knownSockets.set(socket, {
549
+ id: socketId,
550
+ idle_timeout_handle: idleTimeoutHandle,
551
+ });
552
+ });
553
+ const cleanup = () => {
554
+ const maybeSocketInfo = this.knownSockets.get(socket);
555
+ // clean up a possibly dangling idle timeout handle (preventing leaks)
556
+ if (maybeSocketInfo?.idle_timeout_handle) {
557
+ clearTimeout(maybeSocketInfo.idle_timeout_handle);
558
+ }
559
+ this.logger.trace({
560
+ message: `Socket ${socketId} was closed or ended, 'free' listener removed`,
561
+ });
562
+ if (responseStream && !responseStream.readableEnded) {
563
+ this.logger.warn({
564
+ message: `${op}: socket was closed or ended before the response was fully read. ` +
565
+ 'This can potentially result in an uncaught ECONNRESET error! ' +
566
+ 'Consider fully consuming, draining, or destroying the response stream.',
567
+ args: {
568
+ query: params.query,
569
+ query_id: params.url.searchParams.get('query_id') ?? 'unknown',
570
+ },
571
+ });
572
+ }
573
+ };
574
+ socket.once('end', cleanup);
575
+ socket.once('close', cleanup);
576
+ }
577
+ else {
578
+ clearTimeout(socketInfo.idle_timeout_handle);
579
+ this.logger.trace({
580
+ message: `Reusing socket ${socketInfo.id}`,
581
+ });
582
+ this.knownSockets.set(socket, {
583
+ ...socketInfo,
584
+ idle_timeout_handle: undefined,
585
+ });
586
+ }
587
+ }
588
+ }
589
+ catch (e) {
590
+ logger.error({
591
+ message: 'An error occurred while housekeeping the idle sockets',
592
+ err: e,
593
+ });
594
+ }
595
+ // Socket is "prepared" with idle handlers, continue with our request
596
+ pipeStream();
597
+ // This is for request timeout only. Surprisingly, it is not always enough to set in the HTTP request.
598
+ // The socket won't be destroyed, and it will be returned to the pool.
599
+ socket.setTimeout(this.params.request_timeout, onTimeout);
600
+ };
601
+ function onTimeout() {
602
+ const err = (0, client_common_1.enhanceStackTrace)(new Error('Timeout error.'), currentStackTrace);
603
+ removeRequestListeners();
604
+ try {
605
+ request.destroy();
606
+ }
607
+ catch (e) {
608
+ logger.error({
609
+ message: 'An error occurred while destroying the request',
610
+ err: e,
611
+ });
612
+ }
613
+ reject(err);
614
+ }
615
+ function removeRequestListeners() {
616
+ if (request.socket !== null) {
617
+ request.socket.setTimeout(0); // reset previously set timeout
618
+ request.socket.removeListener('timeout', onTimeout);
619
+ }
620
+ request.removeListener('socket', onSocket);
621
+ request.removeListener('response', onResponse);
622
+ request.removeListener('error', onError);
623
+ request.removeListener('close', onClose);
624
+ if (params.abort_signal !== undefined) {
625
+ request.removeListener('abort', onAbort);
626
+ }
627
+ }
628
+ request.on('socket', onSocket);
629
+ request.on('response', onResponse);
630
+ request.on('error', onError);
631
+ request.on('close', onClose);
632
+ if (params.abort_signal !== undefined) {
633
+ params.abort_signal.addEventListener('abort', onAbort, {
634
+ once: true,
635
+ });
636
+ }
637
+ if (!params.body) {
638
+ try {
639
+ return request.end();
640
+ }
641
+ catch (e) {
642
+ this.logger.error({
643
+ message: 'An error occurred while ending the request without body',
644
+ err: e,
645
+ });
646
+ }
647
+ }
648
+ });
649
+ }
450
650
  }
451
651
  exports.NodeBaseConnection = NodeBaseConnection;
652
+ const PingQuery = `SELECT 'ping'`;
452
653
  //# sourceMappingURL=node_base_connection.js.map