UNPKG

@sqlitecloud/drivers

Version:

SQLiteCloud drivers for Typescript/Javascript in edge, web and node clients

213 lines (212 loc) 12.9 kB
"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;