summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2025-11-24 22:58:47 -0500
committerDax Raad <[email protected]>2025-11-24 22:58:47 -0500
commit69d1381ba334037cc57eef483c1b23b58b0f314e (patch)
treea0514664e068cc43af0b39a4878c50427a313334
parentccec8c479248fdaefaaf9ad9606bfc4eecb042c9 (diff)
downloadopencode-69d1381ba334037cc57eef483c1b23b58b0f314e.tar.gz
opencode-69d1381ba334037cc57eef483c1b23b58b0f314e.zip
core: refactor share system to separate session IDs from share IDs
- Generate shorter share IDs from session IDs for better URL structure - Update API routes to use shareID parameter instead of sessionID - Improve sync mechanism with better data queuing and deduplication - Maintain backward compatibility while improving security and organization
-rw-r--r--packages/enterprise/src/core/share.ts23
-rw-r--r--packages/enterprise/src/core/storage.ts1
-rw-r--r--packages/enterprise/src/routes/api/[...path].ts28
-rw-r--r--packages/enterprise/src/routes/share/[shareID].tsx (renamed from packages/enterprise/src/routes/share/[sessionID].tsx)46
-rw-r--r--packages/opencode/src/share/share-next.ts56
5 files changed, 86 insertions, 68 deletions
diff --git a/packages/enterprise/src/core/share.ts b/packages/enterprise/src/core/share.ts
index c24077ec3..dff7d0d74 100644
--- a/packages/enterprise/src/core/share.ts
+++ b/packages/enterprise/src/core/share.ts
@@ -8,6 +8,7 @@ export namespace Share {
export const Info = z.object({
id: z.string(),
secret: z.string(),
+ sessionID: z.string(),
})
export type Info = z.infer<typeof Info>
@@ -29,19 +30,16 @@ export namespace Share {
data: z.custom<FileDiff[]>(),
}),
z.object({
- type: z.literal("session_status"),
- data: z.custom<SessionStatus>(),
- }),
- z.object({
type: z.literal("model"),
data: z.custom<Model[]>(),
}),
])
export type Data = z.infer<typeof Data>
- export const create = fn(Info.pick({ id: true }), async (body) => {
+ export const create = fn(z.object({ sessionID: z.string() }), async (body) => {
const info: Info = {
- id: body.id,
+ id: body.sessionID.slice(-8),
+ sessionID: body.sessionID,
secret: crypto.randomUUID(),
}
const exists = await get(info.id)
@@ -51,8 +49,8 @@ export namespace Share {
return info
})
- async function get(sessionID: string) {
- return Storage.read<Info>(["share", sessionID])
+ export async function get(id: string) {
+ return Storage.read<Info>(["share", id])
}
export const remove = fn(Info.pick({ id: true, secret: true }), async (body) => {
@@ -66,8 +64,8 @@ export namespace Share {
}
})
- export async function data(sessionID: string) {
- const list = await Storage.list(["share_data", sessionID])
+ export async function data(id: string) {
+ const list = await Storage.list(["share_data", id])
const promises = []
for (const item of list) {
promises.push(
@@ -85,7 +83,7 @@ export namespace Share {
export const sync = fn(
z.object({
- share: Info,
+ share: Info.pick({ id: true, secret: true }),
data: Data.array(),
}),
async (input) => {
@@ -112,9 +110,6 @@ export namespace Share {
case "session_diff":
await Storage.write(["share_data", input.share.id, "session_diff"], item.data)
break
- case "session_status":
- await Storage.write(["share_data", input.share.id, "session_status"], item.data)
- break
case "model":
await Storage.write(["share_data", input.share.id, "model"], item.data)
break
diff --git a/packages/enterprise/src/core/storage.ts b/packages/enterprise/src/core/storage.ts
index fb33ef389..db03e2160 100644
--- a/packages/enterprise/src/core/storage.ts
+++ b/packages/enterprise/src/core/storage.ts
@@ -19,7 +19,6 @@ export namespace Storage {
return {
async read(path: string): Promise<string | undefined> {
try {
- console.log("reading", bucket, path)
const command = new GetObjectCommand({
Bucket: bucket,
Key: path,
diff --git a/packages/enterprise/src/routes/api/[...path].ts b/packages/enterprise/src/routes/api/[...path].ts
index bbca171bd..e77c00de9 100644
--- a/packages/enterprise/src/routes/api/[...path].ts
+++ b/packages/enterprise/src/routes/api/[...path].ts
@@ -37,6 +37,7 @@ app
schema: resolver(
z
.object({
+ id: z.string(),
url: z.string(),
secret: z.string(),
})
@@ -50,17 +51,18 @@ app
validator("json", z.object({ sessionID: z.string() })),
async (c) => {
const body = c.req.valid("json")
- const share = await Share.create({ id: body.sessionID })
+ const share = await Share.create({ sessionID: body.sessionID })
const protocol = c.req.header("x-forwarded-proto") ?? c.req.header("x-forwarded-protocol") ?? "https"
const host = c.req.header("x-forwarded-host") ?? c.req.header("host")
return c.json({
+ id: share.id,
secret: share.secret,
url: `${protocol}://${host}/share/${share.id}`,
})
},
)
.post(
- "/share/:sessionID/sync",
+ "/share/:shareID/sync",
describeRoute({
description: "Sync share data",
operationId: "share.sync",
@@ -75,20 +77,20 @@ app
},
},
}),
- validator("param", z.object({ sessionID: z.string() })),
+ validator("param", z.object({ shareID: z.string() })),
validator("json", z.object({ secret: z.string(), data: Share.Data.array() })),
async (c) => {
- const { sessionID } = c.req.valid("param")
+ const { shareID } = c.req.valid("param")
const body = c.req.valid("json")
await Share.sync({
- share: { id: sessionID, secret: body.secret },
+ share: { id: shareID, secret: body.secret },
data: body.data,
})
return c.json({})
},
)
.get(
- "/share/:sessionID/data",
+ "/share/:shareID/data",
describeRoute({
description: "Get share data",
operationId: "share.data",
@@ -103,14 +105,14 @@ app
},
},
}),
- validator("param", z.object({ sessionID: z.string() })),
+ validator("param", z.object({ shareID: z.string() })),
async (c) => {
- const { sessionID } = c.req.valid("param")
- return c.json(await Share.data(sessionID))
+ const { shareID } = c.req.valid("param")
+ return c.json(await Share.data(shareID))
},
)
.delete(
- "/share/:sessionID",
+ "/share/:shareID",
describeRoute({
description: "Remove a share",
operationId: "share.remove",
@@ -125,12 +127,12 @@ app
},
},
}),
- validator("param", z.object({ sessionID: z.string() })),
+ validator("param", z.object({ shareID: z.string() })),
validator("json", z.object({ secret: z.string() })),
async (c) => {
- const { sessionID } = c.req.valid("param")
+ const { shareID } = c.req.valid("param")
const body = c.req.valid("json")
- await Share.remove({ id: sessionID, secret: body.secret })
+ await Share.remove({ id: shareID, secret: body.secret })
return c.json({})
},
)
diff --git a/packages/enterprise/src/routes/share/[sessionID].tsx b/packages/enterprise/src/routes/share/[shareID].tsx
index 6c29437bc..61f2d23ea 100644
--- a/packages/enterprise/src/routes/share/[sessionID].tsx
+++ b/packages/enterprise/src/routes/share/[shareID].tsx
@@ -25,9 +25,12 @@ const SessionDataMissingError = NamedError.create(
}),
)
-const getData = query(async (sessionID) => {
- const data = await Share.data(sessionID)
+const getData = query(async (shareID) => {
+ const share = await Share.get(shareID)
+ if (!share) throw new SessionDataMissingError({ sessionID: shareID })
+ const data = await Share.data(shareID)
const result: {
+ sessionID: string
session: Session[]
session_diff: {
[sessionID: string]: FileDiff[]
@@ -45,12 +48,13 @@ const getData = query(async (sessionID) => {
[sessionID: string]: Model[]
}
} = {
+ sessionID: share.sessionID,
session: [],
session_diff: {
- [sessionID]: [],
+ [share.sessionID]: [],
},
session_status: {
- [sessionID]: {
+ [share.sessionID]: {
type: "idle",
},
},
@@ -64,10 +68,7 @@ const getData = query(async (sessionID) => {
result.session.push(item.data)
break
case "session_diff":
- result.session_diff[sessionID] = item.data
- break
- case "session_status":
- result.session_status[sessionID] = item.data
+ result.session_diff[share.sessionID] = item.data
break
case "message":
result.message[item.data.sessionID] = result.message[item.data.sessionID] ?? []
@@ -78,24 +79,25 @@ const getData = query(async (sessionID) => {
result.part[item.data.messageID].push(item.data)
break
case "model":
- result.model[sessionID] = item.data
+ result.model[share.sessionID] = item.data
break
}
}
- const match = Binary.search(result.session, sessionID!, (s) => s.id)
- if (!match.found) throw new SessionDataMissingError({ sessionID })
+ const match = Binary.search(result.session, share.sessionID, (s) => s.id)
+ if (!match.found) throw new SessionDataMissingError({ sessionID: share.sessionID })
+ console.log(result)
return result
}, "getShareData")
export const route = {
- preload: ({ params }) => getData(params.sessionID),
+ preload: ({ params }) => getData(params.shareID),
} satisfies RouteDefinition
export default function () {
const params = useParams()
const data = createAsync(async () => {
- if (!params.sessionID) throw new Error("Missing sessionID")
- return getData(params.sessionID)
+ if (!params.shareID) throw new Error("Missing sessionID")
+ return getData(params.shareID)
})
return (
@@ -115,12 +117,12 @@ export default function () {
const [store, setStore] = createStore({
messageId: undefined as string | undefined,
})
- const match = createMemo(() => Binary.search(data().session, params.sessionID!, (s) => s.id))
- if (!match().found) throw new Error(`Session ${params.sessionID} not found`)
+ const match = createMemo(() => Binary.search(data().session, data().sessionID, (s) => s.id))
+ if (!match().found) throw new Error(`Session ${data().sessionID} not found`)
const info = createMemo(() => data().session[match().index])
const messages = createMemo(() =>
- params.sessionID
- ? (data().message[params.sessionID]?.filter((m) => m.role === "user") ?? []).sort(
+ data().sessionID
+ ? (data().message[data().sessionID]?.filter((m) => m.role === "user") ?? []).sort(
(a, b) => b.time.created - a.time.created,
)
: [],
@@ -138,8 +140,8 @@ export default function () {
}
const provider = createMemo(() => activeMessage()?.model?.providerID)
const modelID = createMemo(() => activeMessage()?.model?.modelID)
- const model = createMemo(() => data().model[params.sessionID!]?.find((m) => m.id === modelID()))
- const diffs = createMemo(() => data().session_diff[params.sessionID!] ?? [])
+ const model = createMemo(() => data().model[data().sessionID]?.find((m) => m.id === modelID()))
+ const diffs = createMemo(() => data().session_diff[data().sessionID] ?? [])
const title = () => (
<div class="flex flex-col gap-4 shrink-0">
@@ -167,7 +169,7 @@ export default function () {
<For each={messages()}>
{(message) => (
<SessionTurn
- sessionID={params.sessionID!}
+ sessionID={data().sessionID}
messageID={message.id}
classes={{
root: "min-w-0 w-full relative",
@@ -254,7 +256,7 @@ export default function () {
</>
</Show>
<SessionTurn
- sessionID={params.sessionID!}
+ sessionID={data().sessionID}
messageID={store.messageId ?? firstUserMessage()!.id!}
classes={{ root: "grow", content: "flex flex-col justify-between", container: "pb-20" }}
>
diff --git a/packages/opencode/src/share/share-next.ts b/packages/opencode/src/share/share-next.ts
index b16c793cb..9543149a8 100644
--- a/packages/opencode/src/share/share-next.ts
+++ b/packages/opencode/src/share/share-next.ts
@@ -1,5 +1,6 @@
import { Bus } from "@/bus"
import { Config } from "@/config/config"
+import { ulid } from "ulid"
import type { ModelsDev } from "@/provider/models"
import { Provider } from "@/provider/provider"
import { Session } from "@/session"
@@ -10,6 +11,7 @@ import type * as SDK from "@opencode-ai/sdk"
export namespace ShareNext {
const log = Log.create({ service: "share-next" })
+
export async function init() {
const config = await Config.get()
if (!config.enterprise) return
@@ -70,11 +72,8 @@ export namespace ShareNext {
body: JSON.stringify({ sessionID: sessionID }),
})
.then((x) => x.json())
- .then((x) => x as { url: string; secret: string })
- await Storage.write(["session_share", sessionID], {
- id: sessionID,
- ...result,
- })
+ .then((x) => x as { id: string; url: string; secret: string })
+ await Storage.write(["session_share", sessionID], result)
fullSync(sessionID)
return result
}
@@ -109,20 +108,41 @@ export namespace ShareNext {
data: ModelsDev.Model[]
}
+ const queue = new Map<string, { timeout: NodeJS.Timeout; data: Map<string, Data> }>()
async function sync(sessionID: string, data: Data[]) {
- const url = await Config.get().then((x) => x.enterprise!.url)
- const share = await get(sessionID)
- if (!share) return
- await fetch(`${url}/api/share/${share.id}/sync`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- secret: share.secret,
- data,
- }),
- })
+ const existing = queue.get(sessionID)
+ if (existing) {
+ for (const item of data) {
+ existing.data.set("id" in item ? (item.id as string) : ulid(), item)
+ }
+ return
+ }
+
+ const dataMap = new Map<string, Data>()
+ for (const item of data) {
+ dataMap.set("id" in item ? (item.id as string) : ulid(), item)
+ }
+
+ const timeout = setTimeout(async () => {
+ const queued = queue.get(sessionID)
+ if (!queued) return
+ queue.delete(sessionID)
+ const url = await Config.get().then((x) => x.enterprise!.url)
+ const share = await get(sessionID)
+ if (!share) return
+
+ await fetch(`${url}/api/share/${share.id}/sync`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ secret: share.secret,
+ data: Array.from(queued.data.values()),
+ }),
+ })
+ }, 1000)
+ queue.set(sessionID, { timeout, data: dataMap })
}
export async function remove(sessionID: string) {