diff options
Diffstat (limited to 'packages/app/src/utils')
| -rw-r--r-- | packages/app/src/utils/persist.ts | 72 | ||||
| -rw-r--r-- | packages/app/src/utils/speech.ts | 39 | ||||
| -rw-r--r-- | packages/app/src/utils/worktree.ts | 49 |
3 files changed, 123 insertions, 37 deletions
diff --git a/packages/app/src/utils/persist.ts b/packages/app/src/utils/persist.ts index 129695f86..0ca3abad0 100644 --- a/packages/app/src/utils/persist.ts +++ b/packages/app/src/utils/persist.ts @@ -18,7 +18,52 @@ const LEGACY_STORAGE = "default.dat" const GLOBAL_STORAGE = "opencode.global.dat" const LOCAL_PREFIX = "opencode." const fallback = { disabled: false } -const cache = new Map<string, string>() + +const CACHE_MAX_ENTRIES = 500 +const CACHE_MAX_BYTES = 8 * 1024 * 1024 + +type CacheEntry = { value: string; bytes: number } +const cache = new Map<string, CacheEntry>() +const cacheTotal = { bytes: 0 } + +function cacheDelete(key: string) { + const entry = cache.get(key) + if (!entry) return + cacheTotal.bytes -= entry.bytes + cache.delete(key) +} + +function cachePrune() { + for (;;) { + if (cache.size <= CACHE_MAX_ENTRIES && cacheTotal.bytes <= CACHE_MAX_BYTES) return + const oldest = cache.keys().next().value as string | undefined + if (!oldest) return + cacheDelete(oldest) + } +} + +function cacheSet(key: string, value: string) { + const bytes = value.length * 2 + if (bytes > CACHE_MAX_BYTES) { + cacheDelete(key) + return + } + + const entry = cache.get(key) + if (entry) cacheTotal.bytes -= entry.bytes + cache.delete(key) + cache.set(key, { value, bytes }) + cacheTotal.bytes += bytes + cachePrune() +} + +function cacheGet(key: string) { + const entry = cache.get(key) + if (!entry) return + cache.delete(key) + cache.set(key, entry) + return entry.value +} function quota(error: unknown) { if (error instanceof DOMException) { @@ -63,9 +108,11 @@ function evict(storage: Storage, keep: string, value: string) { for (const item of items) { storage.removeItem(item.key) + cacheDelete(item.key) try { storage.setItem(keep, value) + cacheSet(keep, value) return true } catch (error) { if (!quota(error)) throw error @@ -78,6 +125,7 @@ function evict(storage: Storage, keep: string, value: string) { function write(storage: Storage, key: string, value: string) { try { storage.setItem(key, value) + cacheSet(key, value) return true } catch (error) { if (!quota(error)) throw error @@ -85,13 +133,17 @@ function write(storage: Storage, key: string, value: string) { try { storage.removeItem(key) + cacheDelete(key) storage.setItem(key, value) + cacheSet(key, value) return true } catch (error) { if (!quota(error)) throw error } - return evict(storage, key, value) + const ok = evict(storage, key, value) + if (!ok) cacheSet(key, value) + return ok } function snapshot(value: unknown) { @@ -148,7 +200,7 @@ function localStorageWithPrefix(prefix: string): SyncStorage { return { getItem: (key) => { const name = item(key) - const cached = cache.get(name) + const cached = cacheGet(name) if (fallback.disabled && cached !== undefined) return cached const stored = (() => { @@ -160,12 +212,12 @@ function localStorageWithPrefix(prefix: string): SyncStorage { } })() if (stored === null) return cached ?? null - cache.set(name, stored) + cacheSet(name, stored) return stored }, setItem: (key, value) => { const name = item(key) - cache.set(name, value) + cacheSet(name, value) if (fallback.disabled) return try { if (write(localStorage, name, value)) return @@ -177,7 +229,7 @@ function localStorageWithPrefix(prefix: string): SyncStorage { }, removeItem: (key) => { const name = item(key) - cache.delete(name) + cacheDelete(name) if (fallback.disabled) return try { localStorage.removeItem(name) @@ -191,7 +243,7 @@ function localStorageWithPrefix(prefix: string): SyncStorage { function localStorageDirect(): SyncStorage { return { getItem: (key) => { - const cached = cache.get(key) + const cached = cacheGet(key) if (fallback.disabled && cached !== undefined) return cached const stored = (() => { @@ -203,11 +255,11 @@ function localStorageDirect(): SyncStorage { } })() if (stored === null) return cached ?? null - cache.set(key, stored) + cacheSet(key, stored) return stored }, setItem: (key, value) => { - cache.set(key, value) + cacheSet(key, value) if (fallback.disabled) return try { if (write(localStorage, key, value)) return @@ -218,7 +270,7 @@ function localStorageDirect(): SyncStorage { fallback.disabled = true }, removeItem: (key) => { - cache.delete(key) + cacheDelete(key) if (fallback.disabled) return try { localStorage.removeItem(key) diff --git a/packages/app/src/utils/speech.ts b/packages/app/src/utils/speech.ts index c8acf5241..201c1261b 100644 --- a/packages/app/src/utils/speech.ts +++ b/packages/app/src/utils/speech.ts @@ -78,6 +78,7 @@ export function createSpeechRecognition(opts?: { let lastInterimSuffix = "" let shrinkCandidate: string | undefined let commitTimer: number | undefined + let restartTimer: number | undefined const cancelPendingCommit = () => { if (commitTimer === undefined) return @@ -85,6 +86,26 @@ export function createSpeechRecognition(opts?: { commitTimer = undefined } + const clearRestart = () => { + if (restartTimer === undefined) return + window.clearTimeout(restartTimer) + restartTimer = undefined + } + + const scheduleRestart = () => { + clearRestart() + if (!shouldContinue) return + if (!recognition) return + restartTimer = window.setTimeout(() => { + restartTimer = undefined + if (!shouldContinue) return + if (!recognition) return + try { + recognition.start() + } catch {} + }, 150) + } + const commitSegment = (segment: string) => { const nextCommitted = appendSegment(committedText, segment) if (nextCommitted === committedText) return @@ -214,17 +235,14 @@ export function createSpeechRecognition(opts?: { } recognition.onerror = (e: { error: string }) => { + clearRestart() cancelPendingCommit() lastInterimSuffix = "" shrinkCandidate = undefined if (e.error === "no-speech" && shouldContinue) { setStore("interim", "") if (opts?.onInterim) opts.onInterim("") - setTimeout(() => { - try { - recognition?.start() - } catch {} - }, 150) + scheduleRestart() return } shouldContinue = false @@ -232,6 +250,7 @@ export function createSpeechRecognition(opts?: { } recognition.onstart = () => { + clearRestart() sessionCommitted = "" pendingHypothesis = "" cancelPendingCommit() @@ -243,22 +262,20 @@ export function createSpeechRecognition(opts?: { } recognition.onend = () => { + clearRestart() cancelPendingCommit() lastInterimSuffix = "" shrinkCandidate = undefined setStore("isRecording", false) if (shouldContinue) { - setTimeout(() => { - try { - recognition?.start() - } catch {} - }, 150) + scheduleRestart() } } } const start = () => { if (!recognition) return + clearRestart() shouldContinue = true sessionCommitted = "" pendingHypothesis = "" @@ -274,6 +291,7 @@ export function createSpeechRecognition(opts?: { const stop = () => { if (!recognition) return shouldContinue = false + clearRestart() promotePending() cancelPendingCommit() lastInterimSuffix = "" @@ -287,6 +305,7 @@ export function createSpeechRecognition(opts?: { onCleanup(() => { shouldContinue = false + clearRestart() promotePending() cancelPendingCommit() lastInterimSuffix = "" diff --git a/packages/app/src/utils/worktree.ts b/packages/app/src/utils/worktree.ts index 7c0055920..581afd553 100644 --- a/packages/app/src/utils/worktree.ts +++ b/packages/app/src/utils/worktree.ts @@ -13,7 +13,21 @@ type State = } const state = new Map<string, State>() -const waiters = new Map<string, Array<(state: State) => void>>() +const waiters = new Map< + string, + { + promise: Promise<State> + resolve: (state: State) => void + } +>() + +function deferred() { + const box = { resolve: (_: State) => {} } + const promise = new Promise<State>((resolve) => { + box.resolve = resolve + }) + return { promise, resolve: box.resolve } +} export const Worktree = { get(directory: string) { @@ -27,32 +41,33 @@ export const Worktree = { }, ready(directory: string) { const key = normalize(directory) - state.set(key, { status: "ready" }) - const list = waiters.get(key) - if (!list) return + const next = { status: "ready" } as const + state.set(key, next) + const waiter = waiters.get(key) + if (!waiter) return waiters.delete(key) - for (const fn of list) fn({ status: "ready" }) + waiter.resolve(next) }, failed(directory: string, message: string) { const key = normalize(directory) - state.set(key, { status: "failed", message }) - const list = waiters.get(key) - if (!list) return + const next = { status: "failed", message } as const + state.set(key, next) + const waiter = waiters.get(key) + if (!waiter) return waiters.delete(key) - for (const fn of list) fn({ status: "failed", message }) + waiter.resolve(next) }, wait(directory: string) { const key = normalize(directory) const current = state.get(key) if (current && current.status !== "pending") return Promise.resolve(current) - return new Promise<State>((resolve) => { - const list = waiters.get(key) - if (!list) { - waiters.set(key, [resolve]) - return - } - list.push(resolve) - }) + const existing = waiters.get(key) + if (existing) return existing.promise + + const waiter = deferred() + + waiters.set(key, waiter) + return waiter.promise }, } |
