@sqlitecloud/drivers
Version:
SQLiteCloud drivers for Typescript/Javascript in edge, web and node clients
213 lines (212 loc) • 12.9 kB
JavaScript
"use strict";
/**
* transport-ws.ts - handles low level communication with sqlitecloud server via socket.io websocket
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.SQLiteCloudWebsocketConnection = void 0;
const socket_io_client_1 = require("socket.io-client");
const socket_io_parser_1 = require("socket.io-parser");
const connection_1 = require("./connection");
const rowset_1 = require("./rowset");
const types_1 = require("./types");
const utilities_1 = require("./utilities");
const SocketIODecoderBase = socket_io_parser_1.Decoder;
function createSocketIOParser(maxAttachments) {
class SQLiteCloudSocketIODecoder extends SocketIODecoderBase {
constructor(opts) {
var _a, _b;
const decoderOptions = typeof opts === 'function' ? { reviver: opts } : opts;
super(decoderOptions === null || decoderOptions === void 0 ? void 0 : decoderOptions.reviver);
this.opts || (this.opts = {});
this.opts.maxAttachments = Math.max((_b = (_a = this.opts.maxAttachments) !== null && _a !== void 0 ? _a : decoderOptions === null || decoderOptions === void 0 ? void 0 : decoderOptions.maxAttachments) !== null && _b !== void 0 ? _b : 0, maxAttachments);
}
}
return {
Encoder: socket_io_parser_1.Encoder,
Decoder: SQLiteCloudSocketIODecoder
};
}
function getResponseBlobTransferFormat(response) {
var _a;
return (0, utilities_1.parseWebsocketBlobTransferFormat)(((_a = response === null || response === void 0 ? void 0 : response.capabilities) === null || _a === void 0 ? void 0 : _a.blobTransferFormat) || (response === null || response === void 0 ? void 0 : response.blobTransferFormat), undefined);
}
function getAttachmentLimitError(description, limit) {
const descriptionMessage = description instanceof Error ? description.message : typeof description === 'string' ? description : '';
if (/illegal attachments/i.test(descriptionMessage)) {
return new types_1.SQLiteCloudError(`WebSocket blob response exceeded the configured Socket.IO attachment limit (${limit}). Use websocketBlobFormat=base64-blobs-v1 or increase websocketMaxAttachments.`, {
errorCode: 'ERR_WEBSOCKET_MAX_ATTACHMENTS_EXCEEDED',
cause: description
});
}
}
function getGatewayResponseError(response) {
if ((response === null || response === void 0 ? void 0 : response.error) && typeof response.error === 'object') {
return response.error;
}
if (Array.isArray(response === null || response === void 0 ? void 0 : response.errors) && response.errors[0] && typeof response.errors[0] === 'object') {
return response.errors[0];
}
}
/**
* Implementation of TransportConnection that connects to the database indirectly
* via SQLite Cloud Gateway, a socket.io based deamon that responds to sql query
* requests by returning results and rowsets in json format. The gateway handles
* connect, disconnect, retries, order of operations, timeouts, etc.
*/
class SQLiteCloudWebsocketConnection extends connection_1.SQLiteCloudConnection {
/** True if connection is open */
get connected() {
var _a;
return !!(this.socket && ((_a = this.socket) === null || _a === void 0 ? void 0 : _a.connected));
}
/* Opens a connection with the server and sends the initialization commands. Will throw in case of errors. */
connectTransport(config, callback) {
var _a;
try {
// connection established while we were waiting in line?
console.assert(!this.connected, 'Connection already established');
if (!this.socket) {
this.config = config;
const websocketMaxAttachments = (0, utilities_1.parseWebsocketMaxAttachments)(this.config.websocketMaxAttachments);
// Gateway tenant routing is derived from the Host header. In production, `gatewayurl` is
// a domain suffix (eg `gateway.sqlite.cloud`) appended to the tenant prefix from the core
// hostname (eg crvheg7dhk.g4 from crvheg7dhk.g4.sqlite.cloud) to form the gateway host
// (→ crvheg7dhk.g4.gateway.sqlite.cloud). For local development, pass a `gatewayurl`
// containing `localhost` — the driver routes TCP to it and injects the tenant Host header
// (with the gateway marker label) so the gateway still tenant-routes correctly.
const authToken = this.config.apikey || this.config.token;
const ioOpts = { auth: { token: authToken }, parser: createSocketIOParser(websocketMaxAttachments) };
let gatewayUrl;
if ((_a = this.config.gatewayurl) === null || _a === void 0 ? void 0 : _a.includes('localhost')) {
const raw = this.config.gatewayurl;
gatewayUrl = raw.startsWith('ws://') || raw.startsWith('wss://') ? raw : `ws://${raw}`;
ioOpts.extraHeaders = { Host: buildGatewayHost(this.config.host) };
ioOpts.transports = ['websocket'];
}
else {
const gatewayHost = buildGatewayHost(this.config.host, this.config.gatewayurl);
gatewayUrl = `wss://${gatewayHost}:443`;
}
this.socket = (0, socket_io_client_1.io)(gatewayUrl, ioOpts);
this.socket.on('connect', () => {
callback === null || callback === void 0 ? void 0 : callback.call(this, null);
});
this.socket.on('disconnect', (reason, description) => {
this.close();
callback === null || callback === void 0 ? void 0 : callback.call(this, (reason === 'parse error' && getAttachmentLimitError(description, websocketMaxAttachments)) ||
new types_1.SQLiteCloudError('Disconnected', { errorCode: 'ERR_CONNECTION_ENDED', cause: reason }));
});
this.socket.on('connect_error', (error) => {
this.close();
if ((error === null || error === void 0 ? void 0 : error.message) === 'parse error' || (error === null || error === void 0 ? void 0 : error.cause) === 'parse error') {
callback === null || callback === void 0 ? void 0 : callback.call(this, getAttachmentLimitError((error === null || error === void 0 ? void 0 : error.description) || (error === null || error === void 0 ? void 0 : error.data) || (error === null || error === void 0 ? void 0 : error.cause), websocketMaxAttachments) ||
new types_1.SQLiteCloudError('Connection error', { errorCode: 'ERR_CONNECTION_ERROR', cause: error }));
return;
}
let message = error.message || 'Connection error';
if (typeof error.context == 'object' && error.context.responseText) {
try {
const parsed = JSON.parse(error.context.responseText);
message = (parsed === null || parsed === void 0 ? void 0 : parsed.message) || error.context.responseText;
}
catch (_a) {
message = error.context.responseText;
}
}
callback === null || callback === void 0 ? void 0 : callback.call(this, new types_1.SQLiteCloudError(message, { errorCode: 'ERR_CONNECTION_ERROR' }));
});
this.socket.on('error', (error) => {
this.close();
callback === null || callback === void 0 ? void 0 : callback.call(this, new types_1.SQLiteCloudError('Connection error', { errorCode: 'ERR_CONNECTION_ERROR', cause: error }));
});
}
}
catch (error) {
callback === null || callback === void 0 ? void 0 : callback.call(this, error);
}
return this;
}
/** Will send a command immediately (no queueing), return the rowset/result or throw an error */
transportCommands(commands, callback) {
// 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 };
}
this.socket.emit('GET /v2/weblite/sql', {
sql: commands.query,
bind: (0, utilities_1.encodeBigIntMarkers)(commands.parameters),
database: this.config.database,
row: 'array',
safe_integer_mode: this.config.safe_integer_mode,
capabilities: {
blobTransferFormat: this.config.websocketBlobFormat || types_1.DEFAULT_WEBSOCKET_BLOB_TRANSFER_FORMAT
}
}, (response) => {
const gatewayError = getGatewayResponseError(response);
if (gatewayError) {
// Gateway error fields (errorCode, externalErrorCode, offsetCode) live under `meta`
// for the `errors[]` shape; fall back to top-level for the legacy `error` shape.
const errorFields = (gatewayError.meta && typeof gatewayError.meta === 'object') ? gatewayError.meta : gatewayError;
const error = new types_1.SQLiteCloudError(gatewayError.detail || gatewayError.message || 'Gateway error', Object.assign({}, errorFields));
callback === null || callback === void 0 ? void 0 : callback.call(this, error);
}
else {
const { metadata } = response;
const blobTransferFormat = getResponseBlobTransferFormat(response);
const data = metadata && metadata.numberOfRows !== undefined && metadata.numberOfColumns !== undefined && metadata.columns !== undefined
? (0, utilities_1.decodeWebsocketRowsetData)(response === null || response === void 0 ? void 0 : response.data, metadata, this.config.safe_integer_mode, blobTransferFormat)
: (0, utilities_1.decodeBigIntMarkers)(response === null || response === void 0 ? void 0 : response.data, this.config.safe_integer_mode);
if (data !== undefined && metadata) {
if (metadata.numberOfRows !== undefined && metadata.numberOfColumns !== undefined && metadata.columns !== undefined) {
console.assert(Array.isArray(data), 'SQLiteCloudWebsocketConnection.transportCommands - data is not an array');
// we can recreate a SQLiteCloudRowset from the response which we know to be an array of arrays
const rowset = new rowset_1.SQLiteCloudRowset(metadata, data.flat());
callback === null || callback === void 0 ? void 0 : callback.call(this, null, rowset);
return;
}
}
callback === null || callback === void 0 ? void 0 : callback.call(this, null, data);
}
});
return this;
}
/** Disconnect socket.io from server */
close() {
var _a, _b;
console.assert(this.socket !== null, 'SQLiteCloudWebsocketConnection.close - connection already closed');
if (this.socket) {
(_a = this.socket) === null || _a === void 0 ? void 0 : _a.removeAllListeners();
(_b = this.socket) === null || _b === void 0 ? void 0 : _b.close();
this.socket = undefined;
}
this.operations.clear();
return this;
}
}
exports.SQLiteCloudWebsocketConnection = SQLiteCloudWebsocketConnection;
/** Default gateway domain suffix used when `gatewayurl` is not provided. */
const DEFAULT_GATEWAY_DOMAIN = 'gateway.sqlite.cloud';
/** Builds the gateway hostname from a core hostname, swapping its last two labels (the
* TLD) with the given `gatewayurl` suffix.
*
* Example: buildGatewayHost('crvheg7dhk.g4.sqlite.cloud')
* → 'crvheg7dhk.g4.gateway.sqlite.cloud'
*
* Returns `host` unchanged when it already ends with the suffix (idempotent) or when
* it's too short to extract a tenant prefix (eg 'localhost'). */
function buildGatewayHost(host, gatewayurl) {
if (!host)
return host;
const suffix = gatewayurl || DEFAULT_GATEWAY_DOMAIN;
if (host === suffix || host.endsWith('.' + suffix))
return host;
const parts = host.split('.');
if (parts.length < 3)
return host;
return parts.slice(0, -2).join('.') + '.' + suffix;
}
exports.default = SQLiteCloudWebsocketConnection;