1000 forms do submit. Po 300 process pada. Bez idempotency: zaczynasz od zera, 300 duplikatów. Z idempotency: kontynuujesz od 301.
CREATE TABLE form_submissions (
id BIGSERIAL PRIMARY KEY,
external_id TEXT UNIQUE NOT NULL, -- unique per data row
form_id TEXT NOT NULL,
status TEXT NOT NULL CHECK (status IN ('pending', 'submitted', 'failed')),
data JSONB NOT NULL,
screenshot_path TEXT,
attempts INT NOT NULL DEFAULT 0,
last_error TEXT,
submitted_at TIMESTAMPTZ
);
CREATE INDEX idx_form_status ON form_submissions (form_id, status);
async function processBatch(formDesc, dataRows) {
for (const data of dataRows) {
const externalId = generateId(data); // np. email + timestamp
// Skip if already submitted
const existing = await db.query(
'SELECT status FROM form_submissions WHERE external_id=$1',
[externalId]
);
if (existing.rows.length && existing.rows[0].status === 'submitted') {
continue; // idempotent
}
// Insert pending
await db.query(`
INSERT INTO form_submissions (external_id, form_id, status, data)
VALUES ($1, $2, 'pending', $3)
ON CONFLICT (external_id) DO UPDATE SET attempts = form_submissions.attempts + 1
`, [externalId, formDesc.id, data]);
try {
const result = await runForm(formDesc, data);
await db.query(`
UPDATE form_submissions SET status=$1, screenshot_path=$2, submitted_at=NOW()
WHERE external_id=$3
`, [result.success ? 'submitted' : 'failed', result.screenshotPath, externalId]);
} catch (err) {
await db.query(`
UPDATE form_submissions SET status='failed', last_error=$1
WHERE external_id=$2
`, [err.message, externalId]);
}
}
}