diff --git a/packages/enterprise/src/core/share.ts b/packages/enterprise/src/core/share.ts index d7f5c8b8d52..c6291b75d22 100644 --- a/packages/enterprise/src/core/share.ts +++ b/packages/enterprise/src/core/share.ts @@ -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({ @@ -38,6 +36,81 @@ export namespace Share { ]) export type Data = z.infer + 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() + 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(["share_snapshot", shareID]))?.data + } + + async function writeSnapshot(shareID: string, data: Data[]) { + await Storage.write(["share_snapshot", shareID], { data }) + } + + async function legacy(shareID: string) { + const compaction: Compaction = (await Storage.read(["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(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 = { @@ -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 }) @@ -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) } }) @@ -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(["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(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( diff --git a/packages/enterprise/src/routes/api/[...path].ts b/packages/enterprise/src/routes/api/[...path].ts index e77c00de920..f97788bd03d 100644 --- a/packages/enterprise/src/routes/api/[...path].ts +++ b/packages/enterprise/src/routes/api/[...path].ts @@ -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)) }, ) diff --git a/packages/enterprise/src/routes/share/[shareID].tsx b/packages/enterprise/src/routes/share/[shareID].tsx index 007b4c268df..e755ea75a19 100644 --- a/packages/enterprise/src/routes/share/[shareID].tsx +++ b/packages/enterprise/src/routes/share/[shareID].tsx @@ -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" @@ -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) => ({ @@ -54,12 +53,6 @@ const getData = query(async (shareID) => { session_diff: { [sessionID: string]: FileDiff[] } - session_diff_preload: { - [sessionID: string]: PreloadMultiFileDiffResult[] - } - session_diff_preload_split: { - [sessionID: string]: PreloadMultiFileDiffResult[] - } session_status: { [sessionID: string]: SessionStatus } @@ -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", @@ -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({ - 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({ - 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] ?? [] @@ -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 ( @@ -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 = () => (
@@ -380,18 +329,9 @@ export default function () { 0}>
-