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 { decryptConfigFile } from './crypto' let _initPromise: Promise | null = null let _configFileInitPromise: Promise | null = null let _initializationKey: string | null = null let _sessionTransition: Promise = 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 { 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 baseUrl = options.platformUrl ?? DEFAULT_TENANT_PLATFORM_URL const configUrl = `${baseUrl.replace(/\/$/, '')}/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 const remote = (json.data ?? json) as Record initConfigFromRemote( options, { 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, ) configureHttp({ baseUrl: (remote.apiUrl as string | undefined) ?? baseUrl, debug: options.debug, }) await _notifyInitialized() })().catch(error => { _initPromise = null _initializationKey = null throw error }) return _initPromise } function enqueueSessionTransition(action: () => Promise): Promise { const next = _sessionTransition.catch(() => undefined).then(action) _sessionTransition = next return next } export const XuqmSDK = { /** * 方式 B:手动初始化。 * * 请求平台获取该 appKey 的完整服务配置(IM、Push、文件服务等 URL 及开通状态)。 * 失败时直接抛出,不降级。 * * @param options.appKey 应用标识 * @param options.platformUrl 平台地址;不传则使用内置默认公有平台地址 * @param options.debug 是否开启调试日志 */ async initialize(options: XuqmInitOptions): Promise { if (!options.appKey.trim()) { throw new Error('[XuqmSDK] appKey is required.') } await startInitialization(options) }, /** * 等待初始化完成。在任意子 SDK 内部调用以确保 XuqmSDK 已就绪。 */ async awaitInitialization(): Promise { await awaitInitialization() }, getUserId(): string | null { return getCommonUserId() }, /** * 用户认证核心枢纽。登录成功后调用一次,所有已安装的扩展 SDK 自动同步状态。 * 登出时传 null,触发所有子 SDK 登出。 */ async login(options: XuqmLoginOptions): Promise { 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 { await enqueueSessionTransition(async () => { await _clearAccessToken() if (getCommonUserInfo()) await _setUserInfo(null) _activeSessionKey = null }) }, getUserInfo(): XuqmUserInfo | null { return getCommonUserInfo() }, } export async function awaitInitialization(): Promise { if (isInitialized()) return const pendingInitialization = _configFileInitPromise ?? _initPromise 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 } /** Internal entry used only by the Metro auto-init module. */ export function _initializeFromConfigFile( encryptedContent: string, options?: { debug?: boolean }, ): Promise { if (_configFileInitPromise) return _configFileInitPromise _configFileInitPromise = (async () => { const file = await decryptConfigFile(encryptedContent) const platformUrl = file.serverUrl ?? file.baseUrl ?? undefined await startInitialization( { appKey: file.appKey, platformUrl, debug: options?.debug }, file.signingKey, ) })().catch(error => { _configFileInitPromise = null throw error }) return _configFileInitPromise }