@xandreed/plugin-session-sqlite 0.13.0 → 0.14.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 +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/session-holder.testing.d.ts +1 -0
- package/dist/session-log.adapter.d.ts +4 -1
- package/dist/session-log.adapter.js +19 -21
- package/dist/session-recovery.test.d.ts +1 -0
- package/dist/sessions.conformance.test.d.ts +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -6,4 +6,4 @@ export declare const sessionSqlitePlugin: import("@xandreed/core").TypedPlugin<{
|
|
|
6
6
|
readonly path: string;
|
|
7
7
|
}, SessionLog | TurnAdmission, import("@xandreed/core").SessionLogError, never>;
|
|
8
8
|
export default sessionSqlitePlugin;
|
|
9
|
-
export { SessionLogSqliteLive, sessionLogSqlitePlugin } from "./session-log.adapter.js";
|
|
9
|
+
export { makeSessionLogSqlite, SessionLogSqliteLive, sessionLogSqlitePlugin } from "./session-log.adapter.js";
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { SessionLogSqliteLive } from "./session-log.adapter.js";
|
|
|
4
4
|
const Config = Schema.Struct({ path: Schema.String });
|
|
5
5
|
export const sessionSqlitePlugin = definePlugin({
|
|
6
6
|
id: "@xandreed/plugin-session-sqlite",
|
|
7
|
-
version: "0.
|
|
7
|
+
version: "0.14.0",
|
|
8
8
|
scope: "runtime",
|
|
9
9
|
config: Config,
|
|
10
10
|
defaults: { path: ".efferent/runtime/sessions.db" },
|
|
@@ -12,4 +12,4 @@ export const sessionSqlitePlugin = definePlugin({
|
|
|
12
12
|
layer: ({ path }) => Layer.merge(SessionLogSqliteLive(path), TurnAdmissionOpen)
|
|
13
13
|
});
|
|
14
14
|
export default sessionSqlitePlugin;
|
|
15
|
-
export { SessionLogSqliteLive, sessionLogSqlitePlugin } from "./session-log.adapter.js";
|
|
15
|
+
export { makeSessionLogSqlite, SessionLogSqliteLive, sessionLogSqlitePlugin } from "./session-log.adapter.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -5,7 +5,10 @@ import { SessionLog, SessionLogError } from "@xandreed/core";
|
|
|
5
5
|
* The authoritative session journal in one SQLite file: `session_heads`
|
|
6
6
|
* and `session_log_events`. Removal cascades to events and children. A commit is one
|
|
7
7
|
* IMMEDIATE transaction that checks the revision and the clock before it
|
|
8
|
-
* writes
|
|
8
|
+
* writes. The clock is the caller's Effect `Clock`, read once the transaction
|
|
9
|
+
* holds the write lock: a commit that waited for another connection past its
|
|
10
|
+
* `notAfter` is refused, and events carry the time they were committed. A
|
|
11
|
+
* lock held past the 5 s `busy_timeout` fails with `store.busy`.
|
|
9
12
|
*/
|
|
10
13
|
export declare const makeSessionLogSqlite: (path: string) => Effect.Effect<SessionLog["Service"], SessionLogError, Scope.Scope>;
|
|
11
14
|
/** The session log over one SQLite file (see `makeSessionLogSqlite`). */
|
|
@@ -2,7 +2,7 @@ import { Database } from "bun:sqlite";
|
|
|
2
2
|
import { mkdir } from "node:fs/promises";
|
|
3
3
|
import { chmodSync } from "node:fs";
|
|
4
4
|
import { dirname } from "node:path";
|
|
5
|
-
import { Clock, Effect, Layer, Option, Schema } from "effect";
|
|
5
|
+
import { Clock, Effect, Layer, Match, Option, Schema } from "effect";
|
|
6
6
|
import {
|
|
7
7
|
ConversationId,
|
|
8
8
|
definePlugin,
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
SessionLogError,
|
|
14
14
|
SessionMissing
|
|
15
15
|
} from "@xandreed/core";
|
|
16
|
-
const failure = (code) => (error) => new SessionLogError({ code, message: String(error) }), json = (text) => JSON.parse(text), headOf = (row, now) => ({
|
|
16
|
+
const busy = (error) => typeof error === "object" && error !== null && ("code" in error) && typeof error.code === "string" && error.code.startsWith("SQLITE_BUSY"), failure = (code) => (error) => new SessionLogError({ code: busy(error) ? "store.busy" : code, message: String(error) }), json = (text) => JSON.parse(text), headOf = (row, now) => ({
|
|
17
17
|
header: {
|
|
18
18
|
id: ConversationId.make(row.id),
|
|
19
19
|
owner: row.owner,
|
|
@@ -44,8 +44,8 @@ export const makeSessionLogSqlite = (path) => Effect.gen(function* () {
|
|
|
44
44
|
const db = yield* Effect.acquireRelease(Effect.try({ try: () => new Database(path, { create: !0, strict: !0 }), catch: failure("store.open") }), (database) => Effect.sync(() => database.close())), operation = (work) => Effect.try({ try: work, catch: failure("store.io") });
|
|
45
45
|
yield* operation(() => chmodSync(path, 384));
|
|
46
46
|
yield* operation(() => db.exec(`
|
|
47
|
-
PRAGMA journal_mode = WAL;
|
|
48
47
|
PRAGMA busy_timeout = 5000;
|
|
48
|
+
PRAGMA journal_mode = WAL;
|
|
49
49
|
PRAGMA foreign_keys = ON;
|
|
50
50
|
CREATE TABLE IF NOT EXISTS session_heads (
|
|
51
51
|
id TEXT PRIMARY KEY, owner TEXT NOT NULL, origin TEXT NOT NULL, created_at INTEGER NOT NULL, meta TEXT NOT NULL,
|
|
@@ -61,15 +61,16 @@ export const makeSessionLogSqlite = (path) => Effect.gen(function* () {
|
|
|
61
61
|
const headRow = (id) => db.query("SELECT * FROM session_heads WHERE id = ?").get(id), removeTree = (id) => db.query("DELETE FROM session_heads WHERE id = ?").run(id);
|
|
62
62
|
return SessionLog.of({
|
|
63
63
|
create: (header) => Effect.gen(function* () {
|
|
64
|
-
const now = yield* Clock.
|
|
64
|
+
const { outcome, now } = yield* Clock.clockWith((clock) => operation(() => db.transaction(() => {
|
|
65
|
+
const now = clock.currentTimeMillisUnsafe();
|
|
65
66
|
if (headRow(header.id) !== null)
|
|
66
|
-
return "exists";
|
|
67
|
+
return { outcome: "exists", now };
|
|
67
68
|
if (Option.isSome(header.parent) && headRow(header.parent.value.id) === null)
|
|
68
|
-
return "orphan";
|
|
69
|
+
return { outcome: "orphan", now };
|
|
69
70
|
const parent = Option.getOrNull(header.parent);
|
|
70
71
|
db.query("INSERT INTO session_heads (id, owner, origin, created_at, meta, parent_id, parent_through, turn_at_fork, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)").run(header.id, header.owner, header.origin, header.createdAt, JSON.stringify(header.meta), parent?.id ?? null, parent?.through ?? null, parent?.turnAtFork ?? null, now);
|
|
71
|
-
return "created";
|
|
72
|
-
}).immediate());
|
|
72
|
+
return { outcome: "created", now };
|
|
73
|
+
}).immediate()));
|
|
73
74
|
if (outcome === "exists")
|
|
74
75
|
return yield* Effect.fail(new SessionExists({ session: header.id }));
|
|
75
76
|
if (outcome === "orphan")
|
|
@@ -95,14 +96,14 @@ export const makeSessionLogSqlite = (path) => Effect.gen(function* () {
|
|
|
95
96
|
const kinds = query.kinds.length === 0 ? "" : ` AND kind IN (${query.kinds.map(() => "?").join(", ")})`, limit = Option.match(query.limit, { onNone: () => -1, onSome: (value) => Math.max(0, value) });
|
|
96
97
|
return yield* operation(() => db.query(`SELECT seq, turn, kind, at, data FROM session_log_events WHERE session_id = ? AND seq > ?${kinds} ORDER BY seq LIMIT ?`).all(id, query.after, ...query.kinds, limit).map(eventOf(id)));
|
|
97
98
|
}),
|
|
98
|
-
commit: (id, commit) => Clock.
|
|
99
|
-
const head = db.query("SELECT seq, revision FROM session_heads WHERE id = ?").get(id);
|
|
99
|
+
commit: (id, commit) => Clock.clockWith((clock) => operation(() => db.transaction(() => {
|
|
100
|
+
const at = clock.currentTimeMillisUnsafe(), head = db.query("SELECT seq, revision FROM session_heads WHERE id = ?").get(id);
|
|
100
101
|
if (head === null)
|
|
101
102
|
return { _tag: "Missing" };
|
|
102
103
|
if (head.revision !== commit.expect)
|
|
103
104
|
return { _tag: "Conflict", actual: head.revision };
|
|
104
105
|
if (Option.isSome(commit.notAfter) && at > commit.notAfter.value)
|
|
105
|
-
return { _tag: "Expired" };
|
|
106
|
+
return { _tag: "Expired", at };
|
|
106
107
|
const events = commit.events.map((draft, index) => ({
|
|
107
108
|
session: id,
|
|
108
109
|
seq: head.seq + index + 1,
|
|
@@ -114,15 +115,12 @@ export const makeSessionLogSqlite = (path) => Effect.gen(function* () {
|
|
|
114
115
|
events.forEach((event) => insert.run(id, event.seq, Option.getOrNull(event.turn), event.kind, at, JSON.stringify(event.data)));
|
|
115
116
|
db.query("UPDATE session_heads SET revision = revision + 1, seq = ?, state = COALESCE(?, state), updated_at = ? WHERE id = ?").run(head.seq + events.length, Option.match(commit.state, { onNone: () => null, onSome: (state) => JSON.stringify(state) }), at, id);
|
|
116
117
|
return { _tag: "Done", committed: { revision: head.revision + 1, seq: head.seq + events.length, events, at } };
|
|
117
|
-
}).immediate()).pipe(Effect.flatMap((
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return Effect.fail(new RevisionConflict({ session: id, expected: commit.expect, actual: outcome.actual }));
|
|
124
|
-
return Effect.fail(new LeaseExpired({ session: id, notAfter: Option.getOrElse(commit.notAfter, () => at), now: at }));
|
|
125
|
-
})))),
|
|
118
|
+
}).immediate())).pipe(Effect.flatMap(Match.valueTags({
|
|
119
|
+
Done: (outcome) => Effect.succeed(outcome.committed),
|
|
120
|
+
Missing: () => Effect.fail(new SessionMissing({ session: id })),
|
|
121
|
+
Conflict: (outcome) => Effect.fail(new RevisionConflict({ session: id, expected: commit.expect, actual: outcome.actual })),
|
|
122
|
+
Expired: (outcome) => Effect.fail(new LeaseExpired({ session: id, notAfter: Option.getOrElse(commit.notAfter, () => outcome.at), now: outcome.at }))
|
|
123
|
+
}))),
|
|
126
124
|
remove: (id) => operation(() => db.transaction(() => {
|
|
127
125
|
removeTree(id);
|
|
128
126
|
}).immediate())
|
|
@@ -131,7 +129,7 @@ export const makeSessionLogSqlite = (path) => Effect.gen(function* () {
|
|
|
131
129
|
const LogConfig = Schema.Struct({ path: Schema.String });
|
|
132
130
|
export const sessionLogSqlitePlugin = definePlugin({
|
|
133
131
|
id: "@xandreed/plugin-session-sqlite/log",
|
|
134
|
-
version: "0.
|
|
132
|
+
version: "0.14.0",
|
|
135
133
|
scope: "runtime",
|
|
136
134
|
config: LogConfig,
|
|
137
135
|
defaults: { path: ".efferent/runtime/sessions.db" },
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xandreed/plugin-session-sqlite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"engines": {
|
|
5
5
|
"bun": ">=1.3.0"
|
|
6
6
|
},
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@xandreed/core": "0.
|
|
16
|
+
"@xandreed/core": "0.14.0",
|
|
17
17
|
"effect": "4.0.7",
|
|
18
18
|
"ws": "^8.21.0"
|
|
19
19
|
},
|