diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index bc23406..bfd9062 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -2,10 +2,14 @@
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- - package-ecosystem: "npm"
- directory: "/" # Location of package.json
+ - package-ecosystem: 'npm'
+ directory: '/' # Location of package.json
schedule:
- interval: "weekly"
+ interval: 'weekly'
+ groups:
+ all-dependencies:
+ patterns:
+ - '*'
# Always increase the version requirement
# to match the new version.
versioning-strategy: increase
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 7965f1d..68c3cf2 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -3,6 +3,10 @@ on:
push:
workflow_dispatch:
+concurrency:
+ group: all
+ cancel-in-progress: true
+
env:
DATABASE_URL: ${{ secrets.CHINOOK_DATABASE_URL }}
@@ -81,7 +85,7 @@ jobs:
node_test=$(curl localhost:3000)
if [[ $node_test == *"{\"tracks\":[{\"TrackId\":1,\"Name\":\"For Those About To Rock (We Salute You)\",\"AlbumId\":1,\"MediaTypeId\":1,\"GenreId\":1,\"Composer\":\"Angus Young, Malcolm Young, Brian Johnson\",\"Milliseconds\":343719,\"Bytes\":11170334,\"UnitPrice\":0.99},{"* ]]; then
echo "✅ node with-javascript-express test passed"
- npx kill-port 3000
+ npx kill-port 3000 -y
exit 0
fi
echo "❌ node with-javascript-express test failed"
@@ -98,7 +102,7 @@ jobs:
bun_test=$(curl localhost:3000)
if [[ $bun_test == *"{\"tracks\":[{\"TrackId\":1,\"Name\":\"For Those About To Rock (We Salute You)\",\"AlbumId\":1,\"MediaTypeId\":1,\"GenreId\":1,\"Composer\":\"Angus Young, Malcolm Young, Brian Johnson\",\"Milliseconds\":343719,\"Bytes\":11170334,\"UnitPrice\":0.99},{"* ]]; then
echo "✅ bun with-javascript-express test passed"
- npx kill-port 3000
+ npx kill-port 3000 -y
exit 0
fi
echo "❌ bun with-javascript-express test failed"
@@ -112,7 +116,7 @@ jobs:
deno_test=$(curl localhost:3000)
if [[ $deno_test == *"{\"tracks\":[{\"TrackId\":1,\"Name\":\"For Those About To Rock (We Salute You)\",\"AlbumId\":1,\"MediaTypeId\":1,\"GenreId\":1,\"Composer\":\"Angus Young, Malcolm Young, Brian Johnson\",\"Milliseconds\":343719,\"Bytes\":11170334,\"UnitPrice\":0.99},{"* ]]; then
echo "✅ deno with-javascript-express test passed"
- npx kill-port 3000
+ npx kill-port 3000 -y
exit 0
fi
echo "❌ deno with-javascript-express test failed"
@@ -224,14 +228,14 @@ jobs:
working-directory: examples/with-typescript-nextjs
run: |
npm i
- npm run dev &
+ npx next dev -p 3005 &
sleep 3
- node_test=$(curl localhost:3003)
- node_test2=$(curl localhost:3003/api/hello)
+ node_test=$(curl localhost:3005)
+ node_test2=$(curl localhost:3005/api/hello)
if [[ $node_test == *"next.js json route"* && $node_test2 == *"{\"data\":[{\"TrackId\":1,\"Name\":\"For Those About To Rock (We Salute You)\",\"AlbumId\":1,\"MediaTypeId\":1,\"GenreId\":1,\"Composer\":\"Angus Young, Malcolm Young, Brian Johnson\",\"Milliseconds\":343719,\"Bytes\":11170334,\"UnitPrice\":0.99},{"* ]]; then
echo "✅ node with-typescript-nextjs test passed"
- npx kill-port 3003
+ npx kill-port 3005 -y
exit 0
fi
echo "❌ node with-typescript-nextjs test failed"
@@ -243,13 +247,13 @@ jobs:
if [ "$RUNNER_OS" != "Windows" ]; then
bun i #re-installing dependencies in windows with bash causes a panic
fi
- bun run dev &
+ bun next dev -p 3004 &
sleep 3
- bun_test=$(curl localhost:3003)
- bun_test2=$(curl localhost:3003/api/hello)
+ bun_test=$(curl localhost:3004)
+ bun_test2=$(curl localhost:3004/api/hello)
if [[ $bun_test == *"next.js json route"* && $bun_test2 == *"{\"data\":[{\"TrackId\":1,\"Name\":\"For Those About To Rock (We Salute You)\",\"AlbumId\":1,\"MediaTypeId\":1,\"GenreId\":1,\"Composer\":\"Angus Young, Malcolm Young, Brian Johnson\",\"Milliseconds\":343719,\"Bytes\":11170334,\"UnitPrice\":0.99},{"* ]]; then
echo "✅ bun with-typescript-nextjs test passed"
- npx kill-port 3003
+ npx kill-port 3004 -y
exit 0
fi
echo "❌ bun with-typescript-nextjs test failed"
@@ -264,13 +268,14 @@ jobs:
deno_test=$(curl localhost:3003/api/hello)
if [[ $deno_test == *"{\"data\":[{\"TrackId\":1,\"Name\":\"For Those About To Rock (We Salute You)\",\"AlbumId\":1,\"MediaTypeId\":1,\"GenreId\":1,\"Composer\":\"Angus Young, Malcolm Young, Brian Johnson\",\"Milliseconds\":343719,\"Bytes\":11170334,\"UnitPrice\":0.99},{"* ]]; then
echo "✅ deno with-typescript-nextjs test passed"
- npx kill-port 3003
+ npx kill-port 3003 -y
exit 0
fi
echo "❌ deno with-typescript-nextjs test failed"
exit 1
- name: remove with-typescript-nextjs
+ if: matrix.os != 'ubuntu-latest' #rm: cannot remove examples/with-typescript-nextjs: Directory not empty
run: rm -rf examples/with-typescript-nextjs
- name: node with-javascript-vite
diff --git a/README.md b/README.md
index 2a07d94..f5211b4 100644
--- a/README.md
+++ b/README.md
@@ -54,7 +54,7 @@ let database = new Database('sqlitecloud://user:password@xxx.sqlite.cloud:8860/c
let name = 'Breaking The Rules'
-let results = await database.sql`SELECT * FROM tracks WHERE name = ${name}`
+let results = await database.sql('SELECT * FROM tracks WHERE name = ?', name)
// => returns [{ AlbumId: 1, Name: 'Breaking The Rules', Composer: 'Angus Young... }]
```
@@ -82,7 +82,7 @@ await pubSub.listen(PUBSUB_ENTITY_TYPE.TABLE, 'albums', (error, results, data) =
}
})
-await database.sql`INSERT INTO albums (Title, ArtistId) values ('Brand new song', 1)`
+await database.sql("INSERT INTO albums (Title, ArtistId) values ('Brand new song', 1)")
// Stop listening changes on the table
await pubSub.unlisten(PUBSUB_ENTITY_TYPE.TABLE, 'albums')
diff --git a/examples/with-javascript-browser/index.html b/examples/with-javascript-browser/index.html
index c2f0673..e5b9656 100644
--- a/examples/with-javascript-browser/index.html
+++ b/examples/with-javascript-browser/index.html
@@ -48,7 +48,7 @@
Results:
var database = null;
sendButton.addEventListener('click', () => {
- if (!database) {
+ if (!database || !database.isConnected()) {
// Get the input element by ID
var connectionStringinputElement = document.getElementById('connectionStringInput');
var connectionstring = connectionStringinputElement.value;
diff --git a/examples/with-javascript-expo/components/AddTaskModal.js b/examples/with-javascript-expo/components/AddTaskModal.js
index 5195205..57b6e47 100644
--- a/examples/with-javascript-expo/components/AddTaskModal.js
+++ b/examples/with-javascript-expo/components/AddTaskModal.js
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react";
import { View, StyleSheet, Alert, Platform } from "react-native";
import { TextInput, Button, Modal } from "react-native-paper";
import DropdownMenu from "./DropdownMenu";
-import db from "../db/dbConnection";
+import getDbConnection from "../db/dbConnection";
export default AddTaskModal = ({
modalVisible,
@@ -30,7 +30,7 @@ export default AddTaskModal = ({
const getTags = async () => {
try {
- const tags = await db.sql("SELECT * FROM tags");
+ const tags = await getDbConnection().sql("SELECT * FROM tags");
setTagsList(tags);
} catch (error) {
console.error("Error getting tags", error);
diff --git a/examples/with-javascript-expo/db/dbConnection.js b/examples/with-javascript-expo/db/dbConnection.js
index 736b112..26958e4 100644
--- a/examples/with-javascript-expo/db/dbConnection.js
+++ b/examples/with-javascript-expo/db/dbConnection.js
@@ -1,4 +1,14 @@
import { DATABASE_URL } from "@env";
import { Database } from "@sqlitecloud/drivers";
-export default db = new Database(DATABASE_URL);
\ No newline at end of file
+/**
+ * @type {Database}
+ */
+let database = null;
+
+export default function getDbConnection() {
+ if (!database || !database.isConnected()) {
+ database = new Database(DATABASE_URL);
+ }
+ return database;
+}
\ No newline at end of file
diff --git a/examples/with-javascript-expo/hooks/useCategories.js b/examples/with-javascript-expo/hooks/useCategories.js
index a899fc7..db425b0 100644
--- a/examples/with-javascript-expo/hooks/useCategories.js
+++ b/examples/with-javascript-expo/hooks/useCategories.js
@@ -1,12 +1,12 @@
import { useState, useEffect } from "react";
-import db from "../db/dbConnection";
+import getDbConnection from "../db/dbConnection";
const useCategories = () => {
const [moreCategories, setMoreCategories] = useState(["Work", "Personal"]);
const getCategories = async () => {
try {
- const tags = await db.sql("SELECT * FROM tags");
+ const tags = await getDbConnection().sql("SELECT * FROM tags");
const filteredTags = tags.filter((tag) => {
return tag["name"] !== "Work" && tag["name"] !== "Personal";
});
@@ -21,7 +21,7 @@ const useCategories = () => {
const addCategory = async (newCategory) => {
try {
- await db.sql(
+ await getDbConnection().sql(
"INSERT INTO tags (name) VALUES (?) RETURNING *",
newCategory
);
@@ -33,15 +33,15 @@ const useCategories = () => {
const initializeTables = async () => {
try {
- const createTasksTable = await db.sql(
+ const createTasksTable = await getDbConnection().sql(
"CREATE TABLE IF NOT EXISTS tasks (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, isCompleted INT NOT NULL);"
);
- const createTagsTable = await db.sql(
+ const createTagsTable = await getDbConnection().sql(
"CREATE TABLE IF NOT EXISTS tags (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, UNIQUE(name));"
);
- const createTagsTasksTable = await db.sql(
+ const createTagsTasksTable = await getDbConnection().sql(
"CREATE TABLE IF NOT EXISTS tasks_tags (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, task_id INTEGER NOT NULL, tag_id INTEGER NOT NULL, FOREIGN KEY (task_id) REFERENCES tasks(id), FOREIGN KEY (tag_id) REFERENCES tags(id));"
);
@@ -52,8 +52,8 @@ const useCategories = () => {
) {
console.log("Successfully created tables");
- await db.sql("INSERT OR IGNORE INTO tags (name) VALUES (?)", "Work");
- await db.sql(
+ await getDbConnection().sql("INSERT OR IGNORE INTO tags (name) VALUES (?)", "Work");
+ await getDbConnection().sql(
"INSERT OR IGNORE INTO tags (name) VALUES (?)",
"Personal"
);
diff --git a/examples/with-javascript-expo/hooks/useTasks.js b/examples/with-javascript-expo/hooks/useTasks.js
index 4c9ba85..0a7ccad 100644
--- a/examples/with-javascript-expo/hooks/useTasks.js
+++ b/examples/with-javascript-expo/hooks/useTasks.js
@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from "react";
-import db from "../db/dbConnection";
+import getDbConnection from "../db/dbConnection";
const useTasks = (tag = null) => {
const [taskList, setTaskList] = useState([]);
@@ -8,7 +8,7 @@ const useTasks = (tag = null) => {
try {
let result;
if (tag) {
- result = await db.sql(
+ result = await getDbConnection().sql(
`
SELECT tasks.*, tags.id AS tag_id, tags.name AS tag_name
FROM tasks
@@ -19,11 +19,11 @@ const useTasks = (tag = null) => {
);
setTaskList(result);
} else {
- result = await db.sql`
+ result = await getDbConnection().sql(`
SELECT tasks.*, tags.id AS tag_id, tags.name AS tag_name
FROM tasks
JOIN tasks_tags ON tasks.id = tasks_tags.task_id
- JOIN tags ON tags.id = tasks_tags.tag_id`;
+ JOIN tags ON tags.id = tasks_tags.tag_id`);
setTaskList(result);
}
} catch (error) {
@@ -33,7 +33,7 @@ const useTasks = (tag = null) => {
const updateTask = async (completedStatus, taskId) => {
try {
- await db.sql(
+ await getDbConnection().sql(
"UPDATE tasks SET isCompleted=? WHERE id=? RETURNING *",
completedStatus,
taskId
@@ -47,7 +47,7 @@ const useTasks = (tag = null) => {
const addTaskTag = async (newTask, tag) => {
try {
if (tag.id) {
- const addNewTask = await db.sql(
+ const addNewTask = await getDbConnection().sql(
"INSERT INTO tasks (title, isCompleted) VALUES (?, ?) RETURNING *",
newTask.title,
newTask.isCompleted
@@ -55,13 +55,13 @@ const useTasks = (tag = null) => {
addNewTask[0].tag_id = tag.id;
addNewTask[0].tag_name = tag.name;
setTaskList([...taskList, addNewTask[0]]);
- await db.sql(
+ await getDbConnection().sql(
"INSERT INTO tasks_tags (task_id, tag_id) VALUES (?, ?)",
addNewTask[0].id,
tag.id
);
} else {
- const addNewTaskNoTag = await db.sql(
+ const addNewTaskNoTag = await getDbConnection().sql(
"INSERT INTO tasks (title, isCompleted) VALUES (?, ?) RETURNING *",
newTask.title,
newTask.isCompleted
@@ -75,8 +75,8 @@ const useTasks = (tag = null) => {
const deleteTask = async (taskId) => {
try {
- await db.sql("DELETE FROM tasks_tags WHERE task_id=?", taskId);
- const result = await db.sql("DELETE FROM tasks WHERE id=?", taskId);
+ await getDbConnection().sql("DELETE FROM tasks_tags WHERE task_id=?", taskId);
+ const result = await getDbConnection().sql("DELETE FROM tasks WHERE id=?", taskId);
console.log(`Deleted ${result.totalChanges} task`);
getTasks();
} catch (error) {
diff --git a/examples/with-javascript-express/app.js b/examples/with-javascript-express/app.js
index cdcf99a..ead1269 100644
--- a/examples/with-javascript-express/app.js
+++ b/examples/with-javascript-express/app.js
@@ -15,7 +15,7 @@ app.use(express.json())
/* http://localhost:3001/ returns chinook tracks as json */
app.get('/', async function (req, res, next) {
var database = new sqlitecloud.Database(DATABASE_URL)
- var tracks = await database.sql`USE DATABASE chinook.sqlite; SELECT * FROM tracks LIMIT 20;`
+ var tracks = await database.sql('USE DATABASE chinook.sqlite; SELECT * FROM tracks LIMIT 20;')
res.send({ tracks })
})
diff --git a/examples/with-javascript-vite/src/App.jsx b/examples/with-javascript-vite/src/App.jsx
index bff1ad4..cb79747 100644
--- a/examples/with-javascript-vite/src/App.jsx
+++ b/examples/with-javascript-vite/src/App.jsx
@@ -1,20 +1,29 @@
import { useEffect, useState } from "react";
import { Database } from "@sqlitecloud/drivers";
-const db = new Database(import.meta.env.VITE_DATABASE_URL);
+let db = null
+
+function getDatabase() {
+ if (!db || !db.isConnected()) {
+ db = new Database(import.meta.env.VITE_DATABASE_URL);
+ }
+
+ return db;
+}
+
function App() {
const [data, setData] = useState([]);
const getAlbums = async () => {
- const result = await db.sql`
+ const result = await getDatabase().sql(`
USE DATABASE chinook.sqlite;
SELECT albums.AlbumId as id, albums.Title as title, artists.name as artist
FROM albums
INNER JOIN artists
WHERE artists.ArtistId = albums.ArtistId
LIMIT 20;
- `;
+ `);
setData(result);
};
diff --git a/examples/with-plain-javascript/app.js b/examples/with-plain-javascript/app.js
index 43c1818..f0e44c4 100644
--- a/examples/with-plain-javascript/app.js
+++ b/examples/with-plain-javascript/app.js
@@ -13,7 +13,7 @@ async function selectTracks() {
var database = new sqlitecloud.Database(DATABASE_URL)
// run async query
- var tracks = await database.sql`USE DATABASE chinook.sqlite; SELECT * FROM tracks LIMIT 20;`
+ var tracks = await database.sql('USE DATABASE chinook.sqlite; SELECT * FROM tracks LIMIT 20;')
console.log(`selectTracks returned:`, tracks)
// You can also use all the regular sqlite3 api with callbacks, see:
diff --git a/examples/with-typescript-nextjs/app/api/hello/route.ts b/examples/with-typescript-nextjs/app/api/hello/route.ts
index 5606ef9..a191eff 100644
--- a/examples/with-typescript-nextjs/app/api/hello/route.ts
+++ b/examples/with-typescript-nextjs/app/api/hello/route.ts
@@ -15,7 +15,7 @@ export async function GET(request: NextRequest) {
const database = new Database(DATABASE_URL)
// retrieve rows from chinook database using a plain SQL query
- const tracks = await database.sql`USE DATABASE chinook.sqlite; SELECT * FROM tracks LIMIT 20;`
+ const tracks = await database.sql('USE DATABASE chinook.sqlite; SELECT * FROM tracks LIMIT 20;')
// return as json response
return NextResponse.json<{ data: any }>({ data: tracks })
diff --git a/examples/with-typescript-nextjs/app/page.tsx b/examples/with-typescript-nextjs/app/page.tsx
index 15773a9..08d09e5 100644
--- a/examples/with-typescript-nextjs/app/page.tsx
+++ b/examples/with-typescript-nextjs/app/page.tsx
@@ -1,4 +1,3 @@
-import Image from 'next/image'
import styles from './page.module.css'
export default function Home() {
diff --git a/examples/with-typescript-react-native/App.tsx b/examples/with-typescript-react-native/App.tsx
index 7237bc3..b71df81 100644
--- a/examples/with-typescript-react-native/App.tsx
+++ b/examples/with-typescript-react-native/App.tsx
@@ -11,7 +11,7 @@ export default function App() {
const db = new Database(`${DATABASE_URL}`);
const result =
- await db.sql`USE DATABASE chinook.sqlite; SELECT albums.AlbumId as id, albums.Title as title, artists.name as artist FROM albums INNER JOIN artists WHERE artists.ArtistId = albums.ArtistId LIMIT 20;`;
+ await db.sql('USE DATABASE chinook.sqlite; SELECT albums.AlbumId as id, albums.Title as title, artists.name as artist FROM albums INNER JOIN artists WHERE artists.ArtistId = albums.ArtistId LIMIT 20;');
setAlbums(result);
}
diff --git a/package-lock.json b/package-lock.json
index dbb6823..94efd43 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@sqlitecloud/drivers",
- "version": "1.0.417",
+ "version": "1.0.422",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@sqlitecloud/drivers",
- "version": "1.0.417",
+ "version": "1.0.422",
"license": "MIT",
"dependencies": {
"buffer": "^6.0.3",
diff --git a/package.json b/package.json
index 8273976..1af498b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@sqlitecloud/drivers",
- "version": "1.0.417",
+ "version": "1.0.422",
"description": "SQLiteCloud drivers for Typescript/Javascript in edge, web and node clients",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
diff --git a/src/drivers/connection-tls.ts b/src/drivers/connection-tls.ts
index c632a7e..ed872e7 100644
--- a/src/drivers/connection-tls.ts
+++ b/src/drivers/connection-tls.ts
@@ -2,21 +2,21 @@
* connection-tls.ts - connection via tls socket and sqlitecloud protocol
*/
-import { type SQLiteCloudConfig, SQLiteCloudError, type ErrorCallback, type ResultsCallback, SQLiteCloudCommand } from './types'
import { SQLiteCloudConnection } from './connection'
-import { getInitializationCommands } from './utilities'
import {
+ bufferEndsWith,
+ CMD_COMPRESSED,
+ CMD_ROWSET_CHUNK,
+ decompressBuffer,
formatCommand,
hasCommandLength,
parseCommandLength,
- popData,
- decompressBuffer,
parseRowsetChunks,
- CMD_COMPRESSED,
- CMD_ROWSET_CHUNK,
- bufferEndsWith,
+ popData,
ROWSET_CHUNKS_END
} from './protocol'
+import { type ErrorCallback, type ResultsCallback, SQLiteCloudCommand, type SQLiteCloudConfig, SQLiteCloudError } from './types'
+import { getInitializationCommands } from './utilities'
// explicitly importing buffer library to allow cross-platform support by replacing it
import { Buffer } from 'buffer'
@@ -77,6 +77,10 @@ export class SQLiteCloudTlsConnection extends SQLiteCloudConnection {
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)
@@ -97,6 +101,11 @@ export class SQLiteCloudTlsConnection extends SQLiteCloudConnection {
this.processCommandsFinish(new SQLiteCloudError('Connection closed', { errorCode: 'ERR_CONNECTION_CLOSED' }))
})
+ this.socket.on('timeout', () => {
+ this.close()
+ this.processCommandsFinish(new SQLiteCloudError('Connection ened due to timeout', { errorCode: 'ERR_CONNECTION_TIMEOUT' }))
+ })
+
return this
}
diff --git a/src/drivers/connection-ws.ts b/src/drivers/connection-ws.ts
index cb85cd5..1c9e716 100644
--- a/src/drivers/connection-ws.ts
+++ b/src/drivers/connection-ws.ts
@@ -2,10 +2,10 @@
* transport-ws.ts - handles low level communication with sqlitecloud server via socket.io websocket
*/
-import { SQLiteCloudConfig, SQLiteCloudError, ErrorCallback, ResultsCallback, SQLiteCloudCommand, SQLiteCloudDataTypes } from './types'
-import { SQLiteCloudRowset } from './rowset'
-import { SQLiteCloudConnection } from './connection'
import { io, Socket } from 'socket.io-client'
+import { SQLiteCloudConnection } from './connection'
+import { SQLiteCloudRowset } from './rowset'
+import { ErrorCallback, ResultsCallback, SQLiteCloudCommand, SQLiteCloudConfig, SQLiteCloudError } from './types'
/**
* Implementation of TransportConnection that connects to the database indirectly
@@ -19,7 +19,7 @@ export class SQLiteCloudWebsocketConnection extends SQLiteCloudConnection {
/** True if connection is open */
get connected(): boolean {
- return !!this.socket
+ return !!(this.socket && this.socket?.connected)
}
/* Opens a connection with the server and sends the initialization commands. Will throw in case of errors. */
@@ -32,11 +32,25 @@ export class SQLiteCloudWebsocketConnection extends SQLiteCloudConnection {
const connectionstring = this.config.connectionstring as string
const gatewayUrl = this.config?.gatewayurl || `${this.config.host === 'localhost' ? 'ws' : 'wss'}://${this.config.host as string}:4000`
this.socket = io(gatewayUrl, { auth: { token: connectionstring } })
+
+ this.socket.on('connect', () => {
+ callback?.call(this, null)
+ })
+
+ this.socket.on('disconnect', (reason) => {
+ this.close()
+ callback?.call(this, new SQLiteCloudError('Disconnected', { errorCode: 'ERR_CONNECTION_ENDED', cause: reason }))
+ })
+
+ this.socket.on('error', (error: Error) => {
+ this.close()
+ callback?.call(this, new SQLiteCloudError('Connection error', { errorCode: 'ERR_CONNECTION_ERROR', cause: error }))
+ })
}
- callback?.call(this, null)
} catch (error) {
callback?.call(this, error as Error)
}
+
return this
}
@@ -78,11 +92,12 @@ export class SQLiteCloudWebsocketConnection extends SQLiteCloudConnection {
public close(): this {
console.assert(this.socket !== null, 'SQLiteCloudWebsocketConnection.close - connection already closed')
if (this.socket) {
+ this.socket?.removeAllListeners()
this.socket?.close()
this.socket = undefined
}
+
this.operations.clear()
- this.socket = undefined
return this
}
}
diff --git a/src/drivers/database.ts b/src/drivers/database.ts
index 1227c63..797a221 100644
--- a/src/drivers/database.ts
+++ b/src/drivers/database.ts
@@ -10,10 +10,11 @@
import EventEmitter from 'eventemitter3'
import { SQLiteCloudConnection } from './connection'
import { PubSub } from './pubsub'
+import { OperationsQueue } from './queue'
import { SQLiteCloudRowset } from './rowset'
import { Statement } from './statement'
import {
- ErrorCallback,
+ ErrorCallback as ConnectionCallback,
ResultsCallback,
RowCallback,
RowCountCallback,
@@ -36,11 +37,12 @@ import { isBrowser, popCallback } from './utilities'
*/
export class Database extends EventEmitter {
/** Create and initialize a database from a full configuration object, or connection string */
- constructor(config: SQLiteCloudConfig | string, callback?: ErrorCallback)
- constructor(config: SQLiteCloudConfig | string, mode?: number, callback?: ErrorCallback)
- constructor(config: SQLiteCloudConfig | string, mode?: number | ErrorCallback, callback?: ErrorCallback) {
+ constructor(config: SQLiteCloudConfig | string, callback?: ConnectionCallback)
+ constructor(config: SQLiteCloudConfig | string, mode?: number, callback?: ConnectionCallback)
+ constructor(config: SQLiteCloudConfig | string, mode?: number | ConnectionCallback, callback?: ConnectionCallback) {
super()
this.config = typeof config === 'string' ? { connectionstring: config } : config
+ this.connection = null
// mode is optional and so is callback
// https://github.com/TryGhost/node-sqlite3/wiki/API#new-sqlite3databasefilename--mode--callback
@@ -51,8 +53,8 @@ export class Database extends EventEmitter {
// mode is ignored for now
- // opens first connection to the database automatically
- this.getConnection((error, _connection) => {
+ // opens the connection to the database automatically
+ this.createConnection(error => {
if (callback) {
callback.call(this, error)
}
@@ -62,70 +64,84 @@ export class Database extends EventEmitter {
/** Configuration used to open database connections */
private config: SQLiteCloudConfig
- /** Database connections */
- private connections: SQLiteCloudConnection[] = []
+ /** Database connection */
+ private connection: SQLiteCloudConnection | null
+
+ /** Used to syncronize opening of connection and commands */
+ private operations = new OperationsQueue()
//
// private methods
//
/** Returns first available connection from connection pool */
- private getConnection(callback: ResultsCallback) {
- // TODO sqlitecloud-js / implement database connection pool #10
- if (this.connections?.length > 0) {
- callback?.call(this, null, this.connections[0])
- } else {
- // connect using websocket if tls is not supported or if explicitly requested
- const useWebsocket = isBrowser || this.config?.usewebsocket || this.config?.gatewayurl
- if (useWebsocket) {
- // socket.io transport works in both node.js and browser environments and connects via SQLite Cloud Gateway
+ private createConnection(callback: ConnectionCallback) {
+ // connect using websocket if tls is not supported or if explicitly requested
+ const useWebsocket = isBrowser || this.config?.usewebsocket || this.config?.gatewayurl
+ if (useWebsocket) {
+ // socket.io transport works in both node.js and browser environments and connects via SQLite Cloud Gateway
+ this.operations.enqueue(done => {
import('./connection-ws')
.then(module => {
- this.connections.push(
- new module.default(this.config, error => {
- if (error) {
- this.handleError(this.connections[0], error, callback)
- } else {
- console.assert
- callback?.call(this, null, this.connections[0])
- this.emitEvent('open')
- }
- })
- )
+ this.connection = new module.default(this.config, (error: Error | null) => {
+ if (error) {
+ this.handleError(error, callback)
+ } else {
+ callback?.call(this, null)
+ this.emitEvent('open')
+ }
+
+ done(error)
+ })
})
.catch(error => {
- this.handleError(null, error, callback)
+ this.handleError(error, callback)
+ done(error)
})
- } else {
- // tls sockets work only in node.js environments
+ })
+ } else {
+ this.operations.enqueue(done => {
import('./connection-tls')
.then(module => {
- this.connections.push(
- new module.default(this.config, error => {
- if (error) {
- this.handleError(this.connections[0], error, callback)
- } else {
- console.assert
- callback?.call(this, null, this.connections[0])
- this.emitEvent('open')
- }
- })
- )
+ this.connection = new module.default(this.config, (error: Error | null) => {
+ if (error) {
+ this.handleError(error, callback)
+ } else {
+ callback?.call(this, null)
+ this.emitEvent('open')
+ }
+
+ done(error)
+ })
})
.catch(error => {
- this.handleError(null, error, callback)
+ this.handleError(error, callback)
+ done(error)
})
- }
+ })
}
}
+ private enqueueCommand(command: string | SQLiteCloudCommand, callback?: ResultsCallback): void {
+ this.operations.enqueue(done => {
+ let error: Error | null = null
+
+ // we don't wont to silently open a new connection after a disconnession
+ if (this.connection && this.connection.connected) {
+ this.connection.sendCommands(command, callback)
+ } else {
+ error = new SQLiteCloudError('Connection unavailable. Maybe it got disconnected?', { errorCode: 'ERR_CONNECTION_NOT_ESTABLISHED' })
+ this.handleError(error, callback)
+ }
+
+ done(error)
+ })
+ }
+
/** Handles an error by closing the connection, calling the callback and/or emitting an error event */
- private handleError(connection: SQLiteCloudConnection | null, error: Error, callback?: ErrorCallback): void {
+ private handleError(error: Error, callback?: ConnectionCallback): void {
// an errored connection is thrown out
- if (connection) {
- this.connections = this.connections.filter(c => c !== connection)
- connection.close()
- }
+ this.connection?.close()
if (callback) {
callback.call(this, error)
@@ -186,9 +202,8 @@ export class Database extends EventEmitter {
/** Enable verbose mode */
public verbose(): this {
this.config.verbose = true
- for (const connection of this.connections) {
- connection.verbose()
- }
+ this.connection?.verbose()
+
return this
}
@@ -210,19 +225,13 @@ export class Database extends EventEmitter {
public run(sql: string, ...params: any[]): this {
const { args, callback } = popCallback(params)
const command: SQLiteCloudCommand = { query: sql, parameters: args }
- this.getConnection((error, connection) => {
- if (error || !connection) {
- this.handleError(null, error as Error, callback)
+ this.enqueueCommand(command, (error, results) => {
+ if (error) {
+ this.handleError(error, callback)
} else {
- connection.sendCommands(command, (error, results) => {
- if (error) {
- this.handleError(connection, error, callback)
- } else {
- // context may include id of last row inserted, total changes, etc...
- const context = this.processContext(results)
- callback?.call(context || this, null, context ? context : results)
- }
- })
+ // context may include id of last row inserted, total changes, etc...
+ const context = this.processContext(results)
+ callback?.call(context || this, null, context ? context : results)
}
})
return this
@@ -243,21 +252,15 @@ export class Database extends EventEmitter {
public get(sql: string, ...params: any[]): this {
const { args, callback } = popCallback(params)
const command: SQLiteCloudCommand = { query: sql, parameters: args }
- this.getConnection((error, connection) => {
- if (error || !connection) {
- this.handleError(null, error as Error, callback)
+ this.enqueueCommand(command, (error, results) => {
+ if (error) {
+ this.handleError(error, callback)
} else {
- connection.sendCommands(command, (error, results) => {
- if (error) {
- this.handleError(connection, error, callback)
- } else {
- if (results && results instanceof SQLiteCloudRowset && results.length > 0) {
- callback?.call(this, null, results[0])
- } else {
- callback?.call(this, null)
- }
- }
- })
+ if (results && results instanceof SQLiteCloudRowset && results.length > 0) {
+ callback?.call(this, null, results[0])
+ } else {
+ callback?.call(this, null)
+ }
}
})
return this
@@ -281,21 +284,15 @@ export class Database extends EventEmitter {
public all(sql: string, ...params: any[]): this {
const { args, callback } = popCallback(params)
const command: SQLiteCloudCommand = { query: sql, parameters: args }
- this.getConnection((error, connection) => {
- if (error || !connection) {
- this.handleError(null, error as Error, callback)
+ this.enqueueCommand(command, (error, results) => {
+ if (error) {
+ this.handleError(error, callback)
} else {
- connection.sendCommands(command, (error, results) => {
- if (error) {
- this.handleError(connection, error, callback)
- } else {
- if (results && results instanceof SQLiteCloudRowset) {
- callback?.call(this, null, results)
- } else {
- callback?.call(this, null)
- }
- }
- })
+ if (results && results instanceof SQLiteCloudRowset) {
+ callback?.call(this, null, results)
+ } else {
+ callback?.call(this, null)
+ }
}
})
return this
@@ -322,28 +319,22 @@ export class Database extends EventEmitter {
const { args, callback, complete } = popCallback(params)
const command: SQLiteCloudCommand = { query: sql, parameters: args }
- this.getConnection((error, connection) => {
- if (error || !connection) {
- this.handleError(null, error as Error, callback)
+ this.enqueueCommand(command, (error, rowset) => {
+ if (error) {
+ this.handleError(error, callback)
} else {
- connection.sendCommands(command, (error, rowset) => {
- if (error) {
- this.handleError(connection, error, callback)
- } else {
- if (rowset && rowset instanceof SQLiteCloudRowset) {
- if (callback) {
- for (const row of rowset) {
- callback.call(this, null, row)
- }
- }
- if (complete) {
- ;(complete as RowCountCallback).call(this, null, rowset.numberOfRows)
- }
- } else {
- callback?.call(this, new SQLiteCloudError('Invalid rowset'))
+ if (rowset && rowset instanceof SQLiteCloudRowset) {
+ if (callback) {
+ for (const row of rowset) {
+ callback.call(this, null, row)
}
}
- })
+ if (complete) {
+ ;(complete as RowCountCallback).call(this, null, rowset.numberOfRows)
+ }
+ } else {
+ callback?.call(this, new SQLiteCloudError('Invalid rowset'))
+ }
}
})
return this
@@ -370,19 +361,13 @@ export class Database extends EventEmitter {
* object. When no callback is provided and an error occurs, an error event
* will be emitted on the database object.
*/
- public exec(sql: string, callback?: ErrorCallback): this {
- this.getConnection((error, connection) => {
- if (error || !connection) {
- this.handleError(null, error as Error, callback)
+ public exec(sql: string, callback?: ConnectionCallback): this {
+ this.enqueueCommand(sql, (error, results) => {
+ if (error) {
+ this.handleError(error, callback)
} else {
- connection.sendCommands(sql, (error, results) => {
- if (error) {
- this.handleError(connection, error, callback)
- } else {
- const context = this.processContext(results)
- callback?.call(context ? context : this, null)
- }
- })
+ const context = this.processContext(results)
+ callback?.call(context ? context : this, null)
}
})
return this
@@ -396,12 +381,10 @@ export class Database extends EventEmitter {
* will be emitted on the database object. If closing succeeded, a close event with no
* parameters is emitted, regardless of whether a callback was provided or not.
*/
- public close(callback?: ErrorCallback): void {
- if (this.connections?.length > 0) {
- for (const connection of this.connections) {
- connection.close()
- }
- }
+ public close(callback?: ConnectionCallback): void {
+ this.operations.clear()
+ this.connection?.close()
+
callback?.call(this, null)
this.emitEvent('close')
}
@@ -415,7 +398,7 @@ export class Database extends EventEmitter {
* and an error occurred, an error event with the error object as the only parameter
* will be emitted on the database object.
*/
- public loadExtension(_path: string, callback?: ErrorCallback): this {
+ public loadExtension(_path: string, callback?: ConnectionCallback): this {
// TODO sqlitecloud-js / implement database loadExtension #17
if (callback) {
callback.call(this, new Error('Database.loadExtension - Not implemented'))
@@ -470,19 +453,13 @@ export class Database extends EventEmitter {
}
return new Promise((resolve, reject) => {
- this.getConnection((error, connection) => {
- if (error || !connection) {
+ this.enqueueCommand(commands, (error, results) => {
+ if (error) {
reject(error)
} else {
- connection.sendCommands(commands, (error, results) => {
- if (error) {
- reject(error)
- } else {
- // metadata for operations like insert, update, delete?
- const context = this.processContext(results)
- resolve(context ? context : results)
- }
- })
+ // metadata for operations like insert, update, delete?
+ const context = this.processContext(results)
+ resolve(context ? context : results)
}
})
})
@@ -492,7 +469,7 @@ export class Database extends EventEmitter {
* Returns true if the database connection is open.
*/
public isConnected(): boolean {
- return this.connections?.length > 0 && this.connections[0].connected
+ return this.connection != null && this.connection.connected
}
/**
@@ -505,11 +482,17 @@ export class Database extends EventEmitter {
*/
public async getPubSub(): Promise {
return new Promise((resolve, reject) => {
- this.getConnection((error, connection) => {
- if (error || !connection) {
- reject(error)
- } else {
- resolve(new PubSub(connection))
+ this.operations.enqueue(done => {
+ let error = null
+ try {
+ if (!this.connection) {
+ error = new SQLiteCloudError('Connection not established', { errorCode: 'ERR_CONNECTION_NOT_ESTABLISHED' })
+ reject(error)
+ } else {
+ resolve(new PubSub(this.connection))
+ }
+ } finally {
+ done(error)
}
})
})
diff --git a/src/drivers/pubsub.ts b/src/drivers/pubsub.ts
index bc65c8c..45ae859 100644
--- a/src/drivers/pubsub.ts
+++ b/src/drivers/pubsub.ts
@@ -77,14 +77,14 @@ export class PubSub {
* @param name Channel name
*/
public async removeChannel(name: string): Promise {
- return this.connection.sql(`REMOVE CHANNEL ?;`, name)
+ return this.connection.sql('REMOVE CHANNEL ?;', name)
}
/**
* Send a message to the channel.
*/
public notifyChannel(channelName: string, message: string): Promise {
- return this.connection.sql`NOTIFY ${channelName} ${message};`
+ return this.connection.sql('NOTIFY ? ?;', channelName, message)
}
/**
diff --git a/test/connection-tls.test.ts b/test/connection-tls.test.ts
index e0183ce..8425004 100644
--- a/test/connection-tls.test.ts
+++ b/test/connection-tls.test.ts
@@ -53,8 +53,22 @@ describe('connect', () => {
})
*/
+it(
+ 'should connect with config object string',
+ done => {
+ const configObj = getChinookConfig()
+ const connection = new SQLiteCloudTlsConnection(configObj, error => {
+ expect(error).toBeNull()
+ expect(connection.connected).toBe(true)
+ connection.close()
+ done()
+ })
+ },
+ LONG_TIMEOUT
+)
+
it(
- 'should connect with config object string',
+ 'should connect with config object string and test command',
done => {
const configObj = getChinookConfig()
const connection = new SQLiteCloudTlsConnection(configObj, error => {
@@ -67,7 +81,6 @@ describe('connect', () => {
done()
})
})
- expect(connection).toBeDefined()
},
LONG_TIMEOUT
)
@@ -88,7 +101,6 @@ describe('connect', () => {
done()
})
})
- expect(connection).toBeDefined()
} catch (error) {
console.error(`An error occurred while connecting using api key: ${error}`)
debugger
@@ -127,7 +139,6 @@ describe('connect', () => {
}
})
})
- expect(connection).toBeDefined()
})
it('should connect with insecure connection string', done => {
diff --git a/test/connection-ws.test.ts b/test/connection-ws.test.ts
index 77f9c0e..2772fd3 100644
--- a/test/connection-ws.test.ts
+++ b/test/connection-ws.test.ts
@@ -40,9 +40,9 @@ describe('connection-ws', () => {
let connection: SQLiteCloudWebsocketConnection | null = null
connection = new SQLiteCloudWebsocketConnection(configObj, error => {
expect(error).toBeNull()
+ connection?.close()
done()
})
- connection?.close()
})
it('should not connect with incorrect credentials', done => {
@@ -55,8 +55,8 @@ describe('connection-ws', () => {
const connection = new SQLiteCloudWebsocketConnection(configObj, error => {
expect(error).toBeDefined()
done()
+ connection?.close()
})
- connection?.close()
})
/* TODO RESTORE TEST
it('should connect with connection string', done => {
diff --git a/test/pubsub.test.ts b/test/pubsub.test.ts
index 546cbca..8564954 100644
--- a/test/pubsub.test.ts
+++ b/test/pubsub.test.ts
@@ -191,7 +191,7 @@ describe('pubSub', () => {
await pubSub.setPubSubOnly()
- expect(connection.sql`SELECT 1`).rejects.toThrow('Connection not established')
+ expect(connection.sql`SELECT 1`).rejects.toThrow('Connection unavailable. Maybe it got disconnected?')
expect(pubSub.connected()).toBeTruthy()
await connection2.sql`UPDATE genres SET Name = ${newName} WHERE GenreId = 1`