Recurrence-Plan

Creation and Management of Recurring Events

Table of Contents:

  1. What do we need?

  2. Solution
  3. References

Goals

What do we need?

  1. The functionalities for creating recurring events, with custom recurrence patterns, like the top calendar apps out there.
  2. The functionalities for updating / deleting:
    • a single instance
    • this and following instances
    • all instances
      of a recurring event
  3. A way to track the historical records of a recurring event.

Interfaces

  interface InterfaceEvent {
    // ...existing event fields
    isBaseRecurringEvent: boolean;
    recurrenceRuleId: ObjectId;
    baseRecurringEventId: ObjectId;
    isRecurringEventException: boolean;
  }

  interface InterfaceRecurrenceRule {
    recurrenceStartDate: Date;
    recurrenceEndDate: Date;
    recurrenceRuleString: string;
    // ...recurrence specific properties (frequency, count, interval, etc.)
    latestInstanceDate: Date;
    baseRecurringEventId: ObjectId;
  }

The purpose and need for each of the fields and Interfaces will be explained in the Approach as their necessity arises.

Approach

  1. We are using the rrule libary and following the dynamic generation approach.


Creating recurring events

  1. Create event input:

    • For recurring events, along with the general EventInput, there would also be a RecurrenceRuleInput (which, if not provided, would default to infinite weekly recurrence), containing the recurrence pattern.
  2. After getting the input, we’d follow these steps (createRecurringEvent.ts):

    • Generate a recurrenceRuleString from our RecurrenceRuleInput that would specify our recurrence rule in rrule string format (generateRecurrenceRuleString.ts).

    • Create a BaseRecurringEvent that would just be like creating a normal event with isBaseRecurringEvent: true, let’s name it’s _id to be baseRecurringEventId (This is what we will use as the base event for generating instances.)

    • Get the dates of recurrence using the rrule library (getRecurringInstanceDates.ts):

      • Fix a limitEndDate, say X years ahead from the recurrence start date (depending on the recurrence frequency), that would help determine the date upto which we will generate instances in the createEvent mutation. We’ll leave the rest for dynamic generation during the events query.
      • If recurrenceEndDate: null or recurrenceEndDate > limitEndDate, we’d generate dates up to limitEndDate, and leave the rest for dynamic generation during queries.
      • If recurrenceEndDate < limitEndDate, then we just generate all the dates of recurrence.
    • Both RecurrenceRule & BaseRecurringEvent will contain these recurrenceStartDate and the recurrenceEndDate values as provided in the RecurrenceRuleInput.

      • EventInput:

        • eventStartDate: Start Date of that event instance.
        • eventEndDate: End Date of that event instance.

          These dates will be selected from the create event modal, and would specify the event duration in days. i.e. If for an event, we select eventStartDate: "2024-18-04" & eventEndDate: "2024-20-04", then all of the generated instances of that recurring event will have that two day gap between their start and end dates.

      • RecurrenceRuleInput:

        • recurrenceStartDate: Start Date of recurrence. It will be the same as the eventStartDate we select for the first instance.
        • recurrenceEndDate: End Date of recurrence. By default, it will be null, i.e. default infinite recurrence. It can be changed through the custom recurrence modal.

          Only one of recurrenceEndDate or count will exist. i.e. if we select a specific end date of recurrence, count will be null, if we chose a specific count istead, then recurrenceEndDate will be null.

    • Create a RecurrenceRule document that would contain the recurrenceRuleString and the recurrence fields for easy understanding and debugging, let’s name this document’s _id to be recurranceRuleId. Set it’s latestInstanceDate to be the last date generated during this mutation.

    • Generate the recurring event instances, make associations (attendees, user), and cache them (generateRecurringEventInstances.ts).

    • All of the instances (Event documents) we created in the previous step will be based on the EventInput data, and the remaining instances (if any) will be generated during queries, based on the BaseRecurringEvent document that we created above.

    • All of the instances would have their recurrenceRuleId field set to recurranceRuleId, and the BaseRecurringEventId set to baseRecurringEventId.


Updating recurring events

  1. For single events made recurring (updateSingleEvent.ts):

    • Get the data used to generate the instances (i.e. the current data of the event, and the latest data from the update input).
    • Follow the steps for creating a recurring event.
    • Delete the current event and its associations, as new ones would be made while generating new instances.
  2. While updating a recurring event, we will provide options to update thisInstance, thisAndFollowingInstances, & allInstances of the recurring event (updateRecurringEvent.ts).

    • Appropriate update options will be provided based on whether the recurrenceRule, or the instanceDuration (difference between event’s start and end dates), or both have changed.

      • If neither the recurrenceRule nor the instanceDuration have changed, then we will provide all three update options.
      • If the RecurrenceRule has changed, then we will not provide the option to update thisInstance, i.e. only thisAndFollowingInstances & allInstances.
      • If the instanceDuration has changed, then we will not provide the option to update allInstances, i.e. only thisInstance & thisAndFollowingInstances.
    • Update Options:

      • thisInstance: Just make a regular update on this event instance (updateThisInstance.ts)

        Updating a single recurring event instance will make it an exception instance.

      • thisAndFollowingInstances or allInstances (updateRecurringEventInstances.ts):

        • If neither of the recurrenceRule or the instanceDuration has changed, we will just perform a bulk update on the instances.

        • If either one of the recurrenceRule or the instanceDuration has changed, we will delete the current series, remove their associations and generate a new one:

          • Delete instances conforming to the old RecurrenceRule (We can do this because we are generating events dynamically, i.e. we are only creating instances upto a certain date, so not many documents have to be deleted).
          • Find the latest instance that was following the old RecurrenceRule, say latestInstance, and set the latestInstanceDate and the recurrenceEndDate of the old RecurrenceRule to be this latestInstance’s eventStartDate.
          • Generate new instances based on the new RecurrenceRule and the updated event data.
          • Now, all the previous instances would have a different RecurrenceRule than the current and future ones.
        • Update the BaseRecurringEvent document if required to have values of the current update input (which would then be used as the new base event).

        Here we’re not creating a new BaseRecurringEvent document, just updating the existing one. i.e. For one recurring event, there would only be one BaseRecurringEvent, which would connect all the instances, even accross different recurrence rules.


Deleting recurring events

  1. Deleting this instance only / deleting an exception instance (deleteSingleEvent.ts):

    • Make a regular deletion.
  2. Deleting all instances / this and future instances (deleteRecurringEventInstances.ts):

    • For deleting all instances:

      • Delete all the recurring instances with the current recurrenceRuleId.
      • If this was the latest RecurrenceRule, and there exist one or more RecurrenceRules with the same baseRecurringEventId, find the last one of them (i.e. one before the current RecurrenceRule) and update the eventEndDate of the baseRecurringEvent to be that recurrence rule’s recurrenceEndDate.
    • For this and future instances:

      • Find the event instance that was created previously to the current instance with the current recurrenceRuleId, set the latestInstanceDate and the recurrenceEndDate of the RecurrenceRule to this instance’s eventStartDate. Update the BaseRecurringEvent accordingly if the current RecurrenceRule is the latest (i.e. modifying the eventEndDate of BaseRecurringEvent to this latestInstance’s eventStartDate).
      • Delete all the recurring instances with the same recurrenceRuleId as the current instance, starting from the current date.


Updates would only be done on the BaseRecurringEvent if bulk operations being are done on the instances following the latest RecurrenceRule, because we want to generate new instances (during queries) based on the BaseRecurringEvent.

How do we ensure that?


Querying events

In the query, we would add a function for generating recurring event instances, and then query all the events and return them. Here’s the two step process:


Handling exception instances


Historical Records


References

rrule

The library we’re using that automatically generate the dates of recurrence given a RecurrenceRule.
Official repo: rrule


RecurrenceRule

A document containing the properties that represents the recurrence rule followed by a recurring event.:

  interface InterfaceRecurrenceRule {
    recurrenceStartDate: Date
    recurrenceEndDate: Date
    recurrenceRuleString: string
    frequency: ["DAILY", "WEEKLY", "MONTHLY", "YEARLY"]
    weekdays: ["MONDAY", ... , "SUNDAY"]
    interval: number
    count: number
    weekDayOccurenceInMonth: number
    latestInstanceDate: Date
    baseRecurringEventId: ObjectId
    //...other fields
  }


BaseRecurringEvent

  interface InterfaceEvent {
    //...existing event fields
    isBaseRecurringEvent: true
    eventStartDate: Date // the start of recurrence
    eventEndDate: Date // the `recurrenceEndDate` of the latest recurrence rule
  }


Recurring Event Instance

Every instance of a recurring event would have these fields:

  interface InterfaceEvent {
    //...existing event fields
    eventStartDate: Date
    eventEndDate: Date
    isBaseRecurringEvent: false
    recurrenceRuleId: ObjectId
    baseRecurringEvent: ObjectId
  }


Recurring Event Exception Instance

  interface InterfaceEvent {
    //...existing event fields
    isRecurringEventException: true
  }