Skip to content

Commit 0d8ef5f

Browse files
authored
Merge pull request #37 from pmorris-dev/add-typescript-api-for-observer
feat: add observer TS API and commands (#412486)
2 parents b3bbe73 + ec246db commit 0d8ef5f

44 files changed

Lines changed: 3736 additions & 28 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Rust
2-
/target/
2+
**/target/
33

44
# Node
55
node_modules/
@@ -18,4 +18,7 @@ dist-js/
1818
Thumbs.db
1919

2020
# Tauri
21-
/gen/
21+
**/gen/
22+
23+
# Examples
24+
examples/**/Cargo.lock

Cargo.lock

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

Cargo.toml

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ members = [
55
"crates/sqlx-sqlite-observer",
66
"crates/sqlx-sqlite-toolkit",
77
]
8+
exclude = ["examples/observer-demo/src-tauri"]
89

910
[package]
1011
name = "tauri-plugin-sqlite"
11-
version = "0.1.0"
12-
description = "A Tauri plugin for SQLite database access with connection management"
12+
version = "0.2.0"
13+
description = "A Tauri plugin for SQLite with connection pooling, builder-pattern queries, transactions, and reactive change notifications"
1314
license = "MIT"
1415
edition = "2024"
1516
rust-version = "1.89"
@@ -20,7 +21,6 @@ tauri = "2.9.3"
2021
serde = { version = "1.0.228", features = ["derive"] }
2122
serde_json = "1.0.145"
2223
thiserror = "2.0.17"
23-
log = "0.4.28"
2424
time = "0.3.44"
2525
tokio = { version = "1.48.0", features = ["rt", "sync", "time"] }
2626
indexmap = { version = "2.12.1", features = ["serde"] }
@@ -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-observer/src/hooks.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
//! Use [`is_preupdate_hook_enabled()`] to check at runtime whether the linked
1010
//! SQLite library supports this feature.
1111
12-
use std::ffi::{CStr, CString, c_int, c_void};
12+
use std::ffi::{CStr, CString, c_char, c_int, c_void};
1313
use std::panic::catch_unwind;
1414
use std::ptr;
1515
use std::sync::Arc;
@@ -61,7 +61,7 @@ impl SqliteValue {
6161
SqliteValue::Null
6262
} else {
6363
// SAFETY: SQLite guarantees text is valid UTF-8 with a null terminator
64-
let cstr = unsafe { CStr::from_ptr(text_ptr as *const i8) };
64+
let cstr = unsafe { CStr::from_ptr(text_ptr as *const c_char) };
6565
SqliteValue::Text(cstr.to_string_lossy().into_owned())
6666
}
6767
}
@@ -214,8 +214,8 @@ unsafe extern "C" fn preupdate_callback(
214214
user_data: *mut c_void,
215215
db: *mut sqlite3,
216216
op: c_int,
217-
_database: *const i8,
218-
table: *const i8,
217+
_database: *const c_char,
218+
table: *const c_char,
219219
old_rowid: i64,
220220
new_rowid: i64,
221221
) {

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 = []

crates/sqlx-sqlite-toolkit/src/wrapper.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,15 +292,25 @@ impl DatabaseWrapper {
292292
/// Close the database connection.
293293
///
294294
/// Checkpoints the WAL and closes all connection pools.
295-
pub async fn close(self) -> Result<(), Error> {
295+
/// If observation is enabled, it is disabled first to unregister SQLite hooks
296+
/// and allow the write connection to close cleanly.
297+
pub async fn close(mut self) -> Result<(), Error> {
298+
#[cfg(feature = "observer")]
299+
self.disable_observation();
300+
296301
self.inner.close().await?;
297302
Ok(())
298303
}
299304

300305
/// Close the database connection and remove all database files.
301306
///
302307
/// Removes the main database file, WAL, and SHM files.
303-
pub async fn remove(self) -> Result<(), Error> {
308+
/// If observation is enabled, it is disabled first to unregister SQLite hooks
309+
/// and allow the write connection to close cleanly.
310+
pub async fn remove(mut self) -> Result<(), Error> {
311+
#[cfg(feature = "observer")]
312+
self.disable_observation();
313+
304314
self.inner.remove().await?;
305315
Ok(())
306316
}
@@ -310,9 +320,14 @@ impl DatabaseWrapper {
310320
/// After calling this, write operations will be tracked and subscribers
311321
/// can receive change notifications.
312322
///
323+
/// If observation is already enabled, the previous observer is disabled first.
324+
/// This drops the old broadcast broker, causing existing subscriber streams to
325+
/// terminate. Callers must re-subscribe after re-enabling observation.
326+
///
313327
/// Requires the `observer` feature.
314328
#[cfg(feature = "observer")]
315329
pub fn enable_observation(&mut self, config: ObserverConfig) {
330+
self.disable_observation();
316331
self.observer = Some(ObservableSqliteDatabase::new(
317332
Arc::clone(&self.inner),
318333
config,

eslint.config.cjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ module.exports = [
1717
'**/*.d.ts',
1818
'**/commitlint.config.cjs',
1919
'vitest.config.ts',
20+
'examples/**',
2021
],
2122
},
2223
...config,

0 commit comments

Comments
 (0)