@delali/sirannon-db 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,29 +16,146 @@ Turn any SQLite database into a networked data layer with real-time subscription
16
16
  pnpm add @delali/sirannon-db
17
17
  ```
18
18
 
19
- Requires Node.js >= 22.
19
+ Then install the SQLite driver for your platform:
20
+
21
+ ```bash
22
+ pnpm add better-sqlite3 # Node.js
23
+ pnpm add wa-sqlite # Browser (IndexedDB persistence)
24
+ pnpm add expo-sqlite # React Native (Expo)
25
+ # Node 22+ built-in sqlite and Bun need no extra package
26
+ ```
20
27
 
21
28
  ## Quick start
22
29
 
30
+ ### Node.js
31
+
32
+ ```bash
33
+ pnpm add @delali/sirannon-db better-sqlite3
34
+ ```
35
+
23
36
  ```ts
24
37
  import { Sirannon } from '@delali/sirannon-db'
38
+ import { betterSqlite3 } from '@delali/sirannon-db/driver/better-sqlite3'
39
+
40
+ const driver = betterSqlite3()
41
+ const sirannon = new Sirannon({ driver })
42
+ const db = await sirannon.open('app', './data/app.db')
43
+
44
+ await db.execute('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)')
45
+ await db.execute('INSERT INTO users (name, email) VALUES (?, ?)', ['Ada', 'ada@example.com'])
46
+
47
+ const users = await db.query<{ id: number; name: string }>('SELECT * FROM users')
48
+ ```
49
+
50
+ Node.js 22+ users can skip the extra dependency by using the built-in `node:sqlite` module (requires the `--experimental-sqlite` flag):
51
+
52
+ ```ts
53
+ import { nodeSqlite } from '@delali/sirannon-db/driver/node'
54
+
55
+ const driver = nodeSqlite()
56
+ ```
25
57
 
26
- const sirannon = new Sirannon()
27
- const db = sirannon.open('app', './data/app.db')
58
+ ### Browser
28
59
 
29
- db.execute('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)')
30
- db.execute('INSERT INTO users (name, email) VALUES (?, ?)', ['Ada', 'ada@example.com'])
60
+ ```bash
61
+ pnpm add @delali/sirannon-db wa-sqlite
62
+ ```
63
+
64
+ The browser driver persists data to IndexedDB through a WebAssembly SQLite build. Use `Database.create` directly since `Sirannon` registries are designed for server-side use.
65
+
66
+ ```ts
67
+ import { Database } from '@delali/sirannon-db'
68
+ import { waSqlite } from '@delali/sirannon-db/driver/wa-sqlite'
31
69
 
32
- const users = db.query<{ id: number; name: string }>('SELECT * FROM users')
70
+ const driver = waSqlite({ vfs: 'IDBBatchAtomicVFS' })
71
+ const db = await Database.create('app', '/app.db', driver, {
72
+ readPoolSize: 1,
73
+ walMode: false,
74
+ })
75
+
76
+ await db.execute('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)')
77
+ await db.execute('INSERT INTO users (name, email) VALUES (?, ?)', ['Ada', 'ada@example.com'])
78
+
79
+ const users = await db.query<{ id: number; name: string }>('SELECT * FROM users')
33
80
  ```
34
81
 
35
- ## Three entry points
82
+ ### React Native (Expo)
83
+
84
+ ```bash
85
+ pnpm add @delali/sirannon-db expo-sqlite
86
+ ```
87
+
88
+ ```ts
89
+ import { Sirannon } from '@delali/sirannon-db'
90
+ import { expoSqlite } from '@delali/sirannon-db/driver/expo'
91
+
92
+ const driver = expoSqlite()
93
+ const sirannon = new Sirannon({ driver })
94
+ const db = await sirannon.open('app', 'app.db', {
95
+ readPoolSize: 1,
96
+ })
36
97
 
37
- The package ships three independent exports so you only bundle what you need:
98
+ await db.execute('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)')
99
+ await db.execute('INSERT INTO users (name, email) VALUES (?, ?)', ['Ada', 'ada@example.com'])
100
+
101
+ const users = await db.query<{ id: number; name: string }>('SELECT * FROM users')
102
+ ```
103
+
104
+ ### Bun
105
+
106
+ No extra dependency needed since Bun ships `bun:sqlite` built in.
107
+
108
+ ```ts
109
+ import { Sirannon } from '@delali/sirannon-db'
110
+ import { bunSqlite } from '@delali/sirannon-db/driver/bun'
111
+
112
+ const driver = bunSqlite()
113
+ const sirannon = new Sirannon({ driver })
114
+ const db = await sirannon.open('app', './data/app.db')
115
+ ```
116
+
117
+ ### Standalone databases
118
+
119
+ You can create databases without a `Sirannon` registry on any platform:
120
+
121
+ ```ts
122
+ const db = await Database.create('app', './data/app.db', driver)
123
+ ```
124
+
125
+ ## Pluggable drivers
126
+
127
+ Sirannon-db separates the database engine from the library. You pick the driver that fits your runtime, and the rest of the API stays the same.
128
+
129
+ | Driver | Import | Runtime | Install |
130
+ | --- | --- | --- | --- |
131
+ | better-sqlite3 | `@delali/sirannon-db/driver/better-sqlite3` | Node.js | `pnpm add better-sqlite3` |
132
+ | Node built-in | `@delali/sirannon-db/driver/node` | Node.js >= 22 | None (use `--experimental-sqlite` flag) |
133
+ | wa-sqlite | `@delali/sirannon-db/driver/wa-sqlite` | Browser | `pnpm add wa-sqlite` |
134
+ | Bun | `@delali/sirannon-db/driver/bun` | Bun | None (uses `bun:sqlite`) |
135
+ | Expo | `@delali/sirannon-db/driver/expo` | React Native | `pnpm add expo-sqlite` |
136
+
137
+ ```ts
138
+ import { betterSqlite3 } from '@delali/sirannon-db/driver/better-sqlite3'
139
+ const driver = betterSqlite3()
140
+
141
+ // or for Node 22's built-in sqlite:
142
+ import { nodeSqlite } from '@delali/sirannon-db/driver/node'
143
+ const driver = nodeSqlite()
144
+
145
+ // or for browser with IndexedDB persistence:
146
+ import { waSqlite } from '@delali/sirannon-db/driver/wa-sqlite'
147
+ const driver = waSqlite({ vfs: 'IDBBatchAtomicVFS' })
148
+ ```
149
+
150
+ ## Package exports
151
+
152
+ The package ships independent exports so you only bundle what you need:
38
153
 
39
154
  | Import | What you get |
40
- |---|---|
155
+ | --- | --- |
41
156
  | `@delali/sirannon-db` | Core library: queries, transactions, CDC, migrations, backups, hooks, metrics, lifecycle |
157
+ | `@delali/sirannon-db/driver/*` | SQLite driver adapters (see table above) |
158
+ | `@delali/sirannon-db/file-migrations` | Load `.up.sql` / `.down.sql` files from a directory |
42
159
  | `@delali/sirannon-db/server` | HTTP + WebSocket server powered by uWebSockets.js |
43
160
  | `@delali/sirannon-db/client` | Browser/Node.js client SDK with auto-reconnect and subscription restore |
44
161
 
@@ -47,36 +164,34 @@ The package ships three independent exports so you only bundle what you need:
47
164
  ### Queries and transactions
48
165
 
49
166
  ```ts
50
- const row = db.queryOne<{ count: number }>('SELECT count(*) as count FROM users')
167
+ const row = await db.queryOne<{ count: number }>('SELECT count(*) as count FROM users')
51
168
 
52
- const result = db.execute(
169
+ const result = await db.execute(
53
170
  'INSERT INTO users (name, email) VALUES (?, ?)',
54
171
  ['Grace', 'grace@example.com'],
55
172
  )
56
173
  // result.changes === 1, result.lastInsertRowId === 2
57
174
 
58
- db.executeBatch('INSERT INTO tags (label) VALUES (?)', [
175
+ await db.executeBatch('INSERT INTO tags (label) VALUES (?)', [
59
176
  ['typescript'],
60
177
  ['sqlite'],
61
178
  ['realtime'],
62
179
  ])
63
180
 
64
- const total = db.transaction(tx => {
65
- tx.execute('UPDATE accounts SET balance = balance - 100 WHERE id = ?', [1])
66
- tx.execute('UPDATE accounts SET balance = balance + 100 WHERE id = ?', [2])
67
- const [row] = tx.query<{ balance: number }>('SELECT balance FROM accounts WHERE id = ?', [2])
181
+ const total = await db.transaction(async tx => {
182
+ await tx.execute('UPDATE accounts SET balance = balance - 100 WHERE id = ?', [1])
183
+ await tx.execute('UPDATE accounts SET balance = balance + 100 WHERE id = ?', [2])
184
+ const [row] = await tx.query<{ balance: number }>('SELECT balance FROM accounts WHERE id = ?', [2])
68
185
  return row
69
186
  })
70
187
  ```
71
188
 
72
- Statements are cached in an LRU pool (capacity 128) so repeated queries skip the prepare step.
73
-
74
189
  ### Connection pooling
75
190
 
76
191
  Every database opens with 1 dedicated write connection and N read connections (default 4). WAL mode is enabled by default, allowing concurrent reads during writes.
77
192
 
78
193
  ```ts
79
- const db = sirannon.open('analytics', './data/analytics.db', {
194
+ const db = await sirannon.open('analytics', './data/analytics.db', {
80
195
  readPoolSize: 8,
81
196
  walMode: true,
82
197
  })
@@ -87,7 +202,7 @@ const db = sirannon.open('analytics', './data/analytics.db', {
87
202
  Watch tables for INSERT, UPDATE, and DELETE events in real time. The CDC system installs SQLite triggers that record changes into a tracking table, then polls at a configurable interval.
88
203
 
89
204
  ```ts
90
- db.watch('orders')
205
+ await db.watch('orders')
91
206
 
92
207
  const subscription = db
93
208
  .on('orders')
@@ -104,7 +219,7 @@ const subscription = db
104
219
  subscription.unsubscribe()
105
220
 
106
221
  // Stop tracking entirely:
107
- db.unwatch('orders')
222
+ await db.unwatch('orders')
108
223
  ```
109
224
 
110
225
  ### Migrations
@@ -131,7 +246,10 @@ migrations/
131
246
  #### File-based migrations
132
247
 
133
248
  ```ts
134
- const result = db.migrate('./migrations')
249
+ import { loadMigrations } from '@delali/sirannon-db/file-migrations'
250
+
251
+ const migrations = loadMigrations('./migrations')
252
+ const result = await db.migrate(migrations)
135
253
  // result.applied: entries that ran this time
136
254
  // result.skipped: number of entries already applied
137
255
  ```
@@ -139,14 +257,15 @@ const result = db.migrate('./migrations')
139
257
  #### Rollback
140
258
 
141
259
  ```ts
142
- db.rollback('./migrations') // undo the last applied migration
143
- db.rollback('./migrations', 2) // undo all migrations after version 2
144
- db.rollback('./migrations', 0) // undo everything
260
+ const migrations = loadMigrations('./migrations')
261
+ await db.rollback(migrations) // undo the last applied migration
262
+ await db.rollback(migrations, 2) // undo all migrations after version 2
263
+ await db.rollback(migrations, 0) // undo everything
145
264
  ```
146
265
 
147
266
  #### Programmatic migrations
148
267
 
149
- Pass an array of migration objects instead of a directory path. The `up` and `down` fields accept SQL strings or functions that receive a `Transaction`.
268
+ Pass an array of migration objects instead of loading from files:
150
269
 
151
270
  ```ts
152
271
  const migrations = [
@@ -156,22 +275,11 @@ const migrations = [
156
275
  up: 'CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)',
157
276
  down: 'DROP TABLE users',
158
277
  },
159
- {
160
- version: 2,
161
- name: 'seed_data',
162
- up: (tx) => {
163
- // You can run any code here
164
- tx.execute("INSERT INTO users (name) VALUES (?)", ['Alice'])
165
- },
166
- down: (tx) => {
167
- tx.execute("DELETE FROM users WHERE name = ?", ['Alice'])
168
- },
169
- },
170
278
  ]
171
279
 
172
- db.migrate(migrations)
173
- db.rollback(migrations) // undo last migration
174
- db.rollback(migrations, 0) // undo everything
280
+ await db.migrate(migrations)
281
+ await db.rollback(migrations) // undo last migration
282
+ await db.rollback(migrations, 0) // undo everything
175
283
  ```
176
284
 
177
285
  ### Backups
@@ -179,7 +287,7 @@ db.rollback(migrations, 0) // undo everything
179
287
  One-shot backups use `VACUUM INTO` for a consistent snapshot. Scheduled backups run on a cron expression with automatic file rotation.
180
288
 
181
289
  ```ts
182
- db.backup('./backups/snapshot.db')
290
+ await db.backup('./backups/snapshot.db')
183
291
 
184
292
  db.scheduleBackup({
185
293
  cron: '0 */6 * * *', // every 6 hours
@@ -219,6 +327,7 @@ Plug in callbacks to collect query timing, connection events, and CDC activity.
219
327
 
220
328
  ```ts
221
329
  const sirannon = new Sirannon({
330
+ driver,
222
331
  metrics: {
223
332
  onQueryComplete: m => histogram.observe(m.durationMs),
224
333
  onConnectionOpen: m => gauge.inc({ db: m.databaseId }),
@@ -234,6 +343,7 @@ For multi-tenant setups, the lifecycle manager handles auto-opening, idle timeou
234
343
 
235
344
  ```ts
236
345
  const sirannon = new Sirannon({
346
+ driver,
237
347
  lifecycle: {
238
348
  autoOpen: {
239
349
  resolver: id => ({ path: `/data/tenants/${id}.db` }),
@@ -244,7 +354,7 @@ const sirannon = new Sirannon({
244
354
  })
245
355
 
246
356
  // Databases resolve on first access:
247
- const db = sirannon.get('tenant-42') // opens /data/tenants/tenant-42.db
357
+ const db = await sirannon.resolve('tenant-42') // opens /data/tenants/tenant-42.db
248
358
  ```
249
359
 
250
360
  ## Server
@@ -253,32 +363,23 @@ Expose any `Sirannon` instance over HTTP and WebSocket with a single function ca
253
363
 
254
364
  ```ts
255
365
  import { Sirannon } from '@delali/sirannon-db'
366
+ import { betterSqlite3 } from '@delali/sirannon-db/driver/better-sqlite3'
256
367
  import { createServer } from '@delali/sirannon-db/server'
257
368
 
258
- const sirannon = new Sirannon()
259
- sirannon.open('app', './data/app.db')
260
-
261
- const server = createServer(sirannon, {
262
- port: 9876,
263
- cors: true,
264
- onRequest: ({ headers, path, method, remoteAddress }) => {
265
- if (headers.authorization !== `Bearer ${process.env.API_TOKEN}`) {
266
- return { status: 401, code: 'UNAUTHORIZED', message: 'Invalid or missing token' }
267
- }
268
- },
269
- })
369
+ const driver = betterSqlite3()
370
+ const sirannon = new Sirannon({ driver })
371
+ await sirannon.open('app', './data/app.db')
270
372
 
373
+ const server = createServer(sirannon, { port: 9876 })
271
374
  await server.listen()
272
375
  ```
273
376
 
274
- The `onRequest` hook runs before every database route (HTTP and WebSocket upgrade). Return `void` to allow the request, or return a `{ status, code, message }` object to deny it. The hook receives a `RequestContext` with `headers`, `method`, `path`, `databaseId`, and `remoteAddress`. Health endpoints (`/health`, `/health/ready`) bypass this hook.
275
-
276
- **Important:** The server accepts arbitrary SQL from clients. When exposed beyond localhost, always use `onRequest` to authenticate and authorize requests.
377
+ See the [Security](#security) section for authentication, TLS, and CORS configuration.
277
378
 
278
379
  ### HTTP routes
279
380
 
280
381
  | Method | Path | Description |
281
- |---|---|---|
382
+ | --- | --- | --- |
282
383
  | `POST` | `/db/:id/query` | Execute a SELECT, returns `{ rows }` |
283
384
  | `POST` | `/db/:id/execute` | Execute a mutation, returns `{ changes, lastInsertRowId }` |
284
385
  | `POST` | `/db/:id/transaction` | Execute a batch of statements atomically, returns `{ results }` |
@@ -308,17 +409,16 @@ const users = await db.query<{ id: number; name: string }>('SELECT * FROM users'
308
409
 
309
410
  await db.execute('INSERT INTO users (name) VALUES (?)', ['Turing'])
310
411
 
311
- const sub = await db
312
- .on('users')
313
- .filter({ role: 'admin' })
314
- .subscribe(event => console.log('Admin changed:', event))
412
+ const sub = db.subscribe('users', event => {
413
+ console.log('User changed:', event)
414
+ })
315
415
 
316
416
  // Cleanup:
317
417
  sub.unsubscribe()
318
418
  client.close()
319
419
  ```
320
420
 
321
- Transactions require HTTP transport:
421
+ Transactions use the HTTP transport:
322
422
 
323
423
  ```ts
324
424
  const httpClient = new SirannonClient('http://localhost:9876', {
@@ -335,12 +435,70 @@ await httpDb.transaction([
335
435
  httpClient.close()
336
436
  ```
337
437
 
438
+ ## Security
439
+
440
+ Sirannon-db is designed to be secure by default in its core operations. This section covers what the library handles for you and what you're responsible for when deploying to production.
441
+
442
+ ### Built-in protections
443
+
444
+ - **Parameterized queries** - All SQL execution uses parameter binding through the driver layer, preventing SQL injection. User input never touches query strings directly.
445
+ - **Identifier validation** - CDC table and column names are validated against a strict allowlist regex (`/^[a-zA-Z_][a-zA-Z0-9_]*$/`), and identifiers are escaped with double-quote wrapping.
446
+ - **Path traversal prevention** - Migration and backup paths reject null bytes, `..` segments, and control characters before any filesystem access.
447
+ - **Request size limits** - HTTP bodies and WebSocket payloads are capped at 1 MB, preventing memory exhaustion from oversized requests.
448
+ - **Error isolation** - Errors returned to clients contain a machine-readable code and message. Stack traces and internal details are never leaked.
449
+ - **Connection isolation** - Read and write operations use separate connection pools. Read-only databases enforce immutability at the connection level.
450
+
451
+ ### Authentication and authorization
452
+
453
+ The server accepts arbitrary SQL from clients. When you expose it beyond localhost, always use the `onRequest` hook to authenticate and authorize requests.
454
+
455
+ ```ts
456
+ const server = createServer(sirannon, {
457
+ port: 9876,
458
+ onRequest: ({ headers, path, method, remoteAddress }) => {
459
+ if (headers.authorization !== `Bearer ${process.env.API_TOKEN}`) {
460
+ return { status: 401, code: 'UNAUTHORIZED', message: 'Invalid or missing token' }
461
+ }
462
+ },
463
+ })
464
+ ```
465
+
466
+ The hook runs before every database route (HTTP and WebSocket upgrade). Return `void` to allow the request, or return a `{ status, code, message }` object to deny it. Health endpoints (`/health`, `/health/ready`) bypass this hook.
467
+
468
+ ### TLS and transport security
469
+
470
+ > **Warning:** The built-in server binds plain HTTP and WebSocket without TLS. When you serve traffic outside a trusted network, terminate TLS upstream with a reverse proxy (nginx, Caddy, a cloud load balancer) or your clients' bearer tokens and query payloads will travel in cleartext.
471
+
472
+ Once TLS is in place, update your client URLs to use `https://` and `wss://`:
473
+
474
+ ```ts
475
+ const client = new SirannonClient('https://db.example.com', {
476
+ transport: 'websocket',
477
+ headers: { Authorization: `Bearer ${token}` },
478
+ })
479
+ ```
480
+
481
+ ### CORS
482
+
483
+ CORS is disabled by default. Enable it only if browser clients need direct access, and restrict origins to trusted domains:
484
+
485
+ ```ts
486
+ const server = createServer(sirannon, {
487
+ port: 9876,
488
+ cors: {
489
+ origin: ['https://app.example.com'],
490
+ },
491
+ })
492
+ ```
493
+
494
+ Passing `cors: true` allows all origins, which is fine for local development but should be avoided in production.
495
+
338
496
  ## Error handling
339
497
 
340
498
  All errors extend `SirannonError` with a machine-readable `code` property:
341
499
 
342
500
  | Error | Code | When |
343
- |---|---|---|
501
+ | --- | --- | --- |
344
502
  | `DatabaseNotFoundError` | `DATABASE_NOT_FOUND` | Database ID not in registry |
345
503
  | `DatabaseAlreadyExistsError` | `DATABASE_ALREADY_EXISTS` | Duplicate database ID |
346
504
  | `ReadOnlyError` | `READ_ONLY` | Write attempted on read-only database |
@@ -358,7 +516,7 @@ All errors extend `SirannonError` with a machine-readable `code` property:
358
516
  import { QueryError } from '@delali/sirannon-db'
359
517
 
360
518
  try {
361
- db.execute('INSERT INTO users (id) VALUES (?)', [1])
519
+ await db.execute('INSERT INTO users (id) VALUES (?)', [1])
362
520
  } catch (err) {
363
521
  if (err instanceof QueryError) {
364
522
  console.error(`SQL failed [${err.code}]: ${err.message}`)
@@ -369,28 +527,29 @@ try {
369
527
 
370
528
  ## Configuration reference
371
529
 
530
+ ### `SirannonOptions`
531
+
532
+ | Option | Type | Required | Description |
533
+ | --- | --- | --- | --- |
534
+ | `driver` | `SQLiteDriver` | Yes | The SQLite driver adapter to use |
535
+ | `hooks` | `HookConfig` | No | Before/after hooks for queries, connections, subscriptions |
536
+ | `metrics` | `MetricsConfig` | No | Callbacks for query timing, connection events, CDC activity |
537
+ | `lifecycle` | `LifecycleConfig` | No | Auto-open resolver, idle timeout, max open databases |
538
+
372
539
  ### `DatabaseOptions`
373
540
 
374
541
  | Option | Type | Default | Description |
375
- |---|---|---|---|
542
+ | --- | --- | --- | --- |
376
543
  | `readOnly` | `boolean` | `false` | Open in read-only mode |
377
544
  | `readPoolSize` | `number` | `4` | Number of read connections |
378
545
  | `walMode` | `boolean` | `true` | Enable WAL mode |
379
546
  | `cdcPollInterval` | `number` | `50` | CDC polling interval in ms |
380
547
  | `cdcRetention` | `number` | `3_600_000` | CDC retention period in ms (1 hour) |
381
548
 
382
- ### `SirannonOptions`
383
-
384
- | Option | Type | Description |
385
- |---|---|---|
386
- | `hooks` | `HookConfig` | Before/after hooks for queries, connections, subscriptions |
387
- | `metrics` | `MetricsConfig` | Callbacks for query timing, connection events, CDC activity |
388
- | `lifecycle` | `LifecycleConfig` | Auto-open resolver, idle timeout, max open databases |
389
-
390
549
  ### `ServerOptions`
391
550
 
392
551
  | Option | Type | Default | Description |
393
- |---|---|---|---|
552
+ | --- | --- | --- | --- |
394
553
  | `host` | `string` | `'127.0.0.1'` | Bind address |
395
554
  | `port` | `number` | `9876` | Listen port |
396
555
  | `cors` | `boolean \| CorsOptions` | `false` | CORS configuration |
@@ -399,15 +558,55 @@ try {
399
558
  ### `ClientOptions`
400
559
 
401
560
  | Option | Type | Default | Description |
402
- |---|---|---|---|
561
+ | --- | --- | --- | --- |
403
562
  | `transport` | `'websocket' \| 'http'` | `'websocket'` | Transport protocol |
404
563
  | `headers` | `Record<string, string>` | - | Custom HTTP headers |
405
564
  | `autoReconnect` | `boolean` | `true` | Reconnect on WebSocket disconnect |
406
565
  | `reconnectInterval` | `number` | `1000` | Reconnect delay in ms |
407
566
 
567
+ ## Examples
568
+
569
+ Self-contained example projects live in [`examples/`](examples/) and cover every runtime target:
570
+
571
+ | Example | Runtime | Driver | What it demonstrates |
572
+ | --- | --- | --- | --- |
573
+ | [`node-better-sqlite3`](examples/node-better-sqlite3/) | Node.js | better-sqlite3 | All core features: schema, migrations, CRUD, transactions, CDC, connection pools, metrics, multi-tenant, hooks, backup, shutdown |
574
+ | [`node-native`](examples/node-native/) | Node.js >= 22 | built-in `node:sqlite` | Same features as above using the zero-dependency Node driver |
575
+ | [`web-wa-sqlite`](examples/web-wa-sqlite/) | Browser (Vite) | wa-sqlite + IndexedDB | CRUD, transactions, CDC subscriptions in the browser |
576
+ | [`web-client`](examples/web-client/) | Browser + Node.js | better-sqlite3 (server) | Client SDK connecting to a Sirannon server over HTTP and WebSocket |
577
+
578
+ ### Running the examples
579
+
580
+ From the repository root:
581
+
582
+ ```bash
583
+ pnpm install
584
+ pnpm --filter @delali/sirannon-db build
585
+ ```
586
+
587
+ Then pick an example:
588
+
589
+ ```bash
590
+ # Node.js with better-sqlite3
591
+ cd packages/ts/examples/node-better-sqlite3
592
+ pnpm start
593
+
594
+ # Node.js with built-in sqlite
595
+ cd packages/ts/examples/node-native
596
+ pnpm start
597
+
598
+ # Browser with wa-sqlite (opens Vite dev server)
599
+ cd packages/ts/examples/web-wa-sqlite
600
+ pnpm dev
601
+
602
+ # Client-server (starts both Sirannon server and Vite client)
603
+ cd packages/ts/examples/web-client
604
+ pnpm start
605
+ ```
606
+
408
607
  ## Benchmarks
409
608
 
410
- The benchmark suite compares Sirannon's embedded SQLite performance against Postgres 17 across micro-operations, YCSB, TPC-C, and concurrency scaling. See [`packages/ts/benchmarks/BENCHMARKS.md`](packages/ts/benchmarks/BENCHMARKS.md) for setup instructions, configuration, Docker-based fair comparisons, and statistical analysis methodology.
609
+ The benchmark suite compares Sirannon's embedded SQLite performance against Postgres 17 across micro-operations, YCSB, TPC-C, and concurrency scaling. All benchmarks support driver switching via the `BENCH_DRIVER` environment variable (`better-sqlite3` or `node`). See [`benchmarks/BENCHMARKS.md`](benchmarks/BENCHMARKS.md) for setup instructions, configuration, Docker-based fair comparisons, and statistical analysis methodology.
411
610
 
412
611
  ## Development
413
612
 
@@ -0,0 +1,3 @@
1
+ export { a as BackupScheduler } from '../index-hXiis3N-.js';
2
+ export { e as BackupScheduleOptions } from '../types-DtDutWRU.js';
3
+ import '../types-BFSsG77t.js';
@@ -0,0 +1,2 @@
1
+ export { BackupScheduler } from '../chunk-PXKAKK2V.mjs';
2
+ import '../chunk-O7BHI3CF.mjs';
@@ -0,0 +1,14 @@
1
+ import { SirannonError } from './chunk-O7BHI3CF.mjs';
2
+
3
+ // src/core/driver/define.ts
4
+ function defineDriver(config) {
5
+ if (!config.capabilities || typeof config.open !== "function") {
6
+ throw new SirannonError("Driver must define capabilities and open()", "INVALID_DRIVER");
7
+ }
8
+ return Object.freeze({
9
+ capabilities: Object.freeze({ ...config.capabilities }),
10
+ open: config.open
11
+ });
12
+ }
13
+
14
+ export { defineDriver };