share: speed up share loads (#16165)
parent
eeeb21ff86
commit
1d9dcd2a27
|
|
@ -1,10 +1,8 @@
|
||||||
import { FileDiff, Message, Model, Part, Session } from "@opencode-ai/sdk/v2"
|
import { FileDiff, Message, Model, Part, Session } from "@opencode-ai/sdk/v2"
|
||||||
import { fn } from "@opencode-ai/util/fn"
|
import { fn } from "@opencode-ai/util/fn"
|
||||||
import { iife } from "@opencode-ai/util/iife"
|
import { iife } from "@opencode-ai/util/iife"
|
||||||
import { Identifier } from "@opencode-ai/util/identifier"
|
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { Storage } from "./storage"
|
import { Storage } from "./storage"
|
||||||
import { Binary } from "@opencode-ai/util/binary"
|
|
||||||
|
|
||||||
export namespace Share {
|
export namespace Share {
|
||||||
export const Info = z.object({
|
export const Info = z.object({
|
||||||
|
|
@ -38,6 +36,81 @@ export namespace Share {
|
||||||
])
|
])
|
||||||
export type Data = z.infer<typeof Data>
|
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) => {
|
export const create = fn(z.object({ sessionID: z.string() }), async (body) => {
|
||||||
const isTest = process.env.NODE_ENV === "test" || body.sessionID.startsWith("test_")
|
const isTest = process.env.NODE_ENV === "test" || body.sessionID.startsWith("test_")
|
||||||
const info: Info = {
|
const info: Info = {
|
||||||
|
|
@ -47,7 +120,7 @@ export namespace Share {
|
||||||
}
|
}
|
||||||
const exists = await get(info.id)
|
const exists = await get(info.id)
|
||||||
if (exists) throw new Errors.AlreadyExists(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
|
return info
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -60,8 +133,13 @@ export namespace Share {
|
||||||
if (!share) throw new Errors.NotFound(body.id)
|
if (!share) throw new Errors.NotFound(body.id)
|
||||||
if (share.secret !== body.secret) throw new Errors.InvalidSecret(body.id)
|
if (share.secret !== body.secret) throw new Errors.InvalidSecret(body.id)
|
||||||
await Storage.remove(["share", body.id])
|
await Storage.remove(["share", body.id])
|
||||||
const list = await Storage.list({ prefix: ["share_data", body.id] })
|
const groups = await Promise.all([
|
||||||
for (const item of list) {
|
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)
|
await Storage.remove(item)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -75,59 +153,13 @@ export namespace Share {
|
||||||
const share = await get(input.share.id)
|
const share = await get(input.share.id)
|
||||||
if (!share) throw new Errors.NotFound(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)
|
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) {
|
export async function data(shareID: string) {
|
||||||
console.log("reading compaction")
|
return (await readSnapshot(shareID)) ?? legacy(shareID)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const syncOld = fn(
|
export const syncOld = fn(
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,7 @@ app
|
||||||
validator("param", z.object({ shareID: z.string() })),
|
validator("param", z.object({ shareID: z.string() })),
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const { shareID } = c.req.valid("param")
|
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))
|
return c.json(await Share.data(shareID))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,11 @@ import { DataProvider } from "@opencode-ai/ui/context"
|
||||||
import { FileComponentProvider } from "@opencode-ai/ui/context/file"
|
import { FileComponentProvider } from "@opencode-ai/ui/context/file"
|
||||||
import { WorkerPoolProvider } from "@opencode-ai/ui/context/worker-pool"
|
import { WorkerPoolProvider } from "@opencode-ai/ui/context/worker-pool"
|
||||||
import { createAsync, query, useParams } from "@solidjs/router"
|
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 { Share } from "~/core/share"
|
||||||
import { Logo, Mark } from "@opencode-ai/ui/logo"
|
import { Logo, Mark } from "@opencode-ai/ui/logo"
|
||||||
import { IconButton } from "@opencode-ai/ui/icon-button"
|
import { IconButton } from "@opencode-ai/ui/icon-button"
|
||||||
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
import { ProviderIcon } from "@opencode-ai/ui/provider-icon"
|
||||||
import { createDefaultOptions } from "@opencode-ai/ui/pierre"
|
|
||||||
import { iife } from "@opencode-ai/util/iife"
|
import { iife } from "@opencode-ai/util/iife"
|
||||||
import { Binary } from "@opencode-ai/util/binary"
|
import { Binary } from "@opencode-ai/util/binary"
|
||||||
import { NamedError } from "@opencode-ai/util/error"
|
import { NamedError } from "@opencode-ai/util/error"
|
||||||
|
|
@ -20,11 +19,11 @@ import z from "zod"
|
||||||
import NotFound from "../[...404]"
|
import NotFound from "../[...404]"
|
||||||
import { Tabs } from "@opencode-ai/ui/tabs"
|
import { Tabs } from "@opencode-ai/ui/tabs"
|
||||||
import { MessageNav } from "@opencode-ai/ui/message-nav"
|
import { MessageNav } from "@opencode-ai/ui/message-nav"
|
||||||
import { preloadMultiFileDiff, PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
|
|
||||||
import { FileSSR } from "@opencode-ai/ui/file-ssr"
|
import { FileSSR } from "@opencode-ai/ui/file-ssr"
|
||||||
import { clientOnly } from "@solidjs/start"
|
import { clientOnly } from "@solidjs/start"
|
||||||
import { Meta, Title } from "@solidjs/meta"
|
import { Meta, Title } from "@solidjs/meta"
|
||||||
import { Base64 } from "js-base64"
|
import { Base64 } from "js-base64"
|
||||||
|
import { getRequestEvent } from "solid-js/web"
|
||||||
|
|
||||||
const ClientOnlyWorkerPoolProvider = clientOnly(() =>
|
const ClientOnlyWorkerPoolProvider = clientOnly(() =>
|
||||||
import("@opencode-ai/ui/pierre/worker").then((m) => ({
|
import("@opencode-ai/ui/pierre/worker").then((m) => ({
|
||||||
|
|
@ -54,12 +53,6 @@ const getData = query(async (shareID) => {
|
||||||
session_diff: {
|
session_diff: {
|
||||||
[sessionID: string]: FileDiff[]
|
[sessionID: string]: FileDiff[]
|
||||||
}
|
}
|
||||||
session_diff_preload: {
|
|
||||||
[sessionID: string]: PreloadMultiFileDiffResult<any>[]
|
|
||||||
}
|
|
||||||
session_diff_preload_split: {
|
|
||||||
[sessionID: string]: PreloadMultiFileDiffResult<any>[]
|
|
||||||
}
|
|
||||||
session_status: {
|
session_status: {
|
||||||
[sessionID: string]: SessionStatus
|
[sessionID: string]: SessionStatus
|
||||||
}
|
}
|
||||||
|
|
@ -79,12 +72,6 @@ const getData = query(async (shareID) => {
|
||||||
session_diff: {
|
session_diff: {
|
||||||
[share.sessionID]: [],
|
[share.sessionID]: [],
|
||||||
},
|
},
|
||||||
session_diff_preload: {
|
|
||||||
[share.sessionID]: [],
|
|
||||||
},
|
|
||||||
session_diff_preload_split: {
|
|
||||||
[share.sessionID]: [],
|
|
||||||
},
|
|
||||||
session_status: {
|
session_status: {
|
||||||
[share.sessionID]: {
|
[share.sessionID]: {
|
||||||
type: "idle",
|
type: "idle",
|
||||||
|
|
@ -101,28 +88,6 @@ const getData = query(async (shareID) => {
|
||||||
break
|
break
|
||||||
case "session_diff":
|
case "session_diff":
|
||||||
result.session_diff[share.sessionID] = item.data
|
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
|
break
|
||||||
case "message":
|
case "message":
|
||||||
result.message[item.data.sessionID] = result.message[item.data.sessionID] ?? []
|
result.message[item.data.sessionID] = result.message[item.data.sessionID] ?? []
|
||||||
|
|
@ -143,17 +108,15 @@ const getData = query(async (shareID) => {
|
||||||
}, "getShareData")
|
}, "getShareData")
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
|
getRequestEvent()?.response.headers.set(
|
||||||
|
"Cache-Control",
|
||||||
|
"public, max-age=30, s-maxage=300, stale-while-revalidate=86400",
|
||||||
|
)
|
||||||
|
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const data = createAsync(async () => {
|
const data = createAsync(async () => {
|
||||||
if (!params.shareID) throw new Error("Missing shareID")
|
if (!params.shareID) throw new Error("Missing shareID")
|
||||||
const now = Date.now()
|
return getData(params.shareID)
|
||||||
const data = getData(params.shareID)
|
|
||||||
console.log("getData", Date.now() - now)
|
|
||||||
return data
|
|
||||||
})
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
console.log(data())
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -241,22 +204,8 @@ export default function () {
|
||||||
const provider = createMemo(() => activeMessage()?.model?.providerID)
|
const provider = createMemo(() => activeMessage()?.model?.providerID)
|
||||||
const modelID = createMemo(() => activeMessage()?.model?.modelID)
|
const modelID = createMemo(() => activeMessage()?.model?.modelID)
|
||||||
const model = createMemo(() => data().model[data().sessionID]?.find((m) => m.id === modelID()))
|
const model = createMemo(() => data().model[data().sessionID]?.find((m) => m.id === modelID()))
|
||||||
const diffs = createMemo(() => {
|
const diffs = createMemo(() => data().session_diff[data().sessionID] ?? [])
|
||||||
const diffs = data().session_diff[data().sessionID] ?? []
|
const [diffStyle, setDiffStyle] = createSignal<"unified" | "split">("unified")
|
||||||
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 title = () => (
|
const title = () => (
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
|
|
@ -380,18 +329,9 @@ export default function () {
|
||||||
<Show when={diffs().length > 0}>
|
<Show when={diffs().length > 0}>
|
||||||
<div class="@container relative grow pt-14 flex-1 min-h-0 border-l border-border-weak-base">
|
<div class="@container relative grow pt-14 flex-1 min-h-0 border-l border-border-weak-base">
|
||||||
<SessionReview
|
<SessionReview
|
||||||
class="@4xl:hidden"
|
|
||||||
diffs={diffs()}
|
diffs={diffs()}
|
||||||
classes={{
|
diffStyle={diffStyle()}
|
||||||
root: "pb-20",
|
onDiffStyleChange={setDiffStyle}
|
||||||
header: "px-6",
|
|
||||||
container: "px-6",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<SessionReview
|
|
||||||
split
|
|
||||||
class="hidden @4xl:flex"
|
|
||||||
diffs={splitDiffs()}
|
|
||||||
classes={{
|
classes={{
|
||||||
root: "pb-20",
|
root: "pb-20",
|
||||||
header: "px-6",
|
header: "px-6",
|
||||||
|
|
@ -419,11 +359,7 @@ export default function () {
|
||||||
<Tabs.Content value="session" class="!overflow-hidden">
|
<Tabs.Content value="session" class="!overflow-hidden">
|
||||||
{turns()}
|
{turns()}
|
||||||
</Tabs.Content>
|
</Tabs.Content>
|
||||||
<Tabs.Content
|
<Tabs.Content value="review" class="!overflow-hidden hidden data-[selected]:block">
|
||||||
forceMount
|
|
||||||
value="review"
|
|
||||||
class="!overflow-hidden hidden data-[selected]:block"
|
|
||||||
>
|
|
||||||
<div class="relative h-full pt-8 overflow-y-auto no-scrollbar">
|
<div class="relative h-full pt-8 overflow-y-auto no-scrollbar">
|
||||||
<SessionReview
|
<SessionReview
|
||||||
diffs={diffs()}
|
diffs={diffs()}
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,8 @@ describe.concurrent("core.share", () => {
|
||||||
data,
|
data,
|
||||||
})
|
})
|
||||||
|
|
||||||
const events = await Storage.list({ prefix: ["share_event", share.id] })
|
const snapshot = await Storage.read<{ data: Share.Data[] }>(["share_snapshot", share.id])
|
||||||
expect(events.length).toBe(1)
|
expect(snapshot?.data).toHaveLength(1)
|
||||||
|
|
||||||
await Share.remove({ id: share.id, secret: share.secret })
|
await Share.remove({ id: share.id, secret: share.secret })
|
||||||
})
|
})
|
||||||
|
|
@ -64,8 +64,8 @@ describe.concurrent("core.share", () => {
|
||||||
data: data2,
|
data: data2,
|
||||||
})
|
})
|
||||||
|
|
||||||
const events = await Storage.list({ prefix: ["share_event", share.id] })
|
const snapshot = await Storage.read<{ data: Share.Data[] }>(["share_snapshot", share.id])
|
||||||
expect(events.length).toBe(2)
|
expect(snapshot?.data).toHaveLength(2)
|
||||||
|
|
||||||
await Share.remove({ id: share.id, secret: share.secret })
|
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 })
|
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 () => {
|
test("should throw error for invalid secret", async () => {
|
||||||
const sessionID = Identifier.descending()
|
const sessionID = Identifier.descending()
|
||||||
const share = await Share.create({ sessionID })
|
const share = await Share.create({ sessionID })
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { Bus } from "@/bus"
|
import { Bus } from "@/bus"
|
||||||
import { Config } from "@/config/config"
|
import { Config } from "@/config/config"
|
||||||
import { ulid } from "ulid"
|
|
||||||
import { Provider } from "@/provider/provider"
|
import { Provider } from "@/provider/provider"
|
||||||
import { Session } from "@/session"
|
import { Session } from "@/session"
|
||||||
import { MessageV2 } from "@/session/message-v2"
|
import { MessageV2 } from "@/session/message-v2"
|
||||||
|
|
@ -122,20 +121,35 @@ export namespace ShareNext {
|
||||||
data: SDK.Model[]
|
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<string, { timeout: NodeJS.Timeout; data: Map<string, Data> }>()
|
const queue = new Map<string, { timeout: NodeJS.Timeout; data: Map<string, Data> }>()
|
||||||
async function sync(sessionID: string, data: Data[]) {
|
async function sync(sessionID: string, data: Data[]) {
|
||||||
if (disabled) return
|
if (disabled) return
|
||||||
const existing = queue.get(sessionID)
|
const existing = queue.get(sessionID)
|
||||||
if (existing) {
|
if (existing) {
|
||||||
for (const item of data) {
|
for (const item of data) {
|
||||||
existing.data.set("id" in item ? (item.id as string) : ulid(), item)
|
existing.data.set(key(item), item)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataMap = new Map<string, Data>()
|
const dataMap = new Map<string, Data>()
|
||||||
for (const item of data) {
|
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 () => {
|
const timeout = setTimeout(async () => {
|
||||||
|
|
@ -182,10 +196,14 @@ export namespace ShareNext {
|
||||||
const diffs = await Session.diff(sessionID)
|
const diffs = await Session.diff(sessionID)
|
||||||
const messages = await Array.fromAsync(MessageV2.stream(sessionID))
|
const messages = await Array.fromAsync(MessageV2.stream(sessionID))
|
||||||
const models = await Promise.all(
|
const models = await Promise.all(
|
||||||
messages
|
Array.from(
|
||||||
.filter((m) => m.info.role === "user")
|
new Map(
|
||||||
.map((m) => (m.info as SDK.UserMessage).model)
|
messages
|
||||||
.map((m) => Provider.getModel(m.providerID, m.modelID).then((m) => m)),
|
.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, [
|
await sync(sessionID, [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,19 @@ function sanitize(html: string) {
|
||||||
return DOMPurify.sanitize(html, config)
|
return DOMPurify.sanitize(html, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function escape(text: string) {
|
||||||
|
return text
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/\"/g, """)
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
function fallback(markdown: string) {
|
||||||
|
return escape(markdown).replace(/\r\n?/g, "\n").replace(/\n/g, "<br>")
|
||||||
|
}
|
||||||
|
|
||||||
type CopyLabels = {
|
type CopyLabels = {
|
||||||
copy: string
|
copy: string
|
||||||
copied: string
|
copied: string
|
||||||
|
|
@ -237,7 +250,7 @@ export function Markdown(
|
||||||
const [html] = createResource(
|
const [html] = createResource(
|
||||||
() => local.text,
|
() => local.text,
|
||||||
async (markdown) => {
|
async (markdown) => {
|
||||||
if (isServer) return ""
|
if (isServer) return fallback(markdown)
|
||||||
|
|
||||||
const hash = checksum(markdown)
|
const hash = checksum(markdown)
|
||||||
const key = local.cacheKey ?? hash
|
const key = local.cacheKey ?? hash
|
||||||
|
|
@ -255,7 +268,7 @@ export function Markdown(
|
||||||
if (key && hash) touch(key, { hash, html: safe })
|
if (key && hash) touch(key, { hash, html: safe })
|
||||||
return safe
|
return safe
|
||||||
},
|
},
|
||||||
{ initialValue: "" },
|
{ initialValue: isServer ? fallback(local.text) : "" },
|
||||||
)
|
)
|
||||||
|
|
||||||
let copySetupTimer: ReturnType<typeof setTimeout> | undefined
|
let copySetupTimer: ReturnType<typeof setTimeout> | undefined
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,7 @@ export const SessionReview = (props: SessionReviewProps) => {
|
||||||
const searchHandles = new Map<string, FileSearchHandle>()
|
const searchHandles = new Map<string, FileSearchHandle>()
|
||||||
const readyFiles = new Set<string>()
|
const readyFiles = new Set<string>()
|
||||||
const [store, setStore] = createStore<{ open: string[]; force: Record<string, boolean> }>({
|
const [store, setStore] = createStore<{ open: string[]; force: Record<string, boolean> }>({
|
||||||
open: props.diffs.length > 10 ? [] : props.diffs.map((d) => d.file),
|
open: [],
|
||||||
force: {},
|
force: {},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue