autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
166 lines (165 loc) • 5.75 kB
JavaScript
/**
* EventAggregator — 信号聚类引擎(受 Continue EditAggregator 启发)
*
* 在短时间窗口内将多个同类事件聚合为一个 batch 事件,避免:
* 1. SignalCollector 对相同类型的信号重复触发 AI 分析
* 2. 高频操作(如连续文件保存)产生大量冗余事件
* 3. 多个 Guard 违规在同一编辑中重复推送
*
* 聚合策略:
* 时间窗口 — 同 key 事件在 windowMs 内合并为一个 batch
* 空间聚类 — 支持自定义 key 提取函数(如按文件路径/违规规则分组)
* 去重 —— 已处理的事件在 dedupeWindowMs 内不重复触发
*
* 用法:
* const agg = new EventAggregator({ windowMs: 5000 });
* agg.on('batch', (key, events) => { ... });
* agg.push('file_change', { filePath: 'a.js' });
* // 5 秒内的多次 push 会合并为一次 batch 回调
*/
import Logger from '../../infrastructure/logging/Logger.js';
const DEFAULT_WINDOW_MS = 5000; // 5 秒聚合窗口
const DEFAULT_MAX_BATCH = 50; // 单次 batch 最大事件数
const DEFAULT_DEDUPE_MS = 60_000; // 60 秒去重窗口
export class EventAggregator {
/** >} */
#buckets = new Map();
/** 已处理事件的 hash → 最后处理时间 */
#dedupeMap = new Map();
#listeners = new Map();
#windowMs;
#maxBatch;
#dedupeMs;
#logger;
/**
* @param [opts.windowMs=5000] 聚合时间窗口(毫秒)
* @param [opts.maxBatch=50] 单次 batch 最大事件数
* @param [opts.dedupeMs=60000] 相同事件去重窗口(毫秒)
*/
constructor({ windowMs = DEFAULT_WINDOW_MS, maxBatch = DEFAULT_MAX_BATCH, dedupeMs = DEFAULT_DEDUPE_MS, } = {}) {
this.#windowMs = windowMs;
this.#maxBatch = maxBatch;
this.#dedupeMs = dedupeMs;
this.#logger = Logger.getInstance();
}
/**
* 推送一个事件到聚合器
* @param key 聚合键(如 'file_change', 'guard_violation')
* @param event 事件数据
* @param [opts.dedupeId] 去重标识(默认为 JSON hash)
*/
push(key, event, { dedupeId } = {}) {
// 去重检查
const dedupe = dedupeId || this.#hashEvent(key, event);
const lastSeen = this.#dedupeMap.get(dedupe);
if (lastSeen && Date.now() - lastSeen < this.#dedupeMs) {
this.#logger.debug(`[EventAggregator] dedup skip: ${key}/${dedupe}`);
return;
}
let bucket = this.#buckets.get(key);
if (!bucket) {
bucket = { events: [], timer: null };
this.#buckets.set(key, bucket);
}
bucket.events.push({ ...event, _ts: Date.now() });
// 达到最大 batch 立即触发
if (bucket.events.length >= this.#maxBatch) {
this.#flush(key);
return;
}
// 重置窗口计时器
if (bucket.timer) {
clearTimeout(bucket.timer);
}
bucket.timer = setTimeout(() => this.#flush(key), this.#windowMs);
}
/** 注册 batch 事件监听器 */
on(eventName, fn) {
if (!this.#listeners.has(eventName)) {
this.#listeners.set(eventName, []);
}
this.#listeners.get(eventName).push(fn);
}
/** 立即刷新所有待处理 bucket */
flushAll() {
for (const key of this.#buckets.keys()) {
this.#flush(key);
}
}
/** 停止所有计时器 */
destroy() {
for (const [, bucket] of this.#buckets) {
if (bucket.timer) {
clearTimeout(bucket.timer);
}
}
this.#buckets.clear();
this.#dedupeMap.clear();
this.#listeners.clear();
}
/** 获取待处理事件数 */
get pendingCount() {
let count = 0;
for (const [, bucket] of this.#buckets) {
count += bucket.events.length;
}
return count;
}
// ── 内部方法 ──
#flush(key) {
const bucket = this.#buckets.get(key);
if (!bucket || bucket.events.length === 0) {
return;
}
if (bucket.timer) {
clearTimeout(bucket.timer);
bucket.timer = null;
}
const events = bucket.events.splice(0);
this.#buckets.delete(key);
// 标记去重
for (const evt of events) {
const dedupe = this.#hashEvent(key, evt);
this.#dedupeMap.set(dedupe, Date.now());
}
// 清理过期去重记录
this.#cleanupDedupe();
// 通知监听器
const listeners = this.#listeners.get('batch') || [];
for (const fn of listeners) {
try {
fn(key, events);
}
catch (err) {
this.#logger.warn(`[EventAggregator] listener error: ${err instanceof Error ? err.message : String(err)}`);
}
}
this.#logger.debug(`[EventAggregator] flushed ${events.length} events for key "${key}"`);
}
#hashEvent(key, event) {
// 简单 hash: key + 事件关键字段
const significant = { key };
if (event.filePath) {
significant.f = event.filePath;
}
if (event.ruleName) {
significant.r = event.ruleName;
}
if (event.action) {
significant.a = event.action;
}
if (event.id) {
significant.i = event.id;
}
return JSON.stringify(significant);
}
#cleanupDedupe() {
const now = Date.now();
for (const [hash, ts] of this.#dedupeMap) {
if (now - ts > this.#dedupeMs) {
this.#dedupeMap.delete(hash);
}
}
}
}
export default EventAggregator;