Files
shopify-ai-backup/opencode/packages/app/src/context/global-sync/event-reducer.ts
2026-02-07 20:54:46 +00:00

320 lines
10 KiB
TypeScript

import { Binary } from "@opencode-ai/util/binary"
import { produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
import type {
FileDiff,
Message,
Part,
PermissionRequest,
Project,
QuestionRequest,
Session,
SessionStatus,
Todo,
} from "@opencode-ai/sdk/v2/client"
import type { State, VcsCache } from "./types"
import { trimSessions } from "./session-trim"
export function applyGlobalEvent(input: {
event: { type: string; properties?: unknown }
project: Project[]
setGlobalProject: (next: Project[] | ((draft: Project[]) => void)) => void
refresh: () => void
}) {
if (input.event.type === "global.disposed") {
input.refresh()
return
}
if (input.event.type !== "project.updated") return
const properties = input.event.properties as Project
const result = Binary.search(input.project, properties.id, (s) => s.id)
if (result.found) {
input.setGlobalProject((draft) => {
draft[result.index] = { ...draft[result.index], ...properties }
})
return
}
input.setGlobalProject((draft) => {
draft.splice(result.index, 0, properties)
})
}
function cleanupSessionCaches(store: Store<State>, setStore: SetStoreFunction<State>, sessionID: string) {
if (!sessionID) return
const hasAny =
store.message[sessionID] !== undefined ||
store.session_diff[sessionID] !== undefined ||
store.todo[sessionID] !== undefined ||
store.permission[sessionID] !== undefined ||
store.question[sessionID] !== undefined ||
store.session_status[sessionID] !== undefined
if (!hasAny) return
setStore(
produce((draft) => {
const messages = draft.message[sessionID]
if (messages) {
for (const message of messages) {
const id = message?.id
if (!id) continue
delete draft.part[id]
}
}
delete draft.message[sessionID]
delete draft.session_diff[sessionID]
delete draft.todo[sessionID]
delete draft.permission[sessionID]
delete draft.question[sessionID]
delete draft.session_status[sessionID]
}),
)
}
export function applyDirectoryEvent(input: {
event: { type: string; properties?: unknown }
store: Store<State>
setStore: SetStoreFunction<State>
push: (directory: string) => void
directory: string
loadLsp: () => void
vcsCache?: VcsCache
}) {
const event = input.event
switch (event.type) {
case "server.instance.disposed": {
input.push(input.directory)
return
}
case "session.created": {
const info = (event.properties as { info: Session }).info
const result = Binary.search(input.store.session, info.id, (s) => s.id)
if (result.found) {
input.setStore("session", result.index, reconcile(info))
break
}
const next = input.store.session.slice()
next.splice(result.index, 0, info)
const trimmed = trimSessions(next, { limit: input.store.limit, permission: input.store.permission })
input.setStore("session", reconcile(trimmed, { key: "id" }))
if (!info.parentID) input.setStore("sessionTotal", (value) => value + 1)
break
}
case "session.updated": {
const info = (event.properties as { info: Session }).info
const result = Binary.search(input.store.session, info.id, (s) => s.id)
if (info.time.archived) {
if (result.found) {
input.setStore(
"session",
produce((draft) => {
draft.splice(result.index, 1)
}),
)
}
cleanupSessionCaches(input.store, input.setStore, info.id)
if (info.parentID) break
input.setStore("sessionTotal", (value) => Math.max(0, value - 1))
break
}
if (result.found) {
input.setStore("session", result.index, reconcile(info))
break
}
const next = input.store.session.slice()
next.splice(result.index, 0, info)
const trimmed = trimSessions(next, { limit: input.store.limit, permission: input.store.permission })
input.setStore("session", reconcile(trimmed, { key: "id" }))
break
}
case "session.deleted": {
const info = (event.properties as { info: Session }).info
const result = Binary.search(input.store.session, info.id, (s) => s.id)
if (result.found) {
input.setStore(
"session",
produce((draft) => {
draft.splice(result.index, 1)
}),
)
}
cleanupSessionCaches(input.store, input.setStore, info.id)
if (info.parentID) break
input.setStore("sessionTotal", (value) => Math.max(0, value - 1))
break
}
case "session.diff": {
const props = event.properties as { sessionID: string; diff: FileDiff[] }
input.setStore("session_diff", props.sessionID, reconcile(props.diff, { key: "file" }))
break
}
case "todo.updated": {
const props = event.properties as { sessionID: string; todos: Todo[] }
input.setStore("todo", props.sessionID, reconcile(props.todos, { key: "id" }))
break
}
case "session.status": {
const props = event.properties as { sessionID: string; status: SessionStatus }
input.setStore("session_status", props.sessionID, reconcile(props.status))
break
}
case "message.updated": {
const info = (event.properties as { info: Message }).info
const messages = input.store.message[info.sessionID]
if (!messages) {
input.setStore("message", info.sessionID, [info])
break
}
const result = Binary.search(messages, info.id, (m) => m.id)
if (result.found) {
input.setStore("message", info.sessionID, result.index, reconcile(info))
break
}
input.setStore(
"message",
info.sessionID,
produce((draft) => {
draft.splice(result.index, 0, info)
}),
)
break
}
case "message.removed": {
const props = event.properties as { sessionID: string; messageID: string }
input.setStore(
produce((draft) => {
const messages = draft.message[props.sessionID]
if (messages) {
const result = Binary.search(messages, props.messageID, (m) => m.id)
if (result.found) messages.splice(result.index, 1)
}
delete draft.part[props.messageID]
}),
)
break
}
case "message.part.updated": {
const part = (event.properties as { part: Part }).part
const parts = input.store.part[part.messageID]
if (!parts) {
input.setStore("part", part.messageID, [part])
break
}
const result = Binary.search(parts, part.id, (p) => p.id)
if (result.found) {
input.setStore("part", part.messageID, result.index, reconcile(part))
break
}
input.setStore(
"part",
part.messageID,
produce((draft) => {
draft.splice(result.index, 0, part)
}),
)
break
}
case "message.part.removed": {
const props = event.properties as { messageID: string; partID: string }
const parts = input.store.part[props.messageID]
if (!parts) break
const result = Binary.search(parts, props.partID, (p) => p.id)
if (result.found) {
input.setStore(
produce((draft) => {
const list = draft.part[props.messageID]
if (!list) return
const next = Binary.search(list, props.partID, (p) => p.id)
if (!next.found) return
list.splice(next.index, 1)
if (list.length === 0) delete draft.part[props.messageID]
}),
)
}
break
}
case "vcs.branch.updated": {
const props = event.properties as { branch: string }
const next = { branch: props.branch }
input.setStore("vcs", next)
if (input.vcsCache) input.vcsCache.setStore("value", next)
break
}
case "permission.asked": {
const permission = event.properties as PermissionRequest
const permissions = input.store.permission[permission.sessionID]
if (!permissions) {
input.setStore("permission", permission.sessionID, [permission])
break
}
const result = Binary.search(permissions, permission.id, (p) => p.id)
if (result.found) {
input.setStore("permission", permission.sessionID, result.index, reconcile(permission))
break
}
input.setStore(
"permission",
permission.sessionID,
produce((draft) => {
draft.splice(result.index, 0, permission)
}),
)
break
}
case "permission.replied": {
const props = event.properties as { sessionID: string; requestID: string }
const permissions = input.store.permission[props.sessionID]
if (!permissions) break
const result = Binary.search(permissions, props.requestID, (p) => p.id)
if (!result.found) break
input.setStore(
"permission",
props.sessionID,
produce((draft) => {
draft.splice(result.index, 1)
}),
)
break
}
case "question.asked": {
const question = event.properties as QuestionRequest
const questions = input.store.question[question.sessionID]
if (!questions) {
input.setStore("question", question.sessionID, [question])
break
}
const result = Binary.search(questions, question.id, (q) => q.id)
if (result.found) {
input.setStore("question", question.sessionID, result.index, reconcile(question))
break
}
input.setStore(
"question",
question.sessionID,
produce((draft) => {
draft.splice(result.index, 0, question)
}),
)
break
}
case "question.replied":
case "question.rejected": {
const props = event.properties as { sessionID: string; requestID: string }
const questions = input.store.question[props.sessionID]
if (!questions) break
const result = Binary.search(questions, props.requestID, (q) => q.id)
if (!result.found) break
input.setStore(
"question",
props.sessionID,
produce((draft) => {
draft.splice(result.index, 1)
}),
)
break
}
case "lsp.updated": {
input.loadLsp()
break
}
}
}