Skip to content

Commit 68d186c

Browse files
committed
Clean up LightningFS on failed clone and eviction
- Wipe partial git objects on clone failure so retries start clean instead of reading corrupt pack data - Delete LightningFS IndexedDB databases when repos are deleted, evicted, or cleared from the results cache - Extract shared repoToFsName helper in url.ts
1 parent a0386ca commit 68d186c

3 files changed

Lines changed: 47 additions & 13 deletions

File tree

src/lib/cache.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { openDB, type IDBPDatabase } from 'idb'
22
import type { AnalysisResult } from './types'
3+
import { repoUrlToFsName } from './url'
34

45
const dbName = 'git-strata'
56
const dbVersion = 2
@@ -57,6 +58,15 @@ const notifyCacheChange = () => {
5758
}
5859
}
5960

61+
/** Delete the LightningFS IndexedDB database for a repo. Fire-and-forget. */
62+
const deleteLfsDatabase = (repoUrl: string): void => {
63+
try {
64+
indexedDB.deleteDatabase(repoUrlToFsName(repoUrl))
65+
} catch {
66+
// Invalid URL or indexedDB unavailable — nothing to clean up
67+
}
68+
}
69+
6070
const estimateSize = (value: unknown): number => {
6171
try {
6272
return new Blob([JSON.stringify(value)]).size
@@ -144,15 +154,19 @@ export const deleteRepo = async (repoUrl: string): Promise<void> => {
144154
tx.objectStore(storeName).delete(repoUrl)
145155
tx.objectStore(metaStoreName).delete(repoUrl)
146156
await tx.done
157+
deleteLfsDatabase(repoUrl)
147158
notifyCacheChange()
148159
}
149160

150161
export const clearAll = async (): Promise<void> => {
151162
const db = await getDb()
163+
// Read all repo URLs before clearing so we can delete their LightningFS databases
164+
const metas = (await db.getAll(metaStoreName)) as CachedRepoInfo[]
152165
const tx = db.transaction([storeName, metaStoreName], 'readwrite')
153166
tx.objectStore(storeName).clear()
154167
tx.objectStore(metaStoreName).clear()
155168
await tx.done
169+
for (const meta of metas) deleteLfsDatabase(meta.repoUrl)
156170
notifyCacheChange()
157171
}
158172

@@ -187,5 +201,6 @@ const evictIfNeeded = async (db: IDBPDatabase, neededBytes: number, excludeUrl:
187201
tx.objectStore(metaStoreName).delete(url)
188202
}
189203
await tx.done
204+
for (const url of toDelete) deleteLfsDatabase(url)
190205
}
191206
}

src/lib/url.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,13 @@ export const parseRepoUrl = (input: string): ParsedRepo => {
6969
export const repoToDir = (parsed: ParsedRepo): string => {
7070
return `/${parsed.host}/${parsed.owner}/${parsed.repo}`
7171
}
72+
73+
/** LightningFS IndexedDB database name for a parsed repo. */
74+
export const repoToFsName = (parsed: ParsedRepo): string => {
75+
return `git-strata-${parsed.host}-${parsed.owner}-${parsed.repo}`
76+
}
77+
78+
/** LightningFS IndexedDB database name from a normalized repo URL (like https://github.com/owner/repo). */
79+
export const repoUrlToFsName = (repoUrl: string): string => {
80+
return repoToFsName(parseRepoUrl(repoUrl))
81+
}

src/lib/worker/analyzer.worker.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { cloneRepo, detectDefaultBranch, fetchRepo, waitForBodyCleanups } from '
1111
import { fillDateGaps, getCommitsByDate, type DailyCommit } from '../git/history'
1212
import { countLinesForCommit, countLinesForCommitIncremental, LruMap } from '../git/count'
1313
import type { FileState } from '../git/count'
14-
import { parseRepoUrl, repoToDir } from '../url'
14+
import { parseRepoUrl, repoToDir, repoToFsName } from '../url'
1515

1616
// Configure LogTape for the worker context
1717
configureSync({
@@ -47,7 +47,7 @@ const classifyError = (error: unknown): { message: string; kind: ErrorKind } =>
4747
) {
4848
if (lower.includes('network') || lower.includes('offline') || lower.includes('lost'))
4949
return {
50-
message: 'Network connection lost. Your partial download is saved — reconnect and try again.',
50+
message: 'Network connection lost. Reconnect and try again.',
5151
kind: 'network-lost',
5252
}
5353
return {
@@ -191,7 +191,7 @@ const analyzerApi = {
191191
const dir = repoToDir(parsed)
192192

193193
// Initialize lightning-fs with a unique name for persistence
194-
const fsName = `git-strata-${parsed.host}-${parsed.owner}-${parsed.repo}`
194+
const fsName = repoToFsName(parsed)
195195
const fs = new LightningFS(fsName)
196196

197197
try {
@@ -216,15 +216,24 @@ const analyzerApi = {
216216
url: parsed.url,
217217
branch: defaultBranch,
218218
})
219-
await cloneRepo({
220-
fs,
221-
dir,
222-
url: parsed.url,
223-
corsProxy,
224-
defaultBranch,
225-
onProgress,
226-
signal,
227-
})
219+
try {
220+
await cloneRepo({
221+
fs,
222+
dir,
223+
url: parsed.url,
224+
corsProxy,
225+
defaultBranch,
226+
onProgress,
227+
signal,
228+
})
229+
} catch (cloneError) {
230+
// Wipe partially-written git objects so the next retry starts clean.
231+
// Without this, a failed clone leaves corrupt data in IndexedDB and
232+
// subsequent attempts read half-written pack files.
233+
logger.warning('Clone failed, wiping LightningFS database "{fsName}"', { fsName })
234+
new LightningFS(fsName, { wipe: true })
235+
throw cloneError
236+
}
228237
logger.info('Clone complete')
229238

230239
if (signal.aborted) {
@@ -329,7 +338,7 @@ const analyzerApi = {
329338
const signal = abortController.signal
330339
const parsed = parseRepoUrl(repoInput)
331340
const dir = repoToDir(parsed)
332-
const fsName = `git-strata-${parsed.host}-${parsed.owner}-${parsed.repo}`
341+
const fsName = repoToFsName(parsed)
333342
const fs = new LightningFS(fsName)
334343
const defaultBranch = cachedResult.defaultBranch
335344

0 commit comments

Comments
 (0)