Files
remanso/src/modules/repo/services/repo.ts
Julien Calixte fc4ed188d7 feat(repo): add Octokit request timeouts and retry UI
Before, every GitHub call awaited indefinitely with no AbortSignal,
so a "lie-fi" connection (technically online, effectively dead) left
FluxNote stuck on the skeleton loader and the freshness badge spinning
forever. Inject an 8s AbortSignal.timeout via octokit.hook.before
(20s override on the recursive tree fetch), classify failures in the
userRepo store as auth vs network, and surface a "Couldn't reach
GitHub" retry button when no cached content is available.
2026-05-16 23:28:11 +02:00

149 lines
3.3 KiB
TypeScript

import { markdownBuilder } from "@/hooks/useMarkdown.hook"
import { prepareNoteCache } from "@/modules/note/cache/prepareNoteCache"
import { RepoFile } from "@/modules/repo/interfaces/RepoFile"
import { UserSettings } from "@/modules/repo/interfaces/UserSettings"
import { getOctokit } from "@/modules/repo/services/octo"
export const getFiles = async (
owner: string,
repo: string
): Promise<RepoFile[]> => {
if (!owner || !repo) {
return []
}
const octokit = await getOctokit()
const commits = await octokit.request("GET /repos/{owner}/{repo}/commits", {
owner,
repo
})
const lastCommit = commits.data.shift()
if (!lastCommit) {
return []
}
const treeResponse = await octokit.request(
"GET /repos/{owner}/{repo}/git/trees/{tree_sha}",
{
owner,
repo,
tree_sha: lastCommit.commit.tree.sha,
recursive: "true",
request: { signal: AbortSignal.timeout(20_000) }
}
)
return treeResponse?.data.tree.filter((t) => t.type === "blob") ?? []
}
export const getCachedMainReadme = async (owner: string, repo: string) => {
if (!owner || !repo) {
return null
}
const { render } = markdownBuilder()
const { getCachedNote } = prepareNoteCache(`${owner}-${repo}-README`)
const { note: cachedReadme } = await getCachedNote()
if (!cachedReadme) {
return null
}
return render(cachedReadme.content)
}
export const getMainReadme = async (owner: string, repo: string) => {
if (!owner || !repo) {
return null
}
const { render } = markdownBuilder()
const { getCachedNote, saveCacheNote } = prepareNoteCache(
`${owner}-${repo}-README`
)
try {
const octokit = await getOctokit()
const README = await octokit.repos.getReadme({
owner,
repo
})
if (README) {
saveCacheNote(README.data.content)
return render(README.data.content)
}
} catch (error) {
console.warn(error)
const { note: cachedReadme } = await getCachedNote()
if (cachedReadme) {
return render(cachedReadme.content)
}
// No cached fallback — surface the error so the caller can classify
// network/timeout failures vs auth/404 and tailor the UI accordingly.
throw error
}
return null
}
export const getUserSettingsContent = async (
user: string,
repo: string,
files: RepoFile[]
): Promise<Omit<UserSettings, "chosenFontFamily"> | null> => {
const configFile = files.find((file) => file.path === ".remanso.json")
if (!configFile?.sha) {
return null
}
const content = await queryFileContent(user, repo, configFile.sha)
if (!content) {
return null
}
const raw = JSON.parse(atob(content)) as UserSettings & {
t?: string
p?: string
}
const { t, p, ...rest } = raw
return {
...rest,
chosenTitleFont: t,
chosenBodyFont: p
}
}
export const queryFileContent = async (
user: string,
repo: string,
sha: string
) => {
if (!user || !repo) {
return null
}
try {
const octokit = await getOctokit()
const file = await octokit.request(
"GET /repos/{owner}/{repo}/git/blobs/{file_sha}",
{
owner: user,
repo: repo,
file_sha: sha
}
)
return file?.data.content ?? null
} catch (error) {
console.warn("queryFileContent failed", { user, repo, sha, error })
return null
}
}