feat(comments): 60s dedupe window + idempotency key

- Server window raised from 5s to 60s so slow double-fires (observed
  on /videos/CRKAf: same body saved 3-7 times, seconds to minutes
  apart) collapse into one comment.
- X-Idempotency-Key honored: the client sends a per-attempt UUID; the
  server caches the resulting comment id for 5 minutes under that key.
  A retry with the same key returns the first-written comment without
  touching the DB a second time.
- Comment and reply submit JS both send the key via fetch().

The existing _busy flag + disabled submit button already covers the
fast double-click case on the client; this hardens the server side so
duplicates can't slip through even when the client guard is bypassed.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
ghassanandClaude Opus 4.7 committed 2026-10-21 00:38:03 +03:00
1 parent 3122420a3d
commit c9123bc7b8
2 files changed
+51 -7

No files matched your search

@@ -1135,9 +1135,17 @@ async function postComment() {
submitBtn.disabled = true;
submitBtn.textContent = 'Posting…';
try {
// Idempotency key: random per attempt. On retry the client reuses the
// same key so the server returns the original comment instead of
// writing a second one.
const idemKey = (window.crypto?.randomUUID?.() || (Date.now() + '-' + Math.random().toString(36).slice(2)));
const r = await fetch('/videos/' + YTC.videoId + '/comments', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': YTC.csrf },
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': YTC.csrf,
'X-Idempotency-Key': idemKey,
},
body: JSON.stringify({ body: text })
});
if (!(r.headers.get('content-type') || '').includes('application/json')) {
@@ -1178,9 +1186,14 @@ async function postReply(vid, parentId) {
btn.textContent = 'Posting…';
}
try {
const idemKey = (window.crypto?.randomUUID?.() || (Date.now() + '-' + Math.random().toString(36).slice(2)));
const r = await fetch('/videos/' + vid + '/comments', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': YTC.csrf },
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': YTC.csrf,
'X-Idempotency-Key': idemKey,
},
body: JSON.stringify({ body: text, parent_id: parentId })
});
if (!(r.headers.get('content-type') || '').includes('application/json')) {