Files
openchamber/packages/ui/src/sync/bulk-archive-echo.ts
T
Iuliia Ivashko 0ef189f3c8 perf(sessions): archive a worktree's sessions through one server batch
Removing a worktree archived its sessions one SDK call at a time and
then re-rendered the whole sidebar once per streamed session.updated
echo. On a worktree with 121 sessions that meant 14.8s of main-thread
work, 121 requests, and 328 localStorage writes.

- Add POST /api/openchamber/sessions/archive: validates a batch (max
  500 ids, per-request archivedAt), archives sequentially, and reports
  partial failures instead of dropping the batch. VS Code serves no
  such route and answers 501; the shared UI then falls back to the
  per-session path.
- Plan batches from the sessions this client actually holds, live
  directory stores first, so worktree-only sessions still batch.
- Claim (id, archivedAt) pairs before the request and consume the
  matching session.updated echoes, so the server's own confirmations
  no longer fan out into 121 store publications. Runtime-scoped, TTL
  30s, released on response or fallback; non-matching updates pass.
- Make the managed-chats persistence a real trailing debounce instead
  of a 50ms throttle, so a burst of publications coalesces into one
  localStorage write.

Benchmark (121 sessions, production build, real Chrome): 14785ms ->
~1030ms, long tasks 100 -> 1, global store publications 236 -> 1,
persistence writes 328 -> 3.
2026-09-04 16:50:03 +03:00

50 lines
1.7 KiB
TypeScript

import type { Event } from "@opencode-ai/sdk/v2/client"
import { subscribeRuntimeEndpointWillChange } from "@/lib/runtime-switch"
const BULK_ARCHIVE_ECHO_TTL_MS = 30_000
const pendingEchoes = new Map<string, Map<string, { archivedAt: number; expiresAt: number }>>()
subscribeRuntimeEndpointWillChange(() => pendingEchoes.clear())
export const registerBulkArchiveEchoes = (
runtimeKey: string,
sessions: Iterable<{ id: string; archivedAt: number }>,
now = Date.now(),
): void => {
let runtimeEchoes = pendingEchoes.get(runtimeKey)
if (!runtimeEchoes) {
runtimeEchoes = new Map()
pendingEchoes.set(runtimeKey, runtimeEchoes)
}
for (const session of sessions) {
runtimeEchoes.set(session.id, {
archivedAt: session.archivedAt,
expiresAt: now + BULK_ARCHIVE_ECHO_TTL_MS,
})
}
}
export const releaseBulkArchiveEchoes = (runtimeKey: string, sessionIds: Iterable<string>): void => {
const runtimeEchoes = pendingEchoes.get(runtimeKey)
if (!runtimeEchoes) return
for (const sessionId of sessionIds) runtimeEchoes.delete(sessionId)
if (runtimeEchoes.size === 0) pendingEchoes.delete(runtimeKey)
}
export const shouldConsumeBulkArchiveEcho = (
event: Event,
runtimeKey: string,
now = Date.now(),
): boolean => {
if (event.type !== "session.updated") return false
const runtimeEchoes = pendingEchoes.get(runtimeKey)
const expected = runtimeEchoes?.get(event.properties.info.id)
if (!expected) return false
if (expected.expiresAt < now) {
runtimeEchoes?.delete(event.properties.info.id)
if (runtimeEchoes?.size === 0) pendingEchoes.delete(runtimeKey)
return false
}
return event.properties.info.time.archived === expected.archivedAt
}