From 347f7d3fb5d484ff72c96f4601e94dd9fedc065a Mon Sep 17 00:00:00 2001 From: Andrea Donetti Date: Wed, 20 May 2026 17:14:53 -0600 Subject: [PATCH 1/2] fix: allow larger socket.io binary rowsets socket.io-parser 4.2.6 rejects binary packets with more than 10 attachments by default. SQLite Cloud rowsets with BLOB values can exceed that count, causing the browser websocket driver to fail with a parser error before the binary payloads are delivered. Add socket.io-parser as an explicit dependency and pass a custom parser to socket.io-client. The custom decoder raises maxAttachments while preserving the standard encoder and normal Socket.IO transport negotiation. Add a focused unit assertion for the websocket connection options so the parser override remains wired into future builds. --- package-lock.json | 1 + package.json | 7 ++++--- src/drivers/connection-ws.ts | 21 ++++++++++++++++++++- test/connection-ws-unit.test.ts | 23 +++++++++++++++++++++++ 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 13aef8b..95e49f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "eventemitter3": "^5.0.1", "lz4js": "^0.2.0", "socket.io-client": "^4.8.1", + "socket.io-parser": "~4.2.4", "whatwg-url": "^14.2.0" }, "devDependencies": { diff --git a/package.json b/package.json index e2d430f..41f1147 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sqlitecloud/drivers", - "version": "1.0.837", + "version": "1.0.870", "description": "SQLiteCloud drivers for Typescript/Javascript in edge, web and node clients", "main": "./lib/index.js", "types": "./lib/index.d.ts", @@ -45,13 +45,14 @@ "buffer": "^6.0.3", "eventemitter3": "^5.0.1", "lz4js": "^0.2.0", - "socket.io-client": "^4.8.1", + "socket.io-client": "^4.8.3", + "socket.io-parser": "~4.2.4", "whatwg-url": "^14.2.0" }, "peerDependencies": { + "@craftzdog/react-native-buffer": "*", "react-native-quick-base64": "*", "react-native-tcp-socket": "*", - "@craftzdog/react-native-buffer": "*", "react-native-url-polyfill": "*" }, "peerDependenciesMeta": { diff --git a/src/drivers/connection-ws.ts b/src/drivers/connection-ws.ts index 5f3cf33..a7d178a 100644 --- a/src/drivers/connection-ws.ts +++ b/src/drivers/connection-ws.ts @@ -3,11 +3,30 @@ */ import { io, Socket } from 'socket.io-client' +import { Decoder as SocketIODecoder, Encoder as SocketIOEncoder } from 'socket.io-parser' import { SQLiteCloudConnection } from './connection' import { SQLiteCloudRowset } from './rowset' import { ErrorCallback, ResultsCallback, SQLiteCloudCommand, SQLiteCloudConfig, SQLiteCloudError } from './types' import { decodeBigIntMarkers, encodeBigIntMarkers } from './utilities' +const MAX_SOCKET_IO_ATTACHMENTS = 100000 +const SocketIODecoderBase = SocketIODecoder as unknown as new (...args: any[]) => { opts?: { maxAttachments?: number } } + +class SQLiteCloudSocketIODecoder extends SocketIODecoderBase { + constructor(opts?: any) { + super(typeof opts === 'function' ? opts : opts?.reviver) + + if (this.opts) { + this.opts.maxAttachments = Math.max(this.opts.maxAttachments ?? 0, MAX_SOCKET_IO_ATTACHMENTS) + } + } +} + +const sqliteCloudSocketIOParser = { + Encoder: SocketIOEncoder, + Decoder: SQLiteCloudSocketIODecoder +} + /** * Implementation of TransportConnection that connects to the database indirectly * via SQLite Cloud Gateway, a socket.io based deamon that responds to sql query @@ -32,7 +51,7 @@ export class SQLiteCloudWebsocketConnection extends SQLiteCloudConnection { this.config = config const connectionstring = this.config.connectionstring as string const gatewayUrl = this.config?.gatewayurl || `${this.config.host === 'localhost' ? 'ws' : 'wss'}://${this.config.host as string}:443` - this.socket = io(gatewayUrl, { auth: { token: connectionstring } }) + this.socket = io(gatewayUrl, { auth: { token: connectionstring }, parser: sqliteCloudSocketIOParser }) this.socket.on('connect', () => { callback?.call(this, null) diff --git a/test/connection-ws-unit.test.ts b/test/connection-ws-unit.test.ts index bc93aeb..cbb345f 100644 --- a/test/connection-ws-unit.test.ts +++ b/test/connection-ws-unit.test.ts @@ -3,10 +3,33 @@ */ import { describe, expect, it, jest } from '@jest/globals' +import { io } from 'socket.io-client' import { SQLiteCloudWebsocketConnection } from '../src/drivers/connection-ws' import { decodeBigIntMarkers, encodeBigIntMarkers } from '../src/drivers/utilities' +jest.mock('socket.io-client', () => ({ + io: jest.fn() +})) + describe('websocket bigint markers', () => { + it('should connect with a parser that allows large binary rowsets', () => { + const socket = { connected: false, on: jest.fn() } + const mockedIo = io as jest.MockedFunction + mockedIo.mockReturnValue(socket as any) + + const connection = Object.create(SQLiteCloudWebsocketConnection.prototype) as any + const connectionstring = 'sqlitecloud://host.sqlite.cloud/database?apikey=secret' + connection.connectTransport({ connectionstring, host: 'host.sqlite.cloud' }, jest.fn()) + + expect(mockedIo).toHaveBeenCalledWith('wss://host.sqlite.cloud:443', { + auth: { token: connectionstring }, + parser: expect.objectContaining({ + Encoder: expect.any(Function), + Decoder: expect.any(Function) + }) + }) + }) + it('should encode bigint values before sending JSON payloads', () => { expect( encodeBigIntMarkers({ From 976ea934d3176776c8043f19f7e6adaf88bbbed2 Mon Sep 17 00:00:00 2001 From: TizianoT Date: Thu, 21 May 2026 08:57:49 +0200 Subject: [PATCH 2/2] docs: add generic websocket browser example Add a minimal Vite + TypeScript example that queries SQLite Cloud over WebSocket from the browser. Uses a generic runQuery helper and consumes the driver from a locally packed tarball via npm run sync-driver, documented in the README. --- .gitignore | 3 + examples/with-websocket-browser/.env.example | 4 + examples/with-websocket-browser/.gitignore | 3 + examples/with-websocket-browser/README.md | 84 +++ examples/with-websocket-browser/index.html | 77 +++ .../with-websocket-browser/package-lock.json | 498 ++++++++++++++++++ examples/with-websocket-browser/package.json | 19 + examples/with-websocket-browser/src/main.ts | 53 ++ examples/with-websocket-browser/src/query.ts | 104 ++++ examples/with-websocket-browser/tsconfig.json | 18 + package.json | 2 +- 11 files changed, 864 insertions(+), 1 deletion(-) create mode 100644 examples/with-websocket-browser/.env.example create mode 100644 examples/with-websocket-browser/.gitignore create mode 100644 examples/with-websocket-browser/README.md create mode 100644 examples/with-websocket-browser/index.html create mode 100644 examples/with-websocket-browser/package-lock.json create mode 100644 examples/with-websocket-browser/package.json create mode 100644 examples/with-websocket-browser/src/main.ts create mode 100644 examples/with-websocket-browser/src/query.ts create mode 100644 examples/with-websocket-browser/tsconfig.json diff --git a/.gitignore b/.gitignore index 8597dc8..a868e47 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,6 @@ test/assets/testing-*.db test/assets/1brc/1brc_*.csv test/assets/1brc/1brc_*.sql reports/test-report.html + +# local tarball for examples (npm pack) +sqlitecloud-drivers.tgz diff --git a/examples/with-websocket-browser/.env.example b/examples/with-websocket-browser/.env.example new file mode 100644 index 0000000..3ac3a62 --- /dev/null +++ b/examples/with-websocket-browser/.env.example @@ -0,0 +1,4 @@ +# Optional: prefill the connection string field on load. +# Anything exposed via VITE_* is bundled into the client, so only use this for +# local development / demo databases — never ship a production secret this way. +VITE_DATABASE_URL=sqlitecloud://host.sqlite.cloud:8860/chinook.sqlite?apikey=YOUR_KEY diff --git a/examples/with-websocket-browser/.gitignore b/examples/with-websocket-browser/.gitignore new file mode 100644 index 0000000..9c97bbd --- /dev/null +++ b/examples/with-websocket-browser/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +.env diff --git a/examples/with-websocket-browser/README.md b/examples/with-websocket-browser/README.md new file mode 100644 index 0000000..276fdb7 --- /dev/null +++ b/examples/with-websocket-browser/README.md @@ -0,0 +1,84 @@ +# SQLite Cloud — WebSocket browser example + +A minimal [Vite](https://vitejs.dev/) + TypeScript app that uses +[`@sqlitecloud/drivers`](https://www.npmjs.com/package/@sqlitecloud/drivers) +to query SQLite Cloud **over WebSocket** from the browser. + +This example consumes the driver from a locally packed tarball +(`file:../../sqlitecloud-drivers.tgz`) so you can test changes in this repo +before publishing — see [Using a local build of the driver](#using-a-local-build-of-the-driver). + +In the browser the driver cannot open a raw TLS socket, so it automatically +connects through the **SQLite Cloud Gateway** using `socket.io` (WebSocket). +This example also passes `usewebsocket: true` explicitly so the same +`runQuery` helper works under Node.js too. + +## Files + +- [`src/query.ts`](src/query.ts) — opens a WebSocket connection, + runs one SQL statement and normalizes the result into `{ rows }`. +- [`src/main.ts`](src/main.ts) — wires the form in `index.html` to `runQuery`. +- [`index.html`](index.html) — small UI to enter a connection string + SQL. + +## Run it + +```bash +npm install # installs deps, incl. the locally packed @sqlitecloud/drivers tarball +npm run dev # starts Vite on http://localhost:5173 +``` + +Open the page, paste your SQLite Cloud connection string, e.g. + +``` +sqlitecloud://host.sqlite.cloud:8860/chinook.sqlite?apikey=YOUR_KEY +``` + +and click **Run query**. + +Optionally copy `.env.example` to `.env` and set `VITE_DATABASE_URL` to prefill +the connection string field. Note that any `VITE_*` value is bundled into the +client, so only use it for local/demo databases — never a production secret. + +## Pointing at a specific gateway + +By default the gateway URL is derived from the host (`wss://:443`). To +target a different gateway, add `gatewayurl` to the config in +[`src/main.ts`](src/main.ts): + +```ts +{ connectionstring, gatewayurl: 'wss://my-gateway.example.com:443' } +``` + +## Using a local build of the driver + +This example does not depend on the published npm package. Instead it installs +`@sqlitecloud/drivers` from a tarball built from this repo, pinned in +`package.json` as: + +```json +"@sqlitecloud/drivers": "file:../../sqlitecloud-drivers.tgz" +``` + +To rebuild that tarball from your current source and reinstall it, run: + +```bash +npm run sync-driver +``` + +This script: + +1. builds the driver in the repo root (`npm run build`); +2. runs `npm pack` to produce `sqlitecloud-drivers.tgz` — the exact tarball that + would be published to npm (same file set, same `package.json` exports); +3. clears `node_modules/@sqlitecloud`, the Vite cache, and the lockfile; +4. reinstalls from the fresh tarball. + +Because it consumes the packed tarball rather than a symlinked workspace, +`sync-driver` faithfully simulates a real `npm install @sqlitecloud/drivers` — +so you can validate local driver changes against this example before publishing. +Re-run it whenever you change the driver source. + +> `sync-driver` clears `node_modules/.vite`, so a plain `npm run dev` picks up the +> new build. If you ever rebuild the tarball without `sync-driver`, start Vite with +> `npm run dev -- --force` to bust its stale dependency cache (the `file:` version +> never changes, so Vite can't detect the new contents on its own). diff --git a/examples/with-websocket-browser/index.html b/examples/with-websocket-browser/index.html new file mode 100644 index 0000000..8ea079b --- /dev/null +++ b/examples/with-websocket-browser/index.html @@ -0,0 +1,77 @@ + + + + + + SQLite Cloud — WebSocket browser example + + + +

SQLite Cloud — WebSocket browser example

+

+ Installs @sqlitecloud/drivers from npm and queries SQLite Cloud over WebSocket (through the SQLite Cloud Gateway) using a + runQuery helper. +

+ + + + + + + +
+ + +
+ + + +

Result

+
+ + + + diff --git a/examples/with-websocket-browser/package-lock.json b/examples/with-websocket-browser/package-lock.json new file mode 100644 index 0000000..7725da9 --- /dev/null +++ b/examples/with-websocket-browser/package-lock.json @@ -0,0 +1,498 @@ +{ + "name": "with-websocket-browser", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "with-websocket-browser", + "version": "0.0.0", + "dependencies": { + "@sqlitecloud/drivers": "file:../../sqlitecloud-drivers.tgz" + }, + "devDependencies": { + "typescript": "^5.8.3", + "vite": "^5.4.11" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "license": "MIT" + }, + "node_modules/@sqlitecloud/drivers": { + "version": "1.0.870", + "resolved": "file:../../sqlitecloud-drivers.tgz", + "integrity": "sha512-IjbKzRXJH54mpxu96bfRVFr+7mxDXATB2MFF05UhvZ/MpejGeC02bazJ2ZWWy9e0vOcvKMsOWrl1bU3xkZnycQ==", + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3", + "eventemitter3": "^5.0.1", + "lz4js": "^0.2.0", + "socket.io-client": "^4.8.3", + "socket.io-parser": "~4.2.4", + "whatwg-url": "^14.2.0" + }, + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "@craftzdog/react-native-buffer": "*", + "react-native-quick-base64": "*", + "react-native-tcp-socket": "*", + "react-native-url-polyfill": "*" + }, + "peerDependenciesMeta": { + "@craftzdog/react-native-buffer": { + "optional": true + }, + "react-native-quick-base64": { + "optional": true + }, + "react-native-tcp-socket": { + "optional": true + }, + "react-native-url-polyfill": { + "optional": true + } + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/buffer": { + "version": "6.0.3", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-client": { + "version": "6.6.5", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.20.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/lz4js": { + "version": "0.2.0", + "license": "ISC" + }, + "node_modules/ms": { + "version": "2.1.3", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.15", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.6", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ws": { + "version": "8.20.1", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "engines": { + "node": ">=0.4.0" + } + } + } +} diff --git a/examples/with-websocket-browser/package.json b/examples/with-websocket-browser/package.json new file mode 100644 index 0000000..9bbf799 --- /dev/null +++ b/examples/with-websocket-browser/package.json @@ -0,0 +1,19 @@ +{ + "name": "with-websocket-browser", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "sync-driver": "(cd ../.. && npm run build && rm -f sqlitecloud-drivers.tgz && mv \"$(npm pack | tail -1)\" sqlitecloud-drivers.tgz) && rm -rf node_modules/@sqlitecloud node_modules/.vite package-lock.json && npm install" + }, + "dependencies": { + "@sqlitecloud/drivers": "file:../../sqlitecloud-drivers.tgz" + }, + "devDependencies": { + "typescript": "^5.8.3", + "vite": "^5.4.11" + } +} diff --git a/examples/with-websocket-browser/src/main.ts b/examples/with-websocket-browser/src/main.ts new file mode 100644 index 0000000..a7eb43d --- /dev/null +++ b/examples/with-websocket-browser/src/main.ts @@ -0,0 +1,53 @@ +// +// main.ts - wires the small UI in index.html to `runQuery` +// + +import { runQuery, isClientError } from './query' + +const $ = (id: string) => document.getElementById(id) as T + +const connectionStringInput = $('connectionString') +const sqlInput = $('sql') +const arrayModeInput = $('arrayMode') +const runButton = $('run') +const output = $('output') + +// Prefill from a Vite env var if provided (see .env.example), otherwise leave the +// placeholder so the user can paste their own connection string. +connectionStringInput.value = import.meta.env.VITE_DATABASE_URL ?? '' + +const run = async () => { + const connectionstring = connectionStringInput.value.trim() + const sql = sqlInput.value.trim() + + if (!connectionstring) { + output.textContent = 'Please provide a connection string.' + return + } + + runButton.disabled = true + output.textContent = 'Running…' + + try { + const { rows } = await runQuery( + arrayModeInput.checked, + sql, + [], + // In the browser the driver connects over WebSocket through the SQLite + // Cloud Gateway. Passing only the connection string is enough; the gateway + // URL is derived from the host (wss://:443). + { connectionstring } + ) + output.textContent = JSON.stringify(rows, null, 2) + } catch (error) { + if (isClientError(error)) { + output.textContent = `${error.message}\n\n${JSON.stringify(error.details, null, 2)}` + } else { + output.textContent = String(error) + } + } finally { + runButton.disabled = false + } +} + +runButton.addEventListener('click', run) diff --git a/examples/with-websocket-browser/src/query.ts b/examples/with-websocket-browser/src/query.ts new file mode 100644 index 0000000..aff0c93 --- /dev/null +++ b/examples/with-websocket-browser/src/query.ts @@ -0,0 +1,104 @@ +// +// query.ts +// +// A small browser-friendly query helper. It opens a SQLite Cloud connection +// over WebSocket (via the SQLite Cloud Gateway), runs a single SQL statement +// and normalizes the result into `{ rows }`. +// +// The error types are kept local to this file so the example has no backend +// dependencies and can be bundled for the browser as-is. +// + +import { Database, SQLiteCloudConfig } from '@sqlitecloud/drivers' + +/** Shape of a structured error thrown by `runQuery`. */ +export interface QueryError { + name: string + status: number + endpoint: string + message: string + body?: unknown +} + +/** Error thrown by `runQuery` carrying structured details. */ +export class ClientError extends Error { + details: QueryError + + constructor(message: string, details: QueryError) { + super(message) + this.name = 'ClientError' + this.details = details + } +} + +/** Type guard for {@link ClientError}. */ +export function isClientError(error: unknown): error is ClientError { + return error instanceof ClientError +} + +/** + * Open a WebSocket connection to SQLite Cloud, run a single SQL statement + * and return the result normalized into `{ rows }`. + * + * @param arrayMode when true rows are returned as arrays (via `row.getData()`), + * otherwise as plain objects. + * @param sql the SQL statement to execute. May contain `?` placeholders. + * @param params positional parameters bound to the statement. + * @param config SQLite Cloud connection config. In the browser the driver + * always connects via WebSocket; pass `gatewayurl` to point at + * a specific gateway, or rely on the default derived from the host. + */ +export const runQuery = async ( + arrayMode = false, + sql: string, + params: unknown[] = [], + config: SQLiteCloudConfig +): Promise<{ rows: unknown[] }> => { + let client: Database | null = null + + try { + // Wrap the Database connection in a Promise to handle async connection errors. + // `usewebsocket` is implied in the browser, but we set it explicitly so the + // same code also works under Node.js / tests. + client = await new Promise((resolve, reject) => { + const db = new Database({ ...config, usewebsocket: true }, error => { + if (error) { + console.log('Database connection error:', error) + reject(error) + } else { + resolve(db) + } + }) + }) + + // Execute the SQL query with the provided parameters. + const result = await client.sql(sql, ...params) + + let rows: unknown[] + if (Array.isArray(result)) { + // Transform rows to arrays if arrayMode is true, otherwise return as objects. + rows = arrayMode ? result.map((row: any) => row.getData()) : result + } else if (result instanceof ArrayBuffer) { + rows = [{ byteLength: result.byteLength }] + } else { + // Scalar result (e.g. a write returning lastID/changes or a single value). + rows = [{ result }] + } + + return { rows } + } catch (error: any) { + if (isClientError(error)) { + throw error + } + const errorMessage = error?.message || 'Failed to execute database query' + throw new ClientError(`Database query failed: ${errorMessage}`, { + name: 'DatabaseQueryError', + status: 500, + endpoint: 'runQuery', + message: `Database query failed: ${errorMessage}`, + body: { sql, params, originalError: error } + }) + } finally { + if (client) client.close() + } +} diff --git a/examples/with-websocket-browser/tsconfig.json b/examples/with-websocket-browser/tsconfig.json new file mode 100644 index 0000000..5876957 --- /dev/null +++ b/examples/with-websocket-browser/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "types": ["vite/client"] + }, + "include": ["src"] +} diff --git a/package.json b/package.json index 41f1147..b3a2c59 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sqlitecloud/drivers", - "version": "1.0.870", + "version": "1.0.871", "description": "SQLiteCloud drivers for Typescript/Javascript in edge, web and node clients", "main": "./lib/index.js", "types": "./lib/index.d.ts",