This commit is contained in:
parent
a0821f1ae7
commit
1457fb4920
1 changed files with 61 additions and 24 deletions
|
|
@ -10,24 +10,31 @@ import {
|
|||
* Materializes each Event's recurrence rule into `eventOccurrence` rows for
|
||||
* a rolling window.
|
||||
*
|
||||
* Per event we wipe every future occurrence and regenerate from the current
|
||||
* rule. Occurrences are derivative pointers to the parent event, so there
|
||||
* is no editor-edited state to preserve — rule edits and recurring→once
|
||||
* transitions stay trivially correct.
|
||||
* Two modes:
|
||||
* - `replace` (default): wipe every future occurrence and regenerate from
|
||||
* the current rule. Used by the Events `afterChange` hook, where the rule
|
||||
* or date may have changed — wiping keeps rule edits and recurring→once
|
||||
* transitions trivially correct.
|
||||
* - `append`: never delete; only create occurrences whose slot doesn't yet
|
||||
* exist. Used by the periodic backfill job, where the rule hasn't changed
|
||||
* and we only want to extend the window forward without churning (and
|
||||
* re-id'ing) occurrences that are already correct.
|
||||
*
|
||||
* Weekday and time-of-day come directly from `event.date`. Calendar-date
|
||||
* generation lives in scheduleOccurrences.ts (DST-safe by construction:
|
||||
* dates are built at local midnight and then stitched with the event's
|
||||
* HH:mm).
|
||||
*
|
||||
* Exposed both as a Payload Jobs Queue task (weekly cron backfill) and as a
|
||||
* direct function called from the Events `afterChange` hook so edits are
|
||||
* reflected immediately without waiting for a queue worker.
|
||||
* Exposed both as a Payload Jobs Queue task (cron backfill) and as a direct
|
||||
* function called from the Events `afterChange` hook so edits are reflected
|
||||
* immediately without waiting for a queue worker.
|
||||
*/
|
||||
|
||||
const DEFAULT_WEEKS_AHEAD = 8
|
||||
const MS_PER_WEEK = 7 * 24 * 60 * 60 * 1000
|
||||
|
||||
type RegenerateMode = 'replace' | 'append'
|
||||
|
||||
type GenerateEventOccurrencesInput = {
|
||||
weeksAhead?: number
|
||||
eventId?: string
|
||||
|
|
@ -60,19 +67,22 @@ export const regenerateOccurrencesForEvent = async ({
|
|||
req,
|
||||
weeksAhead = DEFAULT_WEEKS_AHEAD,
|
||||
now = new Date(),
|
||||
mode = 'replace',
|
||||
}: {
|
||||
event: EventLike
|
||||
payload: Payload
|
||||
req?: PayloadRequest
|
||||
weeksAhead?: number
|
||||
now?: Date
|
||||
mode?: RegenerateMode
|
||||
}): Promise<GenerateEventOccurrencesOutput> => {
|
||||
const horizon = new Date(now.getTime() + weeksAhead * MS_PER_WEEK)
|
||||
|
||||
// Snapshot cancelled dates so manual cancellations survive the wipe/regen.
|
||||
// Matching by ISO date works because regen derives timestamps from the same
|
||||
// event.date stepped by whole calendar days — shi)fts in event.date
|
||||
// intentionally drop stale cancellations.
|
||||
// Existing future occurrences. Used to (replace) snapshot cancellations so
|
||||
// they survive the wipe, and (append) dedup so we don't recreate slots that
|
||||
// already exist. Matching by ISO date works because regen derives
|
||||
// timestamps from the same event.date stepped by whole calendar days —
|
||||
// shifts in event.date intentionally drop stale cancellations.
|
||||
const existing = await payload.find({
|
||||
collection: 'eventOccurrence',
|
||||
where: {
|
||||
|
|
@ -91,7 +101,19 @@ export const regenerateOccurrencesForEvent = async ({
|
|||
.filter((occ) => occ.cancelled === true && typeof occ.date === 'string')
|
||||
.map((occ) => new Date(occ.date as string).toISOString()),
|
||||
)
|
||||
const existingSlots = new Set(
|
||||
existing.docs
|
||||
.filter((occ) => typeof occ.date === 'string')
|
||||
.map((occ) => new Date(occ.date as string).toISOString()),
|
||||
)
|
||||
|
||||
let deleted = 0
|
||||
let created = 0
|
||||
let skipped = 0
|
||||
|
||||
if (mode === 'replace') {
|
||||
// Wipe future occurrences and rebuild from the current rule. Used by the
|
||||
// Event hook, where the rule/date may have changed.
|
||||
const wipe = await payload.delete({
|
||||
collection: 'eventOccurrence',
|
||||
where: {
|
||||
|
|
@ -102,9 +124,8 @@ export const regenerateOccurrencesForEvent = async ({
|
|||
},
|
||||
req,
|
||||
})
|
||||
let deleted = wipe.docs.length
|
||||
let created = 0
|
||||
let skipped = 0
|
||||
deleted = wipe.docs.length
|
||||
}
|
||||
|
||||
if (!event.date) {
|
||||
return { created, deleted, skipped: skipped + 1 }
|
||||
|
|
@ -128,16 +149,21 @@ export const regenerateOccurrencesForEvent = async ({
|
|||
const recurrenceType = event.recurrenceType ?? 'none'
|
||||
|
||||
if (recurrenceType === 'none') {
|
||||
const iso = eventDate.toISOString()
|
||||
if (eventDate.getTime() < now.getTime()) {
|
||||
return { created, deleted, skipped: skipped + 1 }
|
||||
}
|
||||
// In append mode the slot may already exist (not wiped) — leave it alone.
|
||||
if (mode === 'append' && existingSlots.has(iso)) {
|
||||
return { created, deleted, skipped: skipped + 1 }
|
||||
}
|
||||
await payload.create({
|
||||
collection: 'eventOccurrence',
|
||||
data: {
|
||||
event: event.id,
|
||||
date: eventDate.toISOString(),
|
||||
date: iso,
|
||||
endDateTime: endFor(eventDate),
|
||||
cancelled: cancelledDates.has(eventDate.toISOString()),
|
||||
cancelled: cancelledDates.has(iso),
|
||||
generated: true,
|
||||
},
|
||||
req,
|
||||
|
|
@ -152,6 +178,12 @@ export const regenerateOccurrencesForEvent = async ({
|
|||
const dates = expandEventOccurrences(event, { now, horizon: effectiveEnd })
|
||||
for (const date of dates) {
|
||||
const iso = date.toISOString()
|
||||
// In append mode the future occurrences were never wiped, so leave any
|
||||
// slot that already has an occurrence alone.
|
||||
if (mode === 'append' && existingSlots.has(iso)) {
|
||||
skipped += 1
|
||||
continue
|
||||
}
|
||||
await payload.create({
|
||||
collection: 'eventOccurrence',
|
||||
data: {
|
||||
|
|
@ -163,6 +195,7 @@ export const regenerateOccurrencesForEvent = async ({
|
|||
},
|
||||
req,
|
||||
})
|
||||
existingSlots.add(iso)
|
||||
created += 1
|
||||
}
|
||||
|
||||
|
|
@ -214,6 +247,10 @@ export const generateEventOccurrencesTask: TaskConfig<{
|
|||
payload,
|
||||
weeksAhead,
|
||||
now,
|
||||
// Periodic backfill: only extend the window forward. The rule hasn't
|
||||
// changed here, so don't wipe/recreate occurrences that are already
|
||||
// correct (that only happens via the Event hook on an actual edit).
|
||||
mode: 'append',
|
||||
})
|
||||
created += result.created
|
||||
deleted += result.deleted
|
||||
|
|
|
|||
Loading…
Reference in a new issue