Skip to content

Commit 1d9dcd2

Browse files
authored
share: speed up share loads (#16165)
1 parent eeeb21f commit 1d9dcd2

7 files changed

Lines changed: 167 additions & 145 deletions

File tree

packages/enterprise/src/core/share.ts

Lines changed: 86 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import { FileDiff, Message, Model, Part, Session } from "@opencode-ai/sdk/v2"
22
import { fn } from "@opencode-ai/util/fn"
33
import { iife } from "@opencode-ai/util/iife"
4-
import { Identifier } from "@opencode-ai/util/identifier"
54
import z from "zod"
65
import { Storage } from "./storage"
7-
import { Binary } from "@opencode-ai/util/binary"
86

97
export namespace Share {
108
export const Info = z.object({
@@ -38,6 +36,81 @@ export namespace Share {
3836
])
3937
export type Data = z.infer<typeof Data>
4038

39+
type Snapshot = {
40+
data: Data[]
41+
}
42+
43+
type Compaction = {
44+
event?: string
45+
data: Data[]
46+
}
47+
48+
function key(item: Data) {
49+
switch (item.type) {
50+
case "session":
51+
return "session"
52+
case "message":
53+
return `message/${item.data.id}`
54+
case "part":
55+
return `part/${item.data.messageID}/${item.data.id}`
56+
case "session_diff":
57+
return "session_diff"
58+
case "model":
59+
return "model"
60+
}
61+
}
62+
63+
function merge(...items: Data[][]) {
64+
const map = new Map<string, Data>()
65+
for (const list of items) {
66+
for (const item of list) {
67+
map.set(key(item), item)
68+
}
69+
}
70+
return Array.from(map.entries())
71+
.sort(([a], [b]) => a.localeCompare(b))
72+
.map(([, item]) => item)
73+
}
74+
75+
async function readSnapshot(shareID: string) {
76+
return (await Storage.read<Snapshot>(["share_snapshot", shareID]))?.data
77+
}
78+
79+
async function writeSnapshot(shareID: string, data: Data[]) {
80+
await Storage.write<Snapshot>(["share_snapshot", shareID], { data })
81+
}
82+
83+
async function legacy(shareID: string) {
84+
const compaction: Compaction = (await Storage.read<Compaction>(["share_compaction", shareID])) ?? {
85+
data: [],
86+
event: undefined,
87+
}
88+
const list = await Storage.list({
89+
prefix: ["share_event", shareID],
90+
before: compaction.event,
91+
}).then((x) => x.toReversed())
92+
if (list.length === 0) {
93+
if (compaction.data.length > 0) await writeSnapshot(shareID, compaction.data)
94+
return compaction.data
95+
}
96+
97+
const next = merge(
98+
compaction.data,
99+
await Promise.all(list.map(async (event) => await Storage.read<Data[]>(event))).then((x) =>
100+
x.flatMap((item) => item ?? []),
101+
),
102+
)
103+
104+
await Promise.all([
105+
Storage.write(["share_compaction", shareID], {
106+
event: list.at(-1)?.at(-1),
107+
data: next,
108+
}),
109+
writeSnapshot(shareID, next),
110+
])
111+
return next
112+
}
113+
41114
export const create = fn(z.object({ sessionID: z.string() }), async (body) => {
42115
const isTest = process.env.NODE_ENV === "test" || body.sessionID.startsWith("test_")
43116
const info: Info = {
@@ -47,7 +120,7 @@ export namespace Share {
47120
}
48121
const exists = await get(info.id)
49122
if (exists) throw new Errors.AlreadyExists(info.id)
50-
await Storage.write(["share", info.id], info)
123+
await Promise.all([Storage.write(["share", info.id], info), writeSnapshot(info.id, [])])
51124
return info
52125
})
53126

@@ -60,8 +133,13 @@ export namespace Share {
60133
if (!share) throw new Errors.NotFound(body.id)
61134
if (share.secret !== body.secret) throw new Errors.InvalidSecret(body.id)
62135
await Storage.remove(["share", body.id])
63-
const list = await Storage.list({ prefix: ["share_data", body.id] })
64-
for (const item of list) {
136+
const groups = await Promise.all([
137+
Storage.list({ prefix: ["share_snapshot", body.id] }),
138+
Storage.list({ prefix: ["share_compaction", body.id] }),
139+
Storage.list({ prefix: ["share_event", body.id] }),
140+
Storage.list({ prefix: ["share_data", body.id] }),
141+
])
142+
for (const item of groups.flat()) {
65143
await Storage.remove(item)
66144
}
67145
})
@@ -75,59 +153,13 @@ export namespace Share {
75153
const share = await get(input.share.id)
76154
if (!share) throw new Errors.NotFound(input.share.id)
77155
if (share.secret !== input.share.secret) throw new Errors.InvalidSecret(input.share.id)
78-
await Storage.write(["share_event", input.share.id, Identifier.descending()], input.data)
156+
const data = (await readSnapshot(input.share.id)) ?? (await legacy(input.share.id))
157+
await writeSnapshot(input.share.id, merge(data, input.data))
79158
},
80159
)
81160

82-
type Compaction = {
83-
event?: string
84-
data: Data[]
85-
}
86-
87161
export async function data(shareID: string) {
88-
console.log("reading compaction")
89-
const compaction: Compaction = (await Storage.read<Compaction>(["share_compaction", shareID])) ?? {
90-
data: [],
91-
event: undefined,
92-
}
93-
console.log("reading pending events")
94-
const list = await Storage.list({
95-
prefix: ["share_event", shareID],
96-
before: compaction.event,
97-
}).then((x) => x.toReversed())
98-
99-
console.log("compacting", list.length)
100-
101-
if (list.length > 0) {
102-
const data = await Promise.all(list.map(async (event) => await Storage.read<Data[]>(event))).then((x) => x.flat())
103-
for (const item of data) {
104-
if (!item) continue
105-
const key = (item: Data) => {
106-
switch (item.type) {
107-
case "session":
108-
return "session"
109-
case "message":
110-
return `message/${item.data.id}`
111-
case "part":
112-
return `${item.data.messageID}/${item.data.id}`
113-
case "session_diff":
114-
return "session_diff"
115-
case "model":
116-
return "model"
117-
}
118-
}
119-
const id = key(item)
120-
const result = Binary.search(compaction.data, id, key)
121-
if (result.found) {
122-
compaction.data[result.index] = item
123-
} else {
124-
compaction.data.splice(result.index, 0, item)
125-
}
126-
}
127-
compaction.event = list.at(-1)?.at(-1)
128-
await Storage.write(["share_compaction", shareID], compaction)
129-
}
130-
return compaction.data
162+
return (await readSnapshot(shareID)) ?? legacy(shareID)
131163
}
132164

133165
export const syncOld = fn(

packages/enterprise/src/routes/api/[...path].ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ app
108108
validator("param", z.object({ shareID: z.string() })),
109109
async (c) => {
110110
const { shareID } = c.req.valid("param")
111+
c.header("Cache-Control", "public, max-age=30, s-maxage=300, stale-while-revalidate=86400")
111112
return c.json(await Share.data(shareID))
112113
},
113114
)

packages/enterprise/src/routes/share/[shareID].tsx

Lines changed: 13 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@ import { DataProvider } from "@opencode-ai/ui/context"
55
import { FileComponentProvider } from "@opencode-ai/ui/context/file"
66
import { WorkerPoolProvider } from "@opencode-ai/ui/context/worker-pool"
77
import { createAsync, query, useParams } from "@solidjs/router"
8-
import { createEffect, createMemo, ErrorBoundary, For, Match, Show, Switch } from "solid-js"
8+
import { createMemo, createSignal, ErrorBoundary, For, Match, Show, Switch } from "solid-js"
99
import { Share } from "~/core/share"
1010
import { Logo, Mark } from "@opencode-ai/ui/logo"
1111
import { IconButton } from "@opencode-ai/ui/icon-button"
1212
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
13-
import { createDefaultOptions } from "@opencode-ai/ui/pierre"
1413
import { iife } from "@opencode-ai/util/iife"
1514
import { Binary } from "@opencode-ai/util/binary"
1615
import { NamedError } from "@opencode-ai/util/error"
@@ -20,11 +19,11 @@ import z from "zod"
2019
import NotFound from "../[...404]"
2120
import { Tabs } from "@opencode-ai/ui/tabs"
2221
import { MessageNav } from "@opencode-ai/ui/message-nav"
23-
import { preloadMultiFileDiff, PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
2422
import { FileSSR } from "@opencode-ai/ui/file-ssr"
2523
import { clientOnly } from "@solidjs/start"
2624
import { Meta, Title } from "@solidjs/meta"
2725
import { Base64 } from "js-base64"
26+
import { getRequestEvent } from "solid-js/web"
2827

2928
const ClientOnlyWorkerPoolProvider = clientOnly(() =>
3029
import("@opencode-ai/ui/pierre/worker").then((m) => ({
@@ -54,12 +53,6 @@ const getData = query(async (shareID) => {
5453
session_diff: {
5554
[sessionID: string]: FileDiff[]
5655
}
57-
session_diff_preload: {
58-
[sessionID: string]: PreloadMultiFileDiffResult<any>[]
59-
}
60-
session_diff_preload_split: {
61-
[sessionID: string]: PreloadMultiFileDiffResult<any>[]
62-
}
6356
session_status: {
6457
[sessionID: string]: SessionStatus
6558
}
@@ -79,12 +72,6 @@ const getData = query(async (shareID) => {
7972
session_diff: {
8073
[share.sessionID]: [],
8174
},
82-
session_diff_preload: {
83-
[share.sessionID]: [],
84-
},
85-
session_diff_preload_split: {
86-
[share.sessionID]: [],
87-
},
8875
session_status: {
8976
[share.sessionID]: {
9077
type: "idle",
@@ -101,28 +88,6 @@ const getData = query(async (shareID) => {
10188
break
10289
case "session_diff":
10390
result.session_diff[share.sessionID] = item.data
104-
await Promise.all([
105-
Promise.all(
106-
item.data.map(async (diff) =>
107-
preloadMultiFileDiff<any>({
108-
oldFile: { name: diff.file, contents: diff.before },
109-
newFile: { name: diff.file, contents: diff.after },
110-
options: createDefaultOptions("unified"),
111-
// annotations,
112-
}),
113-
),
114-
).then((r) => (result.session_diff_preload[share.sessionID] = r)),
115-
Promise.all(
116-
item.data.map(async (diff) =>
117-
preloadMultiFileDiff<any>({
118-
oldFile: { name: diff.file, contents: diff.before },
119-
newFile: { name: diff.file, contents: diff.after },
120-
options: createDefaultOptions("split"),
121-
// annotations,
122-
}),
123-
),
124-
).then((r) => (result.session_diff_preload_split[share.sessionID] = r)),
125-
])
12691
break
12792
case "message":
12893
result.message[item.data.sessionID] = result.message[item.data.sessionID] ?? []
@@ -143,17 +108,15 @@ const getData = query(async (shareID) => {
143108
}, "getShareData")
144109

145110
export default function () {
111+
getRequestEvent()?.response.headers.set(
112+
"Cache-Control",
113+
"public, max-age=30, s-maxage=300, stale-while-revalidate=86400",
114+
)
115+
146116
const params = useParams()
147117
const data = createAsync(async () => {
148118
if (!params.shareID) throw new Error("Missing shareID")
149-
const now = Date.now()
150-
const data = getData(params.shareID)
151-
console.log("getData", Date.now() - now)
152-
return data
153-
})
154-
155-
createEffect(() => {
156-
console.log(data())
119+
return getData(params.shareID)
157120
})
158121

159122
return (
@@ -241,22 +204,8 @@ export default function () {
241204
const provider = createMemo(() => activeMessage()?.model?.providerID)
242205
const modelID = createMemo(() => activeMessage()?.model?.modelID)
243206
const model = createMemo(() => data().model[data().sessionID]?.find((m) => m.id === modelID()))
244-
const diffs = createMemo(() => {
245-
const diffs = data().session_diff[data().sessionID] ?? []
246-
const preloaded = data().session_diff_preload[data().sessionID] ?? []
247-
return diffs.map((diff) => ({
248-
...diff,
249-
preloaded: preloaded.find((d) => d.newFile.name === diff.file),
250-
}))
251-
})
252-
const splitDiffs = createMemo(() => {
253-
const diffs = data().session_diff[data().sessionID] ?? []
254-
const preloaded = data().session_diff_preload_split[data().sessionID] ?? []
255-
return diffs.map((diff) => ({
256-
...diff,
257-
preloaded: preloaded.find((d) => d.newFile.name === diff.file),
258-
}))
259-
})
207+
const diffs = createMemo(() => data().session_diff[data().sessionID] ?? [])
208+
const [diffStyle, setDiffStyle] = createSignal<"unified" | "split">("unified")
260209

261210
const title = () => (
262211
<div class="flex flex-col gap-4">
@@ -380,18 +329,9 @@ export default function () {
380329
<Show when={diffs().length > 0}>
381330
<div class="@container relative grow pt-14 flex-1 min-h-0 border-l border-border-weak-base">
382331
<SessionReview
383-
class="@4xl:hidden"
384332
diffs={diffs()}
385-
classes={{
386-
root: "pb-20",
387-
header: "px-6",
388-
container: "px-6",
389-
}}
390-
/>
391-
<SessionReview
392-
split
393-
class="hidden @4xl:flex"
394-
diffs={splitDiffs()}
333+
diffStyle={diffStyle()}
334+
onDiffStyleChange={setDiffStyle}
395335
classes={{
396336
root: "pb-20",
397337
header: "px-6",
@@ -419,11 +359,7 @@ export default function () {
419359
<Tabs.Content value="session" class="!overflow-hidden">
420360
{turns()}
421361
</Tabs.Content>
422-
<Tabs.Content
423-
forceMount
424-
value="review"
425-
class="!overflow-hidden hidden data-[selected]:block"
426-
>
362+
<Tabs.Content value="review" class="!overflow-hidden hidden data-[selected]:block">
427363
<div class="relative h-full pt-8 overflow-y-auto no-scrollbar">
428364
<SessionReview
429365
diffs={diffs()}

0 commit comments

Comments
 (0)