refactor(v2): use effect sqlite session storage

This commit is contained in:
Kit Langton 2026-05-20 20:34:02 -04:00
commit dcbd244dd7
7 changed files with 183 additions and 124 deletions

View file

@ -429,12 +429,14 @@
"@clack/prompts": "1.0.0-alpha.1", "@clack/prompts": "1.0.0-alpha.1",
"@effect/opentelemetry": "catalog:", "@effect/opentelemetry": "catalog:",
"@effect/platform-node": "catalog:", "@effect/platform-node": "catalog:",
"@effect/sql-sqlite-bun": "catalog:",
"@gitlab/opencode-gitlab-auth": "1.3.3", "@gitlab/opencode-gitlab-auth": "1.3.3",
"@lydell/node-pty": "catalog:", "@lydell/node-pty": "catalog:",
"@modelcontextprotocol/sdk": "1.27.1", "@modelcontextprotocol/sdk": "1.27.1",
"@octokit/graphql": "9.0.2", "@octokit/graphql": "9.0.2",
"@octokit/rest": "catalog:", "@octokit/rest": "catalog:",
"@openauthjs/openauth": "catalog:", "@openauthjs/openauth": "catalog:",
"@opencode-ai/effect-drizzle-sqlite": "workspace:*",
"@opencode-ai/llm": "workspace:*", "@opencode-ai/llm": "workspace:*",
"@opencode-ai/plugin": "workspace:*", "@opencode-ai/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*", "@opencode-ai/script": "workspace:*",

View file

@ -1,7 +1,8 @@
/* oxlint-disable */ /* oxlint-disable */
import type { MigrationConfig } from "drizzle-orm/migrator" import type { MigrationConfig, MigrationFromJournalConfig, MigrationsJournal } from "drizzle-orm/migrator"
import { readMigrationFiles } from "drizzle-orm/migrator" import { readMigrationFiles } from "drizzle-orm/migrator"
import type { AnyRelations } from "drizzle-orm/relations" import type { AnyRelations } from "drizzle-orm/relations"
import crypto from "node:crypto"
import { migrate as coreMigrate } from "../sqlite-core/effect/session" import { migrate as coreMigrate } from "../sqlite-core/effect/session"
import type { EffectSQLiteDatabase } from "./driver" import type { EffectSQLiteDatabase } from "./driver"
@ -12,3 +13,21 @@ export function migrate<TRelations extends AnyRelations>(
const migrations = readMigrationFiles(config) const migrations = readMigrationFiles(config)
return coreMigrate(migrations, db.session, config) return coreMigrate(migrations, db.session, config)
} }
export function migrateFromJournal<TRelations extends AnyRelations>(
db: EffectSQLiteDatabase<TRelations>,
journal: MigrationsJournal,
config: Omit<MigrationFromJournalConfig, "migrationsJournal"> = {},
) {
return coreMigrate(
journal.map((migration) => ({
sql: migration.sql.split("--> statement-breakpoint"),
bps: true,
folderMillis: migration.timestamp,
hash: crypto.createHash("sha256").update(migration.sql).digest("hex"),
name: migration.name,
})),
db.session,
{ migrationsFolder: "", migrationsTable: config.migrationsTable },
)
}

View file

@ -1,6 +1,6 @@
export { EffectLogger } from "drizzle-orm/effect-core" export { EffectLogger } from "drizzle-orm/effect-core"
export * from "./effect-sqlite/driver" export * from "./effect-sqlite/driver"
export * from "./effect-sqlite/session" export * from "./effect-sqlite/session"
export { migrate } from "./effect-sqlite/migrator" export { migrate, migrateFromJournal } from "./effect-sqlite/migrator"
export * as EffectDrizzleSqlite from "." export * as EffectDrizzleSqlite from "."

View file

@ -96,12 +96,14 @@
"@clack/prompts": "1.0.0-alpha.1", "@clack/prompts": "1.0.0-alpha.1",
"@effect/opentelemetry": "catalog:", "@effect/opentelemetry": "catalog:",
"@effect/platform-node": "catalog:", "@effect/platform-node": "catalog:",
"@effect/sql-sqlite-bun": "catalog:",
"@gitlab/opencode-gitlab-auth": "1.3.3", "@gitlab/opencode-gitlab-auth": "1.3.3",
"@lydell/node-pty": "catalog:", "@lydell/node-pty": "catalog:",
"@modelcontextprotocol/sdk": "1.27.1", "@modelcontextprotocol/sdk": "1.27.1",
"@octokit/graphql": "9.0.2", "@octokit/graphql": "9.0.2",
"@octokit/rest": "catalog:", "@octokit/rest": "catalog:",
"@openauthjs/openauth": "catalog:", "@openauthjs/openauth": "catalog:",
"@opencode-ai/effect-drizzle-sqlite": "workspace:*",
"@opencode-ai/llm": "workspace:*", "@opencode-ai/llm": "workspace:*",
"@opencode-ai/plugin": "workspace:*", "@opencode-ai/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*", "@opencode-ai/script": "workspace:*",

View file

@ -1,5 +1,6 @@
import { type SQLiteBunDatabase } from "drizzle-orm/bun-sqlite" import { type SQLiteBunDatabase } from "drizzle-orm/bun-sqlite"
import { migrate } from "drizzle-orm/bun-sqlite/migrator" import { migrate } from "drizzle-orm/bun-sqlite/migrator"
import type { MigrationsJournal } from "drizzle-orm/migrator"
import { type SQLiteTransaction } from "drizzle-orm/sqlite-core" import { type SQLiteTransaction } from "drizzle-orm/sqlite-core"
export * from "drizzle-orm" export * from "drizzle-orm"
import { RuntimeFlags } from "@/effect/runtime-flags" import { RuntimeFlags } from "@/effect/runtime-flags"
@ -47,13 +48,19 @@ export type Transaction = SQLiteTransaction<"sync", void>
type Client = ReturnType<typeof init> type Client = ReturnType<typeof init>
type Journal = { sql: string; timestamp: number; name: string }[] export type Journal = MigrationsJournal
// Drizzle's migrate overloads trigger expensive variance checks here; narrow to the journal overload we actually use.
const migrateFromJournal = migrate as unknown as (db: SQLiteBunDatabase, entries: Journal) => void
function applyMigrations(db: SQLiteBunDatabase, entries: Journal) { function applyMigrations(db: SQLiteBunDatabase, entries: Journal) {
migrateFromJournal(db, entries) migrate(db, entries)
}
export function migrationJournal(flags: Pick<DatabaseFlags, "skipMigrations"> = readRuntimeFlags()) {
const entries =
typeof OPENCODE_MIGRATIONS !== "undefined"
? OPENCODE_MIGRATIONS
: migrations(path.join(import.meta.dirname, "../../migration"))
if (!flags.skipMigrations) return entries
return entries.map((item) => ({ ...item, sql: "select 1;" }))
} }
function time(tag: string) { function time(tag: string) {
@ -74,17 +81,17 @@ function migrations(dir: string): Journal {
.filter((entry) => entry.isDirectory()) .filter((entry) => entry.isDirectory())
.map((entry) => entry.name) .map((entry) => entry.name)
const sql = dirs const sql: Journal = dirs
.map((name) => { .map((name) => {
const file = path.join(dir, name, "migration.sql") const file = path.join(dir, name, "migration.sql")
if (!existsSync(file)) return if (!existsSync(file)) return undefined
return { return {
sql: readFileSync(file, "utf-8"), sql: readFileSync(file, "utf-8"),
timestamp: time(name), timestamp: time(name),
name, name,
} }
}) })
.filter(Boolean) as Journal .filter((entry) => entry !== undefined)
return sql.sort((a, b) => a.timestamp - b.timestamp) return sql.sort((a, b) => a.timestamp - b.timestamp)
} }
@ -94,7 +101,7 @@ let loaded = false
export const Client = Object.assign( export const Client = Object.assign(
(flags: DatabaseFlags = readRuntimeFlags()): Client => { (flags: DatabaseFlags = readRuntimeFlags()): Client => {
if (loaded) return client as Client if (loaded && client) return client
const dbPath = getPath(flags) const dbPath = getPath(flags)
log.info("opening database", { path: dbPath }) log.info("opening database", { path: dbPath })
@ -109,20 +116,12 @@ export const Client = Object.assign(
db.run("PRAGMA wal_checkpoint(PASSIVE)") db.run("PRAGMA wal_checkpoint(PASSIVE)")
// Apply schema migrations // Apply schema migrations
const entries = const entries = migrationJournal(flags)
typeof OPENCODE_MIGRATIONS !== "undefined"
? OPENCODE_MIGRATIONS
: migrations(path.join(import.meta.dirname, "../../migration"))
if (entries.length > 0) { if (entries.length > 0) {
log.info("applying migrations", { log.info("applying migrations", {
count: entries.length, count: entries.length,
mode: typeof OPENCODE_MIGRATIONS !== "undefined" ? "bundled" : "dev", mode: typeof OPENCODE_MIGRATIONS !== "undefined" ? "bundled" : "dev",
}) })
if (flags.skipMigrations) {
for (const item of entries) {
item.sql = "select 1;"
}
}
applyMigrations(db, entries) applyMigrations(db, entries)
} }
@ -159,19 +158,19 @@ export function use<T>(callback: (trx: TxOrDb) => T): T {
if (err instanceof LocalContext.NotFound) { if (err instanceof LocalContext.NotFound) {
const effects: (() => void | Promise<void>)[] = [] const effects: (() => void | Promise<void>)[] = []
const result = ctx.provide({ effects, tx: Client() }, () => callback(Client())) const result = ctx.provide({ effects, tx: Client() }, () => callback(Client()))
for (const effect of effects) effect() for (const effect of effects) void effect()
return result return result
} }
throw err throw err
} }
} }
export function effect(fn: () => any | Promise<any>) { export function effect(fn: () => void | Promise<void>) {
const bound = EffectBridge.bind(fn) const bound = EffectBridge.bind(fn)
try { try {
ctx.use().effects.push(bound) ctx.use().effects.push(bound)
} catch { } catch {
bound() void bound()
} }
} }
@ -190,7 +189,9 @@ export function transaction<T>(
const effects: (() => void | Promise<void>)[] = [] const effects: (() => void | Promise<void>)[] = []
const txCallback = EffectBridge.bind((tx: TxOrDb) => ctx.provide({ tx, effects }, () => callback(tx))) const txCallback = EffectBridge.bind((tx: TxOrDb) => ctx.provide({ tx, effects }, () => callback(tx)))
const result = Client().transaction(txCallback, { behavior: options?.behavior }) const result = Client().transaction(txCallback, { behavior: options?.behavior })
for (const effect of effects) effect() for (const effect of effects) void effect()
// Drizzle's transaction type does not preserve our NotPromise<T> constraint through the callback wrapper.
// oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion
return result as NotPromise<T> return result as NotPromise<T>
} }
throw err throw err

View file

@ -1,87 +1,110 @@
import { SessionMessageTable, SessionTable } from "@/session/session.sql" import { SessionMessageTable, SessionTable } from "@/session/session.sql"
import { and, asc, Database, desc, eq, gt, gte, isNull, like, lt, or, type SQL } from "@/storage/db" import { and, asc, Database as LegacyDatabase, desc, eq, gt, gte, isNull, like, lt, or, type SQL } from "@/storage/db"
import { SqliteClient } from "@effect/sql-sqlite-bun"
import { SessionMessage } from "@opencode-ai/core/session-message" import { SessionMessage } from "@opencode-ai/core/session-message"
import { Effect, Layer, Schema } from "effect" import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite"
import { Context, Effect, Layer, Schema } from "effect"
import { SessionStorage } from "./session" import { SessionStorage } from "./session"
const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Message) const decodeMessage = Schema.decodeUnknownSync(SessionMessage.Message)
const decodeSessionRow = Schema.decodeUnknownSync(SessionStorage.SessionRow) const decodeSessionRow = Schema.decodeUnknownSync(SessionStorage.SessionRow)
const makeDatabase = EffectDrizzleSqlite.makeWithDefaults()
type DatabaseShape = Effect.Success<typeof makeDatabase>
export class Database extends Context.Service<Database, DatabaseShape>()("@opencode/v2/session/StorageSql/Database") {}
export const databaseLayer = Layer.unwrap(
Effect.sync(() => {
const filename = LegacyDatabase.getPath()
return Layer.effect(
Database,
Effect.gen(function* () {
const db = yield* makeDatabase
yield* db.run("PRAGMA journal_mode = WAL")
yield* db.run("PRAGMA synchronous = NORMAL")
yield* db.run("PRAGMA busy_timeout = 5000")
yield* db.run("PRAGMA cache_size = -64000")
yield* db.run("PRAGMA foreign_keys = ON")
yield* db.run("PRAGMA wal_checkpoint(PASSIVE)")
yield* EffectDrizzleSqlite.migrateFromJournal(db, LegacyDatabase.migrationJournal())
return db
}),
).pipe(Layer.provide(SqliteClient.layer({ filename, disableWAL: filename === ":memory:" })))
}),
)
export const layer = Layer.effect( export const layer = Layer.effect(
SessionStorage.Service, SessionStorage.Service,
Effect.gen(function* () { Effect.gen(function* () {
const get: SessionStorage.Interface["get"] = Effect.fn("SessionStorageSql.get")((sessionID) => const db = yield* Database
attempt(() =>
Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get()),
).pipe(Effect.map((row) => (row ? fromSessionRow(row) : undefined))),
)
const list: SessionStorage.Interface["list"] = Effect.fn("SessionStorageSql.list")((input) => const get: SessionStorage.Interface["get"] = Effect.fn("SessionStorageSql.get")(function* (sessionID) {
attempt(() => { const row = yield* attempt(db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get())
const direction = input.cursor?.direction ?? "next" return row ? fromSessionRow(row) : undefined
const order = SessionStorage.pageOrder(input.order ?? "desc", direction) })
const sortColumn = SessionTable.time_updated
const conditions: SQL[] = []
if (input.directory) conditions.push(eq(SessionTable.directory, input.directory))
if (input.path)
conditions.push(or(eq(SessionTable.path, input.path), like(SessionTable.path, `${input.path}/%`))!)
if (input.workspaceID) conditions.push(eq(SessionTable.workspace_id, input.workspaceID))
if (input.roots) conditions.push(isNull(SessionTable.parent_id))
if (input.start) conditions.push(gte(sortColumn, input.start))
if (input.search) conditions.push(like(SessionTable.title, `%${input.search}%`))
if (input.cursor) conditions.push(sessionCursorBoundary(input.cursor, order))
return Database.use((db) => { const list: SessionStorage.Interface["list"] = Effect.fn("SessionStorageSql.list")(function* (input) {
const query = db const direction = input.cursor?.direction ?? "next"
.select() const order = SessionStorage.pageOrder(input.order ?? "desc", direction)
.from(SessionTable) const sortColumn = SessionTable.time_updated
.where(conditions.length > 0 ? and(...conditions) : undefined) const conditions: SQL[] = []
.orderBy( if (input.directory) conditions.push(eq(SessionTable.directory, input.directory))
order === "asc" ? asc(sortColumn) : desc(sortColumn), if (input.path)
order === "asc" ? asc(SessionTable.id) : desc(SessionTable.id), conditions.push(or(eq(SessionTable.path, input.path), like(SessionTable.path, `${input.path}/%`))!)
) if (input.workspaceID) conditions.push(eq(SessionTable.workspace_id, input.workspaceID))
const rows = input.limit === undefined ? query.all() : query.limit(input.limit).all() if (input.roots) conditions.push(isNull(SessionTable.parent_id))
return direction === "previous" ? rows.toReversed() : rows if (input.start) conditions.push(gte(sortColumn, input.start))
}) if (input.search) conditions.push(like(SessionTable.title, `%${input.search}%`))
}).pipe(Effect.map((rows) => rows.map(fromSessionRow))), if (input.cursor) conditions.push(sessionCursorBoundary(input.cursor, order))
)
const messages: SessionStorage.Interface["messages"] = Effect.fn("SessionStorageSql.messages")((input) => const query = db
attempt(() => { .select()
const direction = input.cursor?.direction ?? "next" .from(SessionTable)
const order = SessionStorage.pageOrder(input.order ?? "desc", direction) .where(conditions.length > 0 ? and(...conditions) : undefined)
const boundary = input.cursor ? messageCursorBoundary(input.cursor, order) : undefined .orderBy(
const where = boundary order === "asc" ? asc(sortColumn) : desc(sortColumn),
? and(eq(SessionMessageTable.session_id, input.sessionID), boundary) order === "asc" ? asc(SessionTable.id) : desc(SessionTable.id),
: eq(SessionMessageTable.session_id, input.sessionID) )
const rows = yield* attempt(input.limit === undefined ? query : query.limit(input.limit))
return (direction === "previous" ? rows.toReversed() : rows).map(fromSessionRow)
})
return Database.use((db) => { const messages: SessionStorage.Interface["messages"] = Effect.fn("SessionStorageSql.messages")(function* (input) {
const query = db const direction = input.cursor?.direction ?? "next"
.select() const order = SessionStorage.pageOrder(input.order ?? "desc", direction)
.from(SessionMessageTable) const boundary = input.cursor ? messageCursorBoundary(input.cursor, order) : undefined
.where(where) const where = boundary
.orderBy( ? and(eq(SessionMessageTable.session_id, input.sessionID), boundary)
order === "asc" ? asc(SessionMessageTable.time_created) : desc(SessionMessageTable.time_created), : eq(SessionMessageTable.session_id, input.sessionID)
order === "asc" ? asc(SessionMessageTable.id) : desc(SessionMessageTable.id),
) const query = db
const rows = input.limit === undefined ? query.all() : query.limit(input.limit).all() .select()
return direction === "previous" ? rows.toReversed() : rows .from(SessionMessageTable)
}) .where(where)
}).pipe(Effect.map((rows) => rows.map((row) => decodeMessage({ ...row.data, id: row.id, type: row.type })))), .orderBy(
) order === "asc" ? asc(SessionMessageTable.time_created) : desc(SessionMessageTable.time_created),
order === "asc" ? asc(SessionMessageTable.id) : desc(SessionMessageTable.id),
)
const rows = yield* attempt(input.limit === undefined ? query : query.limit(input.limit))
return (direction === "previous" ? rows.toReversed() : rows).map((row) =>
decodeMessage({ ...row.data, id: row.id, type: row.type }),
)
})
const context: SessionStorage.Interface["context"] = Effect.fn("SessionStorageSql.context")((sessionID) => const context: SessionStorage.Interface["context"] = Effect.fn("SessionStorageSql.context")((sessionID) =>
attempt(() => Effect.gen(function* () {
Database.use((db) => { const compaction = yield* attempt(
const compaction = db db
.select() .select()
.from(SessionMessageTable) .from(SessionMessageTable)
.where(and(eq(SessionMessageTable.session_id, sessionID), eq(SessionMessageTable.type, "compaction"))) .where(and(eq(SessionMessageTable.session_id, sessionID), eq(SessionMessageTable.type, "compaction")))
.orderBy(desc(SessionMessageTable.time_created), desc(SessionMessageTable.id)) .orderBy(desc(SessionMessageTable.time_created), desc(SessionMessageTable.id))
.limit(1) .limit(1)
.get() .get(),
)
return db const rows = yield* attempt(
db
.select() .select()
.from(SessionMessageTable) .from(SessionMessageTable)
.where( .where(
@ -98,23 +121,25 @@ export const layer = Layer.effect(
: undefined, : undefined,
), ),
) )
.orderBy(asc(SessionMessageTable.time_created), asc(SessionMessageTable.id)) .orderBy(asc(SessionMessageTable.time_created), asc(SessionMessageTable.id)),
.all() )
}),
).pipe(Effect.map((rows) => rows.map((row) => decodeMessage({ ...row.data, id: row.id, type: row.type })))), return rows.map((row) => decodeMessage({ ...row.data, id: row.id, type: row.type }))
}),
) )
return SessionStorage.Service.of({ get, list, messages, context }) return SessionStorage.Service.of({ get, list, messages, context })
}), }),
) )
export const defaultLayer = layer export const defaultLayer = layer.pipe(Layer.provide(databaseLayer.pipe(Layer.orDie)))
function attempt<A>(body: () => A) { function attempt<A, E, R>(effect: Effect.Effect<A, E, R>) {
return Effect.try({ return effect.pipe(
try: body, Effect.mapError(
catch: (cause) => new SessionStorage.StorageError({ message: "Session storage SQL operation failed", cause }), (cause) => new SessionStorage.StorageError({ message: "Session storage SQL operation failed", cause }),
}) ),
)
} }
function sessionCursorBoundary(cursor: SessionStorage.SessionCursor, order: SessionStorage.SortOrder) { function sessionCursorBoundary(cursor: SessionStorage.SessionCursor, order: SessionStorage.SortOrder) {

View file

@ -3,7 +3,6 @@ import { ProjectID } from "@/project/schema"
import { ProjectTable } from "@/project/project.sql" import { ProjectTable } from "@/project/project.sql"
import { SessionID } from "@/session/schema" import { SessionID } from "@/session/schema"
import { SessionMessageTable, SessionTable } from "@/session/session.sql" import { SessionMessageTable, SessionTable } from "@/session/session.sql"
import { Database } from "@/storage/db"
import { SessionStorage } from "@/v2/storage/session" import { SessionStorage } from "@/v2/storage/session"
import { SessionStorageMemory } from "@/v2/storage/session-memory" import { SessionStorageMemory } from "@/v2/storage/session-memory"
import { SessionStorageSql } from "@/v2/storage/session-sql" import { SessionStorageSql } from "@/v2/storage/session-sql"
@ -157,15 +156,17 @@ function sessionStorageContract<R, E>(name: string, layer: Layer.Layer<SessionSt
) )
} }
const sqlSeeds: Seeds<never> = { const sqlLayer = SessionStorageSql.layer.pipe(Layer.provideMerge(SessionStorageSql.databaseLayer))
reset: Effect.sync(resetSqlSeeds),
project: Effect.sync(seedProject), const sqlSeeds: Seeds<SessionStorageSql.Database> = {
session: (input) => Effect.sync(() => seedSession(input)), reset: resetSqlSeeds(),
userMessage: (input) => Effect.sync(() => seedUserMessage(input)), project: seedProject(),
compaction: (input) => Effect.sync(() => seedCompaction(input)), session: seedSession,
userMessage: seedUserMessage,
compaction: seedCompaction,
} }
sessionStorageContract("SessionStorageSql", SessionStorageSql.defaultLayer, sqlSeeds) sessionStorageContract("SessionStorageSql", sqlLayer, sqlSeeds)
const memorySeeds: Seeds<never> = { const memorySeeds: Seeds<never> = {
reset: Effect.sync(() => { reset: Effect.sync(() => {
@ -190,8 +191,9 @@ const memorySeeds: Seeds<never> = {
sessionStorageContract("SessionStorageMemory", memoryLayer, memorySeeds) sessionStorageContract("SessionStorageMemory", memoryLayer, memorySeeds)
function seedProject() { function seedProject() {
Database.use((db) => return Effect.gen(function* () {
db const db = yield* SessionStorageSql.Database
yield* db
.insert(ProjectTable) .insert(ProjectTable)
.values({ .values({
id: projectID, id: projectID,
@ -201,14 +203,17 @@ function seedProject() {
sandboxes: [], sandboxes: [],
}) })
.onConflictDoNothing() .onConflictDoNothing()
.run(), .run()
) .pipe(Effect.orDie)
})
} }
function resetSqlSeeds() { function resetSqlSeeds() {
Database.use((db) => { return Effect.gen(function* () {
db.delete(SessionMessageTable).where(eq(SessionMessageTable.session_id, sessionA)).run() const db = yield* SessionStorageSql.Database
db.delete(SessionTable) yield* db.delete(SessionMessageTable).where(eq(SessionMessageTable.session_id, sessionA)).run().pipe(Effect.orDie)
yield* db
.delete(SessionTable)
.where( .where(
or( or(
eq(SessionTable.id, sessionA), eq(SessionTable.id, sessionA),
@ -218,13 +223,15 @@ function resetSqlSeeds() {
), ),
) )
.run() .run()
db.delete(ProjectTable).where(eq(ProjectTable.id, projectID)).run() .pipe(Effect.orDie)
yield* db.delete(ProjectTable).where(eq(ProjectTable.id, projectID)).run().pipe(Effect.orDie)
}) })
} }
function seedSession(input: { id: SessionID; title: string; directory?: string; path: string; updated: number }) { function seedSession(input: { id: SessionID; title: string; directory?: string; path: string; updated: number }) {
Database.use((db) => return Effect.gen(function* () {
db const db = yield* SessionStorageSql.Database
yield* db
.insert(SessionTable) .insert(SessionTable)
.values({ .values({
id: input.id, id: input.id,
@ -243,20 +250,21 @@ function seedSession(input: { id: SessionID; title: string; directory?: string;
time_created: input.updated, time_created: input.updated,
time_updated: input.updated, time_updated: input.updated,
}) })
.run(), .run()
) .pipe(Effect.orDie)
})
} }
function seedUserMessage(input: { id: SessionMessage.ID; text: string; time: number }) { function seedUserMessage(input: { id: SessionMessage.ID; text: string; time: number }) {
const encoded = encodeMessage(makeUserMessage(input)) const encoded = encodeMessage(makeUserMessage(input))
const { id: _, type: __, ...data } = encoded const { id: _, type: __, ...data } = encoded
seedMessage(input.id, "user", input.time, data) return seedMessage(input.id, "user", input.time, data)
} }
function seedCompaction(input: { id: SessionMessage.ID; summary: string; time: number }) { function seedCompaction(input: { id: SessionMessage.ID; summary: string; time: number }) {
const encoded = encodeMessage(makeCompaction(input)) const encoded = encodeMessage(makeCompaction(input))
const { id: _, type: __, ...data } = encoded const { id: _, type: __, ...data } = encoded
seedMessage(input.id, "compaction", input.time, data) return seedMessage(input.id, "compaction", input.time, data)
} }
function makeSessionRow(input: { id: SessionID; title: string; directory?: string; path: string; updated: number }) { function makeSessionRow(input: { id: SessionID; title: string; directory?: string; path: string; updated: number }) {
@ -311,8 +319,9 @@ function seedMessage(
time: number, time: number,
data: typeof SessionMessageTable.$inferInsert.data, data: typeof SessionMessageTable.$inferInsert.data,
) { ) {
Database.use((db) => return Effect.gen(function* () {
db const db = yield* SessionStorageSql.Database
yield* db
.insert(SessionMessageTable) .insert(SessionMessageTable)
.values({ .values({
id, id,
@ -322,8 +331,9 @@ function seedMessage(
time_updated: time, time_updated: time,
data, data,
}) })
.run(), .run()
) .pipe(Effect.orDie)
})
} }
function appendMemoryMessage(message: SessionMessage.Message) { function appendMemoryMessage(message: SessionMessage.Message) {