import { ICloudbase } from '@cloudbase/types' import { ICloudbaseComponent } from '@cloudbase/types/component' import { ICallContainerOptions, IContianerConfig } from '@cloudbase/types/container' import { constants, utils, helpers, langEvent } from '@cloudbase/utilities' import { isBuffer, parseURL, serializeRequestBody } from './utils' import { COMPONENT_NAME } from './constants' declare const cloudbase: ICloudbase const { ERRORS, COMMUNITY_SITE_URL } = constants const { execCallback, printWarn } = utils const { catchErrorsDecorator } = helpers // eslint-disable-next-line import/no-mutable-exports export let t = str => str /* eslint-disable */ let getExportFunction: Function = () => { throw new Error(`getExportFunction ${t('未实现')}`) } let loadJSExportFunction: Function = () => { throw new Error(`loadJSExportFunction ${t('未实现')}`) } if (globalThis.IS_MP_BUILD) { getExportFunction = function () { new Error(t('小程序不支持 wasm 加载')) } loadJSExportFunction = function (jsUrl) { if (jsUrl) { new Error(t('小程序不支持动态 js 加载')) } return { initContainer: (globalThis as any).cloudbase_private_link.initContainer, callContainer: (globalThis as any).cloudbase_private_link.callContainer, } } } else { const loader = require('./utils/loader') getExportFunction = loader.getExportFunction loadJSExportFunction = loader.loadJSExportFunction } /* eslint-enable */ class InvalieParamsError extends Error { code: any constructor(scope, message) { const msg = JSON.stringify({ code: ERRORS.INVALID_PARAMS, msg: `[${scope}] ${message}`, }) super(msg) this.code = ERRORS.INVALID_PARAMS } toJSON() { return { code: this.code, message: this.message, } } } interface ICallTcbContainerOptions extends ICallContainerOptions { success: (res: { statusCode: string | number; header: ICallContainerOptions['header']; data: any }) => void fail: (res: { data: { code: number; message: string } }) => void } interface IInitTcbContainerOptions { config: Omit success: (res: { statusCode: string | number; header: ICallContainerOptions['header']; data: any }) => void fail: (res: { data: { code: number; message: string } }) => void } function genContainerError(data, scope = COMPONENT_NAME, defaultMessage = 'call container error') { const error = data instanceof Error ? data : new Error(`[${scope}] ${data?.message || defaultMessage}`) ;(error as any).code = data?.code || 'UNKNOWN_ERROR' return error } export class CloudbaseContainers { private readonly config: IContianerConfig & { jsUrl?: { vender?: string; vm: string } } private wasm: Promise private containerInitPromise: /* Promise*/ any | null private defaultHeaders: { [x: string]: any } constructor(config: IContianerConfig) { this.config = { ...config, jsUrl: typeof config.jsUrl === 'string' ? { vm: config.jsUrl, } : (config.jsUrl as any), } t = config.i18n?.t || (str => str) if (!this.config.wasmUrl && this.config.publicKey) { // 非 wams 且有 public key 则可使用默认的 js-sdk,无需校验 } else if (!this.config.wasmUrl && !this.config.jsUrl?.vm && !(globalThis as any).cloudbase_private_link) { throw new InvalieParamsError(`${COMPONENT_NAME}`, t('缺少 privatelink sdk 地址')) } if (this.config.wasmUrl) { this.wasm = getExportFunction(this.config.wasmUrl).catch((e) => { if (this.config.jsUrl) { console.warn('load wams error, fall back to use js', e) return loadJSExportFunction(this.config.jsUrl) } if ((globalThis as any).cloudbase_private_link) { return (globalThis as any).cloudbase_private_link } throw e }) } else if (this.config.jsUrl || this.config.publicKey) { this.wasm = loadJSExportFunction(this.config.jsUrl) } else { this.wasm = Promise.resolve({ initContainer: (globalThis as any).cloudbase_private_link.initContainer, callContainer: (globalThis as any).cloudbase_private_link.callContainer, }) } langEvent.bus.on(langEvent.LANG_CHANGE_EVENT, (params) => { t = params.data?.i18n?.t || (str => str) this.defaultHeaders = { ...this.defaultHeaders, [langEvent.LANG_CHANGE_EVENT]: params.data?.i18n?.lang } }) } @catchErrorsDecorator({ customInfo: { className: 'Cloudbase', methodName: 'callContainer', }, title: '调用失败', messages: [ '请确认以下各项:', ' 1 - 调用 callContainer() 的语法或参数是否正确', ' 2 - 域名 & 路径是否存在', `如果问题依然存在,建议到官方问答社区提问或寻找帮助:${COMMUNITY_SITE_URL}`, ], }) public async callContainer(options: ICallContainerOptions, callback?: Function) { await this.initContainer(this.config) const SCOPE = `${COMPONENT_NAME}.callContainer` const { callContainer }: { callContainer: (options: ICallTcbContainerOptions) => void } = (await this.wasm) as any const { url, method = 'GET', data, timeout = 60000, dataType = 'json', responseType = 'text', ...restOptions } = { ...options, } if (!url) { throw new InvalieParamsError(SCOPE, 'invalid request url') } if (!method) { throw new InvalieParamsError(SCOPE, 'invalid request method') } const header = { ...this.defaultHeaders, ...options.header } try { const res = await new Promise((resolve, reject) => { const params = { url, method, header, data: serializeRequestBody(header, data, method), timeout, dataType, responseType, ...restOptions, } if ( data && (isBuffer(params.data) || typeof params.data === 'string') && (data as any)?.length > 1000 * 1024 * 10 ) { reject(new InvalieParamsError(SCOPE, 'body too large')) return } if (method === 'GET') { let { data: getBody } = params if (getBody) { params.data = undefined } if (!isBuffer(getBody) && typeof getBody !== 'string') { const list = Object.entries(getBody || {}).reduce( (list, [key, value]: [string, string | number | boolean]) => { list.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`) return list }, [], ) if (list.length) { getBody = list.join('&') } } if (typeof params.data === 'string') { const parsedUrl = parseURL(url) if (parsedUrl.search) { parsedUrl.search += `&${params.data}` } else { parsedUrl.search = `?${params.data}` } params.url = parsedUrl.href params.data = undefined } } callContainer({ ...params, success: resolve, fail: (res) => { const { data } = res || {} reject(genContainerError(data, SCOPE, 'call container error')) }, }) }) return execCallback(callback, null, res) } catch (e) { execCallback(callback, e) } } async initContainer(config: Omit) { const { initContainer }: { initContainer: (options: IInitTcbContainerOptions) => void } = (await this.wasm) as any if (!this.containerInitPromise) { this.containerInitPromise = new Promise((resolve, reject) => { initContainer({ config, success: (res) => { if (String(res.statusCode) !== '200') { reject(genContainerError(res.data, `${COMPONENT_NAME}.initContainer`, 'init container fail')) } resolve(res) }, fail: (res) => { reject(genContainerError(res.data, `${COMPONENT_NAME}.initContainer`, 'init container fail')) this.containerInitPromise = null }, }) }) } return this.containerInitPromise } } const component: ICloudbaseComponent = { name: COMPONENT_NAME, namespace: 'container', entity(config: IContianerConfig) { if (this.containerInstance) { printWarn(ERRORS.INVALID_OPERATION, 'every cloudbase instance should has only one container object') return this.containerInstance } const instance = new CloudbaseContainers(config) this.containerInstance = instance this.callContainer = instance.callContainer.bind(instance) return this.containerInstance }, } try { cloudbase.registerComponent(component) } catch (e) {} export function registerContainers(app: Pick) { try { app.registerComponent(component) } catch (e) { console.warn(e) } }