skip to Main Content

I’m trying to synchronize calendars, but the receiving server only wants "bite-sized" data, so I decided to sync PER USER PER MONTH.

Thus, I made a double loop:

First I loop through all users, and within that loop, I loop through the months if the date-range spans multiple months.

However, because the sync function is asynchronous, it executes multiple resources and months at the same time, because the loop doesn’t wait for completion.

I know similar questions have been asked before, but for some reason I just cannot get it to work.

Here are my functions:

function loopThroughMonths(resourceIds, startDate, endDate) {
  startDateObject = new Date(startDate);
  endDateObject = new Date(endDate);

  // Check how many months our date range spans:

  var dateRangeMonths = monthDiff(startDateObject, endDateObject);

  if (dateRangeMonths > 0) {
    // Loop through each month

    for (let i = 0; i <= dateRangeMonths; i++) {
      if (i == 0) {
        // For the first month, the starting date is equal to the start of the date range

        var loopStartDate = startDate;
        var loopEndDate = formatDate(
          lastDayOfMonth(
            startDateObject.getFullYear(),
            startDateObject.getMonth(),
          ),
        );
      }

      if (i != 0 && i != dateRangeMonths) {
        var loopMonth = new Date(
          startDateObject.getFullYear(),
          startDateObject.getMonth() + i,
          1,
        );
        var loopStartDate = formatDate(
          firstDayOfMonth(loopMonth.getFullYear(), loopMonth.getMonth()),
        );
        var loopEndDate = formatDate(
          lastDayOfMonth(loopMonth.getFullYear(), loopMonth.getMonth()),
        );
      }

      if (i == dateRangeMonths) {
        // For the last month, the end date is equal to the last date of the date range.

        var loopStartDate = formatDate(
          firstDayOfMonth(
            endDateObject.getFullYear(),
            endDateObject.getMonth(),
          ),
        );
        var loopEndDate = endDate;
      }

      loopThroughResources(resourceIds, 0, loopStartDate, loopEndDate);
    }
  } else {
    // Date range falls within 1 month, just proceed to looping through resources

    loopThroughResources(resourceIds, 0, startDate, endDate);
  }
}

function loopThroughResources(resourceIds, i, loopStartDate, loopEndDate) {
  if (i == resourceIds.length) {
    $("#start_exchange")
      .text("Synchroniseren")
      .removeAttr("disabled")
      .css("cursor", "pointer");
    return;
  }
  var resourceId = resourceIds[i];

  $("#exchange_output").append(
    "Start synchroniseren naar Outlook-agenda van " +
      resourceNames.get(resourceId) +
      " van " +
      loopStartDate +
      " tot " +
      loopEndDate +
      "...<br>",
  );

  $.post(
    "sync.php",
    {
      resourceId: resourceId,
      startDate: loopStartDate,
      endDate: loopEndDate,
    },
    function (response) {
      $("#exchange_output").append(response);
      i = i + 1;
      loopThroughResources(resourceIds, i, loopStartDate, loopEndDate);
    },
  );
}

So to explain:

loopThroughMonths first checks if startDate and endDate differ more than 0 months.
If so, it looks through each month. If not, it just immediately executes loopThroughResources.

In case the dateRangeMonths spans multiple months, we loop through them using a for loop and perform the loopThroughResources function for every month.

Thus, if we say:

Synchronise resources A, B, C from 2023-12-27 till 2024-02-16

It will do:

Sync 2023-12-27 till 2023-12-31 (part of december 2023) for resource A
Sync 2023-12-27 till 2023-12-31 (part of december 2023) for resource B
Sync 2023-12-27 till 2023-12-31 (part of december 2023) for resource C
Sync 2024-01-01 till 2024-01-31 (whole january 2023) for resource A
Sync 2024-01-01 till 2024-01-31 (whole january 2023) for resource B
Sync 2024-01-01 till 2024-01-31 (whole january 2023) for resource C
Sync 2024-02-01 till 2024-02-16 (part of february 2023) for resource A
Sync 2024-02-01 till 2024-02-16 (part of february 2023) for resource B
Sync 2024-02-01 till 2024-02-16 (part of february 2023) for resource C

The code works, but it is not waiting for all resources to complete (i.e. before the loopThroughResources function is done) before moving on to the next month.

For the resources, I even made it so that it waits until syncing resource A is complete before it proceeds to resource B, by calling the function from the $.post complete function, but I basically need another wait for the ENTIRE loopThroughResources function (and I’m guessing it needs to be something with Promises.all…)

I know I have to do something with promises, but I just can’t get it to work….
Any help would be greatly appreciated.

2

Answers


  1. You’ll have a better time if you separate the concerns of what you want to do and doing it:

    • figuring out the "date chunks" for the given range (my date math may be off here, especially sans external libraries)
    • figuring out the jobs, i.e. combinations of resource IDs + chunks
    • sending the requests
    function toDate(start) {
      return start.toISOString().split("T")[0];
    }
    
    function getFirstAndLastDayOfMonth(date) {
      let start = new Date(date);
      let end = new Date(date);
      end.setMonth(end.getMonth() + 1);
      end.setDate(0);
      return {
        start: toDate(start),
        end: toDate(end),
      };
    }
    
    function getDateRanges(startDate, endDate) {
      let startDateObject = new Date(startDate);
      let endDateObject = new Date(endDate);
      let ranges = [];
      let date = new Date(startDateObject);
      date.setDate(15);
      while (date < endDateObject) {
        ranges.push(getFirstAndLastDayOfMonth(date));
        date.setMonth(date.getMonth() + 1);
      }
      // Adjust start and end
      ranges[0].start = toDate(startDateObject);
      ranges[ranges.length - 1].end = toDate(endDateObject);
      return ranges;
    }
    
    function getSyncJobs(resourceIds, startDate, endDate) {
      const jobs = [];
      const ranges = getDateRanges(startDate, endDate);
      for (let resourceId of resourceIds) {
        for (let range of ranges) {
          jobs.push({
            resourceId,
            startDate: range.start,
            endDate: range.end,
          });
        }
      }
      return jobs;
    }
    
    function doJob(params, done) {
      // (do $.post here instead of `setTimeout`...)
      console.log(params);
      setTimeout(done, 500);
    }
    
    function go(done) {
      // Build a queue of sync jobs we consume in `doNextJob`
      const syncJobs = getSyncJobs(
        ["A", "B", "C"],
        "2023-12-27",
        "2024-02-16",
      );
    
      function doNextJob() {
        console.log(`Remaining jobs: ${syncJobs.length}`);
        if (syncJobs.length === 0) {
          return done();
        }
        const job = syncJobs.shift();
        doJob(job, doNextJob);
      }
    
      doNextJob();
    }
    
    go(function () {
      console.log("All done!");
    });

    If you can promisify $.post into something you can await, you can get rid of the callbacks altogether and the execution becomes just

    async function doJob(job) {
        console.log(job);
        await new Promise(resolve => setTimeout(resolve, 500));
    }
    
    async function go() {
      // Build a queue of sync jobs
      const syncJobs = getSyncJobs(
        ["A", "B", "C"],
        "2023-12-27",
        "2024-02-16",
      );
    
      for(let job of syncJobs) {
          await doJob(job);
      }
      console.log("All done!");
    }
    
    Login or Signup to reply.
  2. I agree with AKX that the concerns of the program should be separated. One thing I’m doing differently is keeping everything as date objects until the very last moment when we use console.log to print the formatted date. This is useful because it allows other parts of our program to access the date properties. There is no advantage to generating all of the jobs beforehand. Other parts of the program are immensely simplified –

    function formatDate(d) { return d.toISOString().split("T")[0] }
    
    function sleep(ms) { return new Promise(r => setTimeout(r, ms)) }
    
    function *dateRanges(start, end) {
      if (start.getFullYear() == end.getFullYear() && start.getMonth() == end.getMonth())
        return yield [start, end]
      yield [
        start,
        new Date(start.getFullYear(), start.getMonth() + 1, 0), // end of month  
      ]
      yield *dateRanges(
        new Date(start.getFullYear(), start.getMonth() + 1, 1), // begin of next month
        end,
      ) 
    }
    
    async function doTask(resourceId, start, end) {
      console.log(`${formatDate(start)} - ${formatDate(end)}: Resource ${resourceId}`)
      await sleep(500)
    }
    
    async function doProcess(resourceIds, start, end) {
      for (const range of dateRanges(start, end))
        for (const resourceId of resourceIds)
          await doTask(resourceId, range[0], range[1])
      return "done"
    }
    
    doProcess(["A","B","C"], new Date(2023,11,27), new Date(2024,01,16)).then(console.log)
    .as-console-wrapper { min-height: 100%; top: 0; }
    2023-12-27 - 2023-12-31: Resource A
    2023-12-27 - 2023-12-31: Resource B
    2023-12-27 - 2023-12-31: Resource C
    2024-01-01 - 2024-01-31: Resource A
    2024-01-01 - 2024-01-31: Resource B
    2024-01-01 - 2024-01-31: Resource C
    2024-02-01 - 2024-02-16: Resource A
    2024-02-01 - 2024-02-16: Resource B
    2024-02-01 - 2024-02-16: Resource C
    done
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search