Files
ghatreh_user/src/config/load-version.ts
T
mohmdsdgandCursor a9d0cba909 feat: load program-settings from SuperAdmin with 3-host failover
Use /api/public/program-settings only and retry montzar/montech/montest
three rounds before showing the bootstrap error.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-18 16:10:35 +03:30

190 lines
4.9 KiB
TypeScript

import {
allowSuperAdminFallback,
getSuperAdminBases,
isSuperAdminUrl,
resolveTenantDomain,
SA_RETRY_ROUNDS,
} from '@/config/superadmin'
type VersionResponse =
| string
| number
| {
version?: string | number
data?: string | number | { version?: string | number; value?: string | number }
value?: string | number
user?: unknown
}
const VERSION_SUCCESS_URL_KEY = 'VERSION_Success_URL'
export const VERSION_RETRY_ROUNDS = SA_RETRY_ROUNDS
let versionFetchPromise: Promise<string | null> | null = null
function getProtocol() {
return window.location.protocol === 'https:' ? 'https:' : 'http:'
}
function getVersionUrls() {
const protocol = getProtocol()
const domain = encodeURIComponent(resolveTenantDomain())
const urls = getSuperAdminBases().map((base) => `${base}/get_version?domain=${domain}`)
if (allowSuperAdminFallback()) {
urls.push(
'https://tablo.talasevin.ir/get_version',
'https://abshode-service.montgit.ir/server/get_version',
`${protocol}//212.23.201.173:8381/get_version`,
`${protocol}//91.212.174.137:4040/server/get_version`,
)
}
return urls
}
function sortVersionUrls(urls: string[]) {
const lastSuccessUrl = localStorage.getItem(VERSION_SUCCESS_URL_KEY)
if (
!lastSuccessUrl ||
!urls.includes(lastSuccessUrl) ||
!isSuperAdminUrl(lastSuccessUrl)
) {
if (lastSuccessUrl && !isSuperAdminUrl(lastSuccessUrl)) {
localStorage.removeItem(VERSION_SUCCESS_URL_KEY)
}
return [...urls]
}
return [lastSuccessUrl, ...urls.filter((url) => url !== lastSuccessUrl)]
}
function clearStaleVersionSuccessUrl(url: string) {
if (localStorage.getItem(VERSION_SUCCESS_URL_KEY) === url) {
localStorage.removeItem(VERSION_SUCCESS_URL_KEY)
}
}
function readUserVersion(user: unknown) {
if (!user || typeof user !== 'object' || Array.isArray(user)) {
return ''
}
const record = user as { version?: string | number; value?: string | number }
const version = record.version ?? record.value
return version === undefined || version === null ? '' : String(version).trim()
}
function normalizeVersionResponse(response: VersionResponse) {
if (typeof response === 'string' || typeof response === 'number') {
return String(response).trim()
}
if (!response) return ''
if (typeof response.data === 'string' || typeof response.data === 'number') {
return String(response.data).trim()
}
const directVersion =
response.version ??
response.value ??
(typeof response.data === 'object' ? response.data?.version ?? response.data?.value : undefined)
if (directVersion !== undefined && directVersion !== null) {
return String(directVersion).trim()
}
return readUserVersion(response.user)
}
async function fetchVersion(url: string, timeout = 3500): Promise<string | null> {
const controller = new AbortController()
const timeoutId = window.setTimeout(() => controller.abort(), timeout)
try {
const response = await fetch(url, {
method: 'GET',
headers: {
Accept: 'application/json',
},
cache: 'no-store',
signal: controller.signal,
})
if (!response.ok) {
throw new Error(`Version request failed with status ${response.status}`)
}
const contentType = response.headers.get('content-type') ?? ''
if (!contentType.includes('application/json')) {
throw new Error('Version response is not JSON')
}
const data = (await response.json()) as VersionResponse
const version = normalizeVersionResponse(data)
if (!version) {
throw new Error('Version response is empty')
}
if (isSuperAdminUrl(url)) {
localStorage.setItem(VERSION_SUCCESS_URL_KEY, url)
} else {
clearStaleVersionSuccessUrl(url)
}
return version
} finally {
window.clearTimeout(timeoutId)
}
}
async function fetchVersionWithRetry(rounds = VERSION_RETRY_ROUNDS): Promise<string | null> {
const urls = sortVersionUrls(getVersionUrls())
for (let round = 1; round <= rounds; round++) {
for (let index = 0; index < urls.length; index++) {
const url = urls[index]
const timeout = index === urls.length - 1 ? 6000 : 3500
try {
const version = await fetchVersion(url, timeout)
if (version) {
return version
}
} catch (error) {
clearStaleVersionSuccessUrl(url)
console.warn(`Version failed from ${url}. Round: ${round}`, error)
}
}
}
return null
}
export function resetVersionCheckCache() {
versionFetchPromise = null
}
export async function loadRemoteAppVersion(options?: {
rounds?: number
force?: boolean
}): Promise<string | null> {
const rounds = options?.rounds ?? VERSION_RETRY_ROUNDS
const force = options?.force ?? false
if (force) {
resetVersionCheckCache()
}
if (versionFetchPromise) {
return versionFetchPromise
}
versionFetchPromise = fetchVersionWithRetry(rounds).finally(() => {
versionFetchPromise = null
})
return versionFetchPromise
}