UNPKG

@qiniu/miku-delivery-mp-ks

Version:

Kuaishou Mini Program SDK for Miku Delivery

147 lines (146 loc) 5.58 kB
import { Polyfilled_URL } from '../utils/url'; import { mpPlatform, version } from '../utils/env'; export function random(items, getWeight = () => 1) { if (items.length === 0) return null; const weights = items.map(getWeight); const sum = weights.reduce((s, w) => s + w, 0); let val = Math.random() * sum; for (let i = 0; i < weights.length; i++) { const weight = weights[i]; if (val < weight) return items[i]; val -= weight; } return items[0]; // 其实不会走到这儿 } /** * 代替上面的 random 方法,需要达到以下效果: * - 同一客户端尽量将同一资源的请求调度到同一个 ECDN 节点,以发挥小程序框架的图片自动缓存能力 * - 不同客户端对同一资源的调度节点应尽量随机,以避免 ECDN 节点间负载不均衡 */ export function select(items, clientID, cacheKey, getWeight = () => 1) { if (items.length === 0) return null; if (items.length === 1) return items[0]; // cacheKey 所有字符的 ascii 码值总和好像可以满足 "相同 cacheKey 结果一样, 不同 cacheKey 结果随机" 这一需求 const randomKey = clientID + cacheKey; let codeSum = 0; for (let i = 0; i < randomKey.length; i++) { codeSum += randomKey.charCodeAt(i); } const random = (codeSum % items.length + 0.5) / items.length; const weights = items.map(getWeight); const sum = weights.reduce((s, w) => s + w, 0); let val = random * sum; for (let i = 0; i < weights.length; i++) { const weight = weights[i]; if (val < weight) return items[i]; val -= weight; } return items[0]; } export function isAbortError(e) { // 对于 AbortError 通过比较 name 而不是 instanceOf 来识别,考虑: // 1. 可能经过同上的线程间传递,丢失了本来的 constructor / prototype 信息 // 2. 兼容 DOMException name `AbortError`,详见 https://developer.mozilla.org/en-US/docs/Web/API/DOMException#aborterror return e && e.name === 'AbortError'; } /** * 判断是不是应该基于给定的异常来禁用目标节点 * 如果是跟目标节点无关,无须切换目标节点简单重试就能成功的异常(一般是端上自己的问题),这里返回 false, * 对应地 dns do 逻辑不会因为该异常而将目标节点从可用集合中剔除,因此避免对后续请求的影响 */ export function shouldDisableTarget(e) { // 注意这里的依赖关系是不太合理的,dns 模块不应该需要知道某个 http-client(如 how)的实现细节 // TODO: 改为基于某种关于 error 形状的约定,由 http / http-client 自己去识别各种不同的错误并按约定的形状暴露给 dns if (isAbortError(e)) return false; return true; } /** Resolve 失败(服务不可用、网络问题、...) */ export class ResolveError extends Error { constructor( /** 作为请求失败起因的原 error */ cause) { super(cause + ''); Object.defineProperty(this, "cause", { enumerable: true, configurable: true, writable: true, value: cause }); Object.defineProperty(this, "name", { enumerable: true, configurable: true, writable: true, value: 'ResolveError' }); } } /** 当前请求没有可用的 ECDN 节点 */ export class NoAvailableNodeError extends Error { constructor() { super(...arguments); Object.defineProperty(this, "name", { enumerable: true, configurable: true, writable: true, value: 'NoAvailableNodeError' }); } } /** 使用 Resolve 结果中的 ECDN 节点处理请求失败 */ export class DoWithNodeError extends Error { constructor( /** 作为请求失败起因的原 error */ cause) { super(cause + ''); Object.defineProperty(this, "cause", { enumerable: true, configurable: true, writable: true, value: cause }); Object.defineProperty(this, "name", { enumerable: true, configurable: true, writable: true, value: 'DoWithNodeError' }); } } /** Resolve 决策走 Local DNS,而不走 ECDN 节点 */ export class LocalDNSError extends Error { constructor() { super(...arguments); Object.defineProperty(this, "name", { enumerable: true, configurable: true, writable: true, value: 'LocalDNSError' }); } } const mikuAgentNames = { wx: 'miku-delivery-mp', tt: 'miku-delivery-mp-tt', ks: 'miku-delivery-mp-ks' }; const mikuAgentName = mikuAgentNames[mpPlatform]; export function withResolved(originalUrl, resolved) { const urlObj = new Polyfilled_URL(originalUrl); const originalHost = urlObj.host; // 使用 resolved.host 作为 host,将原 host 拼在 path 中 urlObj.protocol = 'https:'; urlObj.host = `${resolved.host}:${resolved.https}`; urlObj.pathname = `/${originalHost}${urlObj.pathname}`; let url = urlObj.toString(); // 拼接 X-Miku-Agent 信息,详见 https://github.com/qbox/miku/issues/246#issuecomment-1301555368 const sep = url.includes('?') ? '&' : '?'; const extra = ['X-Miku-Agent', `${mikuAgentName}/v${version}`].map(encodeURIComponent).join('='); url = url + sep + extra; return url; }