@godspeedai/cognate-store-sqlite 0.1.1 → 0.2.0
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 +75 -38
- package/package.json +2 -2
- package/src/index.ts +33 -0
package/README.md
CHANGED
|
@@ -1,40 +1,77 @@
|
|
|
1
1
|
# @godspeedai/cognate-store-sqlite
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
`
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
3
|
+
Durable, transactional event storage for Cognate applications, built on `bun:sqlite`. One SQLite
|
|
4
|
+
file holds an application's entire authoritative history: events, records, idempotency results,
|
|
5
|
+
outbox intents, and blobs — committed atomically per decision.
|
|
6
|
+
|
|
7
|
+
**When to use this package:** any Cognate app that must survive restarts. Use
|
|
8
|
+
[`@godspeedai/cognate-store-memory`](https://www.npmjs.com/package/@godspeedai/cognate-store-memory)
|
|
9
|
+
for tests and throwaway prototypes instead. Both implement the same
|
|
10
|
+
`@godspeedai/cognate-events` store contract, so an app can switch stores with one line.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
bun add @godspeedai/cognate-store-sqlite
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Requires [Bun](https://bun.sh) >= 1.4.0 (the store uses Bun's built-in `bun:sqlite`).
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { openSqliteStore } from "@godspeedai/cognate-store-sqlite";
|
|
24
|
+
|
|
25
|
+
const store = openSqliteStore({ path: "./app.sqlite" });
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Pass the store to `createRuntime` from
|
|
29
|
+
[`@godspeedai/cognate-runtime-bun`](https://www.npmjs.com/package/@godspeedai/cognate-runtime-bun)
|
|
30
|
+
to make an entire agent runtime durable — `createRuntime` also accepts a file path directly
|
|
31
|
+
(`store: "./app.sqlite"`), which selects this store:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { createRuntime } from "@godspeedai/cognate-runtime-bun";
|
|
35
|
+
|
|
36
|
+
const runtime = await createRuntime({ store: "./app.sqlite" /* agents, policy, ... */ });
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The store implements the `DurableStore` contract from
|
|
40
|
+
[`@godspeedai/cognate-events`](https://www.npmjs.com/package/@godspeedai/cognate-events):
|
|
41
|
+
`commit` applies an atomic batch per decision (stream appends with expected versions, record
|
|
42
|
+
compare-and-swap writes, outbox intents, idempotency results), and `ConcurrencyError` is thrown
|
|
43
|
+
when a concurrent writer loses a stream-version race.
|
|
44
|
+
|
|
45
|
+
## What the schema guarantees
|
|
46
|
+
|
|
47
|
+
Every committed decision is one SQLite transaction (`BEGIN IMMEDIATE`): envelope validation,
|
|
48
|
+
per-stream version checks, record compare-and-swap writes, outbox intents, and idempotency
|
|
49
|
+
recording succeed or roll back together. A duplicate idempotency key returns the originally
|
|
50
|
+
recorded result instead of re-applying the decision.
|
|
51
|
+
|
|
52
|
+
Durability is configured for correctness over speed: `journal_mode=WAL` with
|
|
53
|
+
`synchronous=FULL` (fsync on every commit), so a decision is never reported committed before
|
|
54
|
+
SQLite has confirmed it. Payload erasure overwrites freed pages (`secure_delete`) and
|
|
55
|
+
checkpoints the WAL, so erased content does not linger in sidecar files.
|
|
56
|
+
|
|
57
|
+
## Limitations
|
|
58
|
+
|
|
59
|
+
- **One writer process.** Concurrent `commit` calls in one process serialize naturally, but
|
|
60
|
+
multiple processes writing the same file will hit `SQLITE_BUSY` after the 5-second busy
|
|
61
|
+
timeout rather than being serialized by this store.
|
|
62
|
+
- **Back up carefully.** Copy the `.sqlite` file plus its `-wal`/`-shm` siblings only after a
|
|
63
|
+
WAL checkpoint (or use SQLite's backup API) — a bare copy while WAL is active can miss
|
|
64
|
+
committed data.
|
|
65
|
+
|
|
66
|
+
## Related packages
|
|
67
|
+
|
|
68
|
+
- [`@godspeedai/cognate-events`](https://www.npmjs.com/package/@godspeedai/cognate-events) — the
|
|
69
|
+
store contract and conformance kit this package implements.
|
|
70
|
+
- [`@godspeedai/cognate-store-memory`](https://www.npmjs.com/package/@godspeedai/cognate-store-memory) —
|
|
71
|
+
the in-memory implementation of the same contract.
|
|
72
|
+
- [`@godspeedai/cognate`](https://www.npmjs.com/package/@godspeedai/cognate) — the SDK umbrella;
|
|
73
|
+
the easiest way to get a runtime backed by this store.
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
Apache-2.0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@godspeedai/cognate-store-sqlite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Authoritative DurableStore on bun:sqlite (spec §13).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -22,6 +22,6 @@
|
|
|
22
22
|
".": "./src/index.ts"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@godspeedai/cognate-events": "^0.
|
|
25
|
+
"@godspeedai/cognate-events": "^0.2.0"
|
|
26
26
|
}
|
|
27
27
|
}
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
OutboxSettlement,
|
|
16
16
|
ProjectionCheckpoint,
|
|
17
17
|
ProjectionCommit,
|
|
18
|
+
ProjectionSnapshot,
|
|
18
19
|
Json,
|
|
19
20
|
} from "@godspeedai/cognate-events";
|
|
20
21
|
import { migrate } from "./schema.ts";
|
|
@@ -313,6 +314,15 @@ export function openSqliteStore(options: SqliteStoreOptions): DurableStore {
|
|
|
313
314
|
db.query("DELETE FROM payloads WHERE event_id = ?").run(eventId);
|
|
314
315
|
db.query("UPDATE events SET erasure_reason = ?, erasure_at = ? WHERE event_id = ?").run(reason, erasedAt, eventId);
|
|
315
316
|
|
|
317
|
+
// Idempotency results are derived copies: a stored result may quote the erased payload (the
|
|
318
|
+
// continuation-transition shape) or fold earlier events of the same stream (the updateState
|
|
319
|
+
// shape). Erasure tombstones such results so replay can never re-disclose the content.
|
|
320
|
+
db.query(
|
|
321
|
+
`UPDATE idempotency SET result = ?
|
|
322
|
+
WHERE EXISTS (SELECT 1 FROM json_each(idempotency.event_ids) je WHERE je.value = ?)
|
|
323
|
+
OR EXISTS (SELECT 1 FROM json_each(idempotency.event_ids) je JOIN events e ON e.event_id = je.value WHERE e.stream_id = ?)`,
|
|
324
|
+
).run(JSON.stringify({ __erased: { reason, erasedAt } }), eventId, original.stream_id);
|
|
325
|
+
|
|
316
326
|
ensureStream("$erasures");
|
|
317
327
|
const sequence = streamVersionRow("$erasures") + 1;
|
|
318
328
|
const tombstone = insertEventRow(
|
|
@@ -472,6 +482,29 @@ export function openSqliteStore(options: SqliteStoreOptions): DurableStore {
|
|
|
472
482
|
return rows.map((row) => ({ key: row.key, value: JSON.parse(row.value) as Json }));
|
|
473
483
|
},
|
|
474
484
|
|
|
485
|
+
async readProjectionSnapshot(name: string): Promise<ProjectionSnapshot> {
|
|
486
|
+
assertOpen();
|
|
487
|
+
// One statement, one snapshot: the checkpoint can never be ahead of the rows it is returned
|
|
488
|
+
// with, even with another process folding concurrently (DEBT-021).
|
|
489
|
+
const row = db
|
|
490
|
+
.query(
|
|
491
|
+
`SELECT
|
|
492
|
+
(SELECT json_object('name', name, 'version', version, 'position', position, 'status', status,
|
|
493
|
+
'error_position', error_position, 'error_event_id', error_event_id, 'error_message', error_message)
|
|
494
|
+
FROM projection_checkpoints WHERE name = ?1) AS checkpoint,
|
|
495
|
+
(SELECT json_group_array(json_object('key', ordered.key, 'value', json(ordered.value)))
|
|
496
|
+
FROM (SELECT key, value FROM projection_state WHERE name = ?1 ORDER BY key) AS ordered) AS rows`,
|
|
497
|
+
)
|
|
498
|
+
.get(name) as Row;
|
|
499
|
+
const checkpoint = row.checkpoint ? JSON.parse(row.checkpoint as string) : undefined;
|
|
500
|
+
return {
|
|
501
|
+
checkpoint: checkpoint
|
|
502
|
+
? { ...checkpoint, error: checkpoint.error_event_id != null ? { position: checkpoint.error_position, eventId: checkpoint.error_event_id, message: checkpoint.error_message } : null }
|
|
503
|
+
: undefined,
|
|
504
|
+
rows: (JSON.parse(row.rows as string) as { key: string; value: Json }[]).map((entry) => ({ key: entry.key, value: entry.value })),
|
|
505
|
+
};
|
|
506
|
+
},
|
|
507
|
+
|
|
475
508
|
async resetProjection(name: string): Promise<void> {
|
|
476
509
|
assertOpen();
|
|
477
510
|
const txn = db.transaction((n: string): void => {
|