mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
perf(markdown): coalesce draft version hashing with publication (#35107)
* perf(markdown): coalesce draft version hashing with publication * test(markdown): assert draft projection contents and drop test-local baseline --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { act } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { AppState } from '@/store/types'
|
||||
import { createTestStore } from '@/store/slices/store-test-helpers'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
getState: vi.fn<() => AppState>(),
|
||||
subscribe: vi.fn((_listener: (state: AppState, previous: AppState) => void) => () => {}),
|
||||
publish: vi.fn<() => Promise<void>>(),
|
||||
hashes: { calls: 0, chars: 0 }
|
||||
}))
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: Object.assign(
|
||||
(selector: (state: AppState) => unknown) => selector(mocks.getState()),
|
||||
{
|
||||
getState: mocks.getState,
|
||||
subscribe: mocks.subscribe
|
||||
}
|
||||
)
|
||||
}))
|
||||
vi.mock('../runtime/sync-runtime-graph/graph-publication', () => ({
|
||||
syncRuntimeGraph: mocks.publish,
|
||||
setTrailingGraphSyncScheduler: vi.fn()
|
||||
}))
|
||||
vi.mock('../runtime/sync-runtime-graph/editor-draft-hash', async (importOriginal) => {
|
||||
const actual = await importOriginal<{ stableHashString: typeof stableHashString }>()
|
||||
return {
|
||||
stableHashString: (text: string) => {
|
||||
mocks.hashes.calls += 1
|
||||
mocks.hashes.chars += text.length
|
||||
return actual.stableHashString(text)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
import { useRuntimeGraphSync } from './use-runtime-graph-sync'
|
||||
import { graphState } from '../runtime/sync-runtime-graph/graph-state'
|
||||
import { buildMobileSessionTabSnapshots } from '../runtime/sync-runtime-graph/mobile-session-snapshots'
|
||||
import { resetRuntimeMobileSyncProjectionCachesForTests } from '../runtime/sync-runtime-graph/sync-projections'
|
||||
import { stableHashString } from '../runtime/sync-runtime-graph/editor-draft-hash'
|
||||
import type { RuntimeMobileSessionTabsSnapshot } from '../../../shared/runtime-types'
|
||||
|
||||
function Probe({ revision: _revision }: { revision: number }): null {
|
||||
useRuntimeGraphSync()
|
||||
return null
|
||||
}
|
||||
|
||||
const file = {
|
||||
id: 'markdown',
|
||||
filePath: '/repo/README.md',
|
||||
relativePath: 'README.md',
|
||||
worktreeId: 'wt-1',
|
||||
language: 'markdown',
|
||||
mode: 'edit' as const,
|
||||
isDirty: true
|
||||
}
|
||||
|
||||
describe('runtime graph draft hashing at publication', () => {
|
||||
let root: Root
|
||||
let store: ReturnType<typeof createTestStore>
|
||||
let snapshots: RuntimeMobileSessionTabsSnapshot[][]
|
||||
let revision = 0
|
||||
let releasePublication: (() => void) | undefined
|
||||
|
||||
async function render(): Promise<void> {
|
||||
revision += 1
|
||||
await act(async () => root.render(<Probe revision={revision} />))
|
||||
}
|
||||
async function flush(): Promise<void> {
|
||||
await act(async () => vi.advanceTimersByTimeAsync(16))
|
||||
}
|
||||
function latestVersion(): string | undefined {
|
||||
return snapshots
|
||||
.at(-1)
|
||||
?.flatMap((snapshot) => snapshot.tabs)
|
||||
.find((tab) => tab.type === 'markdown')?.documentVersion
|
||||
}
|
||||
function resetCounts(): void {
|
||||
mocks.hashes.calls = 0
|
||||
mocks.hashes.chars = 0
|
||||
mocks.publish.mockClear()
|
||||
snapshots = []
|
||||
}
|
||||
beforeEach(() => {
|
||||
releasePublication = undefined
|
||||
vi.useFakeTimers()
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
|
||||
resetRuntimeMobileSyncProjectionCachesForTests()
|
||||
graphState.mobileSessionSnapshotCacheByWorktree.clear()
|
||||
store = createTestStore()
|
||||
store.setState({
|
||||
workspaceSessionReady: true,
|
||||
openFiles: [file],
|
||||
editorDrafts: { markdown: 'initial' }
|
||||
})
|
||||
mocks.getState.mockImplementation(store.getState)
|
||||
mocks.subscribe.mockImplementation(store.subscribe)
|
||||
snapshots = []
|
||||
mocks.publish.mockImplementation(async () => {
|
||||
snapshots.push(buildMobileSessionTabSnapshots(store.getState(), false))
|
||||
})
|
||||
resetCounts()
|
||||
root = createRoot(document.createElement('div'))
|
||||
})
|
||||
afterEach(async () => {
|
||||
await act(async () => {
|
||||
root.unmount()
|
||||
releasePublication?.()
|
||||
})
|
||||
vi.useRealTimers()
|
||||
vi.unstubAllGlobals()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it.each([40_000, 400_000])(
|
||||
'hashes only the final %i-character Markdown draft in a burst',
|
||||
async (size) => {
|
||||
store.setState({ editorDrafts: { markdown: 'x'.repeat(size) } })
|
||||
await render()
|
||||
await flush()
|
||||
resetCounts()
|
||||
for (let index = 1; index <= 100; index += 1) {
|
||||
store.getState().setEditorDraft(file.id, 'x'.repeat(size + index))
|
||||
}
|
||||
expect(mocks.hashes.calls).toBe(0)
|
||||
expect(mocks.publish).not.toHaveBeenCalled()
|
||||
await flush()
|
||||
expect(mocks.hashes).toEqual({ calls: 1, chars: size + 100 })
|
||||
expect(mocks.publish).toHaveBeenCalledTimes(1)
|
||||
const version = stableHashString(store.getState().editorDrafts.markdown)
|
||||
expect(latestVersion()).toBe(version)
|
||||
resetCounts()
|
||||
store.setState({ editorDrafts: { ...store.getState().editorDrafts } })
|
||||
await flush()
|
||||
expect(mocks.publish).not.toHaveBeenCalled()
|
||||
expect(mocks.hashes.calls).toBe(0)
|
||||
}
|
||||
)
|
||||
|
||||
it('defers disabled edits until re-enable and cancels pending work on unmount', async () => {
|
||||
store.setState({ workspaceSessionReady: false })
|
||||
await render()
|
||||
resetCounts()
|
||||
for (let index = 0; index < 100; index += 1) {
|
||||
store.getState().setEditorDraft(file.id, `disabled-${index}`)
|
||||
}
|
||||
await flush()
|
||||
expect(mocks.hashes.calls).toBe(0)
|
||||
expect(mocks.publish).not.toHaveBeenCalled()
|
||||
store.setState({ workspaceSessionReady: true })
|
||||
await render()
|
||||
await flush()
|
||||
expect(latestVersion()).toBe(stableHashString('disabled-99'))
|
||||
resetCounts()
|
||||
store.getState().setEditorDraft(file.id, 'retired')
|
||||
await act(async () => root.render(null))
|
||||
await flush()
|
||||
expect(mocks.hashes.calls).toBe(0)
|
||||
expect(mocks.publish).not.toHaveBeenCalled()
|
||||
expect(vi.getTimerCount()).toBe(0)
|
||||
})
|
||||
|
||||
it('publishes a trailing edit after an in-flight publication resolves', async () => {
|
||||
await render()
|
||||
await flush()
|
||||
resetCounts()
|
||||
mocks.publish.mockImplementationOnce(async () => {
|
||||
snapshots.push(buildMobileSessionTabSnapshots(store.getState(), false))
|
||||
await new Promise<void>((resolve) => {
|
||||
releasePublication = resolve
|
||||
})
|
||||
})
|
||||
store.getState().setEditorDraft(file.id, 'first')
|
||||
await flush()
|
||||
expect(latestVersion()).toBe(stableHashString('first'))
|
||||
resetCounts()
|
||||
store.setState({ workspaceSessionReady: false })
|
||||
await render()
|
||||
store.getState().setEditorDraft(file.id, 'last')
|
||||
await flush()
|
||||
expect(mocks.hashes.calls).toBe(0)
|
||||
expect(mocks.publish).not.toHaveBeenCalled()
|
||||
store.setState({ workspaceSessionReady: true })
|
||||
await render()
|
||||
await act(async () => releasePublication?.())
|
||||
await flush()
|
||||
expect(mocks.publish).toHaveBeenCalledTimes(1)
|
||||
expect(latestVersion()).toBe(stableHashString('last'))
|
||||
})
|
||||
|
||||
it('retains preview-source admission, meaningful clear, and Markdown eligibility changes', async () => {
|
||||
const source = { ...file, id: 'source', language: 'typescript', filePath: '/repo/source.ts' }
|
||||
const preview = {
|
||||
...file,
|
||||
id: 'preview',
|
||||
mode: 'markdown-preview' as const,
|
||||
markdownPreviewSourceFileId: source.id
|
||||
}
|
||||
store.setState({ openFiles: [source, preview], editorDrafts: { source: 'preview exact' } })
|
||||
await render()
|
||||
await flush()
|
||||
expect(latestVersion()).toBe(stableHashString('preview exact'))
|
||||
resetCounts()
|
||||
store.getState().clearEditorDraft(source.id)
|
||||
await flush()
|
||||
expect(latestVersion()).toBe('file:source')
|
||||
expect(mocks.hashes.calls).toBe(0)
|
||||
store.setState({ openFiles: [source], editorDrafts: { source: 'ordinary source' } })
|
||||
await flush()
|
||||
resetCounts()
|
||||
store.getState().setEditorDraft(source.id, 'ordinary edit')
|
||||
await flush()
|
||||
expect(mocks.publish).not.toHaveBeenCalled()
|
||||
expect(mocks.hashes.calls).toBe(0)
|
||||
store.setState({ openFiles: [{ ...source, language: 'markdown' }] })
|
||||
await flush()
|
||||
expect(latestVersion()).toBe(stableHashString('ordinary edit'))
|
||||
})
|
||||
})
|
||||
@@ -16,7 +16,6 @@ vi.mock('./sync-runtime-graph/editor-draft-hash', async (importOriginal) => {
|
||||
}
|
||||
})
|
||||
|
||||
const { stableHashString } = await import('./sync-runtime-graph/editor-draft-hash')
|
||||
const {
|
||||
AGENT_STATUS_SYNC_UPDATED_AT_BUCKET_MS_FOR_TESTS: AGENT_STATUS_SYNC_UPDATED_AT_BUCKET_MS,
|
||||
buildRuntimeMobileAgentStatusProjectionForTests,
|
||||
@@ -39,14 +38,6 @@ function mainAgentKey(mainAgent: MainAgentStatus) {
|
||||
return mainAgent ? [mainAgent.state, mainAgent.outcome ?? null, mainAgent.stateStartedAt] : null
|
||||
}
|
||||
|
||||
function referenceEditorDraftsProjection(editorDrafts: AppState['editorDrafts']): string {
|
||||
return JSON.stringify(
|
||||
Object.fromEntries(
|
||||
Object.entries(editorDrafts).map(([fileId, content]) => [fileId, stableHashString(content)])
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function referenceOpenFilesProjection(openFiles: AppState['openFiles']): string {
|
||||
return JSON.stringify(
|
||||
openFiles.map((file) => ({
|
||||
@@ -304,65 +295,42 @@ describe('editor draft projection on the typing path', () => {
|
||||
expect(draftHashCounter.calls).toBe(0)
|
||||
const markdown = { ...state, openFiles: [makeOpenFile(0, { language: 'markdown' })] }
|
||||
const markdownKey = getRuntimeMobileSessionSyncKey(markdown, state, sourceKey, false)
|
||||
expect(markdownKey.editorDraftsProjection).not.toBe(sourceKey.editorDraftsProjection)
|
||||
expect(draftHashCounter.calls).toBe(1)
|
||||
expect([...markdownKey.editorDraftsProjection]).toEqual([['file-0', 'draft']])
|
||||
expect(runtimeMobileSessionSyncKeysEqual(markdownKey, sourceKey)).toBe(false)
|
||||
expect(draftHashCounter.calls).toBe(0)
|
||||
state = { ...markdown, editorDrafts: { 'file-0': 'edited draft' } }
|
||||
const editedKey = getRuntimeMobileSessionSyncKey(state, markdown, markdownKey, false)
|
||||
expect(editedKey.editorDraftsProjection).not.toBe(markdownKey.editorDraftsProjection)
|
||||
expect(draftHashCounter.calls).toBe(2)
|
||||
expect([...editedKey.editorDraftsProjection]).toEqual([['file-0', 'edited draft']])
|
||||
expect(draftHashCounter.calls).toBe(0)
|
||||
const clean = { ...state, editorDrafts: {} }
|
||||
const cleanKey = getRuntimeMobileSessionSyncKey(clean, state, editedKey, false)
|
||||
expect(cleanKey.editorDraftsProjection).toBe('{}')
|
||||
expect(cleanKey.editorDraftsProjection.size).toBe(0)
|
||||
expect(
|
||||
buildMobileSessionTabSnapshots(clean, false).flatMap((snapshot) => snapshot.tabs)
|
||||
).toContainEqual(expect.objectContaining({ id: 'file-0', documentVersion: 'file:file-0' }))
|
||||
const closed = { ...state, openFiles: [] }
|
||||
const closedKey = getRuntimeMobileSessionSyncKey(closed, state, editedKey, false)
|
||||
expect(closedKey.editorDraftsProjection).toBe('{}')
|
||||
expect(closedKey.editorDraftsProjection.size).toBe(0)
|
||||
expect(getEditorDraftVersionByFileId(closed.editorDrafts, closed.openFiles).size).toBe(0)
|
||||
expect(draftHashCounter.calls).toBe(2)
|
||||
expect(draftHashCounter.calls).toBe(0)
|
||||
})
|
||||
|
||||
it('hashes only the edited file per keystroke, not every open dirty file', () => {
|
||||
it('hashes the final edited draft at snapshot capture after hash-free typing', () => {
|
||||
const typedCharacters = 100
|
||||
const totalDraftChars = DRAFT_FILE_COUNT * DRAFT_CHARS_PER_FILE
|
||||
|
||||
// Baseline: the pre-change uncached projection, driven by the same counter.
|
||||
let drafts = makeDrafts()
|
||||
referenceEditorDraftsProjection(drafts)
|
||||
draftHashCounter.calls = 0
|
||||
draftHashCounter.chars = 0
|
||||
for (let keystroke = 0; keystroke < typedCharacters; keystroke += 1) {
|
||||
drafts = { ...drafts, 'file-0': `${drafts['file-0']}a` }
|
||||
referenceEditorDraftsProjection(drafts)
|
||||
}
|
||||
const before = { calls: draftHashCounter.calls, chars: draftHashCounter.chars }
|
||||
|
||||
resetRuntimeMobileSyncProjectionCachesForTests()
|
||||
let memoDrafts = makeDrafts()
|
||||
const openFiles = Array.from({ length: DRAFT_FILE_COUNT }, (_, index) =>
|
||||
makeOpenFile(index, { language: 'markdown' })
|
||||
)
|
||||
buildRuntimeMobileEditorDraftsProjection(memoDrafts, openFiles)
|
||||
getEditorDraftVersionByFileId(memoDrafts, openFiles)
|
||||
draftHashCounter.calls = 0
|
||||
draftHashCounter.chars = 0
|
||||
for (let keystroke = 0; keystroke < typedCharacters; keystroke += 1) {
|
||||
memoDrafts = { ...memoDrafts, 'file-0': `${memoDrafts['file-0']}a` }
|
||||
buildRuntimeMobileEditorDraftsProjection(memoDrafts, openFiles)
|
||||
}
|
||||
const after = { calls: draftHashCounter.calls, chars: draftHashCounter.chars }
|
||||
|
||||
// Keystroke k has grown file-0 by k characters, so the exact totals are closed form.
|
||||
const growth = (typedCharacters * (typedCharacters + 1)) / 2
|
||||
expect(before).toEqual({
|
||||
calls: typedCharacters * DRAFT_FILE_COUNT,
|
||||
chars: typedCharacters * totalDraftChars + growth
|
||||
})
|
||||
expect(after).toEqual({
|
||||
calls: typedCharacters,
|
||||
chars: typedCharacters * DRAFT_CHARS_PER_FILE + growth
|
||||
})
|
||||
expect(before.chars / after.chars).toBeGreaterThan(DRAFT_FILE_COUNT - 0.1)
|
||||
expect(draftHashCounter).toEqual({ calls: 0, chars: 0 })
|
||||
getEditorDraftVersionByFileId(memoDrafts, openFiles)
|
||||
expect(draftHashCounter).toEqual({ calls: 1, chars: DRAFT_CHARS_PER_FILE + typedCharacters })
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// Share FNV-1a draft stamps across mobile keys/snapshots. Cost scales with text length; memoize per file or record spreads rehash every draft.
|
||||
export function stableHashString(value: string): string {
|
||||
let hash = 2166136261
|
||||
for (let i = 0; i < value.length; i += 1) {
|
||||
|
||||
@@ -140,6 +140,24 @@ export function getRuntimeMobileSessionSyncKey(
|
||||
}
|
||||
}
|
||||
|
||||
function editorDraftsEqual(
|
||||
a: ReadonlyMap<string, string>,
|
||||
b: ReadonlyMap<string, string>
|
||||
): boolean {
|
||||
if (a === b) {
|
||||
return true
|
||||
}
|
||||
if (a.size !== b.size) {
|
||||
return false
|
||||
}
|
||||
for (const [fileId, content] of a) {
|
||||
if (b.get(fileId) !== content) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export function runtimeMobileSessionSyncKeysEqual(
|
||||
a: RuntimeMobileSessionSyncKey,
|
||||
b: RuntimeMobileSessionSyncKey
|
||||
@@ -168,6 +186,6 @@ export function runtimeMobileSessionSyncKeysEqual(
|
||||
a.tabsProjection === b.tabsProjection &&
|
||||
a.openFilesProjection === b.openFilesProjection &&
|
||||
a.browserProjection === b.browserProjection &&
|
||||
a.editorDraftsProjection === b.editorDraftsProjection
|
||||
editorDraftsEqual(a.editorDraftsProjection, b.editorDraftsProjection)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -220,7 +220,31 @@ export function buildRuntimeMobileBrowserProjection(state: AppState): string {
|
||||
)},"pagesByWorkspace":${buildBrowserPagesProjection(getBrowserPagesByWorkspace(state))}}`
|
||||
}
|
||||
|
||||
// Monaco re-spreads drafts per keystroke; memoize each file’s unchanged string so typing does not rehash all dirty files.
|
||||
let cachedDraftSourceFileIds: {
|
||||
source: AppState['openFiles']
|
||||
ids: ReadonlySet<string>
|
||||
} | null = null
|
||||
|
||||
function getEditorDraftSourceFileIds(openFiles: AppState['openFiles']): ReadonlySet<string> {
|
||||
if (cachedDraftSourceFileIds?.source === openFiles) {
|
||||
return cachedDraftSourceFileIds.ids
|
||||
}
|
||||
const indexes = getOpenFileIndexes(openFiles)
|
||||
const ids = new Set<string>()
|
||||
for (const file of openFiles) {
|
||||
if (!isMobilePublishableOpenFile(file)) {
|
||||
continue
|
||||
}
|
||||
const source = getMobileMarkdownSourceFile(file, indexes.byWorktreeAndId.get(file.worktreeId))
|
||||
if (source) {
|
||||
ids.add(source.id)
|
||||
}
|
||||
}
|
||||
cachedDraftSourceFileIds = { source: openFiles, ids }
|
||||
return ids
|
||||
}
|
||||
|
||||
// Reuse unchanged file hashes between coalesced snapshot captures.
|
||||
function getEditorDraftHashCache(
|
||||
editorDrafts: AppState['editorDrafts'],
|
||||
openFiles: AppState['openFiles']
|
||||
@@ -230,26 +254,11 @@ function getEditorDraftHashCache(
|
||||
return cached
|
||||
}
|
||||
|
||||
let sourceFileIds = cached?.openFiles === openFiles ? cached.sourceFileIds : undefined
|
||||
if (!sourceFileIds) {
|
||||
const indexes = getOpenFileIndexes(openFiles)
|
||||
const ids = new Set<string>()
|
||||
for (const file of openFiles) {
|
||||
if (!isMobilePublishableOpenFile(file)) {
|
||||
continue
|
||||
}
|
||||
const source = getMobileMarkdownSourceFile(file, indexes.byWorktreeAndId.get(file.worktreeId))
|
||||
if (source) {
|
||||
ids.add(source.id)
|
||||
}
|
||||
}
|
||||
sourceFileIds = ids
|
||||
}
|
||||
const sourceFileIds = getEditorDraftSourceFileIds(openFiles)
|
||||
|
||||
const previousEntries = cached?.entries
|
||||
const entries = new Map<string, EditorDraftHashCacheEntry>()
|
||||
const hashByFileId = new Map<string, string>()
|
||||
const parts: string[] = []
|
||||
for (const fileId of sourceFileIds) {
|
||||
const content = editorDrafts[fileId]
|
||||
if (content === undefined) {
|
||||
@@ -260,21 +269,17 @@ function getEditorDraftHashCache(
|
||||
if (previous?.content === content) {
|
||||
entry = previous
|
||||
} else {
|
||||
const fileIdJson = previous?.fileIdJson ?? JSON.stringify(fileId)
|
||||
const hash = stableHashString(content)
|
||||
entry = { content, hash, fileIdJson, projection: `${fileIdJson}:${JSON.stringify(hash)}` }
|
||||
entry = { content, hash }
|
||||
}
|
||||
entries.set(fileId, entry)
|
||||
hashByFileId.set(fileId, entry.hash)
|
||||
parts.push(entry.projection)
|
||||
}
|
||||
const next: EditorDraftHashCache = {
|
||||
source: editorDrafts,
|
||||
openFiles,
|
||||
sourceFileIds,
|
||||
entries,
|
||||
hashByFileId,
|
||||
projection: `{${parts.join(',')}}`
|
||||
hashByFileId
|
||||
}
|
||||
graphState.cachedEditorDraftHashes = next
|
||||
return next
|
||||
@@ -283,11 +288,18 @@ function getEditorDraftHashCache(
|
||||
export function buildRuntimeMobileEditorDraftsProjection(
|
||||
editorDrafts: AppState['editorDrafts'],
|
||||
openFiles: AppState['openFiles']
|
||||
): string {
|
||||
return getEditorDraftHashCache(editorDrafts, openFiles).projection
|
||||
): ReadonlyMap<string, string> {
|
||||
const drafts = new Map<string, string>()
|
||||
for (const fileId of getEditorDraftSourceFileIds(openFiles)) {
|
||||
const content = editorDrafts[fileId]
|
||||
if (content !== undefined) {
|
||||
drafts.set(fileId, content)
|
||||
}
|
||||
}
|
||||
return drafts
|
||||
}
|
||||
|
||||
/** Per-file draft version stamps for the mobile session snapshot; shares the keystroke memo. */
|
||||
/** Per-file version stamps are hashed only when a mobile snapshot is built. */
|
||||
export function getEditorDraftVersionByFileId(
|
||||
editorDrafts: AppState['editorDrafts'],
|
||||
openFiles: AppState['openFiles']
|
||||
@@ -301,4 +313,5 @@ export function resetRuntimeMobileSyncProjectionCachesForTests(): void {
|
||||
graphState.cachedBrowserWorkspacesProjection = null
|
||||
graphState.cachedBrowserPagesProjection = null
|
||||
graphState.cachedEditorDraftHashes = null
|
||||
cachedDraftSourceFileIds = null
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export type RuntimeMobileSessionSyncKey = {
|
||||
tabsProjection: string
|
||||
openFilesProjection: string
|
||||
browserProjection: string
|
||||
editorDraftsProjection: string
|
||||
editorDraftsProjection: ReadonlyMap<string, string>
|
||||
}
|
||||
|
||||
export type OpenFileByWorktreeAndId = Map<string, Map<string, AppState['openFiles'][number]>>
|
||||
@@ -79,20 +79,16 @@ export type BrowserPagesProjectionCache = {
|
||||
entries: Map<string, BrowserPagesProjectionCacheEntry>
|
||||
projection: string
|
||||
}
|
||||
/** One dirty file's FNV draft stamp plus its pre-serialized projection fragment. */
|
||||
/** One dirty file's content and last published draft stamp. */
|
||||
export type EditorDraftHashCacheEntry = {
|
||||
content: string
|
||||
hash: string
|
||||
fileIdJson: string
|
||||
projection: string
|
||||
}
|
||||
export type EditorDraftHashCache = {
|
||||
source: AppState['editorDrafts']
|
||||
openFiles: AppState['openFiles']
|
||||
sourceFileIds: ReadonlySet<string>
|
||||
entries: Map<string, EditorDraftHashCacheEntry>
|
||||
hashByFileId: Map<string, string>
|
||||
projection: string
|
||||
}
|
||||
export type MobileSessionAgentStatusByWorktree = ReadonlyMap<
|
||||
string,
|
||||
|
||||
Reference in New Issue
Block a user