@sqlitecloud/drivers
Version:
SQLiteCloud drivers for Typescript/Javascript in edge, web and node clients
248 lines (247 loc) • 13.2 kB
JavaScript
"use strict";
/**
* connection-tls.ts - connection via tls socket and sqlitecloud protocol
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.SQLiteCloudTlsConnection = void 0;
const connection_1 = require("./connection");
const protocol_1 = require("./protocol");
const types_1 = require("./types");
const utilities_1 = require("./utilities");
const safe_imports_1 = require("./safe-imports");
// explicitly importing buffer library to allow cross-platform support by replacing it
// In React Native: Metro resolves 'buffer' to '@craftzdog/react-native-buffer' via package.json react-native field
// In Web/Node: Uses standard buffer package
const Buffer = (0, safe_imports_1.getSafeBuffer)();
// In React Native: Metro resolves 'tls' to 'react-native-tcp-socket' via package.json react-native field
// In Node: Uses native tls module
// In Browser: Returns null (browser field sets tls to false)
const tls = (0, safe_imports_1.getSafeTLS)();
/**
* Implementation of SQLiteCloudConnection that connects to the database using specific tls APIs
* that connect to native sockets or tls sockets and communicates via raw, binary protocol.
*/
class SQLiteCloudTlsConnection extends connection_1.SQLiteCloudConnection {
constructor() {
super(...arguments);
// processCommands sets up empty buffers, results callback then send the command to the server via socket.write
// onData is called when data is received, it will process the data until all data is retrieved for a response
// when response is complete or there's an error, finish is called to call the results callback set by processCommands...
// buffer to accumulate incoming data until an whole command is received and can be parsed
this.buffer = Buffer.alloc(0);
this.startedOn = new Date();
this.pendingChunks = [];
}
/** True if connection is open */
get connected() {
return !!this.socket;
}
/* Opens a connection with the server and sends the initialization commands. Will throw in case of errors. */
connectTransport(config, callback) {
console.assert(!this.connected, 'SQLiteCloudTlsConnection.connect - connection already established');
// Check if tls is available (it's null in browser contexts)
if (!tls) {
const error = new types_1.SQLiteCloudError('TLS connections are not available in this environment. Use WebSocket connections instead by setting usewebsocket: true in your configuration.', { errorCode: 'ERR_TLS_NOT_AVAILABLE' });
if (callback) {
callback.call(this, error);
return this;
}
throw error;
}
if (this.config.verbose) {
console.debug(`-> connecting ${config === null || config === void 0 ? void 0 : config.host}:${config === null || config === void 0 ? void 0 : config.port}`);
}
this.config = config;
const initializationCommands = (0, utilities_1.getInitializationCommands)(config);
// connect to plain socket, without encryption, only if insecure parameter specified
// this option is mainly for testing purposes and is not available on production nodes
// which would need to connect using tls and proper certificates as per code below
const connectionOptions = {
host: config.host,
port: config.port,
rejectUnauthorized: config.host != 'localhost',
// Server name for the SNI (Server Name Indication) TLS extension.
// https://r2.nodejs.org/docs/v6.11.4/api/tls.html#tls_class_tls_tlssocket
servername: config.host
};
// tls.connect in the react-native-tcp-socket library is tls.connectTLS
let connector = tls.connect;
// @ts-ignore
if (typeof tls.connectTLS !== 'undefined') {
// @ts-ignore
connector = tls.connectTLS;
}
this.processCallback = callback;
this.socket = connector(connectionOptions, () => {
var _a;
if (this.config.verbose) {
console.debug(`SQLiteCloudTlsConnection - connected to ${this.config.host}, authorized: ${(_a = this.socket) === null || _a === void 0 ? void 0 : _a.authorized}`);
}
this.transportCommands(initializationCommands, error => {
if (this.config.verbose) {
console.debug(`SQLiteCloudTlsConnection - initialized connection`);
}
callback === null || callback === void 0 ? void 0 : callback.call(this, error);
});
});
this.socket.setKeepAlive(true);
// disable Nagle algorithm because we want our writes to be sent ASAP
// https://brooker.co.za/blog/2024/05/09/nagle.html
this.socket.setNoDelay(true);
this.socket.on('data', (data) => {
this.processCommandsData(data);
});
this.socket.on('error', (error) => {
this.close();
this.processCommandsFinish(new types_1.SQLiteCloudError('Connection error', { errorCode: 'ERR_CONNECTION_ERROR', cause: error }));
});
this.socket.on('end', () => {
this.close();
if (this.processCallback)
this.processCommandsFinish(new types_1.SQLiteCloudError('Server ended the connection', { errorCode: 'ERR_CONNECTION_ENDED' }));
});
this.socket.on('close', () => {
this.close();
this.processCommandsFinish(new types_1.SQLiteCloudError('Connection closed', { errorCode: 'ERR_CONNECTION_CLOSED' }));
});
this.socket.on('timeout', () => {
this.close();
this.processCommandsFinish(new types_1.SQLiteCloudError('Connection ened due to timeout', { errorCode: 'ERR_CONNECTION_TIMEOUT' }));
});
return this;
}
/** Will send a command immediately (no queueing), return the rowset/result or throw an error */
transportCommands(commands, callback) {
var _a, _b, _c, _d, _e;
// connection needs to be established?
if (!this.socket) {
callback === null || callback === void 0 ? void 0 : callback.call(this, new types_1.SQLiteCloudError('Connection not established', { errorCode: 'ERR_CONNECTION_NOT_ESTABLISHED' }));
return this;
}
if (typeof commands === 'string') {
commands = { query: commands };
}
// reset buffer and rowset chunks, define response callback
this.buffer = Buffer.alloc(0);
this.startedOn = new Date();
this.processCallback = callback;
this.executingCommands = commands;
// compose commands following SCPC protocol
const formattedCommands = (0, protocol_1.formatCommand)(commands);
if ((_a = this.config) === null || _a === void 0 ? void 0 : _a.verbose) {
console.debug(`-> ${formattedCommands}`);
}
const timeoutMs = (_c = (_b = this.config) === null || _b === void 0 ? void 0 : _b.timeout) !== null && _c !== void 0 ? _c : 0;
if (timeoutMs > 0) {
const timeout = setTimeout(() => {
var _a;
callback === null || callback === void 0 ? void 0 : callback.call(this, new types_1.SQLiteCloudError('Connection timeout out', { errorCode: 'ERR_CONNECTION_TIMEOUT' }));
(_a = this.socket) === null || _a === void 0 ? void 0 : _a.destroy();
this.socket = undefined;
}, timeoutMs);
(_d = this.socket) === null || _d === void 0 ? void 0 : _d.write(formattedCommands, () => {
clearTimeout(timeout); // Clear the timeout on successful write
});
}
else {
(_e = this.socket) === null || _e === void 0 ? void 0 : _e.write(formattedCommands);
}
return this;
}
/** Handles data received in response to an outbound command sent by processCommands */
processCommandsData(data) {
var _a, _b, _c, _d, _e, _f, _g;
try {
// append data to buffer as it arrives
if (data.length && data.length > 0) {
// console.debug(`processCommandsData - received ${data.length} bytes`)
this.buffer = Buffer.concat([this.buffer, data]);
}
let dataType = (_a = this.buffer) === null || _a === void 0 ? void 0 : _a.subarray(0, 1).toString();
if ((0, protocol_1.hasCommandLength)(dataType)) {
const commandLength = (0, protocol_1.parseCommandLength)(this.buffer);
const hasReceivedEntireCommand = this.buffer.length - this.buffer.indexOf(' ') - 1 >= commandLength ? true : false;
if (hasReceivedEntireCommand) {
if ((_b = this.config) === null || _b === void 0 ? void 0 : _b.verbose) {
let bufferString = this.buffer.toString('utf8');
if (bufferString.length > 1000) {
bufferString = bufferString.substring(0, 100) + '...' + bufferString.substring(bufferString.length - 40);
}
const elapsedMs = new Date().getTime() - this.startedOn.getTime();
console.debug(`<- ${bufferString} (${bufferString.length} bytes, ${elapsedMs}ms)`);
}
// need to decompress this buffer before decoding?
if (dataType === protocol_1.CMD_COMPRESSED) {
const decompressResults = (0, protocol_1.decompressBuffer)(this.buffer);
if (decompressResults.dataType === protocol_1.CMD_ROWSET_CHUNK) {
this.pendingChunks.push(decompressResults.buffer);
this.buffer = decompressResults.remainingBuffer;
this.processCommandsData(Buffer.alloc(0));
return;
}
else {
const { data } = (0, protocol_1.popData)(decompressResults.buffer, this.config.safe_integer_mode);
(_c = this.processCommandsFinish) === null || _c === void 0 ? void 0 : _c.call(this, null, data);
}
}
else {
if (dataType !== protocol_1.CMD_ROWSET_CHUNK) {
const { data } = (0, protocol_1.popData)(this.buffer, this.config.safe_integer_mode);
(_d = this.processCommandsFinish) === null || _d === void 0 ? void 0 : _d.call(this, null, data);
}
else {
const completeChunk = (0, protocol_1.bufferEndsWith)(this.buffer, protocol_1.ROWSET_CHUNKS_END);
if (completeChunk) {
const parsedData = (0, protocol_1.parseRowsetChunks)([...this.pendingChunks, this.buffer], this.config.safe_integer_mode);
(_e = this.processCommandsFinish) === null || _e === void 0 ? void 0 : _e.call(this, null, parsedData);
}
}
}
}
}
else {
// command with no explicit len so make sure that the final character is a space
const lastChar = this.buffer.subarray(this.buffer.length - 1, this.buffer.length).toString('utf8');
if (lastChar == ' ') {
const { data } = (0, protocol_1.popData)(this.buffer, this.config.safe_integer_mode);
(_f = this.processCommandsFinish) === null || _f === void 0 ? void 0 : _f.call(this, null, data);
}
}
}
catch (error) {
console.error(`processCommandsData - error: ${error}`);
console.assert(error instanceof Error, 'An error occoured while processing data');
if (error instanceof Error) {
(_g = this.processCommandsFinish) === null || _g === void 0 ? void 0 : _g.call(this, error);
}
}
}
/** Completes a transaction initiated by processCommands */
processCommandsFinish(error, result) {
if (error) {
if (this.processCallback) {
console.error('processCommandsFinish - error', error);
}
else {
console.warn('processCommandsFinish - error with no registered callback', error);
}
}
if (this.processCallback) {
this.processCallback(error, result);
}
this.buffer = Buffer.alloc(0);
this.pendingChunks = [];
}
/** Disconnect immediately, release connection, no events. */
close() {
if (this.socket) {
this.socket.removeAllListeners();
this.socket.destroy();
this.socket = undefined;
}
this.operations.clear();
return this;
}
}
exports.SQLiteCloudTlsConnection = SQLiteCloudTlsConnection;
exports.default = SQLiteCloudTlsConnection;