174 lines
6 KiB
TypeScript
174 lines
6 KiB
TypeScript
export * as SessionContextEpoch from "./context-epoch"
|
|
|
|
import { eq } from "drizzle-orm"
|
|
import { DateTime, Effect, Schema } from "effect"
|
|
import type { Database } from "../database/database"
|
|
import { EventV2 } from "../event"
|
|
import { SystemContext } from "../system-context/index"
|
|
import { ContextSnapshotDecodeError } from "./error"
|
|
import { SessionEvent } from "./event"
|
|
import { SessionHistory } from "./history"
|
|
import { SessionInput } from "./input"
|
|
import { SessionMessage } from "./message"
|
|
import { SessionSchema } from "./schema"
|
|
import { SessionContextEpochTable } from "./sql"
|
|
|
|
type DatabaseService = Database.Interface["db"]
|
|
|
|
interface Prepared {
|
|
readonly baseline: string
|
|
readonly baselineSeq: number
|
|
}
|
|
|
|
export function initialize(
|
|
db: DatabaseService,
|
|
context: Effect.Effect<SystemContext.SystemContext>,
|
|
sessionID: SessionSchema.ID,
|
|
): Effect.Effect<Prepared | undefined, SystemContext.InitializationBlocked> {
|
|
return initializeOnce(db, context, sessionID).pipe(Effect.withSpan("SessionContextEpoch.initialize"))
|
|
}
|
|
|
|
export function prepare(
|
|
db: DatabaseService,
|
|
events: EventV2.Interface,
|
|
context: Effect.Effect<SystemContext.SystemContext>,
|
|
sessionID: SessionSchema.ID,
|
|
): Effect.Effect<Prepared, SystemContext.InitializationBlocked | ContextSnapshotDecodeError> {
|
|
return prepareOnce(db, events, context, sessionID).pipe(Effect.withSpan("SessionContextEpoch.prepare"))
|
|
}
|
|
|
|
const prepareOnce = Effect.fnUntraced(function* (
|
|
db: DatabaseService,
|
|
events: EventV2.Interface,
|
|
context: Effect.Effect<SystemContext.SystemContext>,
|
|
sessionID: SessionSchema.ID,
|
|
) {
|
|
const [value, stored, compaction] = yield* Effect.all(
|
|
[context, find(db, sessionID), SessionHistory.latestCompaction(db, sessionID)],
|
|
{ concurrency: "unbounded" },
|
|
)
|
|
if (!stored) {
|
|
const generation = yield* SystemContext.initialize(value)
|
|
const baselineSeq = yield* insert(db, sessionID, generation)
|
|
return { baseline: generation.baseline, baselineSeq }
|
|
}
|
|
|
|
const snapshot = yield* Schema.decodeUnknownEffect(SystemContext.Snapshot)(stored.snapshot).pipe(
|
|
Effect.mapError((error) => new ContextSnapshotDecodeError({ sessionID, details: String(error) })),
|
|
)
|
|
const replacementSeq = compaction !== undefined && compaction.seq > stored.baseline_seq ? compaction.seq : undefined
|
|
const result = replacementSeq
|
|
? yield* SystemContext.replace(value, snapshot)
|
|
: yield* SystemContext.reconcile(value, snapshot)
|
|
if (result._tag === "Unchanged" || result._tag === "ReplacementBlocked") {
|
|
return { baseline: stored.baseline, baselineSeq: stored.baseline_seq }
|
|
}
|
|
if (result._tag === "ReplacementReady") {
|
|
const baselineSeq = replacementSeq ?? (yield* EventV2.latestSequence(db, sessionID))
|
|
yield* replace(db, sessionID, baselineSeq, result.generation)
|
|
return { baseline: result.generation.baseline, baselineSeq }
|
|
}
|
|
|
|
yield* events.publish(
|
|
SessionEvent.ContextUpdated,
|
|
{ sessionID, messageID: SessionMessage.ID.create(), timestamp: yield* DateTime.now, text: result.text },
|
|
{ commit: () => advance(db, sessionID, result.snapshot).pipe(Effect.orDie) },
|
|
)
|
|
return { baseline: stored.baseline, baselineSeq: stored.baseline_seq }
|
|
})
|
|
|
|
const initializeOnce = Effect.fnUntraced(function* (
|
|
db: DatabaseService,
|
|
context: Effect.Effect<SystemContext.SystemContext>,
|
|
sessionID: SessionSchema.ID,
|
|
) {
|
|
if (yield* exists(db, sessionID)) return
|
|
const generation = yield* context.pipe(Effect.flatMap(SystemContext.initialize))
|
|
const baselineSeq = yield* insert(db, sessionID, generation)
|
|
return { baseline: generation.baseline, baselineSeq }
|
|
})
|
|
|
|
const exists = Effect.fn("SessionContextEpoch.exists")(function* (db: DatabaseService, sessionID: SessionSchema.ID) {
|
|
return (
|
|
(yield* db
|
|
.select({ sessionID: SessionContextEpochTable.session_id })
|
|
.from(SessionContextEpochTable)
|
|
.where(eq(SessionContextEpochTable.session_id, sessionID))
|
|
.get()
|
|
.pipe(Effect.orDie)) !== undefined
|
|
)
|
|
})
|
|
|
|
const find = Effect.fn("SessionContextEpoch.find")(function* (db: DatabaseService, sessionID: SessionSchema.ID) {
|
|
return yield* db
|
|
.select()
|
|
.from(SessionContextEpochTable)
|
|
.where(eq(SessionContextEpochTable.session_id, sessionID))
|
|
.get()
|
|
.pipe(Effect.orDie)
|
|
})
|
|
|
|
export const reset = Effect.fn("SessionContextEpoch.reset")(function* (
|
|
db: DatabaseService,
|
|
sessionID: SessionSchema.ID,
|
|
) {
|
|
yield* db
|
|
.delete(SessionContextEpochTable)
|
|
.where(eq(SessionContextEpochTable.session_id, sessionID))
|
|
.run()
|
|
.pipe(Effect.orDie)
|
|
})
|
|
|
|
const insert = Effect.fnUntraced(function* (
|
|
db: DatabaseService,
|
|
sessionID: SessionSchema.ID,
|
|
generation: SystemContext.Generation,
|
|
) {
|
|
const baselineSeq = yield* EventV2.latestSequence(db, sessionID)
|
|
yield* db
|
|
.insert(SessionContextEpochTable)
|
|
.values({
|
|
session_id: sessionID,
|
|
baseline: generation.baseline,
|
|
snapshot: generation.snapshot,
|
|
baseline_seq: baselineSeq,
|
|
})
|
|
.run()
|
|
.pipe(Effect.orDie)
|
|
return baselineSeq
|
|
})
|
|
|
|
const replace = Effect.fnUntraced(function* (
|
|
db: DatabaseService,
|
|
sessionID: SessionSchema.ID,
|
|
baselineSeq: number,
|
|
generation: SystemContext.Generation,
|
|
) {
|
|
const updated = yield* db
|
|
.update(SessionContextEpochTable)
|
|
.set({
|
|
baseline: generation.baseline,
|
|
snapshot: generation.snapshot,
|
|
baseline_seq: baselineSeq,
|
|
})
|
|
.where(eq(SessionContextEpochTable.session_id, sessionID))
|
|
.returning({ sessionID: SessionContextEpochTable.session_id })
|
|
.get()
|
|
.pipe(Effect.orDie)
|
|
if (!updated) return yield* Effect.die("Context Epoch not found")
|
|
})
|
|
|
|
const advance = Effect.fnUntraced(function* (
|
|
db: DatabaseService,
|
|
sessionID: SessionSchema.ID,
|
|
snapshot: SystemContext.Snapshot,
|
|
) {
|
|
const updated = yield* db
|
|
.update(SessionContextEpochTable)
|
|
.set({ snapshot })
|
|
.where(eq(SessionContextEpochTable.session_id, sessionID))
|
|
.returning({ sessionID: SessionContextEpochTable.session_id })
|
|
.get()
|
|
.pipe(Effect.orDie)
|
|
if (!updated) return yield* Effect.die("Context Epoch not found")
|
|
})
|