الأبحاث · جولةُ البرمجة الأولى

CB-3-02 — تتبّع خطأ وإصلاحه · JavaScript/TypeScript · متوسّط

جولةُ البرمجة الأولى · 2026-10-16 · GAIBRA v2.31.03 · واجبُ التشغيل

نتيجةُ الجولة

الترتيبالمتنافسالرمز أمام المحكّمالكلماتالاختباراتOpusGrokالمجموعالصدق (Opus · Grok)
1GAIBRAب1,48913/1395.0100.097.511 · 12
2Claude Sonnetهـ91613/1392.095.093.511 · 11
3ChatGPTج77213/1387.092.089.510 · 10
4DeepSeekأ82013/1382.090.086.010 · 11
5Geminiد3516/1328.252.240.21 · 8

نصُّ السيناريو كما كتبه المولّد

We run a small hotel booking backend, Node 18, plain JavaScript, stdlib only (we deliberately avoid external deps, in-memory Map as the "database" for now). QA just caught that two guests managed to book the same room type with overlapping dates. The availability endpoint said "available" to both, and both book calls succeeded. booking.js (simplified but the real logic is the same): JavaScript // booking.js const rooms = new Map(); // roomId -> \[{guest, checkIn, checkOut}] function overlaps(aIn, aOut, bIn, bOut) { &#x20; return aIn < bOut \&\& bIn < aOut; } function isAvailable(roomId, checkIn, checkOut) { &#x20; const stays = rooms.get(roomId) || \[]; &#x20; return stays.every(s => !overlaps(checkIn, checkOut, s.checkIn, s.checkOut)); } async function book(roomId, checkIn, checkOut, guest) { &#x20; // simulate DB round-trip / validation steps &#x20; await new Promise(r => setTimeout(r, 10)); &#x20; if (!isAvailable(roomId, checkIn, checkOut)) { &#x20; throw new Error(`Room ${roomId} not available for ${checkIn}..${checkOut}`); &#x20; } &#x20; await new Promise(r => setTimeout(r, 10)); &#x20; const stays = rooms.get(roomId) || \[]; &#x20; stays.push({ guest, checkIn, checkOut }); &#x20; rooms.set(roomId, stays); &#x20; return { ok: true, roomId, guest }; } module.exports = { book, isAvailable, rooms }; Repro (repro.js): JavaScript const { book } = require("./booking"); Promise.all(\[ &#x20; book("R-101", "2026-11-01", "2026-11-05", "guest-A"), &#x20; book("R-101", "2026-11-03", "2026-11-07", "guest-B"), ]).then(rs => console.log(rs)); // both say ok:true — should not happen Trace from QA's log isn't an exception — it's the absence of one; both confirmations were emailed. Constraints: keep the module API identical (book, isAvailable exported), stdlib only, no real DB. The fix must hold under Promise.all with many concurrent callers for the same room and for different rooms (no global serialization bottleneck that blocks independent rooms). Dates arrive as ISO strings and can span year boundaries.

الردودُ الخمسة كما وصلت المحكّمين

الرمزُ هو ما رآه المحكّمان؛ والاسمُ كُشف بعد التحكيم.

بGAIBRA97.5 · 1,489 كلمة
Summary The double booking comes from an unguarded gap between the availability check and the write; a per-room promise lock closes it without blocking other rooms. The repro produces exactly the reported symptom when traced by hand. The fix is only as strong as the process boundary, because an in-memory lock protects a single Node process. Next step: confirm how many Node processes serve bookings in production. I traced your original booking.js by hand on repro.js and did not run it. The code does produce the symptom QA saw. Your design treats "check, then book" as one step. Between the two sits an await, so the code assumes an ordering that nothing enforces. Hand trace of repro.js on the original code t = 0 Both book calls start guest-A and guest-B each schedule a 10 ms timer and yield to the event loop. t ≈ 10 ms A checks availability rooms has no entry for R-101, so isAvailable returns true. A schedules its second 10 ms timer and yields. t ≈ 10 ms B checks availability rooms is still empty because A has not written yet. isAvailable returns true and B yields. t ≈ 20 ms A writes, then B writes Both stays are pushed into R-101 and both calls return ok: true. No exception is thrown, which is why the log is silent. The exact point of divergence is the second await inside book. Once a call passes the check, it gives up the thread before it commits, and every other caller for the same room can pass the same check against the same stale state. Twenty concurrent callers would all pass it together. One more defect hides behind the dates. The overlap test compares strings, which is chronological only for strict zero-padded YYYY-MM-DD values. With that format, "2026-12-30" < "2027-01-02" holds across the year boundary. A value like "2026-11-1" or a timestamp with an offset would compare incorrectly, and the code would accept it without complaint. Fix Each room gets its own FIFO promise chain. The check and the write run inside it with nothing able to interleave. Different rooms hold different chains, so they never wait on each other. // booking.js 'use strict'; const ISO\_DATE\_PATTERN = /^(\\d{4})-(\\d{2})-(\\d{2})$/; const SIMULATED\_ROUND\_TRIP\_MS = 10; const rooms = new Map(); // roomId -> \[{guest, checkIn, checkOut}] const roomLockTails = new Map(); // roomId -> promise settled when the last queued holder releases function delay(ms) { &#x20; return new Promise(resolve => setTimeout(resolve, ms)); } function assertNonEmptyString(value, name) { &#x20; if (typeof value !== 'string' || value.trim() === '') { &#x20; throw new TypeError(`${name} must be a non-empty string`); &#x20; } } /\*\* Validates a strict YYYY-MM-DD calendar date; fixed width keeps string order chronological. \*/ function assertIsoDate(value, name) { &#x20; assertNonEmptyString(value, name); &#x20; const match = ISO\_DATE\_PATTERN.exec(value); &#x20; if (!match) { &#x20; throw new RangeError(`${name} must be YYYY-MM-DD, got "${value}"`); &#x20; } &#x20; const year = Number(match\[1]); &#x20; const month = Number(match\[2]); &#x20; const day = Number(match\[3]); &#x20; const probe = new Date(Date.UTC(year, month - 1, day)); &#x20; if (probe.getUTCFullYear() !== year || probe.getUTCMonth() !== month - 1 || probe.getUTCDate() !== day) { &#x20; throw new RangeError(`${name} is not a real calendar date: "${value}"`); &#x20; } } function assertStayRange(roomId, checkIn, checkOut) { &#x20; assertNonEmptyString(roomId, 'roomId'); &#x20; assertIsoDate(checkIn, 'checkIn'); &#x20; assertIsoDate(checkOut, 'checkOut'); &#x20; if (!(checkIn < checkOut)) { &#x20; throw new RangeError(`checkIn ${checkIn} must be before checkOut ${checkOut}`); &#x20; } } function overlaps(aIn, aOut, bIn, bOut) { &#x20; return aIn < bOut \&\& bIn < aOut; } function hasNoConflict(roomId, checkIn, checkOut) { &#x20; const stays = rooms.get(roomId) || \[]; &#x20; return stays.every(s => !overlaps(checkIn, checkOut, s.checkIn, s.checkOut)); } /\*\* &#x20;\* Runs criticalSection exclusively for one roomId; other rooms are unaffected. &#x20;\* Waiters for the same room are served in arrival order. &#x20;\*/ async function withRoomLock(roomId, criticalSection) { &#x20; const previousTail = roomLockTails.get(roomId) || Promise.resolve(); &#x20; let release; &#x20; const gate = new Promise(resolve => { release = resolve; }); &#x20; const myTail = previousTail.then(() => gate); &#x20; roomLockTails.set(roomId, myTail); &#x20; await previousTail; &#x20; try { &#x20; return await criticalSection(); &#x20; } finally { &#x20; release(); &#x20; if (roomLockTails.get(roomId) === myTail) { &#x20; roomLockTails.delete(roomId); &#x20; } &#x20; } } /\*\* &#x20;\* Advisory snapshot: true if no stored stay overlaps \[checkIn, checkOut). &#x20;\* Only book() is authoritative under concurrency. &#x20;\*/ function isAvailable(roomId, checkIn, checkOut) { &#x20; assertStayRange(roomId, checkIn, checkOut); &#x20; return hasNoConflict(roomId, checkIn, checkOut); } /\*\* &#x20;\* Books \[checkIn, checkOut) for guest. The check and the write are atomic per room. &#x20;\* @returns {Promise<{ok: true, roomId: string, guest: string}>} &#x20;\*/ async function book(roomId, checkIn, checkOut, guest) { &#x20; assertStayRange(roomId, checkIn, checkOut); &#x20; assertNonEmptyString(guest, 'guest'); &#x20; await delay(SIMULATED\_ROUND\_TRIP\_MS); // validation round-trip; reads no booking state &#x20; return withRoomLock(roomId, async () => { &#x20; if (!hasNoConflict(roomId, checkIn, checkOut)) { &#x20; throw new Error(`Room ${roomId} not available for ${checkIn}..${checkOut}`); &#x20; } &#x20; await delay(SIMULATED\_ROUND\_TRIP\_MS); // write round-trip, held under the room lock &#x20; const stays = rooms.get(roomId) || \[]; &#x20; stays.push({ guest, checkIn, checkOut }); &#x20; rooms.set(roomId, stays); &#x20; return { ok: true, roomId, guest }; &#x20; }); } module.exports = { book, isAvailable, rooms }; // repro.js 'use strict'; const assert = require('node:assert/strict'); const { book, isAvailable, rooms } = require('./booking'); async function sameRoomOverlapRejectsSecond() { &#x20; const \[a, b] = await Promise.allSettled(\[ &#x20; book('R-101', '2026-11-01', '2026-11-05', 'guest-A'), &#x20; book('R-101', '2026-11-03', '2026-11-07', 'guest-B'), &#x20; ]); &#x20; assert.equal(a.status, 'fulfilled'); &#x20; assert.deepEqual(a.value, { ok: true, roomId: 'R-101', guest: 'guest-A' }); &#x20; assert.equal(b.status, 'rejected'); &#x20; assert.equal(b.reason.message, 'Room R-101 not available for 2026-11-03..2026-11-07'); &#x20; assert.equal(rooms.get('R-101').length, 1); } async function twentyIdenticalCallersExactlyOneWins() { &#x20; const results = await Promise.allSettled( &#x20; Array.from({ length: 20 }, (\_, i) => book('R-303', '2026-11-10', '2026-11-12', `guest-${i}`))); &#x20; assert.equal(results.filter(r => r.status === 'fulfilled').length, 1); &#x20; assert.equal(rooms.get('R-303').length, 1); } async function yearBoundaryOverlapAndAdjacency() { &#x20; const results = await Promise.allSettled(\[ &#x20; book('R-404', '2026-12-30', '2027-01-02', 'guest-X'), &#x20; book('R-404', '2027-01-01', '2027-01-03', 'guest-Y'), &#x20; book('R-404', '2027-01-02', '2027-01-05', 'guest-Z'), &#x20; ]); &#x20; assert.deepEqual(results.map(r => r.status), \['fulfilled', 'rejected', 'fulfilled']); } async function independentRoomIsNotBlocked() { &#x20; const finished = \[]; &#x20; const queue = Array.from({ length: 10 }, (\_, i) => { &#x20; const from = `2026-12-${String(i + 1).padStart(2, '0')}`; &#x20; const to = `2026-12-${String(i + 2).padStart(2, '0')}`; &#x20; return book('R-505', from, to, `q-${i}`).then(() => finished.push(`R-505#${i}`)); &#x20; }); &#x20; const other = book('R-606', '2026-12-01', '2026-12-03', 'solo').then(() => finished.push('R-606')); &#x20; await Promise.all(\[...queue, other]); &#x20; assert.ok(finished.indexOf('R-606') < finished.indexOf('R-505#9')); } async function invalidInputRejected() { &#x20; await assert.rejects(book('R-707', '2026-02-30', '2026-03-02', 'g'), RangeError); &#x20; await assert.rejects(book('R-707', '2026-11-1', '2026-11-03', 'g'), RangeError); &#x20; assert.throws(() => isAvailable('R-707', '2026-11-05', '2026-11-01'), RangeError); } (async () => { &#x20; await sameRoomOverlapRejectsSecond(); &#x20; await twentyIdenticalCallersExactlyOneWins(); &#x20; await yearBoundaryOverlapAndAdjacency(); &#x20; await independentRoomIsNotBlocked(); &#x20; await invalidInputRejected(); &#x20; console.log('all self-checks passed'); })().catch(err => { console.error(err); process.exitCode = 1; }); Run node repro.js. Any failed assertion sets a non-zero exit code. The new code behaves differently from the original in these ways: Overlapping same-room calls: the later arrival is now rejected with the original error message, and same-room bookings commit in lock-arrival order. Malformed dates: non-YYYY-MM-DD or impossible dates now raise RangeError from both book and isAvailable. Date ranges: zero-length or reversed ranges now raise RangeError. Empty values: an empty roomId or guest now raises TypeError. Validation timing: invalid input fails before the first delay rather than after it. New module state: the module holds a second internal Map, which empties itself when a room's queue drains. Unchanged: the return value and the exports are the same as before. I did not add cancellation. Your constraint "keep the module API identical" rules out an extra AbortSignal parameter. Root Cause Explanation JavaScript's single thread makes only synchronous stretches atomic. Every await is a point where other callers can run. The problem is systemic rather than isolated: any read, then await, then write against the shared Map has the same race. That includes cancel or modify paths if they follow this shape, which I cannot see here. isAvailable remains a snapshot that can be stale the moment it returns, so the UI's "available" should be read as a hint, and only book gives the final answer. This work covers the engineering. What a double booking costs you in refunds or compensation is a separate question that this fix does not address. Prevention Make it a review rule that no await may separate a check from its commit unless both sit inside the room lock. Keep the Promise.all tests above in CI. ESLint's require-atomic-updates rule covers a narrower pattern and would not have caught the Map push. QA had no error message to give you, so one fact decides whether this fix is enough. Does production run a single Node process, or several (cluster, PM2, multiple containers)? With one process, this lock closes the race. With several, each process holds its own rooms Map, and the fix must move to a shared store with an atomic conditional write. My recommendation is to ship this lock now if you run a single process. The decision is yours. What I do not know: The code to be checked
هـClaude Sonnet93.5 · 916 كلمة
The double booking is fixed. Your repro now gives one fulfilled and one rejected, and the stress test passes. Cause: book() checked availability and then, after another await, pushed the stay. Both callers passed the check before either one inserted, so both succeeded. That is a check-then-act race across an await boundary. Fix: Each room now has its own promise-chain lock, and the availability check and the insert run together inside it. The API is unchanged and it uses only the stdlib. Because the lock is per room, independent rooms never wait on each other, and a rejected booking doesn’t block the queue behind it. I also remove each lock entry when its queue goes idle, so the lock map doesn’t grow. Dates: isAvailable now compares parsed timestamps instead of raw ISO strings. It throws on unparseable dates, and book rejects a check-in on or after check-out. Checkout equal to the next check-in is still allowed, so back-to-back stays work. Stress test (stress.js): Your repro: exactly one booking succeeds. Year boundary: the overlapping stay is rejected and the adjacent one is accepted. 500 concurrent random bookings across 20 rooms: no overlaps in any room (154 ms). 100 independent rooms: 21 ms, so there is no global serialization. Rejected middle booking: the bookings queued behind it still succeed. Limits: This lock only protects a single Node process. With multiple processes or instances, you’ll need the real DB’s constraint or transaction (e.g. an exclusion constraint on room and date range) to enforce the same guarantee. Files are in /home/claude/booking: booking.js stress.js Booking JS  // booking.js const rooms = new Map(); // roomId -> \[{guest, checkIn, checkOut}] // Per-room FIFO lock: roomId -> tail of that room's promise chain. // Independent rooms never wait on each other. const locks = new Map(); function withRoomLock(roomId, fn) { &#x20; const prev = locks.get(roomId) || Promise.resolve(); &#x20; const run = prev.then(fn); // starts only after the previous holder finished &#x20; const tail = run.then(() => {}, () => {}); // a failure must not poison the chain &#x20; locks.set(roomId, tail); &#x20; tail.then(() => { &#x20; // Drop the entry once the queue is idle so the Map doesn't grow forever. &#x20; if (locks.get(roomId) === tail) locks.delete(roomId); &#x20; }); &#x20; return run; } function toMs(value) { &#x20; const t = Date.parse(value); &#x20; if (Number.isNaN(t)) throw new Error(`Invalid date: ${value}`); &#x20; return t; } function overlaps(aIn, aOut, bIn, bOut) { &#x20; return aIn < bOut \&\& bIn < aOut; } function isAvailable(roomId, checkIn, checkOut) { &#x20; const inMs = toMs(checkIn); &#x20; const outMs = toMs(checkOut); &#x20; const stays = rooms.get(roomId) || \[]; &#x20; return stays.every(s => !overlaps(inMs, outMs, toMs(s.checkIn), toMs(s.checkOut))); } async function book(roomId, checkIn, checkOut, guest) { &#x20; if (toMs(checkIn) >= toMs(checkOut)) { &#x20; throw new Error(`Invalid range ${checkIn}..${checkOut}`); &#x20; } &#x20; // simulate DB round-trip / validation steps (outside the lock: no shared state touched) &#x20; await new Promise(r => setTimeout(r, 10)); &#x20; // Check and insert are one critical section per room. &#x20; return withRoomLock(roomId, async () => { &#x20; if (!isAvailable(roomId, checkIn, checkOut)) { &#x20; throw new Error(`Room ${roomId} not available for ${checkIn}..${checkOut}`); &#x20; } &#x20; await new Promise(r => setTimeout(r, 10)); // second round-trip, now safe &#x20; const stays = rooms.get(roomId) || \[]; &#x20; stays.push({ guest, checkIn, checkOut }); &#x20; rooms.set(roomId, stays); &#x20; return { ok: true, roomId, guest }; &#x20; }); } module.exports = { book, isAvailable, rooms }; Stress · JS const assert = require("assert"); const { book, rooms } = require("./booking"); &#x20; (async () => { &#x20; // 1. Original repro &#x20; let rs = await Promise.allSettled(\[ &#x20; book("R-101", "2026-11-01", "2026-11-05", "A"), &#x20; book("R-101", "2026-11-03", "2026-11-07", "B"), &#x20; ]); &#x20; assert.strictEqual(rs.filter(r => r.status === "fulfilled").length, 1); &#x20; &#x20; // 2. Year boundary, adjacent stays (checkout == checkin must be allowed) &#x20; rs = await Promise.allSettled(\[ &#x20; book("R-102", "2026-12-28", "2027-01-02", "A"), &#x20; book("R-102", "2027-01-02", "2027-01-05", "B"), // adjacent: ok &#x20; book("R-102", "2027-01-01", "2027-01-03", "C"), // overlaps both &#x20; ]); &#x20; assert.strictEqual(rs.filter(r => r.status === "fulfilled").length, 2); &#x20; &#x20; // 3. Heavy contention: 500 callers, 20 rooms, random ranges &#x20; const calls = \[]; &#x20; for (let i = 0; i < 500; i++) { &#x20; const day = Math.floor(Math.random() \* 40) + 1; &#x20; const len = Math.floor(Math.random() \* 5) + 1; &#x20; const d = n => new Date(Date.UTC(2026, 11, n)).toISOString().slice(0, 10); &#x20; calls.push(book(`T-${i % 20}`, d(day), d(day + len), `g${i}`)); &#x20; } &#x20; const t0 = Date.now(); &#x20; await Promise.allSettled(calls); &#x20; console.log("500 calls took", Date.now() - t0, "ms"); &#x20; &#x20; // Invariant: no overlaps in any room &#x20; for (const \[id, stays] of rooms) { &#x20; for (let i = 0; i < stays.length; i++) &#x20; for (let j = i + 1; j < stays.length; j++) { &#x20; const a = stays\[i], b = stays\[j]; &#x20; assert(!(a.checkIn < b.checkOut \&\& b.checkIn < a.checkOut), `overlap in ${id}`); &#x20; } &#x20; } &#x20; &#x20; // 4. Independent rooms run in parallel (not serialized) &#x20; const t1 = Date.now(); &#x20; await Promise.all(Array.from({ length: 100 }, (\_, i) => &#x20; book(`P-${i}`, "2026-11-01", "2026-11-02", "g"))); &#x20; const dt = Date.now() - t1; &#x20; assert(dt < 200, `rooms serialized? took ${dt}ms`); &#x20; console.log("100 independent rooms took", dt, "ms"); &#x20; &#x20; // 5. A rejected booking must not block the queue &#x20; const r = await Promise.allSettled(\[ &#x20; book("E-1", "2026-11-01", "2026-11-05", "A"), &#x20; book("E-1", "2026-11-02", "2026-11-03", "B"), &#x20; book("E-1", "2026-11-05", "2026-11-06", "C"), &#x20; ]); &#x20; assert.deepStrictEqual(r.map(x => x.status), \["fulfilled", "rejected", "fulfilled"]); &#x20; console.log("ALL PASS"); })().catch(e => { console.error(e); process.exit(1); }); &#x20;
جChatGPT89.5 · 772 كلمة
The bug is a classic time-of-check to time-of-use (TOCTOU) race condition. Both book() calls reach an await before checking availability. Node.js can interleave their execution so that both calls observe an empty booking list, both pass isAvailable(), and both insert overlapping stays. The fix is to make the availability check and booking insertion one indivisible operation for each room. Since you're using an in-memory Map in Node 18, you can achieve this with a per-room promise queue, using only the standard library. Important design requirements: Same room: booking operations must execute sequentially. Different rooms: bookings must proceed independently. Availability queries: retain the existing synchronous isAvailable() API. Date ranges: preserve the existing half-open interval rule, where checkout on the same day as another guest's check-in is allowed. Fixed booking.js 'use strict'; const rooms = new Map(); // roomId -> \[{ guest, checkIn, checkOut }] const roomQueues = new Map(); // roomId -> promise representing its queue function overlaps(aIn, aOut, bIn, bOut) { &#x20; const aStart = Date.parse(aIn); &#x20; const aEnd = Date.parse(aOut); &#x20; const bStart = Date.parse(bIn); &#x20; const bEnd = Date.parse(bOut); &#x20; return aStart < bEnd \&\& bStart < aEnd; } function validateStay(checkIn, checkOut) { &#x20; const start = Date.parse(checkIn); &#x20; const end = Date.parse(checkOut); &#x20; if (!Number.isFinite(start) || !Number.isFinite(end) || start >= end) { &#x20; throw new Error('Invalid stay: checkOut must be after checkIn'); &#x20; } } function isAvailable(roomId, checkIn, checkOut) { &#x20; validateStay(checkIn, checkOut); &#x20; const stays = rooms.get(roomId) || \[]; &#x20; return stays.every( &#x20; s => !overlaps(checkIn, checkOut, s.checkIn, s.checkOut) &#x20; ); } // Serialize operations for the same room only. function withRoomLock(roomId, action) { &#x20; const previous = roomQueues.get(roomId) || Promise.resolve(); &#x20; let release; &#x20; const gate = new Promise(resolve => { &#x20; release = resolve; &#x20; }); &#x20; // This queue entry resolves only after this operation releases its lock. &#x20; const queued = previous.then(() => gate); &#x20; roomQueues.set(roomId, queued); &#x20; return previous &#x20; .then(action) &#x20; .finally(() => { &#x20; release(); &#x20; // Do not delete a queue entry if another caller is waiting. &#x20; if (roomQueues.get(roomId) === queued) { &#x20; roomQueues.delete(roomId); &#x20; } &#x20; }); } async function book(roomId, checkIn, checkOut, guest) { &#x20; validateStay(checkIn, checkOut); &#x20; return withRoomLock(roomId, async () => { &#x20; // Simulate DB round-trip / validation steps. &#x20; await new Promise(r => setTimeout(r, 10)); &#x20; // Check and insert while holding the same room's lock. &#x20; if (!isAvailable(roomId, checkIn, checkOut)) { &#x20; throw new Error( &#x20; `Room ${roomId} not available for ${checkIn}..${checkOut}` &#x20; ); &#x20; } &#x20; await new Promise(r => setTimeout(r, 10)); &#x20; const stays = rooms.get(roomId) || \[]; &#x20; stays.push({ guest, checkIn, checkOut }); &#x20; rooms.set(roomId, stays); &#x20; return { ok: true, roomId, guest }; &#x20; }); } module.exports = { book, isAvailable, rooms }; Why this works 1\. Same-room bookings are serialized The first caller acquires the logical lock. The second waits until the first finishes its availability check and insertion. It then sees the first booking and rejects if the dates overlap. 2\. Different rooms remain independent Each room has its own queue. A slow booking for R-101 doesn't block a booking for R-202. 3\. Failures release the lock The finally() block releases the queue even when validation or booking fails. A rejected booking therefore doesn't permanently block subsequent callers. Test the race condition Your existing repro.js can stay unchanged. With the corrected module, Promise.all() rejects because one booking fails. To observe both outcomes without stopping at the first rejection, use: const { book, rooms } = require("./booking"); async function main() { &#x20; const results = await Promise.allSettled(\[ &#x20; book("R-101", "2026-11-01", "2026-11-05", "guest-A"), &#x20; book("R-101", "2026-11-03", "2026-11-07", "guest-B"), &#x20; ]); &#x20; console.log(results); &#x20; console.log("Stored bookings:", rooms.get("R-101")); } main(); Expected result: One booking is fulfilled. One booking is rejected with Room R-101 not available.... Exactly one stay is stored for R-101. The winner is whichever request enters the queue first. Additional considerations Checkout boundaries: November 1–5 and November 5–7 are compatible. November 1–5 and November 4–7 overlap. Year boundaries: Parsing ISO dates into timestamps correctly handles dates across December and January. Availability endpoint: isAvailable() remains synchronous and is only a snapshot. A positive result does not guarantee a later booking will succeed; book() must always recheck under the lock. Deployment limitation: This solution guarantees coordination among callers in the same Node.js process. If you later run multiple Node processes or server instances, their in-memory locks won't coordinate. At that point, you need a shared database with a suitable transaction or concurrency-control mechanism. One important detail: this assumes the booking list is modified through book(). Directly inserting into the exported rooms map bypasses the lock and can violate the guarantee.
أDeepSeek86.0 · 820 كلمة
The bug: check-then-act race across await points The root cause is that book() releases the JavaScript event loop twice (await setTimeout(...)) between the availability check and the write. The sequence for your repro is: A calls book, hits await setTimeout(10) → yields. B calls book, hits await setTimeout(10) → yields. A resumes, isAvailable returns true (Map is empty), hits the second await → yields. B resumes, isAvailable returns true (A hasn't written yet), hits the second await → yields. A resumes, pushes its stay. B resumes, pushes its stay. Both book calls observe the same "empty" snapshot. This is a classic TOCTOU (time-of-check to time-of-use) race. Map being single-threaded doesn't save you, because the critical section is split by await. Fix: a per-room async mutex (promise chain), no global lock Keep the API identical, stdlib only. Serialize only callers touching the same roomId; independent rooms proceed in parallel. The pattern is a tiny promise-chaining mutex keyed by room. js // booking.js const rooms = new Map(); // roomId -> \[{guest, checkIn, checkOut}] const locks = new Map(); // roomId -> Promise (tail of the chain) function overlaps(aIn, aOut, bIn, bOut) { &#x20; return aIn < bOut \&\& bIn < aOut; } function isAvailable(roomId, checkIn, checkOut) { &#x20; const stays = rooms.get(roomId) || \[]; &#x20; return stays.every(s => !overlaps(checkIn, checkOut, s.checkIn, s.checkOut)); } // Acquire a per-room mutex. Returns a release() function. function acquire(roomId) { &#x20; const prev = locks.get(roomId) || Promise.resolve(); &#x20; let release; &#x20; const next = new Promise(res => { release = res; }); &#x20; // Tail of the chain is prev, then our critical section, then next. &#x20; locks.set(roomId, prev.then(() => next)); &#x20; return prev.then(() => release); } async function book(roomId, checkIn, checkOut, guest) { &#x20; const release = await acquire(roomId); &#x20; try { &#x20; // simulate DB round-trip / validation steps — now inside the critical section &#x20; await new Promise(r => setTimeout(r, 10)); &#x20; if (!isAvailable(roomId, checkIn, checkOut)) { &#x20; throw new Error(`Room ${roomId} not available for ${checkIn}..${checkOut}`); &#x20; } &#x20; await new Promise(r => setTimeout(r, 10)); &#x20; const stays = rooms.get(roomId) || \[]; &#x20; stays.push({ guest, checkIn, checkOut }); &#x20; rooms.set(roomId, stays); &#x20; return { ok: true, roomId, guest }; &#x20; } finally { &#x20; release(); &#x20; } } module.exports = { book, isAvailable, rooms }; Why this is correct and satisfies the constraints Same API: book, isAvailable, rooms exported unchanged. No global bottleneck: the lock is keyed by roomId. book("R-101", …) and book("R-202", …) never wait on each other. Only same-room callers queue. Holds under Promise.all with many callers: the critical section (check + write) is now atomic with respect to other book calls for the same room. Each queued caller re-runs isAvailable after the previous writer committed, so the second booking sees the first stay and throws. Dates as ISO strings: the fix doesn't touch date handling; overlaps already does lexicographic comparison, which is correct for ISO-8601 YYYY-MM-DD including year boundaries (e.g. "2026-12-30" < "2027-01-02"). If you ever accept datetimes with mixed offsets, that's a separate normalization concern. stdlib only, in-memory Map: yes. Important caveats to flag to the team The lock table grows forever. locks keeps an entry per room even after the queue drains. For a long-lived process with many room IDs, either delete the entry when the chain is idle, or use a ref-counted lock map. A simple safe cleanup: after release(), if locks.get(roomId) is the promise we installed and no one is waiting, locks.delete(roomId). Don't delete blindly — a naive delete can drop a waiter. The safest minimal version keeps the entry (bounded by number of rooms, which is fine for a hotel). isAvailable called directly is still racy as an advisory check. It's fine for a read-only "show availability" endpoint, but the decision must be made inside the lock. Don't let callers do if (isAvailable(...)) await book(...) and assume atomicity — that reintroduces TOCTOU. If you want a single atomic "check and reserve," have them call book and catch the error. Process-level only. This mutex works because there's one Node process. The moment you run multiple instances or move to a real DB, you need a DB-level uniqueness/transaction guarantee (e.g. a serializable transaction, or a unique constraint on overlapping ranges via an exclusion constraint). The in-memory lock is a stopgap for the single-process Map "database." Failure handling: try/finally ensures release() always runs, so a thrown "not available" error doesn't deadlock the room's queue. Minimal test to confirm js // repro.js const { book } = require("./booking"); Promise.allSettled(\[ &#x20; book("R-101", "2026-11-01", "2026-11-05", "guest-A"), &#x20; book("R-101", "2026-11-03", "2026-11-07", "guest-B"), ]).then(rs => console.log(rs.map(r => r.status))); // => \[ 'fulfilled', 'rejected' ] And a year-boundary / different-room sanity check: js Promise.all(\[ &#x20; book("R-101", "2026-12-30", "2027-01-02", "A"), &#x20; book("R-101", "2027-01-01", "2027-01-03", "B"), // overlapping -> rejected &#x20; book("R-202", "2026-12-30", "2027-01-02", "C"), // different room -> ok, no wait ]).then(...) The first two serialize on R-101; the third runs in parallel with them.
دGemini40.2 · 351 كلمة
The root cause of the race condition is the yield point created by await (the simulated I/O delay) between checking availability and updating state. When concurrent calls to book() execute, both pass isAvailable(...) before either has written the new booking to rooms. To fix this without external dependencies or a global bottleneck, you can lock executions per roomId by chaining promises on a key-specific queue. Fixed booking.js JavaScript // booking.js const rooms = new Map(); // roomId -> \[{guest, checkIn, checkOut}] const roomLocks = new Map(); // roomId -> Promise function overlaps(aIn, aOut, bIn, bOut) { &#x20; return aIn < bOut \&\& bIn < aOut; } function isAvailable(roomId, checkIn, checkOut) { &#x20; const stays = rooms.get(roomId) || \[]; &#x20; return stays.every(s => !overlaps(checkIn, checkOut, s.checkIn, s.checkOut)); } // Queue execution sequentially for a given roomId function withRoomLock(roomId, fn) { &#x20; const previousLock = roomLocks.get(roomId) || Promise.resolve(); &#x20; &#x20; // Create a new promise in the chain that resolves when fn finishes &#x20; const nextLock = previousLock &#x20; .then(() => fn()) &#x20; .catch(() => {}); // Catch errors to ensure subsequent calls in the queue run &#x20; roomLocks.set(roomId, nextLock); &#x20; // Return the actual execution promise so errors/results bubble to the caller &#x20; return previousLock.then(() => fn()); } async function book(roomId, checkIn, checkOut, guest) { &#x20; return withRoomLock(roomId, async () => { &#x20; // simulate DB round-trip / validation steps &#x20; await new Promise(r => setTimeout(r, 10)); &#x20; if (!isAvailable(roomId, checkIn, checkOut)) { &#x20; throw new Error(`Room ${roomId} not available for ${checkIn}..${checkOut}`); &#x20; } &#x20; await new Promise(r => setTimeout(r, 10)); &#x20; const stays = rooms.get(roomId) || \[]; &#x20; stays.push({ guest, checkIn, checkOut }); &#x20; rooms.set(roomId, stays); &#x20; return { ok: true, roomId, guest }; &#x20; }); } module.exports = { book, isAvailable, rooms }; Key Improvements Per-Room Mutex via Promise Chaining: Requests targeting the same roomId are queued sequentially, ensuring that availability checks and updates operate atomically per room. Concurrent Multi-Room Scalability: Requests targeting different roomId values run in parallel without blocking each other. Clean API: Preserves all exported function signatures (book, isAvailable, rooms) and relies strictly on Node stdlib features.