Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 86 additions & 54 deletions packages/enterprise/src/core/share.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { FileDiff, Message, Model, Part, Session } from "@opencode-ai/sdk/v2"
import { fn } from "@opencode-ai/util/fn"
import { iife } from "@opencode-ai/util/iife"
import { Identifier } from "@opencode-ai/util/identifier"
import z from "zod"
import { Storage } from "./storage"
import { Binary } from "@opencode-ai/util/binary"

export namespace Share {
export const Info = z.object({
Expand Down Expand Up @@ -38,6 +36,81 @@ export namespace Share {
])
export type Data = z.infer<typeof Data>

type Snapshot = {
data: Data[]
}

type Compaction = {
event?: string
data: Data[]
}

function key(item: Data) {
switch (item.type) {
case "session":
return "session"
case "message":
return `message/${item.data.id}`
case "part":
return `part/${item.data.messageID}/${item.data.id}`
case "session_diff":
return "session_diff"
case "model":
return "model"
}
}

function merge(...items: Data[][]) {
const map = new Map<string, Data>()
for (const list of items) {
for (const item of list) {
map.set(key(item), item)
}
}
return Array.from(map.entries())
.sort(([a], [b]) => a.localeCompare(b))
.map(([, item]) => item)
}

async function readSnapshot(shareID: string) {
return (await Storage.read<Snapshot>(["share_snapshot", shareID]))?.data
}

async function writeSnapshot(shareID: string, data: Data[]) {
await Storage.write<Snapshot>(["share_snapshot", shareID], { data })
}

async function legacy(shareID: string) {
const compaction: Compaction = (await Storage.read<Compaction>(["share_compaction", shareID])) ?? {
data: [],
event: undefined,
}
const list = await Storage.list({
prefix: ["share_event", shareID],
before: compaction.event,
}).then((x) => x.toReversed())
if (list.length === 0) {
if (compaction.data.length > 0) await writeSnapshot(shareID, compaction.data)
return compaction.data
}

const next = merge(
compaction.data,
await Promise.all(list.map(async (event) => await Storage.read<Data[]>(event))).then((x) =>
x.flatMap((item) => item ?? []),
),
)

await Promise.all([
Storage.write(["share_compaction", shareID], {
event: list.at(-1)?.at(-1),
data: next,
}),
writeSnapshot(shareID, next),
])
return next
}

export const create = fn(z.object({ sessionID: z.string() }), async (body) => {
const isTest = process.env.NODE_ENV === "test" || body.sessionID.startsWith("test_")
const info: Info = {
Expand All @@ -47,7 +120,7 @@ export namespace Share {
}
const exists = await get(info.id)
if (exists) throw new Errors.AlreadyExists(info.id)
await Storage.write(["share", info.id], info)
await Promise.all([Storage.write(["share", info.id], info), writeSnapshot(info.id, [])])
return info
})

Expand All @@ -60,8 +133,13 @@ export namespace Share {
if (!share) throw new Errors.NotFound(body.id)
if (share.secret !== body.secret) throw new Errors.InvalidSecret(body.id)
await Storage.remove(["share", body.id])
const list = await Storage.list({ prefix: ["share_data", body.id] })
for (const item of list) {
const groups = await Promise.all([
Storage.list({ prefix: ["share_snapshot", body.id] }),
Storage.list({ prefix: ["share_compaction", body.id] }),
Storage.list({ prefix: ["share_event", body.id] }),
Storage.list({ prefix: ["share_data", body.id] }),
])
for (const item of groups.flat()) {
await Storage.remove(item)
}
})
Expand All @@ -75,59 +153,13 @@ export namespace Share {
const share = await get(input.share.id)
if (!share) throw new Errors.NotFound(input.share.id)
if (share.secret !== input.share.secret) throw new Errors.InvalidSecret(input.share.id)
await Storage.write(["share_event", input.share.id, Identifier.descending()], input.data)
const data = (await readSnapshot(input.share.id)) ?? (await legacy(input.share.id))
await writeSnapshot(input.share.id, merge(data, input.data))
},
)

type Compaction = {
event?: string
data: Data[]
}

export async function data(shareID: string) {
console.log("reading compaction")
const compaction: Compaction = (await Storage.read<Compaction>(["share_compaction", shareID])) ?? {
data: [],
event: undefined,
}
console.log("reading pending events")
const list = await Storage.list({
prefix: ["share_event", shareID],
before: compaction.event,
}).then((x) => x.toReversed())

console.log("compacting", list.length)

if (list.length > 0) {
const data = await Promise.all(list.map(async (event) => await Storage.read<Data[]>(event))).then((x) => x.flat())
for (const item of data) {
if (!item) continue
const key = (item: Data) => {
switch (item.type) {
case "session":
return "session"
case "message":
return `message/${item.data.id}`
case "part":
return `${item.data.messageID}/${item.data.id}`
case "session_diff":
return "session_diff"
case "model":
return "model"
}
}
const id = key(item)
const result = Binary.search(compaction.data, id, key)
if (result.found) {
compaction.data[result.index] = item
} else {
compaction.data.splice(result.index, 0, item)
}
}
compaction.event = list.at(-1)?.at(-1)
await Storage.write(["share_compaction", shareID], compaction)
}
return compaction.data
return (await readSnapshot(shareID)) ?? legacy(shareID)
}

export const syncOld = fn(
Expand Down
1 change: 1 addition & 0 deletions packages/enterprise/src/routes/api/[...path].ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ app
validator("param", z.object({ shareID: z.string() })),
async (c) => {
const { shareID } = c.req.valid("param")
c.header("Cache-Control", "public, max-age=30, s-maxage=300, stale-while-revalidate=86400")
return c.json(await Share.data(shareID))
},
)
Expand Down
90 changes: 13 additions & 77 deletions packages/enterprise/src/routes/share/[shareID].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@ import { DataProvider } from "@opencode-ai/ui/context"
import { FileComponentProvider } from "@opencode-ai/ui/context/file"
import { WorkerPoolProvider } from "@opencode-ai/ui/context/worker-pool"
import { createAsync, query, useParams } from "@solidjs/router"
import { createEffect, createMemo, ErrorBoundary, For, Match, Show, Switch } from "solid-js"
import { createMemo, createSignal, ErrorBoundary, For, Match, Show, Switch } from "solid-js"
import { Share } from "~/core/share"
import { Logo, Mark } from "@opencode-ai/ui/logo"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
import { createDefaultOptions } from "@opencode-ai/ui/pierre"
import { iife } from "@opencode-ai/util/iife"
import { Binary } from "@opencode-ai/util/binary"
import { NamedError } from "@opencode-ai/util/error"
Expand All @@ -20,11 +19,11 @@ import z from "zod"
import NotFound from "../[...404]"
import { Tabs } from "@opencode-ai/ui/tabs"
import { MessageNav } from "@opencode-ai/ui/message-nav"
import { preloadMultiFileDiff, PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
import { FileSSR } from "@opencode-ai/ui/file-ssr"
import { clientOnly } from "@solidjs/start"
import { Meta, Title } from "@solidjs/meta"
import { Base64 } from "js-base64"
import { getRequestEvent } from "solid-js/web"

const ClientOnlyWorkerPoolProvider = clientOnly(() =>
import("@opencode-ai/ui/pierre/worker").then((m) => ({
Expand Down Expand Up @@ -54,12 +53,6 @@ const getData = query(async (shareID) => {
session_diff: {
[sessionID: string]: FileDiff[]
}
session_diff_preload: {
[sessionID: string]: PreloadMultiFileDiffResult<any>[]
}
session_diff_preload_split: {
[sessionID: string]: PreloadMultiFileDiffResult<any>[]
}
session_status: {
[sessionID: string]: SessionStatus
}
Expand All @@ -79,12 +72,6 @@ const getData = query(async (shareID) => {
session_diff: {
[share.sessionID]: [],
},
session_diff_preload: {
[share.sessionID]: [],
},
session_diff_preload_split: {
[share.sessionID]: [],
},
session_status: {
[share.sessionID]: {
type: "idle",
Expand All @@ -101,28 +88,6 @@ const getData = query(async (shareID) => {
break
case "session_diff":
result.session_diff[share.sessionID] = item.data
await Promise.all([
Promise.all(
item.data.map(async (diff) =>
preloadMultiFileDiff<any>({
oldFile: { name: diff.file, contents: diff.before },
newFile: { name: diff.file, contents: diff.after },
options: createDefaultOptions("unified"),
// annotations,
}),
),
).then((r) => (result.session_diff_preload[share.sessionID] = r)),
Promise.all(
item.data.map(async (diff) =>
preloadMultiFileDiff<any>({
oldFile: { name: diff.file, contents: diff.before },
newFile: { name: diff.file, contents: diff.after },
options: createDefaultOptions("split"),
// annotations,
}),
),
).then((r) => (result.session_diff_preload_split[share.sessionID] = r)),
])
break
case "message":
result.message[item.data.sessionID] = result.message[item.data.sessionID] ?? []
Expand All @@ -143,17 +108,15 @@ const getData = query(async (shareID) => {
}, "getShareData")

export default function () {
getRequestEvent()?.response.headers.set(
"Cache-Control",
"public, max-age=30, s-maxage=300, stale-while-revalidate=86400",
)

const params = useParams()
const data = createAsync(async () => {
if (!params.shareID) throw new Error("Missing shareID")
const now = Date.now()
const data = getData(params.shareID)
console.log("getData", Date.now() - now)
return data
})

createEffect(() => {
console.log(data())
return getData(params.shareID)
})

return (
Expand Down Expand Up @@ -241,22 +204,8 @@ export default function () {
const provider = createMemo(() => activeMessage()?.model?.providerID)
const modelID = createMemo(() => activeMessage()?.model?.modelID)
const model = createMemo(() => data().model[data().sessionID]?.find((m) => m.id === modelID()))
const diffs = createMemo(() => {
const diffs = data().session_diff[data().sessionID] ?? []
const preloaded = data().session_diff_preload[data().sessionID] ?? []
return diffs.map((diff) => ({
...diff,
preloaded: preloaded.find((d) => d.newFile.name === diff.file),
}))
})
const splitDiffs = createMemo(() => {
const diffs = data().session_diff[data().sessionID] ?? []
const preloaded = data().session_diff_preload_split[data().sessionID] ?? []
return diffs.map((diff) => ({
...diff,
preloaded: preloaded.find((d) => d.newFile.name === diff.file),
}))
})
const diffs = createMemo(() => data().session_diff[data().sessionID] ?? [])
const [diffStyle, setDiffStyle] = createSignal<"unified" | "split">("unified")

const title = () => (
<div class="flex flex-col gap-4">
Expand Down Expand Up @@ -380,18 +329,9 @@ export default function () {
<Show when={diffs().length > 0}>
<div class="@container relative grow pt-14 flex-1 min-h-0 border-l border-border-weak-base">
<SessionReview
class="@4xl:hidden"
diffs={diffs()}
classes={{
root: "pb-20",
header: "px-6",
container: "px-6",
}}
/>
<SessionReview
split
class="hidden @4xl:flex"
diffs={splitDiffs()}
diffStyle={diffStyle()}
onDiffStyleChange={setDiffStyle}
classes={{
root: "pb-20",
header: "px-6",
Expand Down Expand Up @@ -419,11 +359,7 @@ export default function () {
<Tabs.Content value="session" class="!overflow-hidden">
{turns()}
</Tabs.Content>
<Tabs.Content
forceMount
value="review"
class="!overflow-hidden hidden data-[selected]:block"
>
<Tabs.Content value="review" class="!overflow-hidden hidden data-[selected]:block">
<div class="relative h-full pt-8 overflow-y-auto no-scrollbar">
<SessionReview
diffs={diffs()}
Expand Down
Loading
Loading