180 行
6.0 KiB
TypeScript
180 行
6.0 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 { decryptConfigFile } from './crypto'
|
||
|
||
let _initPromise: Promise<void> | null = null
|
||
let _configFileInitPromise: Promise<void> | 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 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<string, unknown>
|
||
const remote = (json.data ?? json) as Record<string, unknown>
|
||
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<void>): Promise<void> {
|
||
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<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 = _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<void> {
|
||
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
|
||
}
|