Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | 3x 3x 3x 49x 34x 30x 24x 15x 37x 9x 9x 9x 28x 11x 17x 3x | /**
* Owner-left handler — server-side event-driven response to the
* `ownerLeft/{roomId}` RTDB signal (armed by the owner client via
* `onDisconnect().setValue(...)` when entering an ACTIVE room).
*
* Replaces the staleRooms cron's role in detecting and acting on owner
* disconnects. The cron polled every 5 minutes; this handler reacts within
* RTDB's onDisconnect latency (seconds). The operator's refined state machine
* (2026-06-04) determines the action:
*
* - if the owner is still present somewhere (multi-device, reconnect within
* the handler's processing window) — NOOP
* - if the room is no longer ACTIVE (already AWAY/CLOSED via a concurrent
* path) — NOOP
* - if ACTIVE and at least one non-owner is OCCUPIED on a seat — transition
* to OWNER_AWAY (the existing client-driven countdown will then close it)
* - if ACTIVE and no non-owner is seated — close the room IMMEDIATELY
* (the cron's "no holdouts" branch would have closed it; we just don't
* wait the 5-minute tick)
*
* The decision is pure (`decideOwnerLeftAction`), the application is the
* transactional wrapper (`applyOwnerLeftTx`). The orchestrator (which performs
* the RTDB presence re-check, opens the Firestore transaction, applies the
* action, and clears the signal) lives in the listener module.
*/
const { hasNonOwnerSeated } = require('./room-auth');
const { reapStaleRoomTx } = require('./stale-room-reap');
const OWNER_LEFT_ACTION = Object.freeze({
NOOP: 'NOOP',
OWNER_AWAY: 'OWNER_AWAY',
CLOSE_IMMEDIATE: 'CLOSE_IMMEDIATE',
});
/**
* Decide what to do given the room's current Firestore shape + whether the
* owner is still present in RTDB (re-checked by the caller AFTER the signal
* fired, to close the TOCTOU window where the owner reconnects between
* signal arrival and handler execution, or is present on another device).
*
* @param {object|null|undefined} room - the room doc (from Firestore.get())
* @param {boolean} ownerStillPresent - result of the post-signal isUserPresent
* re-check; if true, do nothing
* @returns {string} one of OWNER_LEFT_ACTION values
*/
function decideOwnerLeftAction(room, ownerStillPresent) {
if (ownerStillPresent) return OWNER_LEFT_ACTION.NOOP;
if (!room || !room.state) return OWNER_LEFT_ACTION.NOOP;
if (room.state !== 'ACTIVE') return OWNER_LEFT_ACTION.NOOP;
if (hasNonOwnerSeated(room)) return OWNER_LEFT_ACTION.OWNER_AWAY;
return OWNER_LEFT_ACTION.CLOSE_IMMEDIATE;
}
/**
* Apply the decided action inside a Firestore transaction.
*
* NOOP / unknown actions are intentionally a no-op write (zero t.update calls)
* so the caller can safely invoke this for every signal event without branching
* on the action upstream.
*
* @param {FirebaseFirestore.Transaction} t
* @param {FirebaseFirestore.DocumentReference} roomRef
* @param {object} room - the pre-transition room shape (from t.get())
* @param {string} action - one of OWNER_LEFT_ACTION values
* @param {number} nowMs - server time at the start of this transaction
* @returns {object} the post-transition room shape (room merged with the patch)
*/
function applyOwnerLeftTx(t, roomRef, room, action, nowMs) {
if (action === OWNER_LEFT_ACTION.OWNER_AWAY) {
const patch = { state: 'OWNER_AWAY', ownerLeftAt: nowMs };
t.update(roomRef, patch);
return { ...room, ...patch };
}
if (action === OWNER_LEFT_ACTION.CLOSE_IMMEDIATE) {
// Reuse the close payload + write through reapStaleRoomTx so the close
// shape stays a single source of truth (ownerLeftAt: null invariant from
// PR #996's reviewer-Critical finding lives in buildClosePayload).
return reapStaleRoomTx(t, roomRef, room, nowMs);
}
// NOOP or unknown — return the room as-is, no write.
return room;
}
module.exports = {
OWNER_LEFT_ACTION,
decideOwnerLeftAction,
applyOwnerLeftTx,
};
|