fix(notif): replace status-diff with idempotent flag-claim for game start/finish pushes (F-RACE-001)
This commit is contained in:
parent
e6a8deef67
commit
6e79cd9704
|
|
@ -45,8 +45,12 @@ const admin = __importStar(require("firebase-admin"));
|
|||
exports.onGameSessionUpdate = functions.firestore
|
||||
.document('couples/{coupleId}/sessions/{sessionId}')
|
||||
.onWrite(async (change, context) => {
|
||||
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
|
||||
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
||||
const { coupleId, sessionId } = context.params;
|
||||
// The per-couple active-session lock lives at sessions/_active — it is a pointer, not a
|
||||
// game session, so it must never produce a partner notification.
|
||||
if (sessionId === '_active')
|
||||
return;
|
||||
const db = admin.firestore();
|
||||
const messaging = admin.messaging();
|
||||
// Get the session document
|
||||
|
|
@ -77,30 +81,54 @@ exports.onGameSessionUpdate = functions.firestore
|
|||
const partnerBName = (_f = (_e = userB.data()) === null || _e === void 0 ? void 0 : _e.displayName) !== null && _f !== void 0 ? _f : 'Partner B';
|
||||
const avatarA = (_g = userA.data()) === null || _g === void 0 ? void 0 : _g.photoUrl;
|
||||
const avatarB = (_h = userB.data()) === null || _h === void 0 ? void 0 : _h.photoUrl;
|
||||
// Check if session was just created (status = "active")
|
||||
const previousData = (_j = change.before.data()) !== null && _j !== void 0 ? _j : {};
|
||||
const currentData = (_k = change.after.data()) !== null && _k !== void 0 ? _k : {};
|
||||
const wasInactive = ((_l = previousData.status) !== null && _l !== void 0 ? _l : '') !== 'active';
|
||||
const isActiveNow = currentData.status === 'active';
|
||||
if (wasInactive && isActiveNow) {
|
||||
// New session started — notify the OTHER partner, naming the person who started it.
|
||||
const startedBy = currentData.startedByUserId;
|
||||
const gameType = (_m = currentData.gameType) !== null && _m !== void 0 ? _m : 'wheel';
|
||||
const recipientId = startedBy === partnerA ? partnerB : partnerA;
|
||||
const starterName = startedBy === partnerA ? partnerAName : partnerBName;
|
||||
const starterAvatar = startedBy === partnerA ? avatarA : avatarB;
|
||||
await notifyPartner(db, messaging, recipientId, starterName, gameType, 'partner_started_game', `${starterName} has started a game. Tap to join!`, coupleId, starterAvatar, sessionId);
|
||||
const currentData = (_j = change.after.data()) !== null && _j !== void 0 ? _j : {};
|
||||
if (!change.after.exists)
|
||||
return; // deletion — nothing to notify
|
||||
const status = currentData.status;
|
||||
const sessionRef = db.collection('couples').doc(coupleId).collection('sessions').doc(sessionId);
|
||||
// Detect start/finish from the session's CURRENT state + a one-time flag, NOT from the
|
||||
// change.before→change.after status diff. The atomic session start (F-RACE-001) writes the
|
||||
// session doc AND the sessions/_active pointer in a single transaction; transactional writes
|
||||
// can be delivered to onWrite with change.before === change.after (the "Snapshot has no
|
||||
// readTime" path), which made the inactive→active edge unreliable and intermittently dropped
|
||||
// the partner_started_game push. Claiming a flag inside a transaction makes each notification
|
||||
// fire exactly once no matter how the event is delivered (and prevents double-sends).
|
||||
// ── New session started ──────────────────────────────────────────────
|
||||
if (status === 'active' && !currentData.startNotifiedAt) {
|
||||
const claimed = await db.runTransaction(async (tx) => {
|
||||
const fresh = await tx.get(sessionRef);
|
||||
const d = fresh.data();
|
||||
if (!fresh.exists || !d || d.status !== 'active' || d.startNotifiedAt)
|
||||
return false;
|
||||
tx.update(sessionRef, { startNotifiedAt: admin.firestore.FieldValue.serverTimestamp() });
|
||||
return true;
|
||||
});
|
||||
if (claimed) {
|
||||
const startedBy = currentData.startedByUserId;
|
||||
const gameType = (_k = currentData.gameType) !== null && _k !== void 0 ? _k : 'wheel';
|
||||
const recipientId = startedBy === partnerA ? partnerB : partnerA;
|
||||
const starterName = startedBy === partnerA ? partnerAName : partnerBName;
|
||||
const starterAvatar = startedBy === partnerA ? avatarA : avatarB;
|
||||
await notifyPartner(db, messaging, recipientId, starterName, gameType, 'partner_started_game', `${starterName} has started a game. Tap to join!`, coupleId, starterAvatar, sessionId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Check if session was completed
|
||||
const wasActive = ((_o = previousData.status) !== null && _o !== void 0 ? _o : '') === 'active';
|
||||
const isCompletedNow = currentData.status === 'completed';
|
||||
if (wasActive && isCompletedNow) {
|
||||
// The session is complete (both partners have answered) — the reveal is ready for each of
|
||||
// them, so notify BOTH, each naming the OTHER partner.
|
||||
const gt = (_p = currentData.gameType) !== null && _p !== void 0 ? _p : 'wheel';
|
||||
await notifyPartner(db, messaging, partnerA, partnerBName, gt, 'partner_finished_game', `${partnerBName} finished — tap to see your results!`, coupleId, avatarB, sessionId);
|
||||
await notifyPartner(db, messaging, partnerB, partnerAName, gt, 'partner_finished_game', `${partnerAName} finished — tap to see your results!`, coupleId, avatarA, sessionId);
|
||||
// ── Session completed (reveal ready for both) ────────────────────────
|
||||
if (status === 'completed' && !currentData.finishNotifiedAt) {
|
||||
const claimed = await db.runTransaction(async (tx) => {
|
||||
const fresh = await tx.get(sessionRef);
|
||||
const d = fresh.data();
|
||||
if (!fresh.exists || !d || d.status !== 'completed' || d.finishNotifiedAt)
|
||||
return false;
|
||||
tx.update(sessionRef, { finishNotifiedAt: admin.firestore.FieldValue.serverTimestamp() });
|
||||
return true;
|
||||
});
|
||||
if (claimed) {
|
||||
const gt = (_l = currentData.gameType) !== null && _l !== void 0 ? _l : 'wheel';
|
||||
// Notify BOTH partners, each naming the OTHER.
|
||||
await notifyPartner(db, messaging, partnerA, partnerBName, gt, 'partner_finished_game', `${partnerBName} finished — tap to see your results!`, coupleId, avatarB, sessionId);
|
||||
await notifyPartner(db, messaging, partnerB, partnerAName, gt, 'partner_finished_game', `${partnerAName} finished — tap to see your results!`, coupleId, avatarA, sessionId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -12,6 +12,10 @@ export const onGameSessionUpdate = functions.firestore
|
|||
.onWrite(async (change, context) => {
|
||||
const { coupleId, sessionId } = context.params as { coupleId: string; sessionId: string }
|
||||
|
||||
// The per-couple active-session lock lives at sessions/_active — it is a pointer, not a
|
||||
// game session, so it must never produce a partner notification.
|
||||
if (sessionId === '_active') return
|
||||
|
||||
const db = admin.firestore()
|
||||
const messaging = admin.messaging()
|
||||
|
||||
|
|
@ -48,46 +52,66 @@ export const onGameSessionUpdate = functions.firestore
|
|||
const avatarA = userA.data()?.photoUrl as string | undefined
|
||||
const avatarB = userB.data()?.photoUrl as string | undefined
|
||||
|
||||
// Check if session was just created (status = "active")
|
||||
const previousData = change.before.data() ?? {}
|
||||
const currentData = change.after.data() ?? {}
|
||||
if (!change.after.exists) return // deletion — nothing to notify
|
||||
const status = currentData.status as string | undefined
|
||||
const sessionRef = db.collection('couples').doc(coupleId).collection('sessions').doc(sessionId)
|
||||
|
||||
const wasInactive = (previousData.status ?? '') !== 'active'
|
||||
const isActiveNow = currentData.status === 'active'
|
||||
// Detect start/finish from the session's CURRENT state + a one-time flag, NOT from the
|
||||
// change.before→change.after status diff. The atomic session start (F-RACE-001) writes the
|
||||
// session doc AND the sessions/_active pointer in a single transaction; transactional writes
|
||||
// can be delivered to onWrite with change.before === change.after (the "Snapshot has no
|
||||
// readTime" path), which made the inactive→active edge unreliable and intermittently dropped
|
||||
// the partner_started_game push. Claiming a flag inside a transaction makes each notification
|
||||
// fire exactly once no matter how the event is delivered (and prevents double-sends).
|
||||
|
||||
if (wasInactive && isActiveNow) {
|
||||
// New session started — notify the OTHER partner, naming the person who started it.
|
||||
const startedBy = currentData.startedByUserId
|
||||
const gameType = currentData.gameType ?? 'wheel'
|
||||
const recipientId = startedBy === partnerA ? partnerB : partnerA
|
||||
const starterName = startedBy === partnerA ? partnerAName : partnerBName
|
||||
const starterAvatar = startedBy === partnerA ? avatarA : avatarB
|
||||
await notifyPartner(
|
||||
db, messaging, recipientId, starterName, gameType,
|
||||
'partner_started_game', `${starterName} has started a game. Tap to join!`, coupleId,
|
||||
starterAvatar, sessionId
|
||||
)
|
||||
// ── New session started ──────────────────────────────────────────────
|
||||
if (status === 'active' && !currentData.startNotifiedAt) {
|
||||
const claimed = await db.runTransaction(async (tx) => {
|
||||
const fresh = await tx.get(sessionRef)
|
||||
const d = fresh.data()
|
||||
if (!fresh.exists || !d || d.status !== 'active' || d.startNotifiedAt) return false
|
||||
tx.update(sessionRef, { startNotifiedAt: admin.firestore.FieldValue.serverTimestamp() })
|
||||
return true
|
||||
})
|
||||
if (claimed) {
|
||||
const startedBy = currentData.startedByUserId
|
||||
const gameType = currentData.gameType ?? 'wheel'
|
||||
const recipientId = startedBy === partnerA ? partnerB : partnerA
|
||||
const starterName = startedBy === partnerA ? partnerAName : partnerBName
|
||||
const starterAvatar = startedBy === partnerA ? avatarA : avatarB
|
||||
await notifyPartner(
|
||||
db, messaging, recipientId, starterName, gameType,
|
||||
'partner_started_game', `${starterName} has started a game. Tap to join!`, coupleId,
|
||||
starterAvatar, sessionId
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Check if session was completed
|
||||
const wasActive = (previousData.status ?? '') === 'active'
|
||||
const isCompletedNow = currentData.status === 'completed'
|
||||
|
||||
if (wasActive && isCompletedNow) {
|
||||
// The session is complete (both partners have answered) — the reveal is ready for each of
|
||||
// them, so notify BOTH, each naming the OTHER partner.
|
||||
const gt = currentData.gameType ?? 'wheel'
|
||||
await notifyPartner(
|
||||
db, messaging, partnerA, partnerBName, gt,
|
||||
'partner_finished_game', `${partnerBName} finished — tap to see your results!`, coupleId,
|
||||
avatarB, sessionId
|
||||
)
|
||||
await notifyPartner(
|
||||
db, messaging, partnerB, partnerAName, gt,
|
||||
'partner_finished_game', `${partnerAName} finished — tap to see your results!`, coupleId,
|
||||
avatarA, sessionId
|
||||
)
|
||||
// ── Session completed (reveal ready for both) ────────────────────────
|
||||
if (status === 'completed' && !currentData.finishNotifiedAt) {
|
||||
const claimed = await db.runTransaction(async (tx) => {
|
||||
const fresh = await tx.get(sessionRef)
|
||||
const d = fresh.data()
|
||||
if (!fresh.exists || !d || d.status !== 'completed' || d.finishNotifiedAt) return false
|
||||
tx.update(sessionRef, { finishNotifiedAt: admin.firestore.FieldValue.serverTimestamp() })
|
||||
return true
|
||||
})
|
||||
if (claimed) {
|
||||
const gt = currentData.gameType ?? 'wheel'
|
||||
// Notify BOTH partners, each naming the OTHER.
|
||||
await notifyPartner(
|
||||
db, messaging, partnerA, partnerBName, gt,
|
||||
'partner_finished_game', `${partnerBName} finished — tap to see your results!`, coupleId,
|
||||
avatarB, sessionId
|
||||
)
|
||||
await notifyPartner(
|
||||
db, messaging, partnerB, partnerAName, gt,
|
||||
'partner_finished_game', `${partnerAName} finished — tap to see your results!`, coupleId,
|
||||
avatarA, sessionId
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue