Skip to content

Commit 3019619

Browse files
committed
feat: add observer TypeScript API and Tauri commands
Add observe/subscribe/unsubscribe/unobserve commands that expose the sqlx-sqlite-observer crate to the frontend via Tauri channels, enabling real-time database change notifications for the UI
1 parent e83b9a8 commit 3019619

19 files changed

Lines changed: 1016 additions & 13 deletions

File tree

Cargo.lock

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ members = [
88

99
[package]
1010
name = "tauri-plugin-sqlite"
11-
version = "0.1.0"
12-
description = "A Tauri plugin for SQLite database access with connection management"
11+
version = "0.3.0"
12+
description = "A Tauri plugin for SQLite with connection pooling, builder-pattern queries, transactions, and reactive change notifications"
1313
license = "MIT"
1414
edition = "2024"
1515
rust-version = "1.89"
@@ -34,8 +34,14 @@ sqlx = { version = "0.8.6", features = ["sqlite", "json", "time", "runtime-tokio
3434
# Connection manager
3535
sqlx-sqlite-conn-mgr = { path = "crates/sqlx-sqlite-conn-mgr" }
3636

37-
# High-level toolkit (builders, transactions, decoding)
38-
sqlx-sqlite-toolkit = { path = "crates/sqlx-sqlite-toolkit" }
37+
# Toolkit (high-level API — builders, transactions, decoding)
38+
sqlx-sqlite-toolkit = { path = "crates/sqlx-sqlite-toolkit", features = ["observer"] }
39+
40+
# Observer types (for payload conversion)
41+
sqlx-sqlite-observer = { path = "crates/sqlx-sqlite-observer", features = ["conn-mgr"] }
42+
43+
# Async stream support for observer subscriptions
44+
futures = "0.3.31"
3945

4046
[build-dependencies]
4147
tauri-plugin = { version = "2.5.1", features = ["build"] }

README.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,66 @@ await db.executeTransaction([
397397
* Attachments are connection-scoped and don't persist across queries
398398
* Main database is always accessible without a schema prefix
399399

400+
### Change Notifications
401+
402+
Subscribe to real-time change notifications when rows are inserted, updated, or
403+
deleted. Changes are only published after transactions commit — you never see
404+
partial or rolled-back data.
405+
406+
```typescript
407+
// 1. Enable observation for specific tables
408+
await db.observe(['users', 'posts']);
409+
410+
// 2. Subscribe to changes
411+
const subscription = await db.subscribe(['users'], (event) => {
412+
if (event.event === 'change') {
413+
const { table, operation, primaryKey, newValues, oldValues } = event.data;
414+
415+
console.info(`${operation} on ${table}, row key:`, primaryKey);
416+
417+
if (operation === 'insert' || operation === 'update') {
418+
console.info('New values:', newValues);
419+
}
420+
if (operation === 'update' || operation === 'delete') {
421+
console.info('Old values:', oldValues);
422+
}
423+
} else if (event.event === 'lagged') {
424+
// Consumer fell behind — some notifications were missed
425+
console.warn(`Missed ${event.data.count} notifications`);
426+
}
427+
});
428+
429+
// 3. Changes are now streamed to the callback
430+
await db.execute('INSERT INTO users (name) VALUES ($1)', ['Alice']);
431+
// callback fires: { event: 'change', data: { table: 'users', operation: 'insert', ... } }
432+
433+
// 4. Unsubscribe when done
434+
await subscription.unsubscribe();
435+
436+
// 5. Disable observation entirely (also aborts all active subscriptions)
437+
await db.unobserve();
438+
```
439+
440+
**Configuration:**
441+
442+
```typescript
443+
await db.observe(['users'], {
444+
channelCapacity: 512, // default: 256 — at least the number of writes in your largest transaction
445+
captureValues: false, // default: true — disable to reduce memory per notification
446+
});
447+
```
448+
449+
**Important:**
450+
451+
* Call `observe()` before `subscribe()` — subscribing without observation returns
452+
an error
453+
* Multiple subscriptions can be active on the same database, each filtering by
454+
different tables
455+
* `lagged` events indicate the broadcast channel filled up before the
456+
subscriber could read — increase `channelCapacity`
457+
* Column values (`oldValues`, `newValues`) are typed as `ColumnValue` — a tagged
458+
union of `null`, `integer`, `real`, `text`, or `blob` (base64-encoded)
459+
400460
### Error Handling
401461

402462
```typescript
@@ -419,6 +479,8 @@ Common error codes:
419479
* `IO_ERROR` - File system error
420480
* `MIGRATION_ERROR` - Migration failed
421481
* `MULTIPLE_ROWS_RETURNED` - `fetchOne()` returned multiple rows
482+
* `OBSERVATION_NOT_ENABLED` - Called `subscribe()` before `observe()`
483+
* `OBSERVER_ERROR` - Error from the observer subsystem
422484

423485
### Closing and Removing
424486

@@ -449,6 +511,9 @@ await db.remove(); // Close and DELETE database file(s) - irreversible
449511
| `fetchOne<T>(query, values?)` | Execute SELECT, return single row or `undefined` |
450512
| `close()` | Close connection, returns `true` if was loaded |
451513
| `remove()` | Close and delete database file(s), returns `true` if was loaded |
514+
| `observe(tables, config?)` | Enable change observation for tables |
515+
| `subscribe(tables, onEvent)` | Subscribe to change notifications, returns `Subscription` |
516+
| `unobserve()` | Disable observation and abort all subscriptions |
452517

453518
### Builder Methods
454519

@@ -469,6 +534,12 @@ return builders that are directly awaitable and support method chaining:
469534
| `commit()` | Commit transaction and release write lock |
470535
| `rollback()` | Rollback transaction and release write lock |
471536

537+
### Subscription Methods
538+
539+
| Method | Description |
540+
| ------ | ----------- |
541+
| `unsubscribe()` | Stop receiving change notifications, returns `true` if was active |
542+
472543
### Types
473544

474545
```typescript
@@ -492,6 +563,33 @@ interface SqliteError {
492563
code: string;
493564
message: string;
494565
}
566+
567+
interface ObserverConfig {
568+
channelCapacity?: number; // default: 256
569+
captureValues?: boolean; // default: true
570+
}
571+
572+
type ChangeOperation = 'insert' | 'update' | 'delete';
573+
574+
type ColumnValue =
575+
| { type: 'null' }
576+
| { type: 'integer'; value: number }
577+
| { type: 'real'; value: number }
578+
| { type: 'text'; value: string }
579+
| { type: 'blob'; value: string }; // base64-encoded
580+
581+
interface TableChange {
582+
table: string;
583+
operation?: ChangeOperation;
584+
rowid?: number;
585+
primaryKey: ColumnValue[];
586+
oldValues?: ColumnValue[]; // update, delete
587+
newValues?: ColumnValue[]; // insert, update
588+
}
589+
590+
type TableChangeEvent =
591+
| { event: 'change'; data: TableChange }
592+
| { event: 'lagged'; data: { count: number } };
495593
```
496594

497595
## Rust-Only API

api-iife.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

build.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ fn main() {
1212
"close_all",
1313
"remove",
1414
"get_migration_events",
15+
"observe",
16+
"subscribe",
17+
"unsubscribe",
18+
"unobserve",
1519
])
1620
.build();
1721
}

crates/sqlx-sqlite-toolkit/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ version = "0.8.6"
55
license = "MIT"
66
edition = "2024"
77
rust-version = "1.89"
8+
authors = ["Jeremy Thomerson"]
9+
description = "High-level SQLite API built on sqlx with builder-pattern queries, transactions, and JSON decoding"
10+
repository = "https://github.com/silvermine/tauri-plugin-sqlite"
11+
readme = "README.md"
12+
keywords = ["sqlite", "sqlx", "database", "transactions", "async"]
13+
categories = ["database", "asynchronous"]
814

915
[features]
1016
default = []

0 commit comments

Comments
 (0)