@remit/calendar-service 0.0.18 → 0.0.19

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.
Files changed (73) hide show
  1. package/dist/accept.d.ts +71 -0
  2. package/dist/accept.js +159 -0
  3. package/dist/build.d.ts +79 -0
  4. package/dist/build.js +196 -0
  5. package/dist/correct.d.ts +4 -0
  6. package/dist/correct.js +22 -0
  7. package/dist/errors.d.ts +56 -0
  8. package/dist/errors.js +1 -0
  9. package/dist/etag.d.ts +9 -0
  10. package/{src/etag.ts → dist/etag.js} +1 -3
  11. package/dist/expand.d.ts +73 -0
  12. package/dist/expand.js +188 -0
  13. package/dist/feed.d.ts +81 -0
  14. package/dist/feed.js +157 -0
  15. package/dist/fixtures.d.ts +19 -0
  16. package/dist/fixtures.js +37 -0
  17. package/dist/index.d.ts +16 -0
  18. package/dist/index.js +15 -0
  19. package/dist/memory-store.d.ts +105 -0
  20. package/dist/memory-store.js +319 -0
  21. package/dist/parse.d.ts +39 -0
  22. package/dist/parse.js +87 -0
  23. package/dist/project.d.ts +30 -0
  24. package/dist/project.js +57 -0
  25. package/dist/put.d.ts +52 -0
  26. package/dist/put.js +78 -0
  27. package/dist/reopen.d.ts +14 -0
  28. package/dist/reopen.js +92 -0
  29. package/dist/scope.d.ts +70 -0
  30. package/dist/scope.js +687 -0
  31. package/dist/subscribe.d.ts +96 -0
  32. package/dist/subscribe.js +298 -0
  33. package/dist/suggest.d.ts +74 -0
  34. package/dist/suggest.js +138 -0
  35. package/dist/time.d.ts +76 -0
  36. package/dist/time.js +164 -0
  37. package/dist/window.d.ts +85 -0
  38. package/dist/window.js +161 -0
  39. package/package.json +40 -35
  40. package/src/accept.test.ts +0 -403
  41. package/src/accept.ts +0 -250
  42. package/src/build.test.ts +0 -113
  43. package/src/build.ts +0 -311
  44. package/src/correct.test.ts +0 -173
  45. package/src/correct.ts +0 -40
  46. package/src/errors.ts +0 -62
  47. package/src/etag.test.ts +0 -54
  48. package/src/expand.test.ts +0 -361
  49. package/src/expand.ts +0 -303
  50. package/src/feed.test.ts +0 -334
  51. package/src/feed.ts +0 -203
  52. package/src/feeds/google-secret-address.ics +0 -99
  53. package/src/fixtures.ts +0 -52
  54. package/src/index.ts +0 -128
  55. package/src/memory-store.ts +0 -485
  56. package/src/parse.test.ts +0 -264
  57. package/src/parse.ts +0 -138
  58. package/src/project.test.ts +0 -247
  59. package/src/project.ts +0 -122
  60. package/src/put.test.ts +0 -318
  61. package/src/put.ts +0 -133
  62. package/src/reopen.test.ts +0 -529
  63. package/src/reopen.ts +0 -199
  64. package/src/scope.test.ts +0 -1421
  65. package/src/scope.ts +0 -1003
  66. package/src/subscribe.test.ts +0 -582
  67. package/src/subscribe.ts +0 -433
  68. package/src/suggest.test.ts +0 -376
  69. package/src/suggest.ts +0 -244
  70. package/src/time.ts +0 -260
  71. package/src/window.test.ts +0 -648
  72. package/src/window.ts +0 -281
  73. package/tsconfig.json +0 -8
package/src/etag.test.ts DELETED
@@ -1,54 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import { computeEtag } from "./etag.js";
4
- import { singleEvent } from "./fixtures.js";
5
- import { parseCalendar, serializeCalendar } from "./parse.js";
6
-
7
- const RESOURCE = singleEvent(
8
- "DTSTART:20260826T090000Z",
9
- "DTEND:20260826T100000Z",
10
- "SUMMARY:Quarterly review",
11
- "X-MICROSOFT-CDO-BUSYSTATUS:BUSY",
12
- );
13
-
14
- const reserialize = async (icalData: string): Promise<string> => {
15
- const parsed = await parseCalendar(icalData);
16
- assert.ok(parsed.ok);
17
- return serializeCalendar(parsed.value.component);
18
- };
19
-
20
- describe("computeEtag", () => {
21
- it("is a bare sha256 hex digest, unquoted", () => {
22
- assert.match(computeEtag(RESOURCE), /^[0-9a-f]{64}$/);
23
- });
24
-
25
- it("is stable across a parse and serialize", async () => {
26
- const once = await reserialize(RESOURCE);
27
-
28
- assert.equal(computeEtag(await reserialize(once)), computeEtag(once));
29
- });
30
-
31
- it("moves when the event changes", () => {
32
- assert.notEqual(
33
- computeEtag(RESOURCE),
34
- computeEtag(
35
- singleEvent(
36
- "DTSTART:20260826T090000Z",
37
- "DTEND:20260826T110000Z",
38
- "SUMMARY:Quarterly review",
39
- "X-MICROSOFT-CDO-BUSYSTATUS:BUSY",
40
- ),
41
- ),
42
- );
43
- });
44
-
45
- it("hashes the stored bytes, so line endings are never normalized away", () => {
46
- // Two resources that differ only in line endings are two different
47
- // resources: the store keeps what it was given, and a tag computed over a
48
- // normalized copy would report them as the same bytes.
49
- assert.notEqual(
50
- computeEtag(RESOURCE),
51
- computeEtag(RESOURCE.replace(/\r\n/g, "\n")),
52
- );
53
- });
54
- });
@@ -1,361 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import { CalendarEventStatus, CalendarTransparency } from "@remit/domain-enums";
4
- import {
5
- CALENDAR_EXPANSION_HORIZON_DAYS,
6
- CALENDAR_EXPANSION_MAX_OCCURRENCES,
7
- expandCalendar,
8
- } from "./expand.js";
9
- import { AMSTERDAM_VTIMEZONE, ical, singleEvent } from "./fixtures.js";
10
- import { parseCalendar } from "./parse.js";
11
-
12
- const expand = async (icalData: string, timezone = "") => {
13
- const parsed = await parseCalendar(icalData);
14
- assert.ok(parsed.ok, `expected a parse, got ${JSON.stringify(parsed)}`);
15
- return expandCalendar(parsed.value, timezone);
16
- };
17
-
18
- const amsterdamSeries = (...eventLines: string[]): string =>
19
- ical(
20
- "BEGIN:VCALENDAR",
21
- "VERSION:2.0",
22
- ...AMSTERDAM_VTIMEZONE,
23
- "BEGIN:VEVENT",
24
- "UID:weekly@example.com",
25
- "DTSTART;TZID=Europe/Amsterdam:20261015T090000",
26
- "DTEND;TZID=Europe/Amsterdam:20261015T100000",
27
- ...eventLines,
28
- "END:VEVENT",
29
- "END:VCALENDAR",
30
- );
31
-
32
- describe("expandCalendar", () => {
33
- it("writes a single occurrence under an empty recurrenceId", async () => {
34
- const expansion = await expand(
35
- singleEvent("DTSTART:20260826T090000Z", "DTEND:20260826T100000Z"),
36
- );
37
-
38
- assert.deepEqual(expansion.occurrences, [
39
- {
40
- recurrenceId: "",
41
- startAt: "2026-08-26T09:00:00Z",
42
- endAt: "2026-08-26T10:00:00Z",
43
- allDay: false,
44
- summary: "",
45
- status: CalendarEventStatus.Confirmed,
46
- transparency: CalendarTransparency.Opaque,
47
- },
48
- ]);
49
- assert.equal(expansion.expandedThrough, "");
50
- });
51
-
52
- it("keeps a weekly series at its local hour across the end of DST", async () => {
53
- // 09:00 in Amsterdam is 07:00Z until the clocks go back on 2026-10-25 and
54
- // 08:00Z after. An expansion that iterated instants instead of local times
55
- // would move the meeting an hour for half the year.
56
- const expansion = await expand(
57
- amsterdamSeries("RRULE:FREQ=WEEKLY;COUNT=4"),
58
- );
59
-
60
- assert.deepEqual(
61
- expansion.occurrences.map((occurrence) => occurrence.startAt),
62
- [
63
- "2026-10-15T07:00:00Z",
64
- "2026-10-22T07:00:00Z",
65
- "2026-10-29T08:00:00Z",
66
- "2026-11-05T08:00:00Z",
67
- ],
68
- );
69
- assert.equal(expansion.expandedThrough, "");
70
- });
71
-
72
- it("keeps the same hour when the zone is named but not defined in the resource", async () => {
73
- const expansion = await expand(
74
- ical(
75
- "BEGIN:VCALENDAR",
76
- "VERSION:2.0",
77
- "BEGIN:VEVENT",
78
- "UID:weekly@example.com",
79
- "DTSTART;TZID=Europe/Berlin:20261015T090000",
80
- "DTEND;TZID=Europe/Berlin:20261015T100000",
81
- "RRULE:FREQ=WEEKLY;COUNT=4",
82
- "END:VEVENT",
83
- "END:VCALENDAR",
84
- ),
85
- );
86
-
87
- assert.deepEqual(
88
- expansion.occurrences.map((occurrence) => occurrence.startAt),
89
- [
90
- "2026-10-15T07:00:00Z",
91
- "2026-10-22T07:00:00Z",
92
- "2026-10-29T08:00:00Z",
93
- "2026-11-05T08:00:00Z",
94
- ],
95
- );
96
- });
97
-
98
- it("carries each occurrence's own end", async () => {
99
- const expansion = await expand(
100
- amsterdamSeries("RRULE:FREQ=WEEKLY;COUNT=4"),
101
- );
102
-
103
- assert.deepEqual(
104
- expansion.occurrences.map((occurrence) => occurrence.endAt),
105
- [
106
- "2026-10-15T08:00:00Z",
107
- "2026-10-22T08:00:00Z",
108
- "2026-10-29T09:00:00Z",
109
- "2026-11-05T09:00:00Z",
110
- ],
111
- );
112
- });
113
-
114
- it("drops an EXDATEd occurrence and keeps the rest at their slots", async () => {
115
- const expansion = await expand(
116
- amsterdamSeries(
117
- "RRULE:FREQ=WEEKLY;COUNT=4",
118
- "EXDATE;TZID=Europe/Amsterdam:20261022T090000",
119
- ),
120
- );
121
-
122
- assert.deepEqual(
123
- expansion.occurrences.map((occurrence) => occurrence.recurrenceId),
124
- ["2026-10-15T07:00:00Z", "2026-10-29T08:00:00Z", "2026-11-05T08:00:00Z"],
125
- );
126
- });
127
-
128
- it("moves an overridden occurrence but keeps the slot it replaces", async () => {
129
- const expansion = await expand(
130
- ical(
131
- "BEGIN:VCALENDAR",
132
- "VERSION:2.0",
133
- ...AMSTERDAM_VTIMEZONE,
134
- "BEGIN:VEVENT",
135
- "UID:weekly@example.com",
136
- "DTSTART;TZID=Europe/Amsterdam:20261015T090000",
137
- "DTEND;TZID=Europe/Amsterdam:20261015T100000",
138
- "RRULE:FREQ=WEEKLY;COUNT=3",
139
- "END:VEVENT",
140
- "BEGIN:VEVENT",
141
- "UID:weekly@example.com",
142
- "RECURRENCE-ID;TZID=Europe/Amsterdam:20261022T090000",
143
- "DTSTART;TZID=Europe/Amsterdam:20261022T140000",
144
- "DTEND;TZID=Europe/Amsterdam:20261022T150000",
145
- "END:VEVENT",
146
- "END:VCALENDAR",
147
- ),
148
- );
149
-
150
- const moved = expansion.occurrences.find(
151
- (occurrence) => occurrence.recurrenceId === "2026-10-22T07:00:00Z",
152
- );
153
- assert.equal(moved?.startAt, "2026-10-22T12:00:00Z");
154
- assert.equal(moved?.endAt, "2026-10-22T13:00:00Z");
155
- });
156
-
157
- it("indexes an override whose slot the rule never produces", async () => {
158
- // What a client writes after moving one instance and then editing the
159
- // series: the override's RECURRENCE-ID names a slot the new rule does not
160
- // generate. The iterator walks only the rule's slots, so nothing but an
161
- // explicit pass over the VEVENTs reaches this event.
162
- const expansion = await expand(
163
- ical(
164
- "BEGIN:VCALENDAR",
165
- "VERSION:2.0",
166
- ...AMSTERDAM_VTIMEZONE,
167
- "BEGIN:VEVENT",
168
- "UID:weekly@example.com",
169
- "DTSTART;TZID=Europe/Amsterdam:20261015T090000",
170
- "DTEND;TZID=Europe/Amsterdam:20261015T100000",
171
- "RRULE:FREQ=WEEKLY;COUNT=2",
172
- "END:VEVENT",
173
- "BEGIN:VEVENT",
174
- "UID:weekly@example.com",
175
- "RECURRENCE-ID;TZID=Europe/Amsterdam:20261112T090000",
176
- "DTSTART;TZID=Europe/Amsterdam:20261112T140000",
177
- "DTEND;TZID=Europe/Amsterdam:20261112T153000",
178
- "END:VEVENT",
179
- "END:VCALENDAR",
180
- ),
181
- );
182
-
183
- assert.deepEqual(
184
- expansion.occurrences.map((occurrence) => occurrence.recurrenceId),
185
- ["2026-10-15T07:00:00Z", "2026-10-22T07:00:00Z", "2026-11-12T08:00:00Z"],
186
- );
187
- const stranded = expansion.occurrences.at(-1);
188
- assert.equal(stranded?.startAt, "2026-11-12T13:00:00Z");
189
- assert.equal(stranded?.endAt, "2026-11-12T14:30:00Z");
190
- });
191
-
192
- it("indexes every instance of a resource that has overrides but no rule", async () => {
193
- const expansion = await expand(
194
- ical(
195
- "BEGIN:VCALENDAR",
196
- "VERSION:2.0",
197
- "BEGIN:VEVENT",
198
- "UID:edited@example.com",
199
- "DTSTART:20260826T090000Z",
200
- "DTEND:20260826T100000Z",
201
- "END:VEVENT",
202
- "BEGIN:VEVENT",
203
- "UID:edited@example.com",
204
- "RECURRENCE-ID:20260902T090000Z",
205
- "DTSTART:20260902T110000Z",
206
- "DTEND:20260902T120000Z",
207
- "END:VEVENT",
208
- "END:VCALENDAR",
209
- ),
210
- );
211
-
212
- assert.deepEqual(
213
- expansion.occurrences.map((occurrence) => occurrence.startAt),
214
- ["2026-08-26T09:00:00Z", "2026-09-02T11:00:00Z"],
215
- );
216
- });
217
-
218
- it("reads an override's end in the override's own zone", async () => {
219
- // The moved instance was rewritten in a different zone from the series,
220
- // and its DTEND names a third. Resolving either with the master's TZID
221
- // puts this instance an hour out and changes how long it lasts.
222
- const expansion = await expand(
223
- ical(
224
- "BEGIN:VCALENDAR",
225
- "VERSION:2.0",
226
- "BEGIN:VEVENT",
227
- "UID:crosszone@example.com",
228
- "DTSTART;TZID=Europe/Berlin:20260826T090000",
229
- "DTEND;TZID=Europe/Berlin:20260826T100000",
230
- "RRULE:FREQ=WEEKLY;COUNT=2",
231
- "END:VEVENT",
232
- "BEGIN:VEVENT",
233
- "UID:crosszone@example.com",
234
- "RECURRENCE-ID;TZID=Europe/Berlin:20260902T090000",
235
- "DTSTART;TZID=Europe/London:20260902T090000",
236
- "DTEND;TZID=Europe/Lisbon:20260902T103000",
237
- "END:VEVENT",
238
- "END:VCALENDAR",
239
- ),
240
- );
241
-
242
- assert.deepEqual(
243
- expansion.occurrences.map((occurrence) => [
244
- occurrence.recurrenceId,
245
- occurrence.startAt,
246
- occurrence.endAt,
247
- ]),
248
- [
249
- [
250
- "2026-08-26T07:00:00Z",
251
- "2026-08-26T07:00:00Z",
252
- "2026-08-26T08:00:00Z",
253
- ],
254
- [
255
- "2026-09-02T07:00:00Z",
256
- "2026-09-02T08:00:00Z",
257
- "2026-09-02T09:30:00Z",
258
- ],
259
- ],
260
- );
261
- });
262
-
263
- it("expands an all-day series as whole days", async () => {
264
- const expansion = await expand(
265
- singleEvent(
266
- "DTSTART;VALUE=DATE:20260826",
267
- "DTEND;VALUE=DATE:20260827",
268
- "RRULE:FREQ=DAILY;COUNT=2",
269
- ),
270
- "Europe/Berlin",
271
- );
272
-
273
- assert.deepEqual(expansion.occurrences, [
274
- {
275
- recurrenceId: "2026-08-25T22:00:00Z",
276
- startAt: "2026-08-25T22:00:00Z",
277
- endAt: "2026-08-26T22:00:00Z",
278
- allDay: true,
279
- summary: "",
280
- status: CalendarEventStatus.Confirmed,
281
- transparency: CalendarTransparency.Opaque,
282
- },
283
- {
284
- recurrenceId: "2026-08-26T22:00:00Z",
285
- startAt: "2026-08-26T22:00:00Z",
286
- endAt: "2026-08-27T22:00:00Z",
287
- allDay: true,
288
- summary: "",
289
- status: CalendarEventStatus.Confirmed,
290
- transparency: CalendarTransparency.Opaque,
291
- },
292
- ]);
293
- });
294
-
295
- it("stops an open-ended series at the horizon and says how far it got", async () => {
296
- const expansion = await expand(
297
- singleEvent(
298
- "DTSTART:20260101T090000Z",
299
- "DTEND:20260101T100000Z",
300
- "RRULE:FREQ=DAILY",
301
- ),
302
- );
303
-
304
- assert.notEqual(expansion.expandedThrough, "");
305
- const last = expansion.occurrences[expansion.occurrences.length - 1];
306
- assert.equal(expansion.expandedThrough, last?.startAt);
307
- assert.ok(
308
- Date.parse(expansion.expandedThrough) -
309
- Date.parse("2026-01-01T09:00:00Z") <=
310
- CALENDAR_EXPANSION_HORIZON_DAYS * 24 * 60 * 60 * 1000,
311
- "the last written occurrence is inside the horizon",
312
- );
313
- });
314
-
315
- it("stops a dense series at the occurrence ceiling", async () => {
316
- const expansion = await expand(
317
- singleEvent(
318
- "DTSTART:20260101T090000Z",
319
- "DTEND:20260101T090100Z",
320
- "RRULE:FREQ=MINUTELY",
321
- ),
322
- );
323
-
324
- assert.equal(
325
- expansion.occurrences.length,
326
- CALENDAR_EXPANSION_MAX_OCCURRENCES,
327
- );
328
- assert.notEqual(expansion.expandedThrough, "");
329
- });
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
-
349
- it("says nothing about a horizon for a series that ends inside it", async () => {
350
- const expansion = await expand(
351
- singleEvent(
352
- "DTSTART:20260101T090000Z",
353
- "DTEND:20260101T100000Z",
354
- "RRULE:FREQ=WEEKLY;UNTIL=20260301T090000Z",
355
- ),
356
- );
357
-
358
- assert.equal(expansion.expandedThrough, "");
359
- assert.equal(expansion.occurrences.length, 9);
360
- });
361
- });