fix: persist weekly brief delivery claims and accepted messages

This commit is contained in:
2026-09-06 14:32:56 +03:00
parent d3a52caf2b
commit 99a7f2f571
6 changed files with 213 additions and 10 deletions
+80
View File
@@ -0,0 +1,80 @@
import { DatabaseSync } from 'node:sqlite';
import { mkdirSync, chmodSync } from 'node:fs';
import path from 'node:path';
import { randomUUID, createHash } from 'node:crypto';
const hash = value => createHash('sha256').update(value).digest('hex');
export function briefWeekKey(now = new Date()) {
const date = new Date(now.getTime() + 3 * 3600000);
date.setUTCDate(date.getUTCDate() - (date.getUTCDay()+6)%7);
return date.toISOString().slice(0,10);
}
/** A claimed message is never blindly replayed after an interrupted provider call. */
export function createBriefOutbox(directory) {
function transaction(work) {
mkdirSync(directory,{recursive:true,mode:0o700});
const file=path.join(directory,'brief.sqlite');
const db = new DatabaseSync(file);
try {
chmodSync(file,0o600);
db.exec(`PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL;
CREATE TABLE IF NOT EXISTS brief_batches(id TEXT PRIMARY KEY,week TEXT NOT NULL,email TEXT NOT NULL,manifest TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS brief_messages(batch TEXT NOT NULL,position INTEGER NOT NULL,state TEXT NOT NULL,lease TEXT,lease_until INTEGER,receipt TEXT,error TEXT,PRIMARY KEY(batch,position));`);
db.exec('BEGIN IMMEDIATE');
const result=work(db);
db.exec('COMMIT');
return result;
} catch(error) { try { db.exec('ROLLBACK'); } catch {} throw error; }
finally { db.close(); }
}
return {
prepare(week,email,messages) {
const id=hash(JSON.stringify([week,email])),manifest=JSON.stringify(messages);
return transaction(db=>{
const row=db.prepare('SELECT manifest FROM brief_batches WHERE id=?').get(id);
if(row) {
const changed=row.manifest!==manifest;
if(changed) db.prepare("UPDATE brief_messages SET state='uncertain',error=? WHERE batch=? AND state='pending'")
.run('Haftalık özet içeriği veya alıcı kanalı değişti; yeniden gönderim öncesi inceleme gerekli.',id);
return {id,changed};
}
db.prepare('INSERT INTO brief_batches VALUES (?,?,?,?)').run(id,week,email,manifest);
const insert=db.prepare("INSERT INTO brief_messages(batch,position,state) VALUES (?,?,'pending')");
messages.forEach((_,i)=>insert.run(id,i));
return {id,changed:false};
});
},
claim(batch,position,clock=Date.now()) {
return transaction(db=>{
const row=db.prepare('SELECT * FROM brief_messages WHERE batch=? AND position=?').get(batch,position);
if(!row)throw new Error('Özet teslimat kaydı bulunamadı.');
if(row.state==='accepted')return {state:'accepted'};
if(row.state==='uncertain')return {state:'uncertain'};
if(row.state==='sending') {
if(row.lease_until>clock)return {state:'busy'};
db.prepare("UPDATE brief_messages SET state='uncertain',error=? WHERE batch=? AND position=?").run('Kesintiye uğrayan gönderim; sağlayıcı kaydı incelenmeli.',batch,position);
return {state:'uncertain'};
}
const lease=randomUUID();
db.prepare("UPDATE brief_messages SET state='sending',lease=?,lease_until=? WHERE batch=? AND position=?").run(lease,clock+120000,batch,position);
return {state:'sending',batch,position,lease};
});
},
accept(claim,receipt) {
if(typeof receipt!=='string'||!receipt.trim())throw new Error('Gönderim kabul kaydı gerekli.');
return transaction(db=>{
const result=db.prepare("UPDATE brief_messages SET state='accepted',receipt=?,error=NULL WHERE batch=? AND position=? AND lease=? AND state='sending'")
.run(receipt,claim.batch,claim.position,claim.lease);
if(result.changes!==1)throw new Error('Özet gönderim kaydı değişti.');
});
},
hold(claim,error) {
return transaction(db=>db.prepare("UPDATE brief_messages SET state='uncertain',error=? WHERE batch=? AND position=? AND lease=? AND state='sending'")
.run(String(error?.message || error),claim.batch,claim.position,claim.lease).changes===1);
},
inspect() {
return transaction(db=>db.prepare('SELECT b.week,b.email,m.* FROM brief_messages m JOIN brief_batches b ON b.id=m.batch ORDER BY b.week DESC,b.email,m.position').all());
},
};
}