Skip to content

Commit cf44c29

Browse files
authored
Merge pull request #102 from sqlitecloud/pub-sub
feat(pubsub): initial implementation
2 parents 76b1bef + a59d2a8 commit cf44c29

File tree

8 files changed

+376
-3
lines changed

8 files changed

+376
-3
lines changed

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,34 @@ We aim for full compatibility with the established [sqlite3 API](https://www.npm
3232

3333
The package is developed entirely in TypeScript and is fully compatible with JavaScript. It doesn't require any native libraries. This makes it a straightforward and effective tool for managing cloud-based databases in a familiar SQLite environment.
3434

35+
## Publish / Subscribe (Pub/Sub)
36+
37+
```ts
38+
import { Database } from '@sqlitecloud/drivers'
39+
import { PubSub, PUBSUB_ENTITY_TYPE } from '@sqlitecloud/drivers/lib/drivers/pubsub'
40+
41+
let database = new Database('sqlitecloud://user:[email protected]:8860/chinook.sqlite')
42+
// or use sqlitecloud://xxx.sqlite.cloud:8860?apikey=xxxxxxx
43+
44+
const pubSub: PubSub = await database.getPubSub()
45+
46+
await pubSub.listen(PUBSUB_ENTITY_TYPE.TABLE, 'albums', (error, results, data) => {
47+
if (results) {
48+
// Changes on albums table will be received here as JSON object
49+
console.log('Received message:', results)
50+
}
51+
})
52+
53+
await database.sql`INSERT INTO albums (Title, ArtistId) values ('Brand new song', 1)`
54+
55+
// Stop listening changes on the table
56+
await pubSub.unlisten(PUBSUB_ENTITY_TYPE.TABLE, 'albums')
57+
```
58+
59+
Pub/Sub is a messaging pattern that allows multiple applications to communicate with each other asynchronously. In the context of SQLiteCloud, Pub/Sub can be used to provide real-time updates and notifications to subscribed applications whenever data changes in the database or it can be used to send payloads (messages) to anyone subscribed to a channel.
60+
61+
Pub/Sub Documentation: [https://docs.sqlitecloud.io/docs/pub-sub](https://docs.sqlitecloud.io/docs/pub-sub)
62+
3563
## More
3664

3765
How do I deploy SQLite in the cloud?

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@sqlitecloud/drivers",
3-
"version": "1.0.178",
3+
"version": "1.0.193",
44
"description": "SQLiteCloud drivers for Typescript/Javascript in edge, web and node clients",
55
"main": "./lib/index.js",
66
"types": "./lib/index.d.ts",

src/drivers/connection-tls.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,9 @@ export class SQLiteCloudTlsConnection extends SQLiteCloudConnection {
207207

208208
if (this.processCallback) {
209209
this.processCallback(error, result)
210-
// this.processCallback = undefined
211210
}
211+
212+
this.buffer = Buffer.alloc(0)
212213
}
213214

214215
/** Disconnect immediately, release connection, no events. */

src/drivers/database.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { Statement } from './statement'
1818
import { ErrorCallback, ResultsCallback, RowCallback, RowsCallback } from './types'
1919
import EventEmitter from 'eventemitter3'
2020
import { isBrowser } from './utilities'
21+
import { PubSub } from './pubsub'
2122

2223
// Uses eventemitter3 instead of node events for browser compatibility
2324
// https://github.com/primus/eventemitter3
@@ -483,4 +484,24 @@ export class Database extends EventEmitter {
483484
})
484485
})
485486
}
487+
488+
/**
489+
* PubSub class provides a Pub/Sub real-time updates and notifications system to
490+
* allow multiple applications to communicate with each other asynchronously.
491+
* It allows applications to subscribe to tables and receive notifications whenever
492+
* data changes in the database table. It also enables sending messages to anyone
493+
* subscribed to a specific channel.
494+
* @returns {PubSub} A PubSub object
495+
*/
496+
public async getPubSub(): Promise<PubSub> {
497+
return new Promise((resolve, reject) => {
498+
this.getConnection((error, connection) => {
499+
if (error || !connection) {
500+
reject(error)
501+
} else {
502+
resolve(new PubSub(connection))
503+
}
504+
})
505+
})
506+
}
486507
}

src/drivers/protocol.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export const CMD_COMPRESSED = '%'
2525
export const CMD_COMMAND = '^'
2626
export const CMD_ARRAY = '='
2727
// const CMD_RAWJSON = '{'
28-
// const CMD_PUBSUB = '|'
28+
export const CMD_PUBSUB = '|'
2929
// const CMD_RECONNECT = '@'
3030

3131
// To mark the end of the Rowset, the special string /LEN 0 0 0 is sent (LEN is always 6 in this case)
@@ -298,6 +298,8 @@ export function popData(buffer: Buffer): { data: SQLiteCloudDataTypes | SQLiteCl
298298
return popResults(buffer.subarray(spaceIndex + 1, commandEnd - 1).toString('utf8'))
299299
case CMD_COMMAND:
300300
return popResults(buffer.subarray(spaceIndex + 1, commandEnd).toString('utf8'))
301+
case CMD_PUBSUB:
302+
return popResults(buffer.subarray(spaceIndex + 1, commandEnd).toString('utf8'))
301303
case CMD_JSON:
302304
return popResults(JSON.parse(buffer.subarray(spaceIndex + 1, commandEnd).toString('utf8')))
303305
case CMD_BLOB:

src/drivers/pubsub.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { SQLiteCloudConnection } from './connection'
2+
import SQLiteCloudTlsConnection from './connection-tls'
3+
import { PubSubCallback } from './types'
4+
5+
export enum PUBSUB_ENTITY_TYPE {
6+
TABLE = 'TABLE',
7+
CHANNEL = 'CHANNEL'
8+
}
9+
10+
/**
11+
* Pub/Sub class to receive changes on database tables or to send messages to channels.
12+
*/
13+
export class PubSub {
14+
constructor(connection: SQLiteCloudConnection) {
15+
this.connection = connection
16+
this.connectionPubSub = new SQLiteCloudTlsConnection(connection.getConfig())
17+
}
18+
19+
private connection: SQLiteCloudConnection
20+
private connectionPubSub: SQLiteCloudConnection
21+
22+
/**
23+
* Listen for a table or channel and start to receive messages to the provided callback.
24+
* @param entityType One of TABLE or CHANNEL'
25+
* @param entityName Name of the table or the channel
26+
* @param callback Callback to be called when a message is received
27+
* @param data Extra data to be passed to the callback
28+
*/
29+
public async listen(entityType: PUBSUB_ENTITY_TYPE, entityName: string, callback: PubSubCallback, data?: any): Promise<any> {
30+
const entity = entityType === 'TABLE' ? 'TABLE ' : ''
31+
32+
const authCommand: string = await this.connection.sql(`LISTEN ${entity}${entityName};`)
33+
34+
return new Promise((resolve, reject) => {
35+
this.connectionPubSub.sendCommands(authCommand, (error, results) => {
36+
if (error) {
37+
callback.call(this, error, null, data)
38+
reject(error)
39+
} else {
40+
// skip results from pubSub auth command
41+
if (results !== 'OK') {
42+
callback.call(this, null, results, data)
43+
}
44+
resolve(results)
45+
}
46+
})
47+
})
48+
}
49+
50+
/**
51+
* Stop receive messages from a table or channel.
52+
* @param entityType One of TABLE or CHANNEL
53+
* @param entityName Name of the table or the channel
54+
*/
55+
public async unlisten(entityType: string, entityName: string): Promise<any> {
56+
const subject = entityType === 'TABLE' ? 'TABLE ' : ''
57+
58+
return this.connection.sql(`UNLISTEN ${subject}?;`, entityName)
59+
}
60+
61+
/**
62+
* Create a channel to send messages to.
63+
* @param name Channel name
64+
* @param failIfExists Raise an error if the channel already exists
65+
*/
66+
public async createChannel(name: string, failIfExists: boolean = true): Promise<any> {
67+
let notExistsCommand = ''
68+
if (!failIfExists) {
69+
notExistsCommand = 'IF NOT EXISTS;'
70+
}
71+
72+
return this.connection.sql(`CREATE CHANNEL ? ${notExistsCommand}`, name)
73+
}
74+
75+
/**
76+
* Send a message to the channel.
77+
*/
78+
public notifyChannel(channelName: string, message: string): Promise<any> {
79+
return this.connection.sql`NOTIFY ${channelName} ${message};`
80+
}
81+
82+
/**
83+
* Ask the server to close the connection to the database and
84+
* to keep only open the Pub/Sub connection.
85+
* Only interaction with Pub/Sub commands will be allowed.
86+
*/
87+
public setPubSubOnly(): Promise<any> {
88+
return new Promise((resolve, reject) => {
89+
this.connection.sendCommands('PUBSUB ONLY;', (error, results) => {
90+
if (error) {
91+
reject(error)
92+
} else {
93+
this.connection.close()
94+
resolve(results)
95+
}
96+
})
97+
})
98+
}
99+
100+
/** True if Pub/Sub connection is open. */
101+
public connected(): boolean {
102+
return this.connectionPubSub.connected
103+
}
104+
105+
/** Close Pub/Sub connection. */
106+
public close(): void {
107+
this.connectionPubSub.close()
108+
}
109+
}

src/drivers/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ export type ResultsCallback<T = any> = (error: Error | null, results?: T) => voi
129129
export type RowsCallback<T = Record<string, any>> = (error: Error | null, rows?: T[]) => void
130130
export type RowCallback<T = Record<string, any>> = (error: Error | null, row?: T) => void
131131
export type RowCountCallback = (error: Error | null, rowCount?: number) => void
132+
export type PubSubCallback<T = any> = (error: Error | null, results?: T, extraData?: T) => void
132133

133134
/**
134135
* Certain responses include arrays with various types of metadata.

0 commit comments

Comments
 (0)