refactor(core): move database schema ownership (#29068)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
parent
6bcb9cb9bb
commit
7f571d36ea
@@ -1,14 +1,17 @@
|
||||
import { Slug } from "@opencode-ai/core/util/slug"
|
||||
import { SessionLegacy } from "@opencode-ai/core/session/legacy"
|
||||
import { serviceUse } from "@opencode-ai/core/effect/service-use"
|
||||
import path from "path"
|
||||
import { BackgroundJob } from "@/background/job"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { Bus } from "@/bus"
|
||||
import { Decimal } from "decimal.js"
|
||||
import type { ProviderMetadata, Usage } from "@opencode-ai/llm"
|
||||
import { InstallationVersion } from "@opencode-ai/core/installation/version"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { makeRuntime } from "@opencode-ai/core/effect/runtime"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
|
||||
import { Database } from "@/storage/db"
|
||||
import { NotFoundError } from "@/storage/storage"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { and } from "drizzle-orm"
|
||||
@@ -19,29 +22,29 @@ import { like } from "drizzle-orm"
|
||||
import { inArray } from "drizzle-orm"
|
||||
import { lt } from "drizzle-orm"
|
||||
import { or } from "drizzle-orm"
|
||||
import { SyncEvent } from "../sync"
|
||||
import type { SQL } from "drizzle-orm"
|
||||
import { PartTable, SessionTable } from "./session.sql"
|
||||
import { ProjectTable } from "../project/project.sql"
|
||||
import { PartTable, SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { ProjectTable } from "@opencode-ai/core/project/sql"
|
||||
import { Storage } from "@/storage/storage"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { MessageV2 } from "./message-v2"
|
||||
import type { InstanceContext } from "../project/instance-context"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
import { ProjectID } from "../project/schema"
|
||||
import { WorkspaceID } from "../control-plane/schema"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
||||
import { SessionID, MessageID, PartID } from "./schema"
|
||||
import { ModelID, ProviderID } from "@/provider/schema"
|
||||
|
||||
import type { Provider } from "@/provider/provider"
|
||||
import { Permission } from "@/permission"
|
||||
import { Global } from "@opencode-ai/core/global"
|
||||
import { Effect, Layer, Option, Context, Schema, Types } from "effect"
|
||||
import { NonNegativeInt, optionalOmitUndefined } from "@opencode-ai/core/schema"
|
||||
import { AbsolutePath, NonNegativeInt, optionalOmitUndefined } from "@opencode-ai/core/schema"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
|
||||
const log = Log.create({ service: "session" })
|
||||
const runtime = makeRuntime(Database.Service, Database.defaultLayer)
|
||||
|
||||
const parentTitlePrefix = "New session - "
|
||||
const childTitlePrefix = "Child session - "
|
||||
@@ -82,8 +85,8 @@ export function fromRow(row: SessionRow): Info {
|
||||
agent: row.agent ?? undefined,
|
||||
model: row.model
|
||||
? {
|
||||
id: ModelID.make(row.model.id),
|
||||
providerID: ProviderID.make(row.model.providerID),
|
||||
id: ProviderV2.ModelID.make(row.model.id),
|
||||
providerID: ProviderV2.ID.make(row.model.providerID),
|
||||
variant: row.model.variant,
|
||||
}
|
||||
: undefined,
|
||||
@@ -112,6 +115,13 @@ export function fromRow(row: SessionRow): Info {
|
||||
}
|
||||
}
|
||||
|
||||
function eventLocation(info: Pick<Info, "directory" | "workspaceID">) {
|
||||
return {
|
||||
directory: AbsolutePath.make(info.directory),
|
||||
workspaceID: info.workspaceID,
|
||||
}
|
||||
}
|
||||
|
||||
export function toRow(info: Info) {
|
||||
return {
|
||||
id: info.id,
|
||||
@@ -202,8 +212,8 @@ const Revert = Schema.Struct({
|
||||
})
|
||||
|
||||
const Model = Schema.Struct({
|
||||
id: ModelID,
|
||||
providerID: ProviderID,
|
||||
id: ProviderV2.ModelID,
|
||||
providerID: ProviderV2.ID,
|
||||
variant: optionalOmitUndefined(Schema.String),
|
||||
})
|
||||
|
||||
@@ -212,8 +222,8 @@ export const Metadata = Schema.Record(Schema.String, Schema.Any)
|
||||
export const Info = Schema.Struct({
|
||||
id: SessionID,
|
||||
slug: Schema.String,
|
||||
projectID: ProjectID,
|
||||
workspaceID: optionalOmitUndefined(WorkspaceID),
|
||||
projectID: ProjectV2.ID,
|
||||
workspaceID: optionalOmitUndefined(WorkspaceV2.ID),
|
||||
directory: Schema.String,
|
||||
path: optionalOmitUndefined(Schema.String),
|
||||
parentID: optionalOmitUndefined(SessionID),
|
||||
@@ -233,7 +243,7 @@ export const Info = Schema.Struct({
|
||||
export type Info = Types.DeepMutable<Schema.Schema.Type<typeof Info>>
|
||||
|
||||
export const ProjectInfo = Schema.Struct({
|
||||
id: ProjectID,
|
||||
id: ProjectV2.ID,
|
||||
name: optionalOmitUndefined(Schema.String),
|
||||
worktree: Schema.String,
|
||||
}).annotate({ identifier: "ProjectSummary" })
|
||||
@@ -253,7 +263,7 @@ export const CreateInput = Schema.optional(
|
||||
model: Schema.optional(Model),
|
||||
metadata: Schema.optional(Metadata),
|
||||
permission: Schema.optional(Permission.Ruleset),
|
||||
workspaceID: Schema.optional(WorkspaceID),
|
||||
workspaceID: Schema.optional(WorkspaceV2.ID),
|
||||
}),
|
||||
)
|
||||
export type CreateInput = Types.DeepMutable<Schema.Schema.Type<typeof CreateInput>>
|
||||
@@ -291,13 +301,23 @@ export type ListInput = {
|
||||
directory?: string
|
||||
scope?: "project"
|
||||
path?: string
|
||||
workspaceID?: WorkspaceID
|
||||
workspaceID?: WorkspaceV2.ID
|
||||
roots?: boolean
|
||||
start?: number
|
||||
search?: string
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export type GlobalListInput = {
|
||||
directory?: string
|
||||
roots?: boolean
|
||||
start?: number
|
||||
cursor?: number
|
||||
search?: string
|
||||
limit?: number
|
||||
archived?: boolean
|
||||
}
|
||||
|
||||
const CreatedEventSchema = Schema.Struct({
|
||||
sessionID: SessionID,
|
||||
info: Info,
|
||||
@@ -317,8 +337,8 @@ const UpdatedTime = Schema.Struct({
|
||||
const UpdatedInfo = Schema.Struct({
|
||||
id: Schema.optional(Schema.NullOr(SessionID)),
|
||||
slug: Schema.optional(Schema.NullOr(Schema.String)),
|
||||
projectID: Schema.optional(Schema.NullOr(ProjectID)),
|
||||
workspaceID: Schema.optional(Schema.NullOr(WorkspaceID)),
|
||||
projectID: Schema.optional(Schema.NullOr(ProjectV2.ID)),
|
||||
workspaceID: Schema.optional(Schema.NullOr(WorkspaceV2.ID)),
|
||||
directory: Schema.optional(Schema.NullOr(Schema.String)),
|
||||
path: Schema.optional(Schema.NullOr(Schema.String)),
|
||||
parentID: Schema.optional(Schema.NullOr(SessionID)),
|
||||
@@ -342,41 +362,25 @@ const UpdatedEventSchema = Schema.Struct({
|
||||
})
|
||||
|
||||
export const Event = {
|
||||
Created: SyncEvent.define({
|
||||
type: "session.created",
|
||||
version: 1,
|
||||
aggregate: "sessionID",
|
||||
schema: CreatedEventSchema,
|
||||
}),
|
||||
Updated: SyncEvent.define({
|
||||
type: "session.updated",
|
||||
version: 1,
|
||||
aggregate: "sessionID",
|
||||
schema: UpdatedEventSchema,
|
||||
busSchema: CreatedEventSchema,
|
||||
}),
|
||||
Deleted: SyncEvent.define({
|
||||
type: "session.deleted",
|
||||
version: 1,
|
||||
aggregate: "sessionID",
|
||||
schema: CreatedEventSchema,
|
||||
}),
|
||||
Diff: BusEvent.define(
|
||||
"session.diff",
|
||||
Schema.Struct({
|
||||
Created: SessionLegacy.Event.Created,
|
||||
Updated: SessionLegacy.Event.Updated,
|
||||
Deleted: SessionLegacy.Event.Deleted,
|
||||
Diff: EventV2.define({
|
||||
type: "session.diff",
|
||||
schema: {
|
||||
sessionID: SessionID,
|
||||
diff: Schema.Array(Snapshot.FileDiff),
|
||||
}),
|
||||
),
|
||||
Error: BusEvent.define(
|
||||
"session.error",
|
||||
Schema.Struct({
|
||||
},
|
||||
}),
|
||||
Error: EventV2.define({
|
||||
type: "session.error",
|
||||
schema: {
|
||||
sessionID: Schema.optional(SessionID),
|
||||
// Reuses MessageV2.Assistant.fields.error (already Schema.optional) so
|
||||
// the derived zod keeps the same discriminated-union shape on the bus.
|
||||
error: MessageV2.Assistant.fields.error,
|
||||
}),
|
||||
),
|
||||
// Reuses SessionLegacy.Assistant.fields.error (already Schema.optional) so
|
||||
// the derived schema keeps the same discriminated-union shape on the event stream.
|
||||
error: SessionLegacy.Assistant.fields.error,
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
export function plan(input: { slug: string; time: { created: number } }, instance: InstanceContext) {
|
||||
@@ -461,6 +465,7 @@ export type NotFound = NotFoundError
|
||||
|
||||
export interface Interface {
|
||||
readonly list: (input?: ListInput) => Effect.Effect<Info[]>
|
||||
readonly listGlobal: (input?: GlobalListInput) => Effect.Effect<GlobalInfo[]>
|
||||
readonly create: (input?: {
|
||||
parentID?: SessionID
|
||||
title?: string
|
||||
@@ -468,7 +473,7 @@ export interface Interface {
|
||||
model?: Schema.Schema.Type<typeof Model>
|
||||
metadata?: typeof Metadata.Type
|
||||
permission?: Permission.Ruleset
|
||||
workspaceID?: WorkspaceID
|
||||
workspaceID?: WorkspaceV2.ID
|
||||
}) => Effect.Effect<Info>
|
||||
readonly fork: (input: { sessionID: SessionID; messageID?: MessageID }) => Effect.Effect<Info, NotFound>
|
||||
readonly touch: (sessionID: SessionID) => Effect.Effect<void>
|
||||
@@ -484,19 +489,24 @@ export interface Interface {
|
||||
}) => Effect.Effect<void>
|
||||
readonly clearRevert: (sessionID: SessionID) => Effect.Effect<void>
|
||||
readonly setSummary: (input: { sessionID: SessionID; summary: Info["summary"] }) => Effect.Effect<void>
|
||||
readonly setShare: (input: { sessionID: SessionID; share: Info["share"] }) => Effect.Effect<void>
|
||||
readonly setWorkspace: (input: { sessionID: SessionID; workspaceID: Info["workspaceID"] }) => Effect.Effect<void>
|
||||
readonly diff: (sessionID: SessionID) => Effect.Effect<Snapshot.FileDiff[]>
|
||||
readonly messages: (input: { sessionID: SessionID; limit?: number }) => Effect.Effect<MessageV2.WithParts[], NotFound>
|
||||
readonly messages: (input: {
|
||||
sessionID: SessionID
|
||||
limit?: number
|
||||
}) => Effect.Effect<SessionLegacy.WithParts[], NotFound>
|
||||
readonly children: (parentID: SessionID) => Effect.Effect<Info[]>
|
||||
readonly remove: (sessionID: SessionID) => Effect.Effect<void, NotFound>
|
||||
readonly updateMessage: <T extends MessageV2.Info>(msg: T) => Effect.Effect<T>
|
||||
readonly updateMessage: <T extends SessionLegacy.Info>(msg: T) => Effect.Effect<T>
|
||||
readonly removeMessage: (input: { sessionID: SessionID; messageID: MessageID }) => Effect.Effect<MessageID>
|
||||
readonly removePart: (input: { sessionID: SessionID; messageID: MessageID; partID: PartID }) => Effect.Effect<PartID>
|
||||
readonly getPart: (input: {
|
||||
sessionID: SessionID
|
||||
messageID: MessageID
|
||||
partID: PartID
|
||||
}) => Effect.Effect<MessageV2.Part | undefined>
|
||||
readonly updatePart: <T extends MessageV2.Part>(part: T) => Effect.Effect<T>
|
||||
}) => Effect.Effect<SessionLegacy.Part | undefined>
|
||||
readonly updatePart: <T extends SessionLegacy.Part>(part: T) => Effect.Effect<T>
|
||||
readonly updatePartDelta: (input: {
|
||||
sessionID: SessionID
|
||||
messageID: MessageID
|
||||
@@ -507,39 +517,61 @@ export interface Interface {
|
||||
/** Finds the first message matching the predicate, searching newest-first. */
|
||||
readonly findMessage: (
|
||||
sessionID: SessionID,
|
||||
predicate: (msg: MessageV2.WithParts) => boolean,
|
||||
) => Effect.Effect<Option.Option<MessageV2.WithParts>, NotFound>
|
||||
predicate: (msg: SessionLegacy.WithParts) => boolean,
|
||||
) => Effect.Effect<Option.Option<SessionLegacy.WithParts>, NotFound>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Session") {}
|
||||
|
||||
export const use = serviceUse(Service)
|
||||
|
||||
export type Patch = Types.DeepMutable<SyncEvent.Event<typeof Event.Updated>["data"]["info"]>
|
||||
|
||||
const db = <T>(fn: (d: Parameters<typeof Database.use>[0] extends (trx: infer D) => any ? D : never) => T) =>
|
||||
Effect.sync(() => Database.use(fn))
|
||||
export type Patch = Omit<Partial<Info>, "time" | "share" | "summary" | "revert" | "permission"> & {
|
||||
time?: Partial<Info["time"]>
|
||||
share?: Partial<NonNullable<Info["share"]>> | null
|
||||
summary?: Info["summary"] | null
|
||||
revert?: Info["revert"] | null
|
||||
permission?: Info["permission"] | null
|
||||
}
|
||||
|
||||
export const layer: Layer.Layer<
|
||||
Service,
|
||||
never,
|
||||
BackgroundJob.Service | Bus.Service | Storage.Service | SyncEvent.Service | RuntimeFlags.Service
|
||||
| BackgroundJob.Service
|
||||
| Storage.Service
|
||||
| RuntimeFlags.Service
|
||||
| Database.Service
|
||||
| EventV2Bridge.Service
|
||||
> = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const { db } = yield* Database.Service
|
||||
const database = yield* Database.Service
|
||||
const background = yield* BackgroundJob.Service
|
||||
const bus = yield* Bus.Service
|
||||
const events = yield* EventV2Bridge.Service
|
||||
const storage = yield* Storage.Service
|
||||
const sync = yield* SyncEvent.Service
|
||||
const flags = yield* RuntimeFlags.Service
|
||||
|
||||
const locationForSession = Effect.fnUntraced(function* (sessionID: SessionID) {
|
||||
const row = yield* db
|
||||
.select({ directory: SessionTable.directory, workspaceID: SessionTable.workspace_id })
|
||||
.from(SessionTable)
|
||||
.where(eq(SessionTable.id, sessionID))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!row) return
|
||||
return {
|
||||
directory: AbsolutePath.make(row.directory),
|
||||
workspaceID: row.workspaceID ?? undefined,
|
||||
}
|
||||
})
|
||||
|
||||
const createNext = Effect.fn("Session.createNext")(function* (input: {
|
||||
id?: SessionID
|
||||
title?: string
|
||||
agent?: string
|
||||
model?: Schema.Schema.Type<typeof Model>
|
||||
parentID?: SessionID
|
||||
workspaceID?: WorkspaceID
|
||||
workspaceID?: WorkspaceV2.ID
|
||||
directory: string
|
||||
path?: string
|
||||
metadata?: typeof Metadata.Type
|
||||
@@ -569,41 +601,78 @@ export const layer: Layer.Layer<
|
||||
}
|
||||
log.info("created", result)
|
||||
|
||||
yield* sync.run(Event.Created, { sessionID: result.id, info: result })
|
||||
|
||||
if (!flags.experimentalWorkspaces) {
|
||||
// This only exist for backwards compatibility. We should not be
|
||||
// manually publishing this event; it is a sync event now
|
||||
yield* bus.publish(Event.Updated, {
|
||||
sessionID: result.id,
|
||||
info: result,
|
||||
})
|
||||
}
|
||||
yield* events.publish(
|
||||
SessionLegacy.Event.Created,
|
||||
{ sessionID: result.id, info: result },
|
||||
{ location: eventLocation(result) },
|
||||
)
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
const get = Effect.fn("Session.get")(function* (id: SessionID) {
|
||||
const row = yield* db((d) => d.select().from(SessionTable).where(eq(SessionTable.id, id)).get())
|
||||
const row = yield* db.select().from(SessionTable).where(eq(SessionTable.id, id)).get().pipe(Effect.orDie)
|
||||
if (!row) return yield* Effect.fail(new NotFoundError({ message: `Session not found: ${id}` }))
|
||||
return fromRow(row)
|
||||
})
|
||||
|
||||
const list = Effect.fn("Session.list")(function* (input?: ListInput) {
|
||||
const ctx = yield* InstanceState.context
|
||||
return Array.from(
|
||||
listByProject({ projectID: ctx.project.id, experimentalWorkspaces: flags.experimentalWorkspaces, ...input }),
|
||||
)
|
||||
return yield* listByProject(db, {
|
||||
projectID: ctx.project.id,
|
||||
experimentalWorkspaces: flags.experimentalWorkspaces,
|
||||
...input,
|
||||
})
|
||||
})
|
||||
|
||||
const listGlobal = Effect.fn("Session.listGlobal")(function* (input?: GlobalListInput) {
|
||||
const conditions: SQL[] = []
|
||||
if (input?.directory) conditions.push(eq(SessionTable.directory, input.directory))
|
||||
if (input?.roots) conditions.push(isNull(SessionTable.parent_id))
|
||||
if (input?.start) conditions.push(gte(SessionTable.time_updated, input.start))
|
||||
if (input?.cursor) conditions.push(lt(SessionTable.time_updated, input.cursor))
|
||||
if (input?.search) conditions.push(like(SessionTable.title, `%${input.search}%`))
|
||||
if (!input?.archived) conditions.push(isNull(SessionTable.time_archived))
|
||||
|
||||
const query =
|
||||
conditions.length > 0
|
||||
? db
|
||||
.select()
|
||||
.from(SessionTable)
|
||||
.where(and(...conditions))
|
||||
: db.select().from(SessionTable)
|
||||
const rows = yield* query
|
||||
.orderBy(desc(SessionTable.time_updated), desc(SessionTable.id))
|
||||
.limit(input?.limit ?? 100)
|
||||
.all()
|
||||
.pipe(Effect.orDie)
|
||||
const ids = [...new Set(rows.map((row) => row.project_id))]
|
||||
const projects = new Map<string, ProjectInfo>()
|
||||
if (ids.length > 0) {
|
||||
const items = yield* db
|
||||
.select({ id: ProjectTable.id, name: ProjectTable.name, worktree: ProjectTable.worktree })
|
||||
.from(ProjectTable)
|
||||
.where(inArray(ProjectTable.id, ids))
|
||||
.all()
|
||||
.pipe(Effect.orDie)
|
||||
for (const item of items) {
|
||||
projects.set(item.id, {
|
||||
id: item.id,
|
||||
name: item.name ?? undefined,
|
||||
worktree: item.worktree,
|
||||
})
|
||||
}
|
||||
}
|
||||
return rows.map((row) => ({ ...fromRow(row), project: projects.get(row.project_id) ?? null }))
|
||||
})
|
||||
|
||||
const children = Effect.fn("Session.children")(function* (parentID: SessionID) {
|
||||
const rows = yield* db((d) =>
|
||||
d
|
||||
.select()
|
||||
.from(SessionTable)
|
||||
.where(and(eq(SessionTable.parent_id, parentID)))
|
||||
.all(),
|
||||
)
|
||||
const rows = yield* db
|
||||
.select()
|
||||
.from(SessionTable)
|
||||
.where(and(eq(SessionTable.parent_id, parentID)))
|
||||
.all()
|
||||
.pipe(Effect.orDie)
|
||||
return rows.map(fromRow)
|
||||
})
|
||||
|
||||
@@ -623,50 +692,59 @@ export const layer: Layer.Layer<
|
||||
yield* remove(child.id)
|
||||
}
|
||||
|
||||
yield* sync.run(Event.Deleted, { sessionID, info: session }, { publish: hasInstance })
|
||||
yield* sync.remove(sessionID)
|
||||
yield* events.publish(
|
||||
SessionLegacy.Event.Deleted,
|
||||
{ sessionID, info: session },
|
||||
{ location: eventLocation(session) },
|
||||
)
|
||||
yield* events.remove(sessionID)
|
||||
} catch (e) {
|
||||
log.error(e)
|
||||
}
|
||||
})
|
||||
|
||||
const updateMessage = <T extends MessageV2.Info>(msg: T): Effect.Effect<T> =>
|
||||
const updateMessage = <T extends SessionLegacy.Info>(msg: T): Effect.Effect<T> =>
|
||||
Effect.gen(function* () {
|
||||
yield* sync.run(MessageV2.Event.Updated, { sessionID: msg.sessionID, info: msg })
|
||||
const location = yield* locationForSession(msg.sessionID)
|
||||
yield* events.publish(SessionLegacy.Event.MessageUpdated, { sessionID: msg.sessionID, info: msg }, { location })
|
||||
return msg
|
||||
}).pipe(Effect.withSpan("Session.updateMessage"))
|
||||
|
||||
const updatePart = <T extends MessageV2.Part>(part: T): Effect.Effect<T> =>
|
||||
const updatePart = <T extends SessionLegacy.Part>(part: T): Effect.Effect<T> =>
|
||||
Effect.gen(function* () {
|
||||
yield* sync.run(MessageV2.Event.PartUpdated, {
|
||||
sessionID: part.sessionID,
|
||||
part: structuredClone(part),
|
||||
time: Date.now(),
|
||||
})
|
||||
const location = yield* locationForSession(part.sessionID)
|
||||
yield* events.publish(
|
||||
SessionLegacy.Event.PartUpdated,
|
||||
{
|
||||
sessionID: part.sessionID,
|
||||
part: structuredClone(part),
|
||||
time: Date.now(),
|
||||
},
|
||||
{ location },
|
||||
)
|
||||
return part
|
||||
}).pipe(Effect.withSpan("Session.updatePart"))
|
||||
|
||||
const getPart: Interface["getPart"] = Effect.fn("Session.getPart")(function* (input) {
|
||||
const row = Database.use((db) =>
|
||||
db
|
||||
.select()
|
||||
.from(PartTable)
|
||||
.where(
|
||||
and(
|
||||
eq(PartTable.session_id, input.sessionID),
|
||||
eq(PartTable.message_id, input.messageID),
|
||||
eq(PartTable.id, input.partID),
|
||||
),
|
||||
)
|
||||
.get(),
|
||||
)
|
||||
const row = yield* db
|
||||
.select()
|
||||
.from(PartTable)
|
||||
.where(
|
||||
and(
|
||||
eq(PartTable.session_id, input.sessionID),
|
||||
eq(PartTable.message_id, input.messageID),
|
||||
eq(PartTable.id, input.partID),
|
||||
),
|
||||
)
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!row) return
|
||||
return {
|
||||
...row.data,
|
||||
id: row.id,
|
||||
sessionID: row.session_id,
|
||||
messageID: row.message_id,
|
||||
} as MessageV2.Part
|
||||
} as SessionLegacy.Part
|
||||
})
|
||||
|
||||
const create = Effect.fn("Session.create")(function* (input?: {
|
||||
@@ -676,7 +754,7 @@ export const layer: Layer.Layer<
|
||||
model?: Schema.Schema.Type<typeof Model>
|
||||
metadata?: typeof Metadata.Type
|
||||
permission?: Permission.Ruleset
|
||||
workspaceID?: WorkspaceID
|
||||
workspaceID?: WorkspaceV2.ID
|
||||
}) {
|
||||
const ctx = yield* InstanceState.context
|
||||
const workspace = yield* InstanceState.workspaceID
|
||||
@@ -721,7 +799,7 @@ export const layer: Layer.Layer<
|
||||
})
|
||||
|
||||
for (const part of msg.parts) {
|
||||
const p: MessageV2.Part = {
|
||||
const p: SessionLegacy.Part = {
|
||||
...part,
|
||||
id: PartID.ascending(),
|
||||
messageID: cloned.id,
|
||||
@@ -736,29 +814,44 @@ export const layer: Layer.Layer<
|
||||
return session
|
||||
})
|
||||
|
||||
const patch = (sessionID: SessionID, info: Patch) => sync.run(Event.Updated, { sessionID, info })
|
||||
const patch = (sessionID: SessionID, info: Patch) =>
|
||||
Effect.gen(function* () {
|
||||
const current = yield* get(sessionID)
|
||||
const next = {
|
||||
...current,
|
||||
...info,
|
||||
time: info.time ? { ...current.time, ...info.time } : current.time,
|
||||
share: info.share === null ? undefined : info.share ? { ...current.share, ...info.share } : current.share,
|
||||
summary: info.summary === null ? undefined : (info.summary ?? current.summary),
|
||||
revert: info.revert === null ? undefined : (info.revert ?? current.revert),
|
||||
permission: info.permission === null ? undefined : (info.permission ?? current.permission),
|
||||
} as Info
|
||||
yield* events.publish(SessionLegacy.Event.Updated, { sessionID, info: next }, { location: eventLocation(next) })
|
||||
})
|
||||
|
||||
const touch = Effect.fn("Session.touch")(function* (sessionID: SessionID) {
|
||||
yield* patch(sessionID, { time: { updated: Date.now() } })
|
||||
yield* patch(sessionID, { time: { updated: Date.now() } }).pipe(Effect.orDie)
|
||||
})
|
||||
|
||||
const setTitle = Effect.fn("Session.setTitle")(function* (input: { sessionID: SessionID; title: string }) {
|
||||
yield* patch(input.sessionID, { title: input.title })
|
||||
yield* patch(input.sessionID, { title: input.title }).pipe(Effect.orDie)
|
||||
})
|
||||
|
||||
const setArchived = Effect.fn("Session.setArchived")(function* (input: { sessionID: SessionID; time?: number }) {
|
||||
yield* patch(input.sessionID, { time: { archived: input.time } })
|
||||
yield* patch(input.sessionID, { time: { archived: input.time } }).pipe(Effect.orDie)
|
||||
})
|
||||
|
||||
const setMetadata = Effect.fn("Session.setMetadata")(function* (input: typeof SetMetadataInput.Type) {
|
||||
yield* patch(input.sessionID, { metadata: input.metadata, time: { updated: Date.now() } })
|
||||
yield* patch(input.sessionID, { metadata: input.metadata, time: { updated: Date.now() } }).pipe(Effect.orDie)
|
||||
})
|
||||
|
||||
const setPermission = Effect.fn("Session.setPermission")(function* (input: {
|
||||
sessionID: SessionID
|
||||
permission: Permission.Ruleset
|
||||
}) {
|
||||
yield* patch(input.sessionID, { permission: [...input.permission], time: { updated: Date.now() } })
|
||||
yield* patch(input.sessionID, { permission: [...input.permission], time: { updated: Date.now() } }).pipe(
|
||||
Effect.orDie,
|
||||
)
|
||||
})
|
||||
|
||||
const setRevert = Effect.fn("Session.setRevert")(function* (input: {
|
||||
@@ -766,18 +859,35 @@ export const layer: Layer.Layer<
|
||||
revert: Info["revert"]
|
||||
summary: Info["summary"]
|
||||
}) {
|
||||
yield* patch(input.sessionID, { summary: input.summary, time: { updated: Date.now() }, revert: input.revert })
|
||||
yield* patch(input.sessionID, {
|
||||
summary: input.summary,
|
||||
time: { updated: Date.now() },
|
||||
revert: input.revert,
|
||||
}).pipe(Effect.orDie)
|
||||
})
|
||||
|
||||
const clearRevert = Effect.fn("Session.clearRevert")(function* (sessionID: SessionID) {
|
||||
yield* patch(sessionID, { time: { updated: Date.now() }, revert: null })
|
||||
yield* patch(sessionID, { time: { updated: Date.now() }, revert: null }).pipe(Effect.orDie)
|
||||
})
|
||||
|
||||
const setSummary = Effect.fn("Session.setSummary")(function* (input: {
|
||||
sessionID: SessionID
|
||||
summary: Info["summary"]
|
||||
}) {
|
||||
yield* patch(input.sessionID, { time: { updated: Date.now() }, summary: input.summary })
|
||||
yield* patch(input.sessionID, { time: { updated: Date.now() }, summary: input.summary }).pipe(Effect.orDie)
|
||||
})
|
||||
|
||||
const setShare = Effect.fn("Session.setShare")(function* (input: { sessionID: SessionID; share: Info["share"] }) {
|
||||
yield* patch(input.sessionID, { share: input.share ?? null, time: { updated: Date.now() } }).pipe(Effect.orDie)
|
||||
})
|
||||
|
||||
const setWorkspace = Effect.fn("Session.setWorkspace")(function* (input: {
|
||||
sessionID: SessionID
|
||||
workspaceID: Info["workspaceID"]
|
||||
}) {
|
||||
yield* patch(input.sessionID, { workspaceID: input.workspaceID, time: { updated: Date.now() } }).pipe(
|
||||
Effect.orDie,
|
||||
)
|
||||
})
|
||||
|
||||
const diff = Effect.fn("Session.diff")(function* (sessionID: SessionID) {
|
||||
@@ -788,14 +898,18 @@ export const layer: Layer.Layer<
|
||||
|
||||
const messages: Interface["messages"] = Effect.fn("Session.messages")(function* (input) {
|
||||
if (input.limit) {
|
||||
return (yield* MessageV2.page({ sessionID: input.sessionID, limit: input.limit })).items
|
||||
return (yield* MessageV2.page({ sessionID: input.sessionID, limit: input.limit }).pipe(
|
||||
Effect.provideService(Database.Service, database),
|
||||
)).items
|
||||
}
|
||||
|
||||
const size = 50
|
||||
const result = [] as MessageV2.WithParts[]
|
||||
const result = [] as SessionLegacy.WithParts[]
|
||||
let before: string | undefined
|
||||
while (true) {
|
||||
const page = yield* MessageV2.page({ sessionID: input.sessionID, limit: size, before })
|
||||
const page = yield* MessageV2.page({ sessionID: input.sessionID, limit: size, before }).pipe(
|
||||
Effect.provideService(Database.Service, database),
|
||||
)
|
||||
if (page.items.length === 0) break
|
||||
for (let i = page.items.length - 1; i >= 0; i--) {
|
||||
const item = page.items[i]
|
||||
@@ -811,10 +925,15 @@ export const layer: Layer.Layer<
|
||||
sessionID: SessionID
|
||||
messageID: MessageID
|
||||
}) {
|
||||
yield* sync.run(MessageV2.Event.Removed, {
|
||||
sessionID: input.sessionID,
|
||||
messageID: input.messageID,
|
||||
})
|
||||
const location = yield* locationForSession(input.sessionID)
|
||||
yield* events.publish(
|
||||
SessionLegacy.Event.MessageRemoved,
|
||||
{
|
||||
sessionID: input.sessionID,
|
||||
messageID: input.messageID,
|
||||
},
|
||||
{ location },
|
||||
)
|
||||
return input.messageID
|
||||
})
|
||||
|
||||
@@ -823,11 +942,16 @@ export const layer: Layer.Layer<
|
||||
messageID: MessageID
|
||||
partID: PartID
|
||||
}) {
|
||||
yield* sync.run(MessageV2.Event.PartRemoved, {
|
||||
sessionID: input.sessionID,
|
||||
messageID: input.messageID,
|
||||
partID: input.partID,
|
||||
})
|
||||
const location = yield* locationForSession(input.sessionID)
|
||||
yield* events.publish(
|
||||
SessionLegacy.Event.PartRemoved,
|
||||
{
|
||||
sessionID: input.sessionID,
|
||||
messageID: input.messageID,
|
||||
partID: input.partID,
|
||||
},
|
||||
{ location },
|
||||
)
|
||||
return input.partID
|
||||
})
|
||||
|
||||
@@ -838,7 +962,7 @@ export const layer: Layer.Layer<
|
||||
field: string
|
||||
delta: string
|
||||
}) {
|
||||
yield* bus.publish(MessageV2.Event.PartDelta, input)
|
||||
yield* events.publish(MessageV2.Event.PartDelta, input)
|
||||
})
|
||||
|
||||
/** Finds the first message matching the predicate, searching newest-first. */
|
||||
@@ -846,7 +970,9 @@ export const layer: Layer.Layer<
|
||||
const size = 50
|
||||
let before: string | undefined
|
||||
while (true) {
|
||||
const page = yield* MessageV2.page({ sessionID, limit: size, before })
|
||||
const page = yield* MessageV2.page({ sessionID, limit: size, before }).pipe(
|
||||
Effect.provideService(Database.Service, database),
|
||||
)
|
||||
if (page.items.length === 0) break
|
||||
for (let i = page.items.length - 1; i >= 0; i--) {
|
||||
const item = page.items[i]
|
||||
@@ -855,11 +981,12 @@ export const layer: Layer.Layer<
|
||||
if (!page.more || !page.cursor) break
|
||||
before = page.cursor
|
||||
}
|
||||
return Option.none<MessageV2.WithParts>()
|
||||
return Option.none<SessionLegacy.WithParts>()
|
||||
})
|
||||
|
||||
return Service.of({
|
||||
list,
|
||||
listGlobal,
|
||||
create,
|
||||
fork,
|
||||
touch,
|
||||
@@ -871,6 +998,8 @@ export const layer: Layer.Layer<
|
||||
setRevert,
|
||||
clearRevert,
|
||||
setSummary,
|
||||
setShare,
|
||||
setWorkspace,
|
||||
diff,
|
||||
messages,
|
||||
children,
|
||||
@@ -888,9 +1017,10 @@ export const layer: Layer.Layer<
|
||||
|
||||
export const defaultLayer = layer.pipe(
|
||||
Layer.provide(BackgroundJob.defaultLayer),
|
||||
Layer.provide(Bus.layer),
|
||||
Layer.provide(Storage.defaultLayer),
|
||||
Layer.provide(SyncEvent.defaultLayer),
|
||||
Layer.provide(Database.defaultLayer),
|
||||
Layer.provide(EventV2Bridge.defaultLayer),
|
||||
Layer.provide(SessionV2.defaultLayer),
|
||||
Layer.provide(RuntimeFlags.defaultLayer),
|
||||
)
|
||||
|
||||
@@ -911,9 +1041,10 @@ const cancelBackgroundJobs = Effect.fn("Session.cancelBackgroundJobs")(function*
|
||||
)
|
||||
})
|
||||
|
||||
function* listByProject(
|
||||
function listByProject(
|
||||
db: Database.Interface["db"],
|
||||
input: ListInput & {
|
||||
projectID: ProjectID
|
||||
projectID: ProjectV2.ID
|
||||
experimentalWorkspaces: boolean
|
||||
},
|
||||
) {
|
||||
@@ -949,18 +1080,17 @@ function* listByProject(
|
||||
|
||||
const limit = input.limit ?? 100
|
||||
|
||||
const rows = Database.use((db) =>
|
||||
db
|
||||
.select()
|
||||
.from(SessionTable)
|
||||
.where(and(...conditions))
|
||||
.orderBy(desc(SessionTable.time_updated))
|
||||
.limit(limit)
|
||||
.all(),
|
||||
)
|
||||
for (const row of rows) {
|
||||
yield fromRow(row)
|
||||
}
|
||||
return db
|
||||
.select()
|
||||
.from(SessionTable)
|
||||
.where(and(...conditions))
|
||||
.orderBy(desc(SessionTable.time_updated))
|
||||
.limit(limit)
|
||||
.all()
|
||||
.pipe(
|
||||
Effect.orDie,
|
||||
Effect.map((rows) => rows.map(fromRow)),
|
||||
)
|
||||
}
|
||||
|
||||
export function* listGlobal(input?: {
|
||||
@@ -995,7 +1125,7 @@ export function* listGlobal(input?: {
|
||||
|
||||
const limit = input?.limit ?? 100
|
||||
|
||||
const rows = Database.use((db) => {
|
||||
const rows = runtime.runSync(({ db }) => {
|
||||
const query =
|
||||
conditions.length > 0
|
||||
? db
|
||||
@@ -1003,19 +1133,20 @@ export function* listGlobal(input?: {
|
||||
.from(SessionTable)
|
||||
.where(and(...conditions))
|
||||
: db.select().from(SessionTable)
|
||||
return query.orderBy(desc(SessionTable.time_updated), desc(SessionTable.id)).limit(limit).all()
|
||||
return query.orderBy(desc(SessionTable.time_updated), desc(SessionTable.id)).limit(limit).all().pipe(Effect.orDie)
|
||||
})
|
||||
|
||||
const ids = [...new Set(rows.map((row) => row.project_id))]
|
||||
const projects = new Map<string, ProjectInfo>()
|
||||
|
||||
if (ids.length > 0) {
|
||||
const items = Database.use((db) =>
|
||||
const items = runtime.runSync(({ db }) =>
|
||||
db
|
||||
.select({ id: ProjectTable.id, name: ProjectTable.name, worktree: ProjectTable.worktree })
|
||||
.from(ProjectTable)
|
||||
.where(inArray(ProjectTable.id, ids))
|
||||
.all(),
|
||||
.all()
|
||||
.pipe(Effect.orDie),
|
||||
)
|
||||
for (const item of items) {
|
||||
projects.set(item.id, {
|
||||
|
||||
Reference in New Issue
Block a user