Replace the failure counter with a list of typed failures (fetch, build, save, readme, unknown) carrying path, sha, message, and affected doc ids so callers can surface diagnostics instead of just a count.
171 lines
4.3 KiB
TypeScript
171 lines
4.3 KiB
TypeScript
import { useAsyncState } from "@vueuse/core"
|
|
import { computed, ref } from "vue"
|
|
|
|
import { data, generateId } from "@/data/data"
|
|
import { DataType } from "@/data/DataType.enum"
|
|
import { buildNoteDocs } from "@/modules/note/cache/prepareNoteCache"
|
|
import { Note } from "@/modules/note/models/Note"
|
|
import { getMainReadme, queryFileContent } from "@/modules/repo/services/repo"
|
|
import { useUserRepoStore } from "@/modules/repo/store/userRepo.store"
|
|
|
|
const CONCURRENCY = 8
|
|
const BULK_FLUSH_SIZE = 50
|
|
|
|
export type CacheFailureKind =
|
|
| "fetch"
|
|
| "build"
|
|
| "save"
|
|
| "readme"
|
|
| "unknown"
|
|
|
|
export type CacheFailure = {
|
|
kind: CacheFailureKind
|
|
path?: string
|
|
sha?: string
|
|
message?: string
|
|
docIds?: string[]
|
|
}
|
|
|
|
const describeError = (error: unknown): string => {
|
|
if (error instanceof Error) return error.message
|
|
return String(error)
|
|
}
|
|
|
|
const runWithConcurrency = async <T>(
|
|
items: T[],
|
|
limit: number,
|
|
worker: (item: T) => Promise<void>
|
|
) => {
|
|
let cursor = 0
|
|
const next = async (): Promise<void> => {
|
|
const i = cursor++
|
|
if (i >= items.length) return
|
|
await worker(items[i])
|
|
return next()
|
|
}
|
|
await Promise.all(
|
|
Array.from({ length: Math.min(limit, items.length) }, next)
|
|
)
|
|
}
|
|
|
|
export const useOfflineNotes = () => {
|
|
const store = useUserRepoStore()
|
|
const totalOfNotes = computed(() => store.files.length)
|
|
|
|
const noteCompleted = ref(0)
|
|
const failures = ref<CacheFailure[]>([])
|
|
const failedNotes = computed(() => failures.value.length)
|
|
|
|
const recordFailure = (failure: CacheFailure) => {
|
|
failures.value.push(failure)
|
|
console.warn("[offline-cache] failure", failure)
|
|
}
|
|
|
|
const cacheAllNotes = async () => {
|
|
const isInitialized = store.user && store.repo && totalOfNotes.value > 0
|
|
|
|
if (!isInitialized) {
|
|
return
|
|
}
|
|
|
|
const cachedNotesFromSha = await data.getAll<DataType.Note, Note>({
|
|
prefix: DataType.Note,
|
|
keys: store.files.map((file) => file.sha).filter(Boolean) as string[],
|
|
includeDocs: false
|
|
})
|
|
|
|
const cachedNotesSet = new Set(cachedNotesFromSha.map((note) => note._id))
|
|
|
|
const filesToFetch = store.files.filter(
|
|
(file) =>
|
|
file.sha && !cachedNotesSet.has(generateId(DataType.Note, file.sha))
|
|
)
|
|
|
|
noteCompleted.value = store.files.length - filesToFetch.length
|
|
failures.value = []
|
|
|
|
const pendingDocs: Note[] = []
|
|
const flush = async () => {
|
|
if (!pendingDocs.length) return
|
|
const batch = pendingDocs.splice(0, pendingDocs.length)
|
|
try {
|
|
await data.bulkUpdate(batch)
|
|
} catch (error) {
|
|
recordFailure({
|
|
kind: "save",
|
|
message: describeError(error),
|
|
docIds: batch
|
|
.map((doc) => doc._id)
|
|
.filter((id): id is string => Boolean(id))
|
|
})
|
|
}
|
|
}
|
|
|
|
const fetchWork = runWithConcurrency(
|
|
filesToFetch,
|
|
CONCURRENCY,
|
|
async (file) => {
|
|
try {
|
|
const content = await queryFileContent(
|
|
store.user,
|
|
store.repo,
|
|
file.sha!
|
|
)
|
|
if (content === null) {
|
|
recordFailure({
|
|
kind: "fetch",
|
|
path: file.path,
|
|
sha: file.sha!
|
|
})
|
|
return
|
|
}
|
|
let docs: Note[]
|
|
try {
|
|
docs = buildNoteDocs(file.sha!, file.path, content)
|
|
} catch (error) {
|
|
recordFailure({
|
|
kind: "build",
|
|
path: file.path,
|
|
sha: file.sha!,
|
|
message: describeError(error)
|
|
})
|
|
return
|
|
}
|
|
pendingDocs.push(...docs)
|
|
if (pendingDocs.length >= BULK_FLUSH_SIZE) {
|
|
await flush()
|
|
}
|
|
} catch (error) {
|
|
recordFailure({
|
|
kind: "unknown",
|
|
path: file.path,
|
|
sha: file.sha,
|
|
message: describeError(error)
|
|
})
|
|
} finally {
|
|
noteCompleted.value++
|
|
}
|
|
}
|
|
)
|
|
|
|
const readmeWork = getMainReadme(store.user, store.repo).catch((error) => {
|
|
recordFailure({ kind: "readme", message: describeError(error) })
|
|
})
|
|
|
|
await Promise.all([fetchWork, readmeWork])
|
|
await flush()
|
|
}
|
|
const { execute, isLoading } = useAsyncState(cacheAllNotes, null, {
|
|
immediate: false
|
|
})
|
|
|
|
return {
|
|
cacheAllNotes: execute,
|
|
isLoading,
|
|
totalOfNotes,
|
|
noteCompleted,
|
|
failedNotes,
|
|
failures
|
|
}
|
|
}
|