@clickhouse/client
Advanced tools
Comparing version 1.0.1 to 1.0.2
@@ -64,7 +64,9 @@ "use strict"; | ||
values_encoder: new utils_1.NodeValuesEncoder(), | ||
make_result_set: ((stream, format, query_id) => new result_set_1.ResultSet(stream, format, query_id)), | ||
close_stream: async (stream) => { | ||
stream.destroy(); | ||
}, | ||
make_result_set: ((stream, format, query_id, log_error) => result_set_1.ResultSet.instance({ | ||
stream, | ||
format, | ||
query_id, | ||
log_error, | ||
})), | ||
}; | ||
//# sourceMappingURL=config.js.map |
/// <reference types="node" /> | ||
/// <reference types="node" /> | ||
/// <reference types="node" /> | ||
import type { ConnBaseQueryParams, Connection, ConnectionParams, ConnExecResult, ConnInsertParams, ConnInsertResult, ConnPingResult, ConnQueryResult } from '@clickhouse/client-common'; | ||
import type { ConnBaseQueryParams, ConnCommandResult, Connection, ConnectionParams, ConnExecResult, ConnInsertParams, ConnInsertResult, ConnPingResult, ConnQueryResult } from '@clickhouse/client-common'; | ||
import type Http from 'http'; | ||
@@ -40,10 +40,10 @@ import Stream from 'stream'; | ||
protected constructor(params: NodeConnectionParams, agent: Http.Agent); | ||
protected buildDefaultHeaders(username: string, password: string, additional_http_headers?: Record<string, string>): Http.OutgoingHttpHeaders; | ||
protected abstract createClientRequest(params: RequestParams): Http.ClientRequest; | ||
private request; | ||
ping(): Promise<ConnPingResult>; | ||
query(params: ConnBaseQueryParams): Promise<ConnQueryResult<Stream.Readable>>; | ||
insert(params: ConnInsertParams<Stream.Readable>): Promise<ConnInsertResult>; | ||
exec(params: ConnBaseQueryParams): Promise<ConnExecResult<Stream.Readable>>; | ||
insert(params: ConnInsertParams<Stream.Readable>): Promise<ConnInsertResult>; | ||
command(params: ConnBaseQueryParams): Promise<ConnCommandResult>; | ||
close(): Promise<void>; | ||
protected buildDefaultHeaders(username: string, password: string, additional_http_headers?: Record<string, string>): Http.OutgoingHttpHeaders; | ||
protected abstract createClientRequest(params: RequestParams): Http.ClientRequest; | ||
private getQueryId; | ||
@@ -55,2 +55,4 @@ private getAbortController; | ||
private parseSummary; | ||
private runExec; | ||
private request; | ||
} |
@@ -56,169 +56,2 @@ "use strict"; | ||
} | ||
buildDefaultHeaders(username, password, additional_http_headers) { | ||
return { | ||
// KeepAlive agent for some reason does not set this on its own | ||
Connection: this.params.keep_alive.enabled ? 'keep-alive' : 'close', | ||
Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`, | ||
'User-Agent': (0, utils_1.getUserAgent)(this.params.application_id), | ||
...additional_http_headers, | ||
}; | ||
} | ||
async request(params, op) { | ||
return new Promise((resolve, reject) => { | ||
const start = Date.now(); | ||
const request = this.createClientRequest(params); | ||
function onError(err) { | ||
removeRequestListeners(); | ||
reject(err); | ||
} | ||
const onResponse = async (_response) => { | ||
this.logResponse(op, request, params, _response, start); | ||
const decompressionResult = (0, compression_1.decompressResponse)(_response); | ||
if ((0, compression_1.isDecompressionError)(decompressionResult)) { | ||
return reject(decompressionResult.error); | ||
} | ||
if ((0, client_common_1.isSuccessfulResponse)(_response.statusCode)) { | ||
return resolve({ | ||
stream: decompressionResult.response, | ||
summary: params.parse_summary | ||
? this.parseSummary(op, _response) | ||
: undefined, | ||
}); | ||
} | ||
else { | ||
reject((0, client_common_1.parseError)(await (0, utils_1.getAsText)(decompressionResult.response))); | ||
} | ||
}; | ||
function onAbort() { | ||
// Prefer 'abort' event since it always triggered unlike 'error' and 'close' | ||
// see the full sequence of events https://nodejs.org/api/http.html#httprequesturl-options-callback | ||
removeRequestListeners(); | ||
request.once('error', function () { | ||
/** | ||
* catch "Error: ECONNRESET" error which shouldn't be reported to users. | ||
* see the full sequence of events https://nodejs.org/api/http.html#httprequesturl-options-callback | ||
* */ | ||
}); | ||
reject(new Error('The user aborted a request.')); | ||
} | ||
function onClose() { | ||
// Adapter uses 'close' event to clean up listeners after the successful response. | ||
// It's necessary in order to handle 'abort' and 'timeout' events while response is streamed. | ||
// It's always the last event, according to https://nodejs.org/docs/latest-v14.x/api/http.html#http_http_request_url_options_callback | ||
removeRequestListeners(); | ||
} | ||
function pipeStream() { | ||
// if request.end() was called due to no data to send | ||
if (request.writableEnded) { | ||
return; | ||
} | ||
const bodyStream = (0, utils_1.isStream)(params.body) | ||
? params.body | ||
: stream_1.default.Readable.from([params.body]); | ||
const callback = (err) => { | ||
if (err) { | ||
removeRequestListeners(); | ||
reject(err); | ||
} | ||
}; | ||
if (params.compress_request) { | ||
stream_1.default.pipeline(bodyStream, zlib_1.default.createGzip(), request, callback); | ||
} | ||
else { | ||
stream_1.default.pipeline(bodyStream, request, callback); | ||
} | ||
} | ||
const onSocket = (socket) => { | ||
if (this.params.keep_alive.enabled) { | ||
const socketInfo = this.knownSockets.get(socket); | ||
// It is the first time we encounter this socket, | ||
// so it doesn't have the idle timeout handler attached to it | ||
if (socketInfo === undefined) { | ||
const socketId = crypto_1.default.randomUUID(); | ||
this.logger.trace({ | ||
message: `Using a fresh socket ${socketId}, setting up a new 'free' listener`, | ||
}); | ||
this.knownSockets.set(socket, { | ||
id: socketId, | ||
idle_timeout_handle: undefined, | ||
}); | ||
// When the request is complete and the socket is released, | ||
// make sure that the socket is removed after `idleSocketTTL`. | ||
socket.on('free', () => { | ||
this.logger.trace({ | ||
message: `Socket ${socketId} was released`, | ||
}); | ||
// Avoiding the built-in socket.timeout() method usage here, | ||
// as we don't want to clash with the actual request timeout. | ||
const idleTimeoutHandle = setTimeout(() => { | ||
this.logger.trace({ | ||
message: `Removing socket ${socketId} after ${this.idleSocketTTL} ms of idle`, | ||
}); | ||
this.knownSockets.delete(socket); | ||
socket.destroy(); | ||
}, this.idleSocketTTL).unref(); | ||
this.knownSockets.set(socket, { | ||
id: socketId, | ||
idle_timeout_handle: idleTimeoutHandle, | ||
}); | ||
}); | ||
const cleanup = () => { | ||
const maybeSocketInfo = this.knownSockets.get(socket); | ||
// clean up a possibly dangling idle timeout handle (preventing leaks) | ||
if (maybeSocketInfo?.idle_timeout_handle) { | ||
clearTimeout(maybeSocketInfo.idle_timeout_handle); | ||
} | ||
this.logger.trace({ | ||
message: `Socket ${socketId} was closed or ended, 'free' listener removed`, | ||
}); | ||
}; | ||
socket.once('end', cleanup); | ||
socket.once('close', cleanup); | ||
} | ||
else { | ||
clearTimeout(socketInfo.idle_timeout_handle); | ||
this.logger.trace({ | ||
message: `Reusing socket ${socketInfo.id}`, | ||
}); | ||
this.knownSockets.set(socket, { | ||
...socketInfo, | ||
idle_timeout_handle: undefined, | ||
}); | ||
} | ||
} | ||
// Socket is "prepared" with idle handlers, continue with our request | ||
pipeStream(); | ||
// This is for request timeout only. Surprisingly, it is not always enough to set in the HTTP request. | ||
// The socket won't be actually destroyed, and it will be returned to the pool. | ||
socket.setTimeout(this.params.request_timeout, onTimeout); | ||
}; | ||
function onTimeout() { | ||
removeRequestListeners(); | ||
request.destroy(); | ||
reject(new Error('Timeout error.')); | ||
} | ||
function removeRequestListeners() { | ||
if (request.socket !== null) { | ||
request.socket.setTimeout(0); // reset previously set timeout | ||
request.socket.removeListener('timeout', onTimeout); | ||
} | ||
request.removeListener('socket', onSocket); | ||
request.removeListener('response', onResponse); | ||
request.removeListener('error', onError); | ||
request.removeListener('close', onClose); | ||
if (params.abort_signal !== undefined) { | ||
request.removeListener('abort', onAbort); | ||
} | ||
} | ||
request.on('socket', onSocket); | ||
request.on('response', onResponse); | ||
request.on('error', onError); | ||
request.on('close', onClose); | ||
if (params.abort_signal !== undefined) { | ||
params.abort_signal.addEventListener('abort', onAbort, { once: true }); | ||
} | ||
if (!params.body) | ||
return request.end(); | ||
}); | ||
} | ||
async ping() { | ||
@@ -294,44 +127,2 @@ const abortController = new AbortController(); | ||
} | ||
async exec(params) { | ||
const query_id = this.getQueryId(params.query_id); | ||
const searchParams = (0, client_common_1.toSearchParams)({ | ||
database: this.params.database, | ||
clickhouse_settings: params.clickhouse_settings, | ||
query_params: params.query_params, | ||
session_id: params.session_id, | ||
query_id, | ||
}); | ||
const { controller, controllerCleanup } = this.getAbortController(params); | ||
try { | ||
const { stream, summary } = await this.request({ | ||
method: 'POST', | ||
url: (0, client_common_1.transformUrl)({ url: this.params.url, searchParams }), | ||
body: params.query, | ||
abort_signal: controller.signal, | ||
parse_summary: true, | ||
}, 'Exec'); | ||
return { | ||
stream, | ||
query_id, | ||
summary, | ||
}; | ||
} | ||
catch (err) { | ||
controller.abort('Exec HTTP request failed'); | ||
this.logRequestError({ | ||
op: 'Exec', | ||
query_id: query_id, | ||
query_params: params, | ||
search_params: searchParams, | ||
err: err, | ||
extra_args: { | ||
clickhouse_settings: params.clickhouse_settings ?? {}, | ||
}, | ||
}); | ||
throw err; // should be propagated to the user | ||
} | ||
finally { | ||
controllerCleanup(); | ||
} | ||
} | ||
async insert(params) { | ||
@@ -378,2 +169,17 @@ const query_id = this.getQueryId(params.query_id); | ||
} | ||
async exec(params) { | ||
return this.runExec({ | ||
...params, | ||
op: 'Exec', | ||
}); | ||
} | ||
async command(params) { | ||
const { stream, query_id, summary } = await this.runExec({ | ||
...params, | ||
op: 'Command', | ||
}); | ||
// ignore the response stream and release the socket immediately | ||
await (0, stream_2.drainStream)(stream); | ||
return { query_id, summary }; | ||
} | ||
async close() { | ||
@@ -384,2 +190,11 @@ if (this.agent !== undefined && this.agent.destroy !== undefined) { | ||
} | ||
buildDefaultHeaders(username, password, additional_http_headers) { | ||
return { | ||
// KeepAlive agent for some reason does not set this on its own | ||
Connection: this.params.keep_alive.enabled ? 'keep-alive' : 'close', | ||
Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`, | ||
'User-Agent': (0, utils_1.getUserAgent)(this.params.application_id), | ||
...additional_http_headers, | ||
}; | ||
} | ||
getQueryId(query_id) { | ||
@@ -454,4 +269,204 @@ return query_id || crypto_1.default.randomUUID(); | ||
} | ||
async runExec(params) { | ||
const query_id = this.getQueryId(params.query_id); | ||
const searchParams = (0, client_common_1.toSearchParams)({ | ||
database: this.params.database, | ||
clickhouse_settings: params.clickhouse_settings, | ||
query_params: params.query_params, | ||
session_id: params.session_id, | ||
query_id, | ||
}); | ||
const { controller, controllerCleanup } = this.getAbortController(params); | ||
try { | ||
const { stream, summary } = await this.request({ | ||
method: 'POST', | ||
url: (0, client_common_1.transformUrl)({ url: this.params.url, searchParams }), | ||
body: params.query, | ||
abort_signal: controller.signal, | ||
parse_summary: true, | ||
}, params.op); | ||
return { | ||
stream, | ||
query_id, | ||
summary, | ||
}; | ||
} | ||
catch (err) { | ||
controller.abort(`${params.op} HTTP request failed`); | ||
this.logRequestError({ | ||
op: params.op, | ||
query_id: query_id, | ||
query_params: params, | ||
search_params: searchParams, | ||
err: err, | ||
extra_args: { | ||
clickhouse_settings: params.clickhouse_settings ?? {}, | ||
}, | ||
}); | ||
throw err; // should be propagated to the user | ||
} | ||
finally { | ||
controllerCleanup(); | ||
} | ||
} | ||
async request(params, op) { | ||
return new Promise((resolve, reject) => { | ||
const start = Date.now(); | ||
const request = this.createClientRequest(params); | ||
function onError(err) { | ||
removeRequestListeners(); | ||
reject(err); | ||
} | ||
const onResponse = async (_response) => { | ||
this.logResponse(op, request, params, _response, start); | ||
const decompressionResult = (0, compression_1.decompressResponse)(_response); | ||
if ((0, compression_1.isDecompressionError)(decompressionResult)) { | ||
return reject(decompressionResult.error); | ||
} | ||
if ((0, client_common_1.isSuccessfulResponse)(_response.statusCode)) { | ||
return resolve({ | ||
stream: decompressionResult.response, | ||
summary: params.parse_summary | ||
? this.parseSummary(op, _response) | ||
: undefined, | ||
}); | ||
} | ||
else { | ||
reject((0, client_common_1.parseError)(await (0, utils_1.getAsText)(decompressionResult.response))); | ||
} | ||
}; | ||
function onAbort() { | ||
// Prefer 'abort' event since it always triggered unlike 'error' and 'close' | ||
// see the full sequence of events https://nodejs.org/api/http.html#httprequesturl-options-callback | ||
removeRequestListeners(); | ||
request.once('error', function () { | ||
/** | ||
* catch "Error: ECONNRESET" error which shouldn't be reported to users. | ||
* see the full sequence of events https://nodejs.org/api/http.html#httprequesturl-options-callback | ||
* */ | ||
}); | ||
reject(new Error('The user aborted a request.')); | ||
} | ||
function onClose() { | ||
// Adapter uses 'close' event to clean up listeners after the successful response. | ||
// It's necessary in order to handle 'abort' and 'timeout' events while response is streamed. | ||
// It's always the last event, according to https://nodejs.org/docs/latest-v14.x/api/http.html#http_http_request_url_options_callback | ||
removeRequestListeners(); | ||
} | ||
function pipeStream() { | ||
// if request.end() was called due to no data to send | ||
if (request.writableEnded) { | ||
return; | ||
} | ||
const bodyStream = (0, utils_1.isStream)(params.body) | ||
? params.body | ||
: stream_1.default.Readable.from([params.body]); | ||
const callback = (err) => { | ||
if (err) { | ||
removeRequestListeners(); | ||
reject(err); | ||
} | ||
}; | ||
if (params.compress_request) { | ||
stream_1.default.pipeline(bodyStream, zlib_1.default.createGzip(), request, callback); | ||
} | ||
else { | ||
stream_1.default.pipeline(bodyStream, request, callback); | ||
} | ||
} | ||
const onSocket = (socket) => { | ||
if (this.params.keep_alive.enabled) { | ||
const socketInfo = this.knownSockets.get(socket); | ||
// It is the first time we encounter this socket, | ||
// so it doesn't have the idle timeout handler attached to it | ||
if (socketInfo === undefined) { | ||
const socketId = crypto_1.default.randomUUID(); | ||
this.logger.trace({ | ||
message: `Using a fresh socket ${socketId}, setting up a new 'free' listener`, | ||
}); | ||
this.knownSockets.set(socket, { | ||
id: socketId, | ||
idle_timeout_handle: undefined, | ||
}); | ||
// When the request is complete and the socket is released, | ||
// make sure that the socket is removed after `idleSocketTTL`. | ||
socket.on('free', () => { | ||
this.logger.trace({ | ||
message: `Socket ${socketId} was released`, | ||
}); | ||
// Avoiding the built-in socket.timeout() method usage here, | ||
// as we don't want to clash with the actual request timeout. | ||
const idleTimeoutHandle = setTimeout(() => { | ||
this.logger.trace({ | ||
message: `Removing socket ${socketId} after ${this.idleSocketTTL} ms of idle`, | ||
}); | ||
this.knownSockets.delete(socket); | ||
socket.destroy(); | ||
}, this.idleSocketTTL).unref(); | ||
this.knownSockets.set(socket, { | ||
id: socketId, | ||
idle_timeout_handle: idleTimeoutHandle, | ||
}); | ||
}); | ||
const cleanup = () => { | ||
const maybeSocketInfo = this.knownSockets.get(socket); | ||
// clean up a possibly dangling idle timeout handle (preventing leaks) | ||
if (maybeSocketInfo?.idle_timeout_handle) { | ||
clearTimeout(maybeSocketInfo.idle_timeout_handle); | ||
} | ||
this.logger.trace({ | ||
message: `Socket ${socketId} was closed or ended, 'free' listener removed`, | ||
}); | ||
}; | ||
socket.once('end', cleanup); | ||
socket.once('close', cleanup); | ||
} | ||
else { | ||
clearTimeout(socketInfo.idle_timeout_handle); | ||
this.logger.trace({ | ||
message: `Reusing socket ${socketInfo.id}`, | ||
}); | ||
this.knownSockets.set(socket, { | ||
...socketInfo, | ||
idle_timeout_handle: undefined, | ||
}); | ||
} | ||
} | ||
// Socket is "prepared" with idle handlers, continue with our request | ||
pipeStream(); | ||
// This is for request timeout only. Surprisingly, it is not always enough to set in the HTTP request. | ||
// The socket won't be actually destroyed, and it will be returned to the pool. | ||
socket.setTimeout(this.params.request_timeout, onTimeout); | ||
}; | ||
function onTimeout() { | ||
removeRequestListeners(); | ||
request.destroy(); | ||
reject(new Error('Timeout error.')); | ||
} | ||
function removeRequestListeners() { | ||
if (request.socket !== null) { | ||
request.socket.setTimeout(0); // reset previously set timeout | ||
request.socket.removeListener('timeout', onTimeout); | ||
} | ||
request.removeListener('socket', onSocket); | ||
request.removeListener('response', onResponse); | ||
request.removeListener('error', onError); | ||
request.removeListener('close', onClose); | ||
if (params.abort_signal !== undefined) { | ||
request.removeListener('abort', onAbort); | ||
} | ||
} | ||
request.on('socket', onSocket); | ||
request.on('response', onResponse); | ||
request.on('error', onError); | ||
request.on('close', onClose); | ||
if (params.abort_signal !== undefined) { | ||
params.abort_signal.addEventListener('abort', onAbort, { once: true }); | ||
} | ||
if (!params.body) | ||
return request.end(); | ||
}); | ||
} | ||
} | ||
exports.NodeBaseConnection = NodeBaseConnection; | ||
//# sourceMappingURL=node_base_connection.js.map |
@@ -22,2 +22,8 @@ /// <reference types="node" /> | ||
}; | ||
export interface ResultSetOptions<Format extends DataFormat> { | ||
stream: Stream.Readable; | ||
format: Format; | ||
query_id: string; | ||
log_error: (error: Error) => void; | ||
} | ||
export declare class ResultSet<Format extends DataFormat | unknown> implements BaseResultSet<Stream.Readable, Format> { | ||
@@ -27,3 +33,4 @@ private _stream; | ||
readonly query_id: string; | ||
constructor(_stream: Stream.Readable, format: Format, query_id: string); | ||
private readonly log_error; | ||
constructor(_stream: Stream.Readable, format: Format, query_id: string, log_error?: (error: Error) => void); | ||
/** See {@link BaseResultSet.text}. */ | ||
@@ -36,2 +43,3 @@ text(): Promise<string>; | ||
close(): void; | ||
static instance<Format extends DataFormat>({ stream, format, query_id, log_error, }: ResultSetOptions<Format>): ResultSet<Format>; | ||
} |
@@ -33,3 +33,3 @@ "use strict"; | ||
class ResultSet { | ||
constructor(_stream, format, query_id) { | ||
constructor(_stream, format, query_id, log_error) { | ||
Object.defineProperty(this, "_stream", { | ||
@@ -53,2 +53,10 @@ enumerable: true, | ||
}); | ||
Object.defineProperty(this, "log_error", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: void 0 | ||
}); | ||
// eslint-disable-next-line no-console | ||
this.log_error = log_error ?? ((err) => console.error(err)); | ||
} | ||
@@ -100,2 +108,3 @@ /** See {@link BaseResultSet.text}. */ | ||
let incompleteChunks = []; | ||
const logError = this.log_error; | ||
const toRows = new stream_1.Transform({ | ||
@@ -154,6 +163,6 @@ transform(chunk, _encoding, callback) { | ||
const pipeline = stream_1.default.pipeline(this._stream, toRows, function pipelineCb(err) { | ||
if (err) { | ||
// FIXME: use logger instead | ||
// eslint-disable-next-line no-console | ||
console.error(err); | ||
if (err && | ||
err.name !== 'AbortError' && | ||
err.message !== resultSetClosedMessage) { | ||
logError(err); | ||
} | ||
@@ -164,7 +173,11 @@ }); | ||
close() { | ||
this._stream.destroy(); | ||
this._stream.destroy(new Error(resultSetClosedMessage)); | ||
} | ||
static instance({ stream, format, query_id, log_error, }) { | ||
return new ResultSet(stream, format, query_id, log_error); | ||
} | ||
} | ||
exports.ResultSet = ResultSet; | ||
const streamAlreadyConsumedMessage = 'Stream has been already consumed'; | ||
const resultSetClosedMessage = 'ResultSet has been closed'; | ||
//# sourceMappingURL=result_set.js.map |
@@ -1,2 +0,2 @@ | ||
declare const _default: "1.0.1"; | ||
declare const _default: "1.0.2"; | ||
export default _default; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.default = '1.0.1'; | ||
exports.default = '1.0.2'; | ||
//# sourceMappingURL=version.js.map |
@@ -5,3 +5,3 @@ { | ||
"homepage": "https://clickhouse.com", | ||
"version": "1.0.1", | ||
"version": "1.0.2", | ||
"license": "Apache-2.0", | ||
@@ -27,4 +27,4 @@ "keywords": [ | ||
"dependencies": { | ||
"@clickhouse/client-common": "1.0.1" | ||
"@clickhouse/client-common": "1.0.2" | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
99893
1314
+ Added@clickhouse/client-common@1.0.2(transitive)
- Removed@clickhouse/client-common@1.0.1(transitive)