new UI and handle cashing
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import type { AppConfig } from './types'
|
||||
|
||||
type RawAppConfig = AppConfig & {
|
||||
user?: Record<string, any>
|
||||
}
|
||||
|
||||
let appConfig: RawAppConfig | null = null
|
||||
|
||||
export function setAppConfig(config: RawAppConfig) {
|
||||
appConfig = config || {}
|
||||
window.__APP_CONFIG__ = appConfig
|
||||
}
|
||||
|
||||
export function getRawAppConfig() {
|
||||
return appConfig ?? window.__APP_CONFIG__ ?? null
|
||||
}
|
||||
|
||||
export function getTenantConfig() {
|
||||
const rawConfig = getRawAppConfig()
|
||||
|
||||
if (rawConfig?.user && typeof rawConfig.user === 'object') {
|
||||
return rawConfig.user
|
||||
}
|
||||
|
||||
if (rawConfig && Object.keys(rawConfig).length > 0) {
|
||||
return rawConfig
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
export function getConfig<T = any>(key: string, fallback?: T): T {
|
||||
const config = getTenantConfig()
|
||||
const value = config?.[key]
|
||||
|
||||
return (value ?? fallback ?? null) as T
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { getConfig } from '@/config/app-config'
|
||||
import { isPwaManifestReady } from '@/modules/pwa/manifest'
|
||||
|
||||
export function applyAppConfig() {
|
||||
if (isPwaManifestReady()) return
|
||||
|
||||
setTitle()
|
||||
setFavicon()
|
||||
setAppleTouchIconFallback()
|
||||
}
|
||||
|
||||
function setTitle() {
|
||||
document.title = getConfig('name_fa', 'در حال بارگذاری...')
|
||||
}
|
||||
|
||||
function getLogoUrl() {
|
||||
return getConfig<string>('avatar') || '/favicon.png'
|
||||
}
|
||||
|
||||
function setFavicon() {
|
||||
let faviconLink =
|
||||
document.querySelector<HTMLLinkElement>("link[rel='icon']") ||
|
||||
document.querySelector<HTMLLinkElement>("link[rel~='icon']")
|
||||
|
||||
if (!faviconLink) {
|
||||
faviconLink = document.createElement('link')
|
||||
faviconLink.rel = 'icon'
|
||||
document.head.appendChild(faviconLink)
|
||||
}
|
||||
|
||||
const logoUrl = getLogoUrl()
|
||||
|
||||
faviconLink.type = 'image/png'
|
||||
faviconLink.href = logoUrl.startsWith('http') ? logoUrl : resolveRemoteLogo(logoUrl)
|
||||
}
|
||||
|
||||
function setAppleTouchIconFallback() {
|
||||
const logoUrl = getLogoUrl()
|
||||
|
||||
if (!logoUrl) return
|
||||
|
||||
if (document.querySelector("link[rel='apple-touch-icon'][data-dynamic-pwa='true']")) {
|
||||
return
|
||||
}
|
||||
|
||||
const sizes = ['180x180', '152x152', '120x120']
|
||||
|
||||
sizes.forEach((size) => {
|
||||
let link = document.querySelector<HTMLLinkElement>(
|
||||
`link[rel='apple-touch-icon'][sizes='${size}']`
|
||||
)
|
||||
|
||||
if (!link) {
|
||||
link = document.createElement('link')
|
||||
link.rel = 'apple-touch-icon'
|
||||
link.sizes = size
|
||||
document.head.appendChild(link)
|
||||
}
|
||||
|
||||
link.href = logoUrl.startsWith('http') ? logoUrl : resolveRemoteLogo(logoUrl)
|
||||
})
|
||||
|
||||
let appleTitle = document.querySelector<HTMLMetaElement>("meta[name='apple-mobile-web-app-title']")
|
||||
|
||||
if (!appleTitle) {
|
||||
appleTitle = document.createElement('meta')
|
||||
appleTitle.name = 'apple-mobile-web-app-title'
|
||||
document.head.appendChild(appleTitle)
|
||||
}
|
||||
|
||||
appleTitle.content = getConfig('name_fa', 'اپ مشتری')
|
||||
}
|
||||
|
||||
function resolveRemoteLogo(path: string) {
|
||||
if (/^(https?:\/\/|data:|blob:)/i.test(path)) return path
|
||||
|
||||
const lastSuccess = localStorage.getItem('CONFIG_Success_URL')
|
||||
const base = lastSuccess
|
||||
? lastSuccess.replace(/\/get_user\/?$/i, '').replace(/\/+$/, '')
|
||||
: 'https://nerkh.gold'
|
||||
|
||||
return `${base}/${path.replace(/^\/+/, '')}`
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
import type { AppConfig } from '@/config/types'
|
||||
import { resetVersionCheckCache } from '@/config/load-version'
|
||||
import { clearCachedPwaManifest } from '@/modules/pwa/manifest-cache'
|
||||
import { getTokenName } from '@/themeConfig'
|
||||
|
||||
const CONFIG_COOKIE_NAME = 'APP_CONFIG_CACHE'
|
||||
const CONFIG_SUCCESS_URL_KEY = 'CONFIG_Success_URL'
|
||||
const VERSION_SUCCESS_URL_KEY = 'VERSION_Success_URL'
|
||||
const CONFIG_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 7
|
||||
const MAX_COOKIE_VALUE_LENGTH = 3500
|
||||
|
||||
const VERSION_STORAGE_KEY = 'version'
|
||||
const VERSION_UPDATE_LOCK_KEY = 'version_update_lock'
|
||||
const VERSION_LAST_RELOAD_KEY = 'version_last_reload'
|
||||
|
||||
type CachedConfigPayload = {
|
||||
cached_at: number
|
||||
config: AppConfig
|
||||
}
|
||||
|
||||
type VersionUpdateLock = {
|
||||
version: string
|
||||
started_at: number
|
||||
}
|
||||
|
||||
function getCookie(name: string) {
|
||||
const cookie = document.cookie.split('; ').find((row) => row.startsWith(`${name}=`))
|
||||
|
||||
if (!cookie) return null
|
||||
|
||||
return cookie.substring(name.length + 1)
|
||||
}
|
||||
|
||||
function setCookie(name: string, value: string, maxAgeSeconds: number) {
|
||||
const secure = window.location.protocol === 'https:' ? '; Secure' : ''
|
||||
|
||||
document.cookie = [
|
||||
`${name}=${value}`,
|
||||
'Path=/',
|
||||
`Max-Age=${maxAgeSeconds}`,
|
||||
'SameSite=Lax',
|
||||
secure
|
||||
].join('; ')
|
||||
}
|
||||
|
||||
export function readCachedAppConfig(): AppConfig | null {
|
||||
try {
|
||||
const rawCookie = getCookie(CONFIG_COOKIE_NAME)
|
||||
|
||||
if (!rawCookie) return null
|
||||
|
||||
const decoded = decodeURIComponent(rawCookie)
|
||||
const payload = JSON.parse(decoded) as CachedConfigPayload
|
||||
|
||||
if (!payload?.config) return null
|
||||
|
||||
return payload.config
|
||||
} catch (error) {
|
||||
console.warn('Failed to read cached app config:', error)
|
||||
clearCachedAppConfig()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function writeCachedAppConfig(config: AppConfig) {
|
||||
try {
|
||||
const payload: CachedConfigPayload = {
|
||||
cached_at: Date.now(),
|
||||
config
|
||||
}
|
||||
|
||||
const encoded = encodeURIComponent(JSON.stringify(payload))
|
||||
|
||||
if (encoded.length > MAX_COOKIE_VALUE_LENGTH) {
|
||||
console.warn(`App config is too large for cookie cache. Size: ${encoded.length}`)
|
||||
return
|
||||
}
|
||||
|
||||
setCookie(CONFIG_COOKIE_NAME, encoded, CONFIG_COOKIE_MAX_AGE_SECONDS)
|
||||
} catch (error) {
|
||||
console.warn('Failed to write app config cache:', error)
|
||||
}
|
||||
}
|
||||
|
||||
export function clearCachedAppConfig() {
|
||||
document.cookie = `${CONFIG_COOKIE_NAME}=; Path=/; Max-Age=0; SameSite=Lax`
|
||||
clearCachedPwaManifest()
|
||||
}
|
||||
|
||||
export function getStoredAppVersion() {
|
||||
return localStorage.getItem(VERSION_STORAGE_KEY)
|
||||
}
|
||||
|
||||
export function writeStoredAppVersion(version: string) {
|
||||
localStorage.setItem(VERSION_STORAGE_KEY, String(version).trim())
|
||||
}
|
||||
|
||||
export function getNormalizedVersion(version: unknown) {
|
||||
if (version === null || version === undefined) return ''
|
||||
return String(version).trim()
|
||||
}
|
||||
|
||||
export function shouldUpdateForVersion(serverVersion: unknown) {
|
||||
const normalizedServerVersion = getNormalizedVersion(serverVersion)
|
||||
|
||||
if (!normalizedServerVersion) return false
|
||||
|
||||
const storedVersion = getStoredAppVersion()
|
||||
|
||||
if (!storedVersion) {
|
||||
writeStoredAppVersion(normalizedServerVersion)
|
||||
return false
|
||||
}
|
||||
|
||||
return storedVersion !== normalizedServerVersion
|
||||
}
|
||||
|
||||
export function canStartVersionUpdate(serverVersion: string) {
|
||||
const version = getNormalizedVersion(serverVersion)
|
||||
|
||||
if (!version) return false
|
||||
|
||||
const lastReloadVersion = sessionStorage.getItem(VERSION_LAST_RELOAD_KEY)
|
||||
|
||||
if (lastReloadVersion === version) {
|
||||
return false
|
||||
}
|
||||
|
||||
const rawLock = sessionStorage.getItem(VERSION_UPDATE_LOCK_KEY)
|
||||
|
||||
if (rawLock) {
|
||||
try {
|
||||
const lock = JSON.parse(rawLock) as VersionUpdateLock
|
||||
const isSameVersion = lock.version === version
|
||||
const isFreshLock = Date.now() - lock.started_at < 60_000
|
||||
|
||||
if (isSameVersion && isFreshLock) {
|
||||
return false
|
||||
}
|
||||
} catch {
|
||||
sessionStorage.removeItem(VERSION_UPDATE_LOCK_KEY)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export function startVersionUpdateLock(serverVersion: string) {
|
||||
const version = getNormalizedVersion(serverVersion)
|
||||
|
||||
const lock: VersionUpdateLock = {
|
||||
version,
|
||||
started_at: Date.now()
|
||||
}
|
||||
|
||||
sessionStorage.setItem(VERSION_UPDATE_LOCK_KEY, JSON.stringify(lock))
|
||||
}
|
||||
|
||||
export function finishVersionUpdateBeforeReload(serverVersion: string) {
|
||||
const version = getNormalizedVersion(serverVersion)
|
||||
|
||||
writeStoredAppVersion(version)
|
||||
|
||||
sessionStorage.setItem(VERSION_LAST_RELOAD_KEY, version)
|
||||
sessionStorage.removeItem(VERSION_UPDATE_LOCK_KEY)
|
||||
}
|
||||
|
||||
export function cancelVersionUpdateLock() {
|
||||
sessionStorage.removeItem(VERSION_UPDATE_LOCK_KEY)
|
||||
}
|
||||
|
||||
function clearReadableCookiesExceptConfigAlreadyHandled() {
|
||||
document.cookie.split(';').forEach((cookie) => {
|
||||
const name = cookie.split('=')[0]?.trim()
|
||||
|
||||
if (!name) return
|
||||
|
||||
document.cookie = `${name}=; Path=/; Max-Age=0; SameSite=Lax`
|
||||
})
|
||||
}
|
||||
|
||||
export async function clearBrowserRuntimeCaches() {
|
||||
try {
|
||||
if ('serviceWorker' in navigator) {
|
||||
const registrations = await navigator.serviceWorker.getRegistrations()
|
||||
|
||||
await Promise.all(registrations.map((registration) => registration.unregister()))
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Could not unregister service workers:', error)
|
||||
}
|
||||
|
||||
try {
|
||||
if (!('caches' in window)) return
|
||||
|
||||
const cacheKeys = await caches.keys()
|
||||
await Promise.all(cacheKeys.map((key) => caches.delete(key)))
|
||||
} catch (error) {
|
||||
console.warn('Could not clear browser caches:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function clearLocalStorageExceptToken() {
|
||||
const tokenKey = getTokenName()
|
||||
const token = localStorage.getItem(tokenKey)
|
||||
|
||||
localStorage.clear()
|
||||
|
||||
if (token) {
|
||||
localStorage.setItem(tokenKey, token)
|
||||
}
|
||||
}
|
||||
|
||||
export function clearAppStorageExceptToken() {
|
||||
clearReadableCookiesExceptConfigAlreadyHandled()
|
||||
clearLocalStorageExceptToken()
|
||||
sessionStorage.clear()
|
||||
}
|
||||
|
||||
export function clearAllBootstrapFetchCaches() {
|
||||
localStorage.removeItem(CONFIG_SUCCESS_URL_KEY)
|
||||
localStorage.removeItem(VERSION_SUCCESS_URL_KEY)
|
||||
clearCachedPwaManifest()
|
||||
resetVersionCheckCache()
|
||||
window.__APP_CONFIG__ = undefined
|
||||
}
|
||||
|
||||
export function hardReloadApp() {
|
||||
const refreshUrl = `${window.location.origin}${window.location.pathname}?__app_refresh=${Date.now()}`
|
||||
window.location.href = refreshUrl
|
||||
}
|
||||
|
||||
export async function refreshConfigCacheForVersion(
|
||||
newVersion: string,
|
||||
fetchConfig: () => Promise<AppConfig>
|
||||
) {
|
||||
const normalizedVersion = getNormalizedVersion(newVersion)
|
||||
|
||||
if (!normalizedVersion) {
|
||||
throw new Error('Version is empty')
|
||||
}
|
||||
|
||||
startVersionUpdateLock(normalizedVersion)
|
||||
|
||||
clearCachedAppConfig()
|
||||
clearAppStorageExceptToken()
|
||||
clearAllBootstrapFetchCaches()
|
||||
await clearBrowserRuntimeCaches()
|
||||
|
||||
const freshConfig = await fetchConfig()
|
||||
|
||||
writeCachedAppConfig(freshConfig)
|
||||
finishVersionUpdateBeforeReload(normalizedVersion)
|
||||
|
||||
return freshConfig
|
||||
}
|
||||
|
||||
export async function resetAppAndRefreshConfig(
|
||||
fetchConfig: () => Promise<AppConfig>,
|
||||
version?: string | null
|
||||
) {
|
||||
const normalizedVersion = getNormalizedVersion(version)
|
||||
|
||||
clearCachedAppConfig()
|
||||
clearAppStorageExceptToken()
|
||||
clearAllBootstrapFetchCaches()
|
||||
await clearBrowserRuntimeCaches()
|
||||
|
||||
const freshConfig = await fetchConfig()
|
||||
|
||||
writeCachedAppConfig(freshConfig)
|
||||
|
||||
if (normalizedVersion) {
|
||||
writeStoredAppVersion(normalizedVersion)
|
||||
}
|
||||
|
||||
return freshConfig
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import type { AppConfig } from '@/config/types'
|
||||
import { readCachedAppConfig, writeCachedAppConfig } from '@/config/config-cache'
|
||||
|
||||
const CONFIG_SUCCESS_URL_KEY = 'CONFIG_Success_URL'
|
||||
|
||||
function getProtocol() {
|
||||
return window.location.protocol === 'https:' ? 'https:' : 'http:'
|
||||
}
|
||||
|
||||
function getConfigUrls() {
|
||||
const protocol = getProtocol()
|
||||
|
||||
return [
|
||||
`${protocol}//nerkh.gold/get_user`,
|
||||
`${protocol}//tablo.talasevin.ir/get_user`,
|
||||
`${protocol}//abshode-service.montgit.ir/server/get_user`,
|
||||
`${protocol}//212.23.201.173:8381/get_user`,
|
||||
`${protocol}//91.212.174.137:4040/server/get_user`
|
||||
]
|
||||
}
|
||||
|
||||
function sortConfigUrls(urls: string[]) {
|
||||
const lastSuccessUrl = localStorage.getItem(CONFIG_SUCCESS_URL_KEY)
|
||||
|
||||
return [...urls].sort((a, b) => {
|
||||
if (a === lastSuccessUrl) return -1
|
||||
if (b === lastSuccessUrl) return 1
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
async function fetchConfig(url: string, timeout = 3000): Promise<AppConfig> {
|
||||
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(`Config request failed with status ${response.status}`)
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type') ?? ''
|
||||
|
||||
if (!contentType.includes('application/json')) {
|
||||
throw new Error('Config response is not JSON')
|
||||
}
|
||||
|
||||
return await response.json()
|
||||
} finally {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchFreshAppConfig(): Promise<AppConfig> {
|
||||
const urls = sortConfigUrls(getConfigUrls())
|
||||
|
||||
for (let index = 0; index < urls.length; index++) {
|
||||
const url = urls[index]
|
||||
|
||||
try {
|
||||
const timeout = index === urls.length - 1 ? 5000 : 2500
|
||||
const config = await fetchConfig(url, timeout)
|
||||
|
||||
localStorage.setItem(CONFIG_SUCCESS_URL_KEY, url)
|
||||
writeCachedAppConfig(config)
|
||||
|
||||
return config
|
||||
} catch (error) {
|
||||
console.warn(`Config failed from ${url}`, error)
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Cannot load app config from any server')
|
||||
}
|
||||
|
||||
export async function loadAppConfig(): Promise<AppConfig> {
|
||||
const cachedConfig = readCachedAppConfig()
|
||||
|
||||
if (cachedConfig) {
|
||||
return cachedConfig
|
||||
}
|
||||
|
||||
return await fetchFreshAppConfig()
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
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'
|
||||
const VERSION_RETRY_ROUNDS = 5
|
||||
|
||||
let versionFetchPromise: Promise<string | null> | null = null
|
||||
let cachedVersionResult: string | null | undefined
|
||||
|
||||
function getVersionUrls() {
|
||||
return [
|
||||
'https://nerkh.gold/get_version',
|
||||
'https://tablo.talasevin.ir/get_version',
|
||||
'https://abshode-service.montgit.ir/server/get_version'
|
||||
]
|
||||
}
|
||||
|
||||
function sortVersionUrls(urls: string[]) {
|
||||
const lastSuccessUrl = localStorage.getItem(VERSION_SUCCESS_URL_KEY)
|
||||
|
||||
if (!lastSuccessUrl) {
|
||||
return urls
|
||||
}
|
||||
|
||||
const others = urls.filter((url) => url !== lastSuccessUrl)
|
||||
return [lastSuccessUrl, ...others]
|
||||
}
|
||||
|
||||
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) {
|
||||
// Non-2xx/timeout should be treated as "request failed" and we may try the next URL.
|
||||
throw new Error(`Version request failed with status ${response.status}`)
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type') ?? ''
|
||||
|
||||
const data = contentType.includes('application/json')
|
||||
? ((await response.json()) as VersionResponse)
|
||||
: ((await response.text()) as VersionResponse)
|
||||
|
||||
const version = normalizeVersionResponse(data)
|
||||
|
||||
// IMPORTANT: if the request returned 200/ok, we stop here even if we couldn't extract a version.
|
||||
// This prevents unnecessary looping when the endpoint is "reachable" but returns an empty payload.
|
||||
if (!version) {
|
||||
return null
|
||||
}
|
||||
|
||||
localStorage.setItem(VERSION_SUCCESS_URL_KEY, 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)
|
||||
// fetchVersion only throws on real request failures (non-ok / timeout).
|
||||
// If it returns null, we got an HTTP 200 but empty body => stop immediately.
|
||||
return version
|
||||
} catch (error) {
|
||||
console.warn(`Version failed from ${url}. Round: ${round}`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function resetVersionCheckCache() {
|
||||
cachedVersionResult = undefined
|
||||
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 (cachedVersionResult !== undefined) {
|
||||
return cachedVersionResult
|
||||
}
|
||||
|
||||
if (versionFetchPromise) {
|
||||
return versionFetchPromise
|
||||
}
|
||||
|
||||
versionFetchPromise = fetchVersionWithRetry(rounds)
|
||||
.then((version) => {
|
||||
cachedVersionResult = version
|
||||
return version
|
||||
})
|
||||
.finally(() => {
|
||||
versionFetchPromise = null
|
||||
})
|
||||
|
||||
return versionFetchPromise
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
export interface AppConfig {
|
||||
mode?: string
|
||||
theme?: string
|
||||
name?: string
|
||||
name_fa?: string
|
||||
|
||||
avatar?: string
|
||||
color?: string
|
||||
background_color?: string
|
||||
secondary_color?: string
|
||||
|
||||
node_server?: string
|
||||
node_ws?: string
|
||||
|
||||
manifest_url?: string
|
||||
|
||||
user?: Record<string, any>
|
||||
|
||||
[key: string]: any
|
||||
}
|
||||
Reference in New Issue
Block a user