2026-09-02 08:26:52 -04:00
|
|
|
import {
|
|
|
|
|
allowSuperAdminFallback,
|
|
|
|
|
getSuperAdminBases,
|
|
|
|
|
isSuperAdminUrl,
|
|
|
|
|
resolveTenantDomain,
|
|
|
|
|
SA_RETRY_ROUNDS,
|
|
|
|
|
} from '@/config/superadmin'
|
|
|
|
|
|
2026-06-16 19:47:28 +03:30
|
|
|
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'
|
2026-09-02 08:26:52 -04:00
|
|
|
export const VERSION_RETRY_ROUNDS = SA_RETRY_ROUNDS
|
2026-06-16 19:47:28 +03:30
|
|
|
|
|
|
|
|
let versionFetchPromise: Promise<string | null> | null = null
|
2026-07-01 22:28:25 +03:30
|
|
|
|
2026-09-02 08:26:52 -04:00
|
|
|
/** فقط SuperAdmin — بدون رجیستری قدیمی */
|
2026-06-16 19:47:28 +03:30
|
|
|
function getVersionUrls() {
|
2026-09-02 08:26:52 -04:00
|
|
|
const domain = encodeURIComponent(resolveTenantDomain())
|
|
|
|
|
return getSuperAdminBases().map((base) => `${base}/get_version?domain=${domain}`)
|
2026-06-16 19:47:28 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function sortVersionUrls(urls: string[]) {
|
|
|
|
|
const lastSuccessUrl = localStorage.getItem(VERSION_SUCCESS_URL_KEY)
|
2026-09-02 08:26:52 -04:00
|
|
|
if (!lastSuccessUrl || !urls.includes(lastSuccessUrl) || !isSuperAdminUrl(lastSuccessUrl)) {
|
|
|
|
|
if (lastSuccessUrl && !isSuperAdminUrl(lastSuccessUrl)) {
|
|
|
|
|
localStorage.removeItem(VERSION_SUCCESS_URL_KEY)
|
|
|
|
|
}
|
|
|
|
|
return [...urls]
|
2026-06-16 19:47:28 +03:30
|
|
|
}
|
2026-09-02 08:26:52 -04:00
|
|
|
return [lastSuccessUrl, ...urls.filter((url) => url !== lastSuccessUrl)]
|
2026-06-16 19:47:28 +03:30
|
|
|
}
|
|
|
|
|
|
2026-07-01 22:28:25 +03:30
|
|
|
function clearStaleVersionSuccessUrl(url: string) {
|
|
|
|
|
if (localStorage.getItem(VERSION_SUCCESS_URL_KEY) === url) {
|
|
|
|
|
localStorage.removeItem(VERSION_SUCCESS_URL_KEY)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 19:47:28 +03:30
|
|
|
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: {
|
2026-09-02 08:26:52 -04:00
|
|
|
Accept: 'application/json',
|
2026-06-16 19:47:28 +03:30
|
|
|
},
|
|
|
|
|
cache: 'no-store',
|
2026-09-02 08:26:52 -04:00
|
|
|
signal: controller.signal,
|
2026-06-16 19:47:28 +03:30
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(`Version request failed with status ${response.status}`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const contentType = response.headers.get('content-type') ?? ''
|
2026-09-02 08:26:52 -04:00
|
|
|
if (!contentType.includes('application/json')) {
|
|
|
|
|
throw new Error('Version response is not JSON')
|
|
|
|
|
}
|
2026-06-16 19:47:28 +03:30
|
|
|
|
2026-09-02 08:26:52 -04:00
|
|
|
const data = (await response.json()) as VersionResponse
|
2026-06-16 19:47:28 +03:30
|
|
|
|
|
|
|
|
const version = normalizeVersionResponse(data)
|
|
|
|
|
|
|
|
|
|
if (!version) {
|
2026-07-01 22:28:25 +03:30
|
|
|
throw new Error('Version response is empty')
|
2026-06-16 19:47:28 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
localStorage.setItem(VERSION_SUCCESS_URL_KEY, url)
|
|
|
|
|
return version
|
|
|
|
|
} finally {
|
|
|
|
|
window.clearTimeout(timeoutId)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-02 08:26:52 -04:00
|
|
|
async function tryFetchVersionFromUrls(
|
|
|
|
|
urls: string[],
|
|
|
|
|
rounds: number,
|
|
|
|
|
): Promise<string | null> {
|
|
|
|
|
if (!urls.length) return null
|
2026-06-16 19:47:28 +03:30
|
|
|
|
|
|
|
|
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)
|
2026-07-01 22:28:25 +03:30
|
|
|
if (version) {
|
|
|
|
|
return version
|
|
|
|
|
}
|
2026-06-16 19:47:28 +03:30
|
|
|
} catch (error) {
|
2026-07-01 22:28:25 +03:30
|
|
|
clearStaleVersionSuccessUrl(url)
|
2026-06-16 19:47:28 +03:30
|
|
|
console.warn(`Version failed from ${url}. Round: ${round}`, error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-02 08:26:52 -04:00
|
|
|
async function fetchVersionWithRetry(rounds = VERSION_RETRY_ROUNDS): Promise<string | null> {
|
|
|
|
|
const urls = sortVersionUrls(getVersionUrls())
|
|
|
|
|
const saUrls = urls.filter(isSuperAdminUrl)
|
|
|
|
|
const otherUrls = urls.filter((url) => !isSuperAdminUrl(url))
|
|
|
|
|
|
|
|
|
|
const lists =
|
|
|
|
|
saUrls.length > 0
|
|
|
|
|
? allowSuperAdminFallback()
|
|
|
|
|
? [saUrls, otherUrls]
|
|
|
|
|
: [saUrls]
|
|
|
|
|
: [urls]
|
|
|
|
|
|
|
|
|
|
for (const list of lists) {
|
|
|
|
|
const version = await tryFetchVersionFromUrls(list, rounds)
|
|
|
|
|
if (version) return version
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 19:47:28 +03:30
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-02 08:26:52 -04:00
|
|
|
versionFetchPromise = fetchVersionWithRetry(rounds).finally(() => {
|
|
|
|
|
versionFetchPromise = null
|
|
|
|
|
})
|
2026-06-16 19:47:28 +03:30
|
|
|
|
|
|
|
|
return versionFetchPromise
|
|
|
|
|
}
|