@remit/calendar-service 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/calendar-service",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "iCalendar (RFC 5545) parsing, projection and recurrence expansion over the calendar store",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -328,6 +328,24 @@ describe("expandCalendar", () => {
328
328
  assert.notEqual(expansion.expandedThrough, "");
329
329
  });
330
330
 
331
+ it("marks a series whose first occurrence falls past the horizon", async () => {
332
+ // The rule's own first instance is excluded, so the iterator's first slot
333
+ // is 2029 and the horizon closes in 2027. Nothing is written, but the
334
+ // series is not complete: `""` here would hide it from the live expansion
335
+ // and the event would render nowhere, ever.
336
+ const expansion = await expand(
337
+ singleEvent(
338
+ "DTSTART:20250301T090000Z",
339
+ "DTEND:20250301T100000Z",
340
+ "RRULE:FREQ=YEARLY;INTERVAL=4",
341
+ "EXDATE:20250301T090000Z",
342
+ ),
343
+ );
344
+
345
+ assert.deepEqual(expansion.occurrences, []);
346
+ assert.equal(expansion.expandedThrough, "2025-03-01T09:00:00Z");
347
+ });
348
+
331
349
  it("says nothing about a horizon for a series that ends inside it", async () => {
332
350
  const expansion = await expand(
333
351
  singleEvent(
package/src/expand.ts CHANGED
@@ -107,7 +107,7 @@ interface Walk {
107
107
  occurrences: CalendarOccurrenceInput[];
108
108
  /** Whether the walk stopped on a bound rather than on the series ending. */
109
109
  truncated: boolean;
110
- /** Start of the last slot the iterator itself produced, or `""` for none. */
110
+ /** Start of the last slot the walk collected, or `""` when it collected none. */
111
111
  lastIteratedStart: string;
112
112
  }
113
113
 
@@ -258,12 +258,19 @@ export const expandCalendar = (
258
258
  maxSteps: Number.POSITIVE_INFINITY,
259
259
  });
260
260
 
261
+ // Every truncated walk is marked, including one that collected nothing — a
262
+ // series whose first occurrence lands past the horizon. Its index is empty
263
+ // from the series start, which is the honest floor to write; `""` would
264
+ // claim the whole series is held and the live expansion would never run.
261
265
  return {
262
266
  occurrences: walk.occurrences,
263
- expandedThrough:
264
- walk.truncated && walk.lastIteratedStart
265
- ? toUtcIso(Date.parse(walk.lastIteratedStart))
266
- : "",
267
+ expandedThrough: walk.truncated
268
+ ? toUtcIso(
269
+ walk.lastIteratedStart
270
+ ? Date.parse(walk.lastIteratedStart)
271
+ : seriesStart,
272
+ )
273
+ : "",
267
274
  };
268
275
  };
269
276
 
@@ -245,6 +245,52 @@ describe("listCalendarInstances", () => {
245
245
  assert.equal(instances[0]?.summary, "Weekly one-to-one");
246
246
  });
247
247
 
248
+ it("expands a series whose index is empty because its first occurrence is past the horizon", async () => {
249
+ const calendar = collection();
250
+ // The rule's own first instance is excluded, so the first occurrence is in
251
+ // 2029 and the horizon closed in 2027. The index holds no row at all, and
252
+ // the only thing that can serve the event is the live expansion.
253
+ const distant = await store(
254
+ calendar,
255
+ "distant.ics",
256
+ ical(
257
+ "BEGIN:VCALENDAR",
258
+ "VERSION:2.0",
259
+ "BEGIN:VEVENT",
260
+ "UID:distant@example.com",
261
+ "DTSTART:20250301T090000Z",
262
+ "DTEND:20250301T100000Z",
263
+ "SUMMARY:Leap day review",
264
+ "RRULE:FREQ=YEARLY;INTERVAL=4",
265
+ "EXDATE:20250301T090000Z",
266
+ "END:VEVENT",
267
+ "END:VCALENDAR",
268
+ ),
269
+ );
270
+
271
+ assert.deepEqual(distant.rows, [], "the index really is empty");
272
+ assert.deepEqual(
273
+ await repositories([distant]).calendarObject.listIncompleteExpansions(
274
+ calendar.calendarId,
275
+ "2029-04-01T00:00:00Z",
276
+ ),
277
+ [distant.object],
278
+ "and the resource is still offered to the live expansion",
279
+ );
280
+
281
+ const instances = await listCalendarInstances(
282
+ repositories([distant]),
283
+ [calendar],
284
+ { from: "2029-03-01T00:00:00Z", to: "2029-04-01T00:00:00Z" },
285
+ );
286
+
287
+ assert.deepEqual(
288
+ instances.map((instance) => instance.startAt),
289
+ ["2029-03-01T09:00:00Z"],
290
+ );
291
+ assert.equal(instances[0]?.summary, "Leap day review");
292
+ });
293
+
248
294
  it("serves a live-expanded series from one source, never twice", async () => {
249
295
  const calendar = collection();
250
296
  const openEnded = await store(