Files
ihaleist/server/ai-job-store.mjs
T

82 lines
6.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.
import {DatabaseSync} from 'node:sqlite';
import {mkdirSync,chmodSync} from 'node:fs';
import {dirname,join} from 'node:path';
import {fileURLToPath} from 'node:url';
import {randomUUID,createHash} from 'node:crypto';
const root=process.env.IHALEIST_USER_DATA_DIR || join(dirname(fileURLToPath(import.meta.url)),'data');
const operations=new Set(['analiz','belge-sor','teklif-denetim','teklif-taslak']);
function dbWork(fn) {
mkdirSync(root,{recursive:true,mode:0o700});const db=new DatabaseSync(join(root,'ai-jobs.sqlite'));chmodSync(join(root,'ai-jobs.sqlite'),0o600);
try {
db.exec(`PRAGMA busy_timeout=5000;
CREATE TABLE IF NOT EXISTS ai_jobs(id TEXT PRIMARY KEY,email TEXT NOT NULL,request_id TEXT NOT NULL,operation TEXT NOT NULL,ikn TEXT,
input TEXT NOT NULL,input_hash TEXT NOT NULL,state TEXT NOT NULL,created_at TEXT NOT NULL,updated_at TEXT NOT NULL,
result TEXT,error TEXT,run_id TEXT,heartbeat INTEGER,UNIQUE(email,request_id));
CREATE INDEX IF NOT EXISTS job_owner ON ai_jobs(email,created_at);
CREATE INDEX IF NOT EXISTS job_queue ON ai_jobs(state,created_at);`);
// Serialize schema inspection with migration across server processes.
db.exec('BEGIN IMMEDIATE');
try {
if(!db.prepare('PRAGMA table_info(ai_jobs)').all().some(column=>column.name==='resolved_input'))db.exec('ALTER TABLE ai_jobs ADD COLUMN resolved_input TEXT');
db.exec('COMMIT');
}catch(error){db.exec('ROLLBACK');throw error;}
return fn(db);
}finally {db.close();}
}
function publicJob(row) {
if(!row)return null;
return {id:row.id,operation:row.operation,ikn:row.ikn,state:row.state,createdAt:row.created_at,updatedAt:row.updated_at,
result:row.result?JSON.parse(row.result):null,error:row.error,inputHash:row.input_hash};
}
export function createAiJob(email,requestId,operation,input) {
if(typeof requestId!=='string'||requestId.length<8||requestId.length>100||!operations.has(operation)||!input||typeof input!=='object'||Array.isArray(input))throw new Error('Geçersiz AI işi.');
const serialized=JSON.stringify(input),hash=createHash('sha256').update(operation+'\n'+serialized).digest('hex');
if(Buffer.byteLength(serialized)>2*1024*1024)throw new Error('AI kaynak verisi çok büyük.');
return dbWork(db=>{
db.exec('BEGIN IMMEDIATE');
try {
const previous=db.prepare('SELECT * FROM ai_jobs WHERE email=? AND request_id=?').get(email,requestId);
if(previous) {if(previous.input_hash!==hash)throw new Error('İstek kimliği farklı kaynak için kullanılmış.');db.exec('COMMIT');return publicJob(previous);}
if(db.prepare("SELECT COUNT(*) n FROM ai_jobs WHERE email=? AND state IN ('queued','running')").get(email).n>=5)throw new Error('En fazla beş etkin AI işi olabilir. Mevcut işleri tamamlayın veya iptal edin.');
const id=randomUUID(),now=new Date().toISOString();
db.prepare("INSERT INTO ai_jobs(id,email,request_id,operation,ikn,input,input_hash,state,created_at,updated_at) VALUES (?,?,?,?,?,?,?,'queued',?,?)")
.run(id,email,requestId,operation,input.ikn||input.tender?.ikn||null,serialized,hash,now,now);
const result=publicJob(db.prepare('SELECT * FROM ai_jobs WHERE id=?').get(id));db.exec('COMMIT');return result;
} catch(error){db.exec('ROLLBACK');throw error;}
});
}
export const getAiJob=(email,id)=>dbWork(db=>{const row=db.prepare('SELECT * FROM ai_jobs WHERE email=? AND id=?').get(email,id);return row?{...publicJob(row),input:JSON.parse(row.resolved_input || row.input)}:null;});
export const listAiJobs=(email,ikn)=>dbWork(db=>db.prepare('SELECT * FROM ai_jobs WHERE email=? AND (? IS NULL OR ikn=?) ORDER BY created_at DESC LIMIT 50').all(email,ikn||null,ikn||null).map(publicJob));
export function claimAiJob(clock=Date.now()) {
return dbWork(db=>{
db.exec('BEGIN IMMEDIATE');
try {
// Do not replay an uncertain provider call automatically after a lost worker.
db.prepare("UPDATE ai_jobs SET state='interrupted',error=?,updated_at=? WHERE state='running' AND heartbeat<?")
.run('İşlem kesildi. Kaynak korunuyor; yeniden başlatabilirsiniz.',new Date(clock).toISOString(),clock-60000);
const row=db.prepare("SELECT * FROM ai_jobs WHERE state='queued' ORDER BY created_at LIMIT 1").get();
if(!row){db.exec('COMMIT');return null;}
const runId=randomUUID();
db.prepare("UPDATE ai_jobs SET state='running',run_id=?,heartbeat=?,updated_at=? WHERE id=?").run(runId,clock,new Date(clock).toISOString(),row.id);
db.exec('COMMIT');return {id:row.id,email:row.email,operation:row.operation,input:JSON.parse(row.resolved_input || row.input),sourcePinned:row.resolved_input!==null,runId};
}catch(error){db.exec('ROLLBACK');throw error;}
});
}
export const heartbeatAiJob=(id,runId)=>dbWork(db=>db.prepare("UPDATE ai_jobs SET heartbeat=? WHERE id=? AND run_id=? AND state='running'").run(Date.now(),id,runId).changes>0);
export const finishAiJob=(id,runId,result,error=null)=>dbWork(db=>db.prepare("UPDATE ai_jobs SET state=?,result=?,error=?,updated_at=? WHERE id=? AND run_id=? AND state='running'")
.run(error?'failed':'completed',error?null:JSON.stringify(result),error,new Date().toISOString(),id,runId).changes>0);
export const cancelAiJob=(email,id)=>dbWork(db=>db.prepare("UPDATE ai_jobs SET state='cancelled',error=NULL,updated_at=? WHERE id=? AND email=? AND state IN ('queued','running','interrupted','failed')").run(new Date().toISOString(),id,email).changes>0);
export const retryAiJob=(email,id)=>dbWork(db=>{
db.exec('BEGIN IMMEDIATE');
try {
const active=db.prepare("SELECT COUNT(*) n FROM ai_jobs WHERE email=? AND state IN ('queued','running')").get(email).n;
if(active>=5)throw new Error('En fazla beş etkin AI işi olabilir.');
const changed=db.prepare("UPDATE ai_jobs SET state='queued',error=NULL,result=NULL,run_id=NULL,updated_at=? WHERE id=? AND email=? AND state IN ('interrupted','failed')").run(new Date().toISOString(),id,email).changes>0;
db.exec('COMMIT');return changed;
}catch(error){db.exec('ROLLBACK');throw error;}
});
// Only the current worker may pin source content; retries cannot replace it.
export const pinAiJobInput=(id,runId,input)=>dbWork(db=>db.prepare("UPDATE ai_jobs SET resolved_input=?,updated_at=? WHERE id=? AND run_id=? AND state='running' AND resolved_input IS NULL")
.run(JSON.stringify(input),new Date().toISOString(),id,runId).changes>0);