۲۰. نوشتن همزمان؛ تعارض نسخه، ارسال دوباره و نتیجهٔ نامعلوم
دو تب میتوانند یک تیکت را با نسخهٔ یکسان بخوانند. اگر هر دو پاسخ بدهند، ذخیرهٔ بدون کنترل ممکن است تغییر تازه را با دادهٔ قدیمی جایگزین کند. در این درس از عدد version به عنوان قرارداد این API استفاده میکنیم. هدف، فهم تصمیم بازخوانی و حفظ پیشنویس است؛ افزایش خودکار عدد در مرورگر راه حل نیست. مقدار معتبر نسخه را سرور پس از تغییر موفق میسازد.
مقایسه و تغییر باید به هم متصل باشند
تیکت تازه در پروژه version:1 دارد. برای پاسخ، بدنهٔ POST /api/tickets/{id}/messages شامل message و version خواندهشده است. سرور نسخهٔ ارسالی را با رکورد جاری مقایسه میکند؛ در اختلاف، 409 و پیام بازخوانی میدهد. اگر یکسان باشد و تغییر معتبر باشد، نسخه یک واحد افزایش مییابد. پس دریافت 409 مجوز تکرار خودکار با عدد جدید نیست؛ کاربر باید تغییرات طرف دیگر را ببیند.
این نمونه را در کنسول صفحهٔ محلی /support/، پس از انتخاب هویت آزمایشی و ایجاد یک تیکت، اجرا کنید. شناسهٔ واقعی همان تیکت را جایگزین کنید. بار اول جزئیات خوانده میشود و هر دو ارسال عمداً نسخهٔ یکسان دارند.
const id = 'شناسهٔ تیکت خودتان';
const currentResponse = await fetch('/api/tickets/' + id);
if (!currentResponse.ok) throw new Error('ابتدا تیکت خودتان را انتخاب کنید');
const current = await currentResponse.json();
const send = message => fetch('/api/tickets/' + id + '/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message, version: current.version })
});
const responses = await Promise.all([
send('پاسخ آزمایشی نخست'), send('پاسخ آزمایشی دوم')
]);
console.log(responses.map(r => r.status).sort()); // [200, 409]کدام متن زودتر ثبت شود وابسته به ترتیب واقعی پردازش است؛ آزمون نباید نام برنده را حدس بزند. انتظار ما دقیقاً یک پذیرش و یک تعارض است. در server.mjs خواندن بدنه await دارد و ممکن است درخواست دیگری در آن فاصله جلو برود. بنابراین رکورد پس از خواندن بدنه دوباره از state گرفته میشود. این جزئیات نشان میدهد تکنخیبودن JavaScript به معنی نبود مسئلهٔ همزمانی نیست.
رابط در تعارض چه کند؟
در projects/support/app.mjs، متن پاسخ پیش از درخواست خوانده و فقط پس از موفقیت فرم reset میشود. اگر api خطای 409 بدهد، task پیام توضیحی نمایش میدهد و متن باقی میماند. دکمهٔ بازخوانی رشته نسخهٔ تازه را میگیرد. دانشجو متن خود را با پیام تازه مقایسه و سپس دوباره ارسال میکند. این تصمیم کوچک جلوی گمشدن نوشتن چنددقیقهای را میگیرد.
نمونهٔ زیر مدل تصمیم رابط است، نه جایگزین آمادهٔ فایل پروژه. آن را در Node یا کنسول اجرا کنید تا ببینید خطا نباید متن را خالی کند.
function settleDraft(draft, result) {
if (result === 'confirmed') return { text: '', action: 'done' };
if (result === 'conflict') return { text: draft, action: 'reload-and-review' };
return { text: draft, action: 'check-server-state' };
}
console.log(settleDraft('پاسخ من', 'conflict'));
// { text: 'پاسخ من', action: 'reload-and-review' }
console.log(settleDraft('پاسخ من', 'network-unknown').text); // پاسخ منخطای ارتباط همیشه به معنی ثبتنشدن نیست
فرض کنید سرور پیام را ذخیره کرده اما پاسخ قبل از رسیدن به مرورگر قطع شده است. تکرار فوری POST ساخت تیکت میتواند تیکت دوم بسازد، چون این نسخهٔ آموزشی برای ساخت رکورد کلید idempotency ندارد. دکمهٔ disabled فقط یک تب را کنترل میکند و جلوی retry لایهٔ دیگر یا درخواست مستقیم را نمیگیرد. برای عملیات نوشتنی، ابتدا وضعیت را بازخوانی کنید؛ در محصول واقعی، قرارداد شناسهٔ عملیات و نگهداری نتیجهٔ آن لازم میشود.
کنترل version پاسخ تیکت، همان idempotency عمومی نیست. ارسال با نسخهٔ قدیمی معمولاً تعارض میگیرد، اما از این ویژگی نباید نتیجه بگیرید همهٔ عملیات پروژه دقیقاً یک بار اجرا میشوند. عملیات ساخت و تغییر نسخه قراردادهای جدا دارند. همچنین abort سمت مرورگر، تغییر ذخیرهشدهٔ سرور را عقب برنمیگرداند. عبارت «لغو شد» در رابط باید محدودهٔ مشخصی داشته باشد.
تمرین، پاسخ راهنما و معیار تحویل
دو تب با یک هویت آزمایشی باز کنید؛ هر دو یک نسخهٔ تیکت را بازخوانی کنند. تب اول پاسخ بدهد، سپس تب دوم متن متفاوت ارسال کند. پاسخ راهنما: تب دوم باید 409 بگیرد، متنش باقی بماند و پس از بازخوانی پیام تازهٔ تب اول را ببیند. این بار نسخه را دستی در DevTools افزایش ندهید؛ هدف تمرین بازبینی تغییر است. سپس تیکت را ببندید و نشان دهید پاسخ به تیکت بسته بدون بازگشایی رد میشود.
تحویل شامل شرح نسخهٔ اولیه و نهایی، وضعیت دو پاسخ و شاهد حفظ پیشنویس است. برای چالش، طراحی متنی کلید idempotency برای ساخت تیکت بنویسید: کلید به چه هویتی وابسته است، چه زمانی منقضی میشود و تکرار کلید با بدنهٔ متفاوت چه رفتاری دارد؟ پاسخ کامل باید ذخیرهٔ همزمان نتیجه را در نظر بگیرد؛ افزودن یک UUID به بدنه بدون منطق سرور کافی نیست. این قابلیت چالش توسعه است و در بستهٔ فعلی پیاده نشده است.
منابع رسمی برای مطالعهٔ بیشتر
https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/409
https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Conditional_requests
تألیف و مثالهای اختصاصی: تحریریه درسآوند.