diff --git a/firebase/functions/index.js b/firebase/functions/index.js index b160884..8070c7f 100644 --- a/firebase/functions/index.js +++ b/firebase/functions/index.js @@ -293,47 +293,152 @@ exports.sendNotificationOnEventCreation = functions.firestore } }); +// Store batches in Firestore instead of memory +async function addToUpdateBatch(eventData, eventId) { + const batchId = `${eventData.familyId}_${eventData.lastModifiedBy}`; + const batchRef = admin.firestore().collection('UpdateBatches').doc(batchId); + + try { + await admin.firestore().runTransaction(async (transaction) => { + const batchDoc = await transaction.get(batchRef); + + if (!batchDoc.exists) { + // Create new batch + transaction.set(batchRef, { + familyId: eventData.familyId, + lastModifiedBy: eventData.lastModifiedBy, + externalOrigin: eventData.externalOrigin, + events: [{ + id: eventId, + title: eventData.title, + startDate: eventData.startDate + }], + createdAt: admin.firestore.FieldValue.serverTimestamp(), + processed: false + }); + } else { + // Update existing batch + const existingEvents = batchDoc.data().events || []; + transaction.update(batchRef, { + events: [...existingEvents, { + id: eventId, + title: eventData.title, + startDate: eventData.startDate + }] + }); + } + }); + } catch (error) { + console.error('Error adding to update batch:', error); + throw error; + } +} + exports.onEventUpdate = functions.firestore .document('Events/{eventId}') .onUpdate(async (change, context) => { const beforeData = change.before.data(); const afterData = change.after.data(); - const {familyId, title, lastModifiedBy} = afterData; + const {familyId, title, lastModifiedBy, externalOrigin, startDate} = afterData; - // Skip if no meaningful changes if (JSON.stringify(beforeData) === JSON.stringify(afterData)) { return null; } try { - // Get push tokens excluding the user who made the change - const pushTokens = await getPushTokensForFamily(familyId, lastModifiedBy); - - const message = `Event "${title}" has been updated`; - await sendNotifications(pushTokens, { - title: "Event Updated", - body: message, - data: { - type: 'event_update', - eventId: context.params.eventId - } - }); - - // Store notification in Firestore - await storeNotification({ - type: 'event_update', + await addToUpdateBatch({ familyId, - content: message, - eventId: context.params.eventId, - excludedUser: lastModifiedBy, - timestamp: admin.firestore.FieldValue.serverTimestamp(), - date: eventData.startDate - }); + title, + lastModifiedBy, + externalOrigin, + startDate + }, context.params.eventId); } catch (error) { - console.error('Error sending event update notification:', error); + console.error('Error in onEventUpdate:', error); } }); +// Separate function to process batches +exports.processUpdateBatches = functions.pubsub + .schedule('every 1 minutes') + .onRun(async (context) => { + const batchesRef = admin.firestore().collection('UpdateBatches'); + + // Find unprocessed batches older than 5 seconds + const cutoff = new Date(Date.now() - 5000); + const snapshot = await batchesRef + .where('processed', '==', false) + .where('createdAt', '<=', cutoff) + .get(); + + const processPromises = snapshot.docs.map(async (doc) => { + const batchData = doc.data(); + + try { + const pushTokens = await getPushTokensForFamily( + batchData.familyId, + batchData.lastModifiedBy + ); + + if (pushTokens.length) { + let message; + if (batchData.externalOrigin) { + message = `Calendar sync completed: ${batchData.events.length} events have been updated`; + } else { + message = batchData.events.length === 1 + ? `Event "${batchData.events[0].title}" has been updated` + : `${batchData.events.length} events have been updated`; + } + + await sendNotifications(pushTokens, { + title: batchData.externalOrigin ? "Calendar Sync Complete" : "Events Updated", + body: message, + data: { + type: 'event_update', + count: batchData.events.length + } + }); + + await storeNotification({ + type: 'event_update', + familyId: batchData.familyId, + content: message, + excludedUser: batchData.lastModifiedBy, + timestamp: admin.firestore.FieldValue.serverTimestamp(), + count: batchData.events.length, + date: batchData.events[0].startDate + }); + } + + // Mark batch as processed + await doc.ref.update({ + processed: true, + processedAt: admin.firestore.FieldValue.serverTimestamp() + }); + + } catch (error) { + console.error(`Error processing batch ${doc.id}:`, error); + } + }); + + await Promise.all(processPromises); + }); + +// Cleanup old batches +exports.cleanupUpdateBatches = functions.pubsub + .schedule('every 24 hours') + .onRun(async (context) => { + const batchesRef = admin.firestore().collection('UpdateBatches'); + const dayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000); + + const oldBatches = await batchesRef + .where('processedAt', '<=', dayAgo) + .get(); + + const deletePromises = oldBatches.docs.map(doc => doc.ref.delete()); + await Promise.all(deletePromises); + }); + // Upcoming Event Reminders exports.checkUpcomingEvents = functions.pubsub .schedule('every 5 minutes')