import type { ReportItemWithContext } from './types'; import type { Reporter } from './Reporter'; export type TransportOptions> = { /** 过滤函数 * @desc 为 `true` 保留,为 `false` 丢弃 */ filter?: (item: ReportItemWithContext) => boolean; /** 采样率 * @desc 若和 `filter` 同时使用,会先 `filter` 后再计算采样结果 */ sampleRate?: number; /** 缓冲区大小,超过该大小会立刻触发发送,设为 `0` 则总是立刻发送 */ bufferSize?: number; /** 清空缓冲区间隔(ms),每隔一段时间检查缓冲区,若不为空则触发发送,设为 `0` 则不启用 */ flushInterval?: number; /** 上报频率限制 * @desc 每 `perSeconds` 秒最多上报 `max` 条数据,若 `max` 为 `0` 则不作限制,若 `perSeconds` 为 `0` 则不限制时间范围,统计总次数 * @example { perSeconds: 180, max: 20 } // 每三分钟(180秒)最多上报 20 条 * @example { perSeconds: 0, max: 0 } // 不作任何频率限制 * @example { perSeconds: 0, max: 20 } // 该实例最多上报 20 条 */ frequencyLimit?: { max: number; perSeconds: number; }; }; export abstract class Transport, R extends Reporter> { static isTransport(obj: any): obj is Transport { return ( obj && typeof obj.init === 'function' && typeof obj.receiveFromReporter === 'function' && typeof obj.flush === 'function' && typeof obj.send === 'function' ); } protected reporter?: R; protected sampleRate = 1; protected filter?: (item: ReportItemWithContext) => boolean = undefined; protected bufferSize = 50; protected flushInterval = 10000; protected flushIntervalTimer?: ReturnType; /** 缓冲区 */ protected buffer: ReportItemWithContext[] = []; /** 上报频率控制 */ protected frequencyLimit: { max: number; perSeconds: number; } = { max: 0, perSeconds: 0, }; private frequencyLimitTimer?: ReturnType; /** 当前频率限制统计区间内已发送的上报项数 */ private frequencyLimitRecordCount = 0; constructor(options?: TransportOptions) { if (options?.filter) { this.filter = options.filter; } if (typeof options?.sampleRate === 'number') { if (options.sampleRate <= 0 || options.sampleRate > 1) { console.warn('[Merlin] 采样率必须在 (0, 1] 范围内'); this.sampleRate = 1; } else { this.sampleRate = options.sampleRate; } } if (options?.bufferSize !== undefined) { this.bufferSize = options.bufferSize; } if (options?.flushInterval !== undefined) { this.flushInterval = options.flushInterval; } if (options?.frequencyLimit !== undefined) { this.frequencyLimit = options.frequencyLimit; } } async init(reporter: R): Promise { this.reporter = reporter; if (!!setInterval && !!clearInterval) { if (this.flushInterval !== 0) { // 在无法获取 setInterval 的情况下,只会按照容量 flush if (this.flushIntervalTimer) { clearInterval(this.flushIntervalTimer); this.flushIntervalTimer = undefined; } this.flushIntervalTimer = setInterval(() => { this.flush(); }, this.flushInterval); } if (this.frequencyLimit.perSeconds !== 0) { // perSeconds 不为 0 时,每隔 perSeconds 清空一下计数 if (this.frequencyLimitTimer) { clearInterval(this.frequencyLimitTimer); this.frequencyLimitTimer = undefined; } this.frequencyLimitTimer = setInterval(() => { this.frequencyLimitRecordCount = 0; }, this.frequencyLimit.perSeconds * 1000); } } } /** 从 Report 获取到上报项,处理并发送 * @param records 上报项 * @param immediate 是否立即发送,默认 `false` */ async receiveFromReporter(records: ReportItemWithContext[], immediate = false) { let recordsToSend = [...records]; if (this.filter) { recordsToSend = recordsToSend.filter(this.filter); } if (this.sampleRate < 1) { recordsToSend = recordsToSend.filter(() => Math.random() < this.sampleRate); } if (recordsToSend.length) { // 加到缓存区 this.buffer.push(...recordsToSend); } if (immediate || this.bufferSize === 0 || this.buffer.length >= this.bufferSize) { await this.flush(); } } protected async flush() { if (!this.buffer.length) return; const buffer = [...this.buffer]; this.buffer.length = 0; // 检查是否到了频率限制,如果已经越过频率限制,则不发送;若未越过,则发送频率限制剩下容量的上报项 if (this.frequencyLimit.max !== 0) { const frequencyLimitLeft = this.frequencyLimit.max - this.frequencyLimitRecordCount; if (frequencyLimitLeft <= 0) { return; } if (frequencyLimitLeft < buffer.length) { buffer.length = frequencyLimitLeft; } this.frequencyLimitRecordCount += buffer.length; } await this.send(buffer); } /** 发送处理后的上报项 */ protected abstract send(records: ReportItemWithContext[]); } // export class BuiltinTransports Transport>> { // readonly ids: (keyof D)[]; // constructor(protected transports: D) { // this.ids = Object.keys(transports); // } // getTransport(opt: keyof D | [keyof D, Record]) { // if (typeof opt === 'string') { // const constructor = this.transports[opt]; // if (constructor) { // return new constructor(); // } // throw new Error(`Unknown builtin transport: ${opt}`); // } // const constructor = this.transports[opt[0]]; // if (constructor) { // return new (constructor as any)(opt[1]); // } // throw new Error(`Unknown builtin transport: ${opt[0]}`); // } // }