/** * WAL / journal(M4):关键动作先追加 JSONL 落盘再执行;重放幂等。 * state/wal-.jsonl:{seq,ts,type,taskId,key,data} * 重放重建:lastSeq、幂等键集合、每任务事件流(供崩溃恢复 DAG 进度)。 */ import { openSync, appendFileSync, readFileSync, existsSync, mkdirSync, closeSync } from "node:fs"; import { resolve } from "node:path"; export class WAL { constructor(stateRoot, owner) { mkdirSync(stateRoot, { recursive: true }); this.owner = owner; this.file = resolve(stateRoot, `wal-${owner.replace(/[^A-Za-z0-9_-]/g, "_")}.jsonl`); this.seq = 0; this.keys = new Set(); this.byTask = new Map(); this._recover(); } _recover() { if (!existsSync(this.file)) return; const lines = readFileSync(this.file, "utf8").split(/\r?\n/).filter(Boolean); for (const line of lines) { let e; try { e = JSON.parse(line); } catch { continue; // 尾部截断行跳过 } this.seq = Math.max(this.seq, e.seq || 0); if (e.key) this.keys.add(e.key); if (e.taskId) { if (!this.byTask.has(e.taskId)) this.byTask.set(e.taskId, []); this.byTask.get(e.taskId).push(e); } } } /** 先写 journal。key 为幂等键:重复 key 返回既有 seq,不重复落盘。 */ append(type, { taskId = null, key = null, data = {} } = {}) { if (key && this.keys.has(key)) { return { duplicated: true, seq: this.seq }; } const entry = { seq: ++this.seq, ts: Date.now(), owner: this.owner, type, taskId, key, data }; const fd = openSync(this.file, "a"); try { appendFileSync(fd, JSON.stringify(entry) + "\n"); } finally { closeSync(fd); // 关闭触发刷盘,保证 kill -9 后可重放 } if (key) this.keys.add(key); if (taskId) { if (!this.byTask.has(taskId)) this.byTask.set(taskId, []); this.byTask.get(taskId).push(entry); } return { duplicated: false, entry, seq: this.seq }; } has(key) { return this.keys.has(key); } events(taskId) { return this.byTask.get(taskId) || []; } /** 崩溃恢复视角:已 claim 但未 settled 的任务及其进度。 */ recoverInFlight() { const out = []; for (const [taskId, events] of this.byTask) { const types = new Set(events.map((e) => e.type)); if (types.has("task.claimed") && !types.has("task.settled")) { out.push({ taskId, events: events.map((e) => ({ type: e.type, key: e.key, data: e.data })) }); } } return out; } }