@delali/sirannon-db 0.1.1 → 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
@@ -1,5 +1,11 @@
1
1
  # sirannon-db
2
2
 
3
+ [![CI](https://github.com/assetcorp/sirannon-db/actions/workflows/ci.yml/badge.svg)](https://github.com/assetcorp/sirannon-db/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/@delali/sirannon-db)](https://www.npmjs.com/package/@delali/sirannon-db)
5
+ [![downloads](https://img.shields.io/npm/dw/@delali/sirannon-db)](https://www.npmjs.com/package/@delali/sirannon-db)
6
+ [![types](https://img.shields.io/badge/types-TypeScript-blue)](https://www.npmjs.com/package/@delali/sirannon-db)
7
+ [![license](https://img.shields.io/npm/l/@delali/sirannon-db)](https://github.com/assetcorp/sirannon-db/blob/main/LICENSE)
8
+
3
9
  Turn any SQLite database into a networked data layer with real-time subscriptions. One library gives you connection pooling, change data capture, migrations, scheduled backups, and a client SDK that talks over HTTP or WebSocket.
4
10
 
5
11
  > *sirannon* means 'gate-stream' in Sindarin.
@@ -10,29 +16,146 @@ Turn any SQLite database into a networked data layer with real-time subscription
10
16
  pnpm add @delali/sirannon-db
11
17
  ```
12
18
 
13
- 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
+ ```
14
27
 
15
28
  ## Quick start
16
29
 
30
+ ### Node.js
31
+
32
+ ```bash
33
+ pnpm add @delali/sirannon-db better-sqlite3
34
+ ```
35
+
36
+ ```ts
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
+ ```
57
+
58
+ ### Browser
59
+
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'
69
+
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')
80
+ ```
81
+
82
+ ### React Native (Expo)
83
+
84
+ ```bash
85
+ pnpm add @delali/sirannon-db expo-sqlite
86
+ ```
87
+
17
88
  ```ts
18
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
+ })
97
+
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
+ ```
19
116
 
20
- const sirannon = new Sirannon()
21
- const db = sirannon.open('app', './data/app.db')
117
+ ### Standalone databases
22
118
 
23
- db.execute('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)')
24
- db.execute('INSERT INTO users (name, email) VALUES (?, ?)', ['Ada', 'ada@example.com'])
119
+ You can create databases without a `Sirannon` registry on any platform:
25
120
 
26
- const users = db.query<{ id: number; name: string }>('SELECT * FROM users')
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' })
27
148
  ```
28
149
 
29
- ## Three entry points
150
+ ## Package exports
30
151
 
31
- The package ships three independent exports so you only bundle what you need:
152
+ The package ships independent exports so you only bundle what you need:
32
153
 
33
154
  | Import | What you get |
34
- |---|---|
155
+ | --- | --- |
35
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 |
36
159
  | `@delali/sirannon-db/server` | HTTP + WebSocket server powered by uWebSockets.js |
37
160
  | `@delali/sirannon-db/client` | Browser/Node.js client SDK with auto-reconnect and subscription restore |
38
161
 
@@ -41,36 +164,34 @@ The package ships three independent exports so you only bundle what you need:
41
164
  ### Queries and transactions
42
165
 
43
166
  ```ts
44
- 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')
45
168
 
46
- const result = db.execute(
169
+ const result = await db.execute(
47
170
  'INSERT INTO users (name, email) VALUES (?, ?)',
48
171
  ['Grace', 'grace@example.com'],
49
172
  )
50
173
  // result.changes === 1, result.lastInsertRowId === 2
51
174
 
52
- db.executeBatch('INSERT INTO tags (label) VALUES (?)', [
175
+ await db.executeBatch('INSERT INTO tags (label) VALUES (?)', [
53
176
  ['typescript'],
54
177
  ['sqlite'],
55
178
  ['realtime'],
56
179
  ])
57
180
 
58
- const total = db.transaction(tx => {
59
- tx.execute('UPDATE accounts SET balance = balance - 100 WHERE id = ?', [1])
60
- tx.execute('UPDATE accounts SET balance = balance + 100 WHERE id = ?', [2])
61
- 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])
62
185
  return row
63
186
  })
64
187
  ```
65
188
 
66
- Statements are cached in an LRU pool (capacity 128) so repeated queries skip the prepare step.
67
-
68
189
  ### Connection pooling
69
190
 
70
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.
71
192
 
72
193
  ```ts
73
- const db = sirannon.open('analytics', './data/analytics.db', {
194
+ const db = await sirannon.open('analytics', './data/analytics.db', {
74
195
  readPoolSize: 8,
75
196
  walMode: true,
76
197
  })
@@ -81,7 +202,7 @@ const db = sirannon.open('analytics', './data/analytics.db', {
81
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.
82
203
 
83
204
  ```ts
84
- db.watch('orders')
205
+ await db.watch('orders')
85
206
 
86
207
  const subscription = db
87
208
  .on('orders')
@@ -98,7 +219,7 @@ const subscription = db
98
219
  subscription.unsubscribe()
99
220
 
100
221
  // Stop tracking entirely:
101
- db.unwatch('orders')
222
+ await db.unwatch('orders')
102
223
  ```
103
224
 
104
225
  ### Migrations
@@ -125,7 +246,10 @@ migrations/
125
246
  #### File-based migrations
126
247
 
127
248
  ```ts
128
- 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)
129
253
  // result.applied: entries that ran this time
130
254
  // result.skipped: number of entries already applied
131
255
  ```
@@ -133,14 +257,15 @@ const result = db.migrate('./migrations')
133
257
  #### Rollback
134
258
 
135
259
  ```ts
136
- db.rollback('./migrations') // undo the last applied migration
137
- db.rollback('./migrations', 2) // undo all migrations after version 2
138
- 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
139
264
  ```
140
265
 
141
266
  #### Programmatic migrations
142
267
 
143
- 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:
144
269
 
145
270
  ```ts
146
271
  const migrations = [
@@ -150,22 +275,11 @@ const migrations = [
150
275
  up: 'CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)',
151
276
  down: 'DROP TABLE users',
152
277
  },
153
- {
154
- version: 2,
155
- name: 'seed_data',
156
- up: (tx) => {
157
- // You can run any code here
158
- tx.execute("INSERT INTO users (name) VALUES (?)", ['Alice'])
159
- },
160
- down: (tx) => {
161
- tx.execute("DELETE FROM users WHERE name = ?", ['Alice'])
162
- },
163
- },
164
278
  ]
165
279
 
166
- db.migrate(migrations)
167
- db.rollback(migrations) // undo last migration
168
- 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
169
283
  ```
170
284
 
171
285
  ### Backups
@@ -173,7 +287,7 @@ db.rollback(migrations, 0) // undo everything
173
287
  One-shot backups use `VACUUM INTO` for a consistent snapshot. Scheduled backups run on a cron expression with automatic file rotation.
174
288
 
175
289
  ```ts
176
- db.backup('./backups/snapshot.db')
290
+ await db.backup('./backups/snapshot.db')
177
291
 
178
292
  db.scheduleBackup({
179
293
  cron: '0 */6 * * *', // every 6 hours
@@ -213,6 +327,7 @@ Plug in callbacks to collect query timing, connection events, and CDC activity.
213
327
 
214
328
  ```ts
215
329
  const sirannon = new Sirannon({
330
+ driver,
216
331
  metrics: {
217
332
  onQueryComplete: m => histogram.observe(m.durationMs),
218
333
  onConnectionOpen: m => gauge.inc({ db: m.databaseId }),
@@ -228,6 +343,7 @@ For multi-tenant setups, the lifecycle manager handles auto-opening, idle timeou
228
343
 
229
344
  ```ts
230
345
  const sirannon = new Sirannon({
346
+ driver,
231
347
  lifecycle: {
232
348
  autoOpen: {
233
349
  resolver: id => ({ path: `/data/tenants/${id}.db` }),
@@ -238,7 +354,7 @@ const sirannon = new Sirannon({
238
354
  })
239
355
 
240
356
  // Databases resolve on first access:
241
- 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
242
358
  ```
243
359
 
244
360
  ## Server
@@ -247,32 +363,23 @@ Expose any `Sirannon` instance over HTTP and WebSocket with a single function ca
247
363
 
248
364
  ```ts
249
365
  import { Sirannon } from '@delali/sirannon-db'
366
+ import { betterSqlite3 } from '@delali/sirannon-db/driver/better-sqlite3'
250
367
  import { createServer } from '@delali/sirannon-db/server'
251
368
 
252
- const sirannon = new Sirannon()
253
- sirannon.open('app', './data/app.db')
254
-
255
- const server = createServer(sirannon, {
256
- port: 9876,
257
- cors: true,
258
- onRequest: ({ headers, path, method, remoteAddress }) => {
259
- if (headers.authorization !== `Bearer ${process.env.API_TOKEN}`) {
260
- return { status: 401, code: 'UNAUTHORIZED', message: 'Invalid or missing token' }
261
- }
262
- },
263
- })
369
+ const driver = betterSqlite3()
370
+ const sirannon = new Sirannon({ driver })
371
+ await sirannon.open('app', './data/app.db')
264
372
 
373
+ const server = createServer(sirannon, { port: 9876 })
265
374
  await server.listen()
266
375
  ```
267
376
 
268
- 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.
269
-
270
- **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.
271
378
 
272
379
  ### HTTP routes
273
380
 
274
381
  | Method | Path | Description |
275
- |---|---|---|
382
+ | --- | --- | --- |
276
383
  | `POST` | `/db/:id/query` | Execute a SELECT, returns `{ rows }` |
277
384
  | `POST` | `/db/:id/execute` | Execute a mutation, returns `{ changes, lastInsertRowId }` |
278
385
  | `POST` | `/db/:id/transaction` | Execute a batch of statements atomically, returns `{ results }` |
@@ -302,17 +409,16 @@ const users = await db.query<{ id: number; name: string }>('SELECT * FROM users'
302
409
 
303
410
  await db.execute('INSERT INTO users (name) VALUES (?)', ['Turing'])
304
411
 
305
- const sub = await db
306
- .on('users')
307
- .filter({ role: 'admin' })
308
- .subscribe(event => console.log('Admin changed:', event))
412
+ const sub = db.subscribe('users', event => {
413
+ console.log('User changed:', event)
414
+ })
309
415
 
310
416
  // Cleanup:
311
417
  sub.unsubscribe()
312
418
  client.close()
313
419
  ```
314
420
 
315
- Transactions require HTTP transport:
421
+ Transactions use the HTTP transport:
316
422
 
317
423
  ```ts
318
424
  const httpClient = new SirannonClient('http://localhost:9876', {
@@ -329,12 +435,70 @@ await httpDb.transaction([
329
435
  httpClient.close()
330
436
  ```
331
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
+
332
496
  ## Error handling
333
497
 
334
498
  All errors extend `SirannonError` with a machine-readable `code` property:
335
499
 
336
500
  | Error | Code | When |
337
- |---|---|---|
501
+ | --- | --- | --- |
338
502
  | `DatabaseNotFoundError` | `DATABASE_NOT_FOUND` | Database ID not in registry |
339
503
  | `DatabaseAlreadyExistsError` | `DATABASE_ALREADY_EXISTS` | Duplicate database ID |
340
504
  | `ReadOnlyError` | `READ_ONLY` | Write attempted on read-only database |
@@ -352,7 +516,7 @@ All errors extend `SirannonError` with a machine-readable `code` property:
352
516
  import { QueryError } from '@delali/sirannon-db'
353
517
 
354
518
  try {
355
- db.execute('INSERT INTO users (id) VALUES (?)', [1])
519
+ await db.execute('INSERT INTO users (id) VALUES (?)', [1])
356
520
  } catch (err) {
357
521
  if (err instanceof QueryError) {
358
522
  console.error(`SQL failed [${err.code}]: ${err.message}`)
@@ -363,28 +527,29 @@ try {
363
527
 
364
528
  ## Configuration reference
365
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
+
366
539
  ### `DatabaseOptions`
367
540
 
368
541
  | Option | Type | Default | Description |
369
- |---|---|---|---|
542
+ | --- | --- | --- | --- |
370
543
  | `readOnly` | `boolean` | `false` | Open in read-only mode |
371
544
  | `readPoolSize` | `number` | `4` | Number of read connections |
372
545
  | `walMode` | `boolean` | `true` | Enable WAL mode |
373
546
  | `cdcPollInterval` | `number` | `50` | CDC polling interval in ms |
374
547
  | `cdcRetention` | `number` | `3_600_000` | CDC retention period in ms (1 hour) |
375
548
 
376
- ### `SirannonOptions`
377
-
378
- | Option | Type | Description |
379
- |---|---|---|
380
- | `hooks` | `HookConfig` | Before/after hooks for queries, connections, subscriptions |
381
- | `metrics` | `MetricsConfig` | Callbacks for query timing, connection events, CDC activity |
382
- | `lifecycle` | `LifecycleConfig` | Auto-open resolver, idle timeout, max open databases |
383
-
384
549
  ### `ServerOptions`
385
550
 
386
551
  | Option | Type | Default | Description |
387
- |---|---|---|---|
552
+ | --- | --- | --- | --- |
388
553
  | `host` | `string` | `'127.0.0.1'` | Bind address |
389
554
  | `port` | `number` | `9876` | Listen port |
390
555
  | `cors` | `boolean \| CorsOptions` | `false` | CORS configuration |
@@ -393,15 +558,55 @@ try {
393
558
  ### `ClientOptions`
394
559
 
395
560
  | Option | Type | Default | Description |
396
- |---|---|---|---|
561
+ | --- | --- | --- | --- |
397
562
  | `transport` | `'websocket' \| 'http'` | `'websocket'` | Transport protocol |
398
563
  | `headers` | `Record<string, string>` | - | Custom HTTP headers |
399
564
  | `autoReconnect` | `boolean` | `true` | Reconnect on WebSocket disconnect |
400
565
  | `reconnectInterval` | `number` | `1000` | Reconnect delay in ms |
401
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
+
402
607
  ## Benchmarks
403
608
 
404
- 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.
405
610
 
406
611
  ## Development
407
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 };