Files
remanso/src/hooks/useOfflineNotes.hook.ts
Julien Calixte 2994839ee5 feat(offline): track structured failure details during caching
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.
2026-05-17 00:07:13 +02:00

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
}
}