@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 CHANGED
@@ -1,5 +1,5 @@
1
1
  # @xandreed/plugin-session-sqlite
2
2
 
3
- Efferent composable agent framework, 0.13.0. Bun only.
3
+ Efferent composable agent framework, 0.14.0. Bun only.
4
4
 
5
5
  Documentation: https://github.com/xandreeddev/efferent
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.13.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; the clock is the process's.
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.currentTimeMillis, outcome = yield* operation(() => db.transaction(() => {
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.currentTimeMillis.pipe(Effect.flatMap((at) => operation(() => db.transaction(() => {
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((outcome) => {
118
- if (outcome._tag === "Done")
119
- return Effect.succeed(outcome.committed);
120
- if (outcome._tag === "Missing")
121
- return Effect.fail(new SessionMissing({ session: id }));
122
- if (outcome._tag === "Conflict")
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.13.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.13.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.13.0",
16
+ "@xandreed/core": "0.14.0",
17
17
  "effect": "4.0.7",
18
18
  "ws": "^8.21.0"
19
19
  },