2026-03-26 13:41:21 +08:00
|
|
|
|
// grid-db/src/events/event-log.js
|
|
|
|
|
|
// Grid-DB · 事件溯源日志
|
|
|
|
|
|
// 不可变事件流 + 审计日志
|
2026-03-26 16:43:56 +08:00
|
|
|
|
// PRJ-GDB-001 · Phase 0 + Phase 1(回放、序列号回放、操作过滤、天眼钩子)
|
2026-03-26 13:41:21 +08:00
|
|
|
|
// 版权:国作登字-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);
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-26 16:43:56 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 从指定时间戳回放事件
|
|
|
|
|
|
* @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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-26 13:41:21 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 获取事件日志状态
|
|
|
|
|
|
* @returns {object}
|
|
|
|
|
|
*/
|
|
|
|
|
|
getStatus() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
totalEvents: this._events.length,
|
|
|
|
|
|
maxEvents: this._maxEvents,
|
|
|
|
|
|
seqNo: this._seqNo,
|
|
|
|
|
|
subscriberCount: this._subscribers.length
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
module.exports = EventLog;
|