@qiniu/miku-delivery-mp-ks
Version:
Kuaishou Mini Program SDK for Miku Delivery
147 lines (146 loc) • 5.58 kB
JavaScript
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;
}