Files
remanso/src/modules/repo/store/userRepo.store.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

273 lines
8.1 KiB
TypeScript

import { defineStore } from "pinia"
import { toRaw } from "vue"
import { data, generateId } from "@/data/data"
import { DataType } from "@/data/DataType.enum"
import { RepoFile } from "@/modules/repo/interfaces/RepoFile"
import { UserSettings } from "@/modules/repo/interfaces/UserSettings"
import { SavedRepo } from "@/modules/repo/models/SavedRepo"
import {
getCachedMainReadme,
getFiles,
getMainReadme,
getUserSettingsContent
} from "@/modules/repo/services/repo"
import { refreshToken } from "@/modules/user/service/signIn"
export type RepoLoadError = "auth" | "network" | null
interface State {
user: string
repo: string
files: RepoFile[]
readme?: string | null
userSettings?: UserSettings | null
needToLogin: boolean
loadError: RepoLoadError
_requestId: number
}
const classifyLoadError = (error: unknown): "auth" | "network" => {
if (error && typeof error === "object") {
const e = error as { name?: string; status?: number }
if (e.name === "TimeoutError" || e.name === "AbortError") return "network"
if (e.status === 401 || e.status === 403 || e.status === 404) return "auth"
if (typeof e.status === "number" && e.status >= 500) return "network"
}
return "network"
}
export const useUserRepoStore = defineStore("USER_REPO_STATE", {
state: (): State => ({
user: "",
repo: "",
files: [],
readme: undefined,
userSettings: undefined,
needToLogin: false,
loadError: null,
_requestId: 0
}),
actions: {
_persistLayout() {
if (!this.userSettings) return
try {
const {
chosenTitleFont,
chosenBodyFont,
chosenFontSize,
chosenFontFamily,
pageWidth
} = this.userSettings
localStorage.setItem(
`remanso:layout:${this.user}:${this.repo}`,
JSON.stringify({
chosenTitleFont,
chosenBodyFont,
chosenFontSize,
chosenFontFamily,
pageWidth
})
)
} catch {
// ignore
}
},
async setUserRepo(user: string, repo: string) {
const requestId = ++this._requestId
this.user = user
this.repo = repo
this.loadError = null
let lsLayout: Partial<UserSettings> = {}
try {
const lsRaw = localStorage.getItem(`remanso:layout:${user}:${repo}`)
if (lsRaw) lsLayout = JSON.parse(lsRaw)
} catch {
// ignore
}
if (Object.keys(lsLayout).length) {
if (!this.userSettings)
this.userSettings = { $type: DataType.UserSettings }
Object.assign(this.userSettings, lsLayout)
}
const savedRepoId = generateId(DataType.SavedRepo, `${user}-${repo}`)
const userSettingsId = `UserSetting-${user}-${repo}`
const [cachedSavedRepo, cachedUserSettings] = await Promise.all([
data.get<DataType.SavedRepo, SavedRepo>(savedRepoId),
data.get<DataType.UserSettings, UserSettings>(userSettingsId)
])
if (requestId !== this._requestId) return
if (cachedSavedRepo) {
this.files = cachedSavedRepo.files
}
if (cachedUserSettings) {
// localStorage layout choices take priority over PouchDB cache
this.userSettings = { ...cachedUserSettings, ...lsLayout }
}
try {
await refreshToken()
} catch (error) {
console.warn("impossible to refresh token", error)
}
if (requestId !== this._requestId) return
getFiles(user, repo)
.then(async (files) => {
if (requestId !== this._requestId) return
data.update<DataType.SavedRepo, SavedRepo>({
_id: savedRepoId,
$type: DataType.SavedRepo,
repo,
user,
files
})
this.files = files
return getUserSettingsContent(user, repo, files)
})
.then((userSettings) => {
if (requestId !== this._requestId) return
const chosenFontFamily = userSettings?.fontFamilies?.find(
(font) => font === this.userSettings?.chosenFontFamily
)
? this.userSettings?.chosenFontFamily
: userSettings?.fontFamily
const chosenFontSize =
this.userSettings?.chosenFontSize ?? userSettings?.fontSize
const chosenTitleFont =
this.userSettings?.chosenTitleFont ??
userSettings?.chosenTitleFont ??
chosenFontFamily
const chosenBodyFont =
this.userSettings?.chosenBodyFont ??
userSettings?.chosenBodyFont ??
chosenFontFamily
if (userSettings) {
this.userSettings = userSettings
} else if (!this.userSettings) {
this.userSettings = { $type: DataType.UserSettings }
}
this.userSettings.chosenFontFamily =
chosenFontFamily ?? this.userSettings.fontFamily
this.userSettings.chosenFontSize =
chosenFontSize ?? this.userSettings.fontSize
this.userSettings.chosenTitleFont = chosenTitleFont
this.userSettings.chosenBodyFont = chosenBodyFont
this._persistLayout()
// Persist only repo config fields — chosen* are localStorage-only
const {
chosenTitleFont: _t,
chosenBodyFont: _b,
chosenFontSize: _s,
chosenFontFamily: _f,
...repoConfig
} = toRaw(this.userSettings)
data.update<DataType.UserSettings, UserSettings>({
...repoConfig,
_id: userSettingsId
})
})
.catch((error) => {
if (requestId !== this._requestId) return
console.warn("getFiles failed", error)
this.loadError = classifyLoadError(error)
})
getCachedMainReadme(user, repo)
.then(async (cachedReadme) => {
if (requestId !== this._requestId) return
if (cachedReadme) this.readme = cachedReadme
const fetched = await getMainReadme(user, repo)
if (requestId !== this._requestId) return
this.readme = fetched
})
.catch((error) => {
if (requestId !== this._requestId) return
console.warn("getMainReadme failed", error)
// Only surface the error UI if we have nothing cached to display.
if (!this.readme) {
this.readme = null
this.loadError = classifyLoadError(error)
}
})
},
addFile(file: RepoFile) {
if (!file.sha) {
return
}
const doesFileExist = this.files.some((f) => f.sha === file.sha)
if (doesFileExist) {
return
}
const savedRepoId = generateId(
DataType.SavedRepo,
`${this.user}-${this.repo}`
)
const newFiles = [
...toRaw(this.files).filter((f) => f.sha !== file.sha),
toRaw(file)
]
data.update<DataType.SavedRepo, SavedRepo>({
_id: savedRepoId,
$type: DataType.SavedRepo,
repo: this.repo,
user: this.user,
files: newFiles
})
this.files = newFiles
},
resetUserRepo() {
this.user = ""
this.repo = ""
this.resetFiles()
this.userSettings = undefined
},
resetFiles() {
this.files = []
this.readme = null
},
setFontFamily(fontFamily: string) {
if (!this.userSettings) {
this.userSettings = { $type: DataType.UserSettings }
}
this.userSettings.chosenFontFamily = fontFamily
this._persistLayout()
},
setFontSize(fontSize: string) {
if (!this.userSettings) {
this.userSettings = { $type: DataType.UserSettings }
}
this.userSettings.chosenFontSize = fontSize
this._persistLayout()
},
setTitleFont(font: string) {
if (!this.userSettings) {
this.userSettings = { $type: DataType.UserSettings }
}
this.userSettings.chosenTitleFont = font
this._persistLayout()
},
setBodyFont(font: string) {
if (!this.userSettings) {
this.userSettings = { $type: DataType.UserSettings }
}
this.userSettings.chosenBodyFont = font
this._persistLayout()
}
}
})