zhizhi/grid-db/src/events/event-log.js

172 lines
4.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// grid-db/src/events/event-log.js
// Grid-DB · 事件溯源日志
// 不可变事件流 + 审计日志
// PRJ-GDB-001 · Phase 0 + Phase 1回放、序列号回放、操作过滤、天眼钩子
// 版权:国作登字-2026-A-00037559
'use strict';
const { randomUUID } = require('crypto');
/**
* EventLog — 事件溯源日志
*
* 每次写操作生成不可变 Event可被天眼订阅实现实时审计。
*
* 本体论锚定:事件日志 = 笔的笔迹记录。
* 每写一笔,笔迹就留在了历史中,不可篡改。
* 天眼通过观察笔迹,发现异常行为。
*
* Phase 0内存事件流保留最近 1000 条)
* Phase 1持久化事件流 + 回放 + 订阅
*/
class EventLog {
/**
* @param {object} [options]
* @param {number} [options.maxEvents] 最大保留事件数
*/
constructor(options = {}) {
this._maxEvents = options.maxEvents || 1000;
this._events = [];
this._subscribers = [];
this._seqNo = 0;
}
/**
* 记录事件
* @param {string} namespace 命名空间
* @param {string} operation 操作类型 (put | delete | scan)
* @param {string} gridCellKey 格点键
* @param {object} [payload] 额外数据
* @returns {object} 事件对象
*/
append(namespace, operation, gridCellKey, payload = null) {
this._seqNo++;
const event = {
eventId: `evt-${randomUUID().slice(0, 12)}`,
seqNo: this._seqNo,
timestamp: new Date().toISOString(),
namespace,
operation,
gridCellKey,
payload
};
this._events.push(event);
// 容量控制
if (this._events.length > this._maxEvents) {
this._events = this._events.slice(-this._maxEvents);
}
// 通知订阅者
for (const sub of this._subscribers) {
try {
sub.handler(event);
} catch {
// 订阅者错误不阻断主流程
}
}
return event;
}
/**
* 获取最近 N 条事件
* @param {number} [n]
* @returns {object[]}
*/
getRecent(n = 20) {
return this._events.slice(-n);
}
/**
* 按命名空间过滤事件
* @param {string} namespace
* @param {number} [limit]
* @returns {object[]}
*/
getByNamespace(namespace, limit = 50) {
return this._events
.filter(e => e.namespace === namespace)
.slice(-limit);
}
/**
* 订阅事件流Phase 0 简版P1 支持持久订阅)
* @param {string} subscriberId
* @param {Function} handler
* @returns {Function} 取消订阅函数
*/
subscribe(subscriberId, handler) {
const sub = { id: subscriberId, handler };
this._subscribers.push(sub);
return () => {
this._subscribers = this._subscribers.filter(s => s.id !== subscriberId);
};
}
/**
* 从指定时间戳回放事件
* @param {string} fromTimestamp ISO 8601 时间戳
* @returns {object[]}
*/
replay(fromTimestamp) {
return this._events.filter(e => e.timestamp >= fromTimestamp);
}
/**
* 从指定序列号回放事件
* @param {number} seqNo 起始序列号(包含)
* @returns {object[]}
*/
replayFromSeqNo(seqNo) {
return this._events.filter(e => e.seqNo >= seqNo);
}
/**
* 按操作类型过滤事件
* @param {string} operation 操作类型 (put | delete | scan)
* @param {number} [limit] 最大返回数
* @returns {object[]}
*/
getByOperation(operation, limit) {
const filtered = this._events.filter(e => e.operation === operation);
if (limit && limit > 0) {
return filtered.slice(-limit);
}
return filtered;
}
/**
* 设置天眼钩子 — 天眼集成占位接口
*
* 天眼通过此钩子实时接收事件流,实现全域审计。
* Phase 1 提供接口Phase 2 实现完整天眼协议。
*
* @param {Function} handler 天眼事件处理函数
*/
setTianyanHook(handler) {
this._tianyanHook = handler;
// 注册为特殊订阅者
if (typeof handler === 'function') {
this.subscribe('__tianyan__', handler);
}
}
/**
* 获取事件日志状态
* @returns {object}
*/
getStatus() {
return {
totalEvents: this._events.length,
maxEvents: this._maxEvents,
seqNo: this._seqNo,
subscriberCount: this._subscribers.length
};
}
}
module.exports = EventLog;