XuqmGroup-RNSDK/packages/common/src/sdk.ts

191 行
6.5 KiB
TypeScript

import {
_notifyInitialized,
_setUserInfo,
getUserId as getCommonUserId,
getUserInfo as getCommonUserInfo,
initConfigFromRemote,
isInitialized,
type XuqmInitOptions,
type XuqmLoginOptions,
type XuqmUserInfo,
} from './config'
import { DEFAULT_TENANT_PLATFORM_URL } from './constants'
import { _clearAccessToken, _saveAccessToken, configureHttp } from './http'
import type { DecryptedConfig } from './crypto'
let _initPromise: Promise<void> | null = null
let _resolvedConfigInitPromise: Promise<void> | null = null
let _resolvedConfigRequest: {
config: DecryptedConfig
options?: { debug?: boolean }
} | null = null
let _initializationKey: string | null = null
let _sessionTransition: Promise<void> = Promise.resolve()
let _activeSessionKey: string | null = null
function initializationKey(options: XuqmInitOptions): string {
return `${options.appKey.trim()}\n${(options.platformUrl ?? DEFAULT_TENANT_PLATFORM_URL).replace(/\/$/, '')}`
}
function startInitialization(options: XuqmInitOptions, signingKey?: string): Promise<void> {
const nextKey = initializationKey(options)
if (isInitialized()) {
if (_initializationKey !== nextKey) {
return Promise.reject(new Error('[XuqmSDK] Already initialized with a different config.'))
}
return Promise.resolve()
}
if (_initPromise) {
if (_initializationKey !== nextKey) {
return Promise.reject(
new Error('[XuqmSDK] Initialization is already running with a different config.'),
)
}
return _initPromise
}
_initializationKey = nextKey
_initPromise = (async () => {
const platformUrl = (options.platformUrl ?? DEFAULT_TENANT_PLATFORM_URL).replace(/\/$/, '')
const configUrl = `${platformUrl}/api/sdk/config?appKey=${encodeURIComponent(options.appKey)}`
const res = await fetch(configUrl)
if (!res.ok) {
throw new Error(`[XuqmSDK] Platform config request failed: ${res.status}`)
}
const json = (await res.json()) as Record<string, unknown>
const remote = (json.data ?? json) as Record<string, unknown>
initConfigFromRemote(
{ ...options, platformUrl },
{
apiUrl: remote.apiUrl as string | undefined,
imWsUrl: remote.imWsUrl as string | undefined,
fileServiceUrl: remote.fileServiceUrl as string | undefined,
imEnabled: remote.imEnabled as boolean | undefined,
pushEnabled: remote.pushEnabled as boolean | undefined,
bugCollectApiUrl: remote.bugCollectApiUrl as string | undefined,
bugCollectEnabled: remote.bugCollectEnabled as boolean | undefined,
},
signingKey,
)
// Xuqm 平台接口始终回到租户平台。remote.apiUrl 是业务扩展服务地址,
// 二者职责不同;混用会把版本检查错误地发送到 App 自身的业务服务。
configureHttp({
baseUrl: platformUrl,
debug: options.debug,
})
await _notifyInitialized()
})().catch(error => {
_initPromise = null
_initializationKey = null
throw error
})
return _initPromise
}
function enqueueSessionTransition(action: () => Promise<void>): Promise<void> {
const next = _sessionTransition.catch(() => undefined).then(action)
_sessionTransition = next
return next
}
export const XuqmSDK = {
/**
* B
*
* appKey IMPush URL
*
*
* @param options.appKey
* @param options.platformUrl 使
* @param options.debug
*/
async initialize(options: XuqmInitOptions): Promise<void> {
if (!options.appKey.trim()) {
throw new Error('[XuqmSDK] appKey is required.')
}
await startInitialization(options)
},
/**
* SDK XuqmSDK
*/
async awaitInitialization(): Promise<void> {
await awaitInitialization()
},
getUserId(): string | null {
return getCommonUserId()
},
/**
* SDK
* null SDK
*/
async login(options: XuqmLoginOptions): Promise<void> {
const userId = options.userId.trim()
if (!userId) throw new Error('[XuqmSDK] userId is required.')
const accessToken = options.accessToken?.trim() || undefined
const normalized = { ...options, accessToken, userId }
const nextSessionKey = JSON.stringify(normalized)
await enqueueSessionTransition(async () => {
await awaitInitialization()
if (_activeSessionKey === nextSessionKey && getCommonUserInfo()) return
if (accessToken) await _saveAccessToken(accessToken)
else await _clearAccessToken()
const { accessToken: _accessToken, ...userInfo } = normalized
await _setUserInfo(userInfo)
_activeSessionKey = nextSessionKey
})
},
async logout(): Promise<void> {
await enqueueSessionTransition(async () => {
await _clearAccessToken()
if (getCommonUserInfo()) await _setUserInfo(null)
_activeSessionKey = null
})
},
getUserInfo(): XuqmUserInfo | null {
return getCommonUserInfo()
},
}
export async function awaitInitialization(): Promise<void> {
if (isInitialized()) return
const pendingInitialization =
_resolvedConfigInitPromise ??
_initPromise ??
(_resolvedConfigRequest
? _initializeFromResolvedConfig(_resolvedConfigRequest.config, _resolvedConfigRequest.options)
: null)
if (!pendingInitialization) {
throw new Error(
'[XuqmSDK] Automatic initialization did not start. Place config.xuqmconfig in src/assets/config and wrap Metro with withXuqmConfig(), or call XuqmSDK.initialize().',
)
}
await pendingInitialization
}
/** Metro 自动初始化专用入口;配置已在可信构建进程中完成解密和格式校验。 */
export function _initializeFromResolvedConfig(
config: DecryptedConfig,
options?: { debug?: boolean },
): Promise<void> {
_resolvedConfigRequest = { config, options }
if (_resolvedConfigInitPromise) return _resolvedConfigInitPromise
_resolvedConfigInitPromise = (async () => {
const platformUrl = config.serverUrl ?? config.baseUrl ?? undefined
await startInitialization(
{ appKey: config.appKey, platformUrl, debug: options?.debug },
config.signingKey,
)
})().catch(error => {
_resolvedConfigInitPromise = null
throw error
})
return _resolvedConfigInitPromise
}