mirror of
https://github.com/stablyai/orca.git
synced 2026-10-09 08:02:35 +00:00
perf(mobile): release closed Markdown document cache entries (#26159)
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
import { randomBytes } from 'node:crypto'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { hookMount, performHookAction } from '../test-support/rpc-recording/hook-mount'
|
||||
import { mountFixture } from '../test-support/rpc-recording/recorder-fixture-shape'
|
||||
import type { RpcResponse } from '../transport/types'
|
||||
import type { MarkdownDocState, MobileSessionTab } from './mobile-session-route-types'
|
||||
import { useMobileSessionDocumentReaders } from './use-mobile-session-document-readers'
|
||||
import { useMobileSessionCloseActions } from './use-mobile-session-close-actions'
|
||||
|
||||
type ReaderScope = Parameters<typeof useMobileSessionDocumentReaders>[0]
|
||||
type CloseScope = Parameters<typeof useMobileSessionCloseActions>[0]
|
||||
|
||||
function success(content: string): RpcResponse {
|
||||
return {
|
||||
id: 'read',
|
||||
ok: true,
|
||||
result: { content, version: 'version', isDirty: false, editable: false },
|
||||
_meta: { runtimeId: 'host' }
|
||||
}
|
||||
}
|
||||
|
||||
function mountDocuments(readReply: () => Promise<RpcResponse>) {
|
||||
let docs = new Map<string, MarkdownDocState>()
|
||||
const tabs: { current: MobileSessionTab[] } = { current: [] }
|
||||
const setDocs: ReaderScope['setMarkdownDocs'] = (update) => {
|
||||
docs = typeof update === 'function' ? update(docs) : update
|
||||
}
|
||||
const closeReply = vi.fn(async (): Promise<RpcResponse> => success(''))
|
||||
const client = {
|
||||
sendRequest: vi.fn(async (method: string): Promise<RpcResponse> =>
|
||||
method === 'markdown.readTab' || method === 'files.read' ? readReply() : closeReply()
|
||||
)
|
||||
}
|
||||
let readers: ReturnType<typeof useMobileSessionDocumentReaders> | undefined
|
||||
let close: ReturnType<typeof useMobileSessionCloseActions> | undefined
|
||||
const hook = hookMount(() => {
|
||||
readers = useMobileSessionDocumentReaders(
|
||||
mountFixture<ReaderScope>({
|
||||
worktreeId: 'folder-workspace',
|
||||
client,
|
||||
setMarkdownDocs: setDocs,
|
||||
setFileDocs: () => {}
|
||||
})
|
||||
)
|
||||
close = useMobileSessionCloseActions(
|
||||
mountFixture<CloseScope>({
|
||||
worktreeId: 'folder-workspace',
|
||||
client,
|
||||
sessionTabsRef: tabs,
|
||||
setSessionTabs: () => {},
|
||||
setFileDocs: () => {},
|
||||
setMarkdownDocs: setDocs,
|
||||
reconcileBufferedDraftsRef: { current: () => {} },
|
||||
closedTabTombstonesRef: { current: new Map() },
|
||||
activeSessionTabIdRef: { current: null },
|
||||
selectedSessionTabIdRef: { current: null },
|
||||
activeSessionTabTypeRef: { current: null },
|
||||
activeHandleRef: { current: null },
|
||||
setActiveSessionTabId: () => {},
|
||||
setActiveHandle: () => {}
|
||||
})
|
||||
)
|
||||
})
|
||||
hook.mount()
|
||||
const tab = (id: string) =>
|
||||
mountFixture<Extract<MobileSessionTab, { type: 'markdown' }>>({
|
||||
type: 'markdown',
|
||||
id,
|
||||
relativePath: id + '.md',
|
||||
isDirty: false
|
||||
})
|
||||
return {
|
||||
get docs() {
|
||||
return docs
|
||||
},
|
||||
closeReply,
|
||||
unmount: hook.unmount,
|
||||
read(id: string) {
|
||||
const target = tab(id)
|
||||
tabs.current = [target]
|
||||
return performHookAction(() => readers?.readMarkdownTab(target))
|
||||
},
|
||||
close(id: string) {
|
||||
return performHookAction(() => close?.handleCloseSessionTab(tab(id)))
|
||||
},
|
||||
dirty(id: string) {
|
||||
const doc = docs.get(id)
|
||||
if (doc?.status !== 'ready') {
|
||||
throw new Error('Missing ready document')
|
||||
}
|
||||
docs.set(id, { ...doc, localContent: 'phone draft', isDirty: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function delayedReply() {
|
||||
let resolve: (reply: RpcResponse) => void = () => {}
|
||||
const promise = new Promise<RpcResponse>((done) => {
|
||||
resolve = done
|
||||
})
|
||||
return { promise, resolve }
|
||||
}
|
||||
|
||||
describe('mobile Markdown document lifetime', () => {
|
||||
it('releases 40 MiB of clean content after twenty reads and successful closes', async () => {
|
||||
const h = mountDocuments(async () => success(randomBytes(1536 * 1024).toString('base64')))
|
||||
try {
|
||||
for (let index = 0; index < 20; index += 1) {
|
||||
await h.read('tab-' + index)
|
||||
const doc = h.docs.get('tab-' + index)
|
||||
expect(doc?.status === 'ready' && Buffer.byteLength(doc.content)).toBe(2 * 1024 * 1024)
|
||||
await h.close('tab-' + index)
|
||||
}
|
||||
expect(h.docs.size).toBe(0)
|
||||
} finally {
|
||||
h.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
it.each(['success', 'failure', 'disk fallback'])(
|
||||
'keeps a closed read released after a late %s',
|
||||
async (kind) => {
|
||||
const delayed = delayedReply()
|
||||
const readReply = vi
|
||||
.fn()
|
||||
.mockReturnValueOnce(delayed.promise)
|
||||
.mockResolvedValue(success('disk'))
|
||||
const h = mountDocuments(readReply)
|
||||
try {
|
||||
const reading = h.read('closed')
|
||||
await h.close('closed')
|
||||
delayed.resolve(
|
||||
kind === 'success'
|
||||
? success('late')
|
||||
: {
|
||||
id: 'read',
|
||||
ok: false,
|
||||
error: {
|
||||
code: kind === 'disk fallback' ? 'renderer_unavailable' : 'tab_not_found',
|
||||
message: 'Unavailable'
|
||||
},
|
||||
_meta: { runtimeId: 'host' }
|
||||
}
|
||||
)
|
||||
await reading
|
||||
expect(h.docs.size).toBe(0)
|
||||
if (kind === 'disk fallback') {
|
||||
expect(readReply).toHaveBeenCalledTimes(2)
|
||||
}
|
||||
} finally {
|
||||
h.unmount()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it('retains the document when the host refuses close', async () => {
|
||||
const h = mountDocuments(async () => success('kept'))
|
||||
try {
|
||||
await h.read('kept')
|
||||
const doc = h.docs.get('kept')
|
||||
h.closeReply.mockResolvedValue({
|
||||
id: 'close',
|
||||
ok: false,
|
||||
error: { code: 'refused', message: 'Refused' },
|
||||
_meta: { runtimeId: 'host' }
|
||||
})
|
||||
await h.close('kept')
|
||||
expect(h.docs.get('kept')).toBe(doc)
|
||||
} finally {
|
||||
h.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
it('preserves unsaved phone drafts after close', async () => {
|
||||
const h = mountDocuments(async () => success('original'))
|
||||
try {
|
||||
await h.read('draft')
|
||||
h.dirty('draft')
|
||||
const draft = h.docs.get('draft')
|
||||
await h.close('draft')
|
||||
expect(h.docs.get('draft')).toBe(draft)
|
||||
} finally {
|
||||
h.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps a newer read when an older request finishes later', async () => {
|
||||
const old = delayedReply()
|
||||
const h = mountDocuments(
|
||||
vi.fn().mockReturnValueOnce(old.promise).mockResolvedValue(success('new'))
|
||||
)
|
||||
try {
|
||||
const first = h.read('same')
|
||||
await h.read('same')
|
||||
const current = h.docs.get('same')
|
||||
old.resolve(success('old'))
|
||||
await first
|
||||
expect(h.docs.get('same')).toBe(current)
|
||||
} finally {
|
||||
h.unmount()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -16,6 +16,7 @@ export function useMobileSessionCloseActions(scope: MobileSessionContentCreateAc
|
||||
terminalsRef,
|
||||
setSessionTabs,
|
||||
setFileDocs,
|
||||
setMarkdownDocs,
|
||||
sessionTabsRef,
|
||||
reconcileBufferedDraftsRef,
|
||||
closedTabTombstonesRef,
|
||||
@@ -119,6 +120,17 @@ export function useMobileSessionCloseActions(scope: MobileSessionContentCreateAc
|
||||
})
|
||||
)
|
||||
if (response.accepted) {
|
||||
if (tab.type === 'markdown') {
|
||||
setMarkdownDocs((prev) => {
|
||||
const doc = prev.get(tab.id)
|
||||
if (!doc || (doc.status === 'ready' && doc.isDirty)) {
|
||||
return prev
|
||||
}
|
||||
const next = new Map(prev)
|
||||
next.delete(tab.id)
|
||||
return next
|
||||
})
|
||||
}
|
||||
if (tab.type === 'file') {
|
||||
setFileDocs((prev) => {
|
||||
if (!prev.has(tab.id)) {
|
||||
|
||||
@@ -17,7 +17,8 @@ export function useMobileSessionDocumentReaders(scope: MobileSessionTabApplicati
|
||||
if (!client) {
|
||||
return
|
||||
}
|
||||
setMarkdownDocs((prev) => new Map(prev).set(tab.id, { status: 'loading' }))
|
||||
const loading = { status: 'loading' } as const
|
||||
setMarkdownDocs((prev) => new Map(prev).set(tab.id, loading))
|
||||
try {
|
||||
const response = await markdownTabRead.request(client, {
|
||||
worktree: `id:${worktreeId}`,
|
||||
@@ -26,19 +27,21 @@ export function useMobileSessionDocumentReaders(scope: MobileSessionTabApplicati
|
||||
if (response.ok) {
|
||||
const result = markdownTabRead.interpret(response)
|
||||
setMarkdownDocs((prev) =>
|
||||
new Map(prev).set(tab.id, {
|
||||
status: 'ready',
|
||||
content: result.content,
|
||||
localContent: result.content,
|
||||
baseVersion: result.version,
|
||||
isDirty: false,
|
||||
editable: result.editable === true,
|
||||
stale: result.isDirty,
|
||||
readOnlyReason: result.readOnlyReason,
|
||||
...(result.truncated === true
|
||||
? { truncated: true, byteLength: result.byteLength }
|
||||
: {})
|
||||
})
|
||||
prev.get(tab.id) === loading
|
||||
? new Map(prev).set(tab.id, {
|
||||
status: 'ready',
|
||||
content: result.content,
|
||||
localContent: result.content,
|
||||
baseVersion: result.version,
|
||||
isDirty: false,
|
||||
editable: result.editable === true,
|
||||
stale: result.isDirty,
|
||||
readOnlyReason: result.readOnlyReason,
|
||||
...(result.truncated === true
|
||||
? { truncated: true, byteLength: result.byteLength }
|
||||
: {})
|
||||
})
|
||||
: prev
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -57,22 +60,26 @@ export function useMobileSessionDocumentReaders(scope: MobileSessionTabApplicati
|
||||
}
|
||||
const fileResult = fallback.value
|
||||
setMarkdownDocs((prev) =>
|
||||
new Map(prev).set(
|
||||
tab.id,
|
||||
buildMarkdownDiskFallbackDoc({
|
||||
content: fileResult.content,
|
||||
truncated: fileResult.truncated,
|
||||
byteLength: fileResult.byteLength,
|
||||
tabIsDirty: tab.isDirty
|
||||
})
|
||||
)
|
||||
prev.get(tab.id) === loading
|
||||
? new Map(prev).set(
|
||||
tab.id,
|
||||
buildMarkdownDiskFallbackDoc({
|
||||
content: fileResult.content,
|
||||
truncated: fileResult.truncated,
|
||||
byteLength: fileResult.byteLength,
|
||||
tabIsDirty: tab.isDirty
|
||||
})
|
||||
)
|
||||
: prev
|
||||
)
|
||||
} catch (err) {
|
||||
setMarkdownDocs((prev) =>
|
||||
new Map(prev).set(tab.id, {
|
||||
status: 'error',
|
||||
message: documentReadErrorMessage(err, "Couldn't load markdown")
|
||||
})
|
||||
prev.get(tab.id) === loading
|
||||
? new Map(prev).set(tab.id, {
|
||||
status: 'error',
|
||||
message: documentReadErrorMessage(err, "Couldn't load markdown")
|
||||
})
|
||||
: prev
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user