@cloudbase/app
Version:
cloudbase javascript sdk core
183 lines (162 loc) • 5.04 kB
text/typescript
import { utils } from '@cloudbase/utilities'
import { ReadableStream } from 'web-streams-polyfill'
import { ERROR, checkCustomUserIdRegex } from './constants'
import { lookupAppId } from './metadata'
/** 从 @cloudbase/utilities 中提取的表单和查询字符串工具 */
export const { isFormData, toQueryString } = utils as {
isFormData: (obj: unknown) => boolean
toQueryString: (data: Record<string, any>) => string
}
/**
* 跨端安全获取环境变量
* Web 端、小游戏端等没有 process 全局对象,直接访问 process.env 会报错
*/
export function getEnv(): Record<string, string | undefined>
export function getEnv(key: string): string | undefined
export function getEnv(key?: string): string | undefined | Record<string, string | undefined> {
if (typeof process === 'undefined' || !process.env) {
return key !== undefined ? undefined : {}
}
return key !== undefined ? process.env[key] : process.env
}
/** 判断当前是否在 SCF(云函数)运行环境中 */
export const checkIsInScf = () => getEnv('TENCENTCLOUD_RUNENV') === 'SCF'
export function checkIsInCBR() {
// CBR = CLOUDBASE_RUN
return !!getEnv('CBR_ENV_ID')
}
const kSumeruEnvSet = new Set(['formal', 'pre', 'test'])
export function checkIsInSumeru() {
// SUMERU_ENV=formal | test | pre
return kSumeruEnvSet.has(getEnv('SUMERU_ENV') || '')
}
export function isNonEmptyString(str: string) {
return typeof str === 'string' && str !== ''
}
export async function checkIsInTencentCloud(): Promise<boolean> {
if (getEnv('TENCENTCLOUD') === 'true') {
return true
}
return isNonEmptyString(await lookupAppId())
}
export const getCurrRunEnvTag = async (): Promise<string> => {
if (checkIsInScf()) {
return 'scf'
} if (checkIsInCBR()) {
return 'cbr'
} if (checkIsInSumeru()) {
return 'sumeru'
} if (await checkIsInTencentCloud()) {
return 'tencentcloud'
}
return 'unknown'
}
/**
* 校验自定义用户 ID 格式
* @throws 格式不合法时抛出 INVALID_PARAM 错误
*/
export const validateUid = (uid: string): void => {
if (typeof uid !== 'string') {
throw { ...ERROR.INVALID_PARAM, message: 'uid must be a string' }
}
if (!checkCustomUserIdRegex.test(uid)) {
throw { ...ERROR.INVALID_PARAM, message: `Invalid uid: "${uid}"` }
}
}
/**
* 安全的 JSON 解析,解析失败时返回原始文本而非抛异常
* @param text - 待解析的字符串
* @returns 解析后的对象,空字符串返回 null,解析失败返回原始文本
*/
export function safeParseJson(text: string): any {
if (!text || !text.trim()) {
return null
}
try {
return JSON.parse(text)
} catch (e) {
console.warn('catch an error', { e, text })
return text
}
}
/**
* 将对象的所有值转为字符串类型
* 用于将 headers 等对象统一为 Record<string, string>
*/
export function obj2StrRecord(obj: object): Record<string, string> {
return Object.entries(obj).reduce<Record<string, string>>((acc, cur: [string, unknown]) => {
const [key, value] = cur
acc[key] = String(value)
return acc
}, {})
}
/**
* 将 HeadersInit 转为可索引的普通对象
* 兼容 Headers 实例和普通对象两种形式
*/
export function headersInit2Indexable(h: HeadersInit) {
if (isHeaders(h)) {
const ret: Record<string, string> = {}
h.forEach((val, key) => {
ret[key] = val
})
return ret
}
return h
function isHeaders(h: HeadersInit): h is Headers {
try {
// Node 低版本可能没有全局 Headers
return h instanceof Headers
} catch (_) {
return false
}
}
}
/**
* 手动解析查询字符串,避免 URLSearchParams 的兼容性问题
* 支持重复 key 自动转为数组
* @param search - 查询字符串,可带前导 ? 或 #
*/
export function parseQueryString(search: string): Record<string, string | string[]> {
const params: any = {}
const cleanSearch = search.replace(/^[?#]/, '')
if (!cleanSearch) {
return params
}
const pairs = cleanSearch.split('&')
pairs.forEach((item) => {
let [key, value] = item.split('=')
key = decodeURIComponent(key)
value = decodeURIComponent(value || '')
if (key) {
if (params[key]) {
// 已存在同名 key,转为数组
if (Array.isArray(params[key])) {
params[key].push(value)
} else {
params[key] = [params[key], value]
}
} else {
params[key] = value
}
}
})
return params
}
/**
* 将 Node ReadableStream 转换为 Web ReadableStream (polyfill)
* 用于 fetch 流式响应在 Node 环境下的适配
*/
export function createWebStreamFromNodeReadableStream(stream: NodeJS.ReadableStream): ReadableStream {
const asyncIterator = stream[Symbol.asyncIterator]()
return new ReadableStream({
async pull(controller) {
const next = await asyncIterator.next()
if (next.done) {
controller.close()
} else {
controller.enqueue(next.value)
}
},
})
}