From 4ee503dd880b175c5fcb74b850555eb665232c60 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Thu, 5 Mar 2026 16:05:58 +0530 Subject: [PATCH 1/4] share: persist snapshots --- packages/enterprise/src/core/share.ts | 140 ++++++++++++-------- packages/enterprise/test/core/share.test.ts | 30 ++++- 2 files changed, 112 insertions(+), 58 deletions(-) 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/test/core/share.test.ts b/packages/enterprise/test/core/share.test.ts index 9e9c06db384..d49d4b76399 100644 --- a/packages/enterprise/test/core/share.test.ts +++ b/packages/enterprise/test/core/share.test.ts @@ -30,8 +30,8 @@ describe.concurrent("core.share", () => { data, }) - const events = await Storage.list({ prefix: ["share_event", share.id] }) - expect(events.length).toBe(1) + const snapshot = await Storage.read<{ data: Share.Data[] }>(["share_snapshot", share.id]) + expect(snapshot?.data).toHaveLength(1) await Share.remove({ id: share.id, secret: share.secret }) }) @@ -64,8 +64,8 @@ describe.concurrent("core.share", () => { data: data2, }) - const events = await Storage.list({ prefix: ["share_event", share.id] }) - expect(events.length).toBe(2) + const snapshot = await Storage.read<{ data: Share.Data[] }>(["share_snapshot", share.id]) + expect(snapshot?.data).toHaveLength(2) await Share.remove({ id: share.id, secret: share.secret }) }) @@ -194,6 +194,28 @@ describe.concurrent("core.share", () => { await Share.remove({ id: share.id, secret: share.secret }) }) + test("should migrate legacy event data into the snapshot", async () => { + const sessionID = Identifier.descending() + const share = await Share.create({ sessionID }) + const data: Share.Data[] = [ + { + type: "part", + data: { id: "part1", sessionID, messageID: "msg1", type: "text", text: "Hello" }, + }, + ] + + await Storage.remove(["share_snapshot", share.id]) + await Storage.write(["share_event", share.id, Identifier.descending()], data) + + const result = await Share.data(share.id) + const snapshot = await Storage.read<{ data: Share.Data[] }>(["share_snapshot", share.id]) + + expect(result).toHaveLength(1) + expect(snapshot?.data).toHaveLength(1) + + await Share.remove({ id: share.id, secret: share.secret }) + }) + test("should throw error for invalid secret", async () => { const sessionID = Identifier.descending() const share = await Share.create({ sessionID }) From 6fa00572471d4ae6bb92487ff004638074dd0938 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Thu, 5 Mar 2026 16:06:04 +0530 Subject: [PATCH 2/4] share-next: dedupe sync batches --- packages/opencode/src/share/share-next.ts | 32 ++++++++++++++++++----- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/share/share-next.ts b/packages/opencode/src/share/share-next.ts index c36616b7ef9..54437627845 100644 --- a/packages/opencode/src/share/share-next.ts +++ b/packages/opencode/src/share/share-next.ts @@ -1,6 +1,5 @@ import { Bus } from "@/bus" import { Config } from "@/config/config" -import { ulid } from "ulid" import { Provider } from "@/provider/provider" import { Session } from "@/session" import { MessageV2 } from "@/session/message-v2" @@ -122,20 +121,35 @@ export namespace ShareNext { data: SDK.Model[] } + 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" + } + } + const queue = new Map }>() async function sync(sessionID: string, data: Data[]) { if (disabled) return const existing = queue.get(sessionID) if (existing) { for (const item of data) { - existing.data.set("id" in item ? (item.id as string) : ulid(), item) + existing.data.set(key(item), item) } return } const dataMap = new Map() for (const item of data) { - dataMap.set("id" in item ? (item.id as string) : ulid(), item) + dataMap.set(key(item), item) } const timeout = setTimeout(async () => { @@ -182,10 +196,14 @@ export namespace ShareNext { const diffs = await Session.diff(sessionID) const messages = await Array.fromAsync(MessageV2.stream(sessionID)) const models = await Promise.all( - messages - .filter((m) => m.info.role === "user") - .map((m) => (m.info as SDK.UserMessage).model) - .map((m) => Provider.getModel(m.providerID, m.modelID).then((m) => m)), + Array.from( + new Map( + messages + .filter((m) => m.info.role === "user") + .map((m) => (m.info as SDK.UserMessage).model) + .map((m) => [`${m.providerID}/${m.modelID}`, m] as const), + ).values(), + ).map((m) => Provider.getModel(m.providerID, m.modelID).then((item) => item)), ) await sync(sessionID, [ { From 5ef1dcb9b7bcb5dde1842b528da79836a765e6bf Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Thu, 5 Mar 2026 16:06:11 +0530 Subject: [PATCH 3/4] share: reduce first-load work --- .../enterprise/src/routes/api/[...path].ts | 1 + .../enterprise/src/routes/share/[shareID].tsx | 118 +++++------------- packages/ui/src/components/markdown.tsx | 17 ++- packages/ui/src/components/session-review.tsx | 2 +- 4 files changed, 47 insertions(+), 91 deletions(-) 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..45ff9307b22 100644 --- a/packages/enterprise/src/routes/share/[shareID].tsx +++ b/packages/enterprise/src/routes/share/[shareID].tsx @@ -3,14 +3,13 @@ import { SessionTurn } from "@opencode-ai/ui/session-turn" import { SessionReview } from "@opencode-ai/ui/session-review" 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 { WorkerPoolProvider, type WorkerPools } 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, onMount, ParentProps, 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,19 +19,25 @@ 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) => ({ - default: (props: { children: any }) => ( - {props.children} - ), - })), -) +function WorkerProvider(props: ParentProps) { + const [pools, setPools] = createSignal({ + unified: undefined, + split: undefined, + }) + + onMount(() => { + import("@opencode-ai/ui/pierre/worker").then((m) => { + setPools(m.getWorkerPools()) + }) + }) + + return {props.children} +} const SessionDataMissingError = NamedError.create( "SessionDataMissingError", @@ -54,12 +59,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 +78,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 +94,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 +114,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 ( @@ -213,7 +182,7 @@ export default function () { - + {iife(() => { @@ -241,22 +210,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 +335,9 @@ export default function () { 0}>
-