@filecage/zeitmeister 0.1.0

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/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # zeitmeister
2
+ zeitmeister is an open source library for scheduling appointments over an unlimited number of calendars.
3
+
4
+ I built it for myself because I wanted a custom, slim solution that doesn't force me to give third parties
5
+ access to my calendars.
6
+
7
+ It uses
8
+ - [`@filecage/ical-ts`](https://github.com/filecage/ical-ts) for iCal parsing
9
+ - [`@jkbrzt/rrule`](https://github.com/jkbrzt/rrule) for RRULE parsing and recurrence creation
10
+ - [`@recal-dev/scheduling-sdk`](https://github.com/recal-dev/scheduling-sdk) for the scheduling logic
11
+ - [`@natelindev/tsdav`](https://github.com/natelindev/tsdav) as CalDAV client
12
+ - [`date-fns`](https://github.com/date-fns/date-fns) for DateTime- and timezone handling
13
+
14
+ ## Calendar Connectors
15
+ zeitmeister supports CalDAV and WebDAV based calendars and implements access via tsdav.
16
+
17
+ You can add a calendar to the config like this:
18
+
19
+ ```ts
20
+ type Calendar = {
21
+ // A unique identifier for this calendar. Only required if you want to reference it as SchedulingCalendar.
22
+ identifier?: string,
23
+
24
+ // Type of the calendar. Be aware that `ical` cannot be used as SchedulingCalendar because it's read-only.
25
+ adapter: 'caldav', // not supported yet: | 'googlecalendar' | 'ical',
26
+
27
+ // URIs of the exact calendars that you want to check for. Can be relative to the server URI
28
+ uris: string[],
29
+
30
+ // Currently only username/password for HTTP basic auth supported
31
+ auth: {
32
+ username: string,
33
+ password: string,
34
+ }
35
+ }
36
+ ```
37
+
38
+ ## Usage
39
+ ### Create instance
40
+ Create an instance of `zeitmeister.Scheduler` with your calendar- and availability configuration:
41
+ ```ts
42
+ import {Scheduler} from 'zeitmeister';
43
+
44
+ const scheduler = new Scheduler({
45
+ calendars,
46
+ availability: [
47
+ {
48
+ // A string in the date-fns format of `EEEE HH:mm`
49
+ start: 'monday 10:00',
50
+ end: 'monday 17:30',
51
+ },
52
+ {
53
+ // Days in the date-fns format of `EEEE`, start/end in the format of `HH:mm`
54
+ days: ['thursday', 'friday'],
55
+ start: '10:30',
56
+ end: '18:00',
57
+ }
58
+ ],
59
+ defaults: {
60
+ duration: '30m',
61
+ padding: '15m',
62
+ planningAhead: '10d',
63
+ },
64
+ scheduleActors: [],
65
+ });
66
+
67
+ type ScheduleActor = (intent: {
68
+ datetime: Date,
69
+ duration: Interval,
70
+ title: string,
71
+ description: string,
72
+ location: {type: 'virtual', url: string} | {type: 'physical', address: string},
73
+ attendee: {
74
+ emailAddress: string,
75
+ name: string,
76
+ }
77
+ }) => Promise<void>;
78
+ ```
79
+
80
+ ### Query for available timeslots
81
+ ```ts
82
+ const slots = scheduler.query({
83
+ start: new Date('2026-02-09'),
84
+ end: new Date('2026-02-21'), // Note: if you omit the "end" date, the scheduler will use `start` + your default planning ahead interval
85
+ });
86
+
87
+ type slot = {
88
+ start: Date,
89
+ end: Date,
90
+ available: boolean, // whether this slot is available or not
91
+ }
92
+ ```
93
+
94
+ This will
95
+ 1. Read your calendars and create busy times from all of your events within the query timeframe that don't have `FBTYPE` set to `FREE`
96
+ 2. Create a negative of busy times from your `availability config` for each week within the query timeframe
97
+ 3. Return available slots
98
+
99
+ ### Schedule a slot
100
+ > [!WARNING] Scheduling is not yet implemented :)
101
+
102
+ ```ts
103
+
104
+ enum ScheduleResult {
105
+ SCHEDULED,
106
+ FAILED_INVALID_INTENT,
107
+ FAILED_SLOT_UNAVAILABLE,
108
+ }
109
+
110
+ const event = scheduler.schedule(scheduleIntent);
111
+ ```
112
+
113
+ This will
114
+ 1. Verify that the intent follows all given rules (duration, padding and within the previously defined availability slots - otherwise returns `ScheduleResult.FAILED_INVALID_INTENT`)
115
+ 2. Get up-to-date calendar data (bypassing caches) to verify that the intended slot really is available (otherwise returns `ScheduleResult.FAILED_SLOT_UNAVAILABLE`)
116
+ 3. Calls all defined schedule actors with the given intent. All actors will be called asynchronously. If any actor fails, an `SchedulingError` will be thrown with all errors that occurred during scheduling. However, actors will not be aborted.
117
+
@@ -0,0 +1,178 @@
1
+ import { Duration as Duration$1 } from "date-fns";
2
+ import { ICS } from "@filecage/ical";
3
+
4
+ //#region src/types/CalendarConfig.d.ts
5
+ type CalendarConfigBase = {
6
+ /**
7
+ * A unique identifier for the calendar. Only required if the calendar is referenced.
8
+ */
9
+ identifier?: string;
10
+ /**
11
+ * Type of the calendar.
12
+ * Be aware that `ical` cannot be used as Scheduling calendar because it's read-only
13
+ */
14
+ adapter: string;
15
+ /**
16
+ * Server URI of the calendar
17
+ */
18
+ server: string;
19
+ /**
20
+ * URIs of the exact calendars that you want to check for
21
+ * Can be relative to the server URI
22
+ */
23
+ uris: string[];
24
+ };
25
+ type CalDavCalendarConfig = Omit<CalendarConfigBase, 'adapter'> & {
26
+ adapter: 'caldav';
27
+ /**
28
+ * Authentication credentials
29
+ * Currently only username/password (HTTP basic auth) supported
30
+ */
31
+ auth: {
32
+ username: string;
33
+ password: string;
34
+ };
35
+ };
36
+ type CalendarConfig = CalDavCalendarConfig;
37
+ //#endregion
38
+ //#region src/types/AvailabilityRule.d.ts
39
+ type Weekday = 'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday' | 'sunday';
40
+ type HourMinute = `${number}:${number}`;
41
+ /**
42
+ * @example {start: 'monday 10:30', end: 'friday 18:00'}
43
+ */
44
+ interface AvailabilityRuleSingle {
45
+ start: `${Weekday} ${HourMinute}`;
46
+ end: `${Weekday} ${HourMinute}`;
47
+ }
48
+ /**
49
+ * @example {start: '10:30', end: '18:30', days: ['monday', 'thursday']}
50
+ */
51
+ interface AvailabilityRuleMultiple {
52
+ start: HourMinute;
53
+ end: HourMinute;
54
+ days?: Weekday[];
55
+ }
56
+ type AvailabilityRule = AvailabilityRuleSingle | AvailabilityRuleMultiple;
57
+ //#endregion
58
+ //#region src/types/ScheduleIntent.d.ts
59
+ type ScheduleIntent = {
60
+ datetime: Date;
61
+ duration: Duration$1;
62
+ title: string;
63
+ description: string;
64
+ location: {
65
+ type: 'virtual';
66
+ url: string;
67
+ } | {
68
+ type: 'physical';
69
+ address: string;
70
+ };
71
+ attendee: {
72
+ emailAddress: string;
73
+ name: string;
74
+ };
75
+ };
76
+ //#endregion
77
+ //#region src/types/ScheduleActor.d.ts
78
+ /**
79
+ * An actor that takes the schedule intent and acts upon it
80
+ * This can be used for actually storing the event, sending an email or starting a workflow.
81
+ *
82
+ * Errors may be thrown. The handler will catch them, wait until all other actors have finished
83
+ * and then throw a combined `SchedulingError` to the callee.
84
+ *
85
+ * Multiple actors will be run in parallel.
86
+ */
87
+ type ScheduleActor = (intent: ScheduleIntent) => Promise<void>;
88
+ //#endregion
89
+ //#region src/types/Timeslot.d.ts
90
+ type Timeslot = {
91
+ start: Date;
92
+ end: Date;
93
+ };
94
+ type AvailabilityTimeslot = Timeslot & {
95
+ available: boolean;
96
+ };
97
+ //#endregion
98
+ //#region src/types/ScheduleResult.d.ts
99
+ declare enum ScheduleResult {
100
+ SCHEDULED = 0,
101
+ FAILED_INVALID_INTENT = 1,
102
+ FAILED_SLOT_UNAVAILABLE = 2
103
+ }
104
+ //#endregion
105
+ //#region src/CalendarProvider.d.ts
106
+ declare class CalendarProvider {
107
+ private readonly config;
108
+ constructor(config: {
109
+ calendars: CalendarConfig[];
110
+ });
111
+ queryAllEvents(query: {
112
+ start: Date;
113
+ end: Date;
114
+ }): AsyncGenerator<{
115
+ start: Date;
116
+ end: Date;
117
+ event: ICS.VEVENT.Published;
118
+ }>;
119
+ }
120
+ //#endregion
121
+ //#region src/types/DurationString.d.ts
122
+ type DurationDaysString = `${number}d`;
123
+ type DurationHoursString = `${number}h`;
124
+ type DurationMinutesString = `${number}m`;
125
+ type DurationSecondsString = `${number}s`;
126
+ type DurationString = `${DurationDaysString | ''}${DurationHoursString | ''}${DurationMinutesString | ''}${DurationSecondsString | ''}`;
127
+ //#endregion
128
+ //#region src/Scheduler.d.ts
129
+ type SchedulerConfig = {
130
+ calendars: CalendarConfig[];
131
+ availability: AvailabilityRule[];
132
+ scheduleActors: ScheduleActor[];
133
+ defaults: {
134
+ /**
135
+ * The default duration of an event in the format of an interval string
136
+ * Example values: 30m, 1h30m
137
+ */
138
+ duration: DurationString;
139
+ /**
140
+ * The default padding / duration between events in the format of an interval string
141
+ * Example values: 10m, 30m
142
+ */
143
+ padding: DurationString;
144
+ /**
145
+ * The default maximum time we're allowing users to plan ahead
146
+ * Example values: 14d, 30d
147
+ */
148
+ planningAhead: DurationString;
149
+ };
150
+ };
151
+ declare class Scheduler {
152
+ private readonly calendarProvider;
153
+ private readonly config;
154
+ static fromConfig(schedulerConfig: SchedulerConfig): Scheduler;
155
+ constructor(calendarProvider: CalendarProvider, config: Omit<SchedulerConfig, 'calendars'>);
156
+ query(query: Timeslot): AsyncGenerator<AvailabilityTimeslot>;
157
+ /**
158
+ *
159
+ * @param {ScheduleIntent} intent
160
+ * @throws SchedulingError
161
+ */
162
+ schedule(intent: ScheduleIntent): Promise<ScheduleResult>;
163
+ }
164
+ //#endregion
165
+ //#region src/types/Duration.d.ts
166
+ interface Duration {
167
+ /** The number of days in the duration */
168
+ days?: number;
169
+ /** The number of hours in the duration */
170
+ hours?: number;
171
+ /** The number of minutes in the duration */
172
+ minutes?: number;
173
+ /** The number of seconds in the duration */
174
+ seconds?: number;
175
+ }
176
+ //#endregion
177
+ export { type AvailabilityRule, type AvailabilityTimeslot, type CalendarConfig, CalendarProvider, type Duration, type DurationString, type HourMinute, Scheduler, type Timeslot, type Weekday };
178
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types/CalendarConfig.ts","../src/types/AvailabilityRule.ts","../src/types/ScheduleIntent.ts","../src/types/ScheduleActor.ts","../src/types/Timeslot.ts","../src/types/ScheduleResult.ts","../src/CalendarProvider.ts","../src/types/DurationString.ts","../src/Scheduler.ts","../src/types/Duration.ts"],"mappings":";;;;KAAK,kBAAA;;;;EAID,UAAA;EAJmB;;;;EAUnB,OAAA;EAKA;;;EAAA,MAAA;EASC;;;;EAHD,IAAA;AAAA;AAAA,KAGC,oBAAA,GAAuB,IAAI,CAAC,kBAAA;EAC7B,OAAA;EAOI;;;AACQ;EAFZ,IAAA;IACI,QAAA;IACA,QAAA;EAAA;AAAA;AAAA,KAIH,cAAA,GAAiB,oBAAoB;;;KCrC9B,OAAA;AAAA,KACA,UAAA;;;;UAKK,sBAAA;EACb,KAAA,KAAU,OAAA,IAAW,UAAA;EACrB,GAAA,KAAQ,OAAA,IAAW,UAAA;AAAA;;;;UAMN,wBAAA;EACb,KAAA,EAAO,UAAA;EACP,GAAA,EAAK,UAAA;EACL,IAAA,GAAO,OAAA;AAAA;AAAA,KAGN,gBAAA,GAAmB,sBAAA,GAAyB,wBAAwB;;;KClBpE,cAAA;EACD,QAAA,EAAU,IAAA;EACV,QAAA,EAAU,UAAQ;EAClB,KAAA;EACA,WAAA;EACA,QAAA;IAAW,IAAA;IAAiB,GAAA;EAAA;IAAgB,IAAA;IAAkB,OAAA;EAAA;EAC9D,QAAA;IACI,YAAA;IACA,IAAA;EAAA;AAAA;;;;;;;;;;;;KCCH,aAAA,IAAiB,MAAA,EAAQ,cAAA,KAAmB,OAAO;;;KCXnD,QAAA;EACD,KAAA,EAAO,IAAA;EACP,GAAA,EAAK,IAAI;AAAA;AAAA,KAGD,oBAAA,GAAuB,QAAQ;EACvC,SAAS;AAAA;;;aCNR,cAAA;EACD,SAAA;EACA,qBAAA;EACA,uBAAA;AAAA;;;cCGiB,gBAAA;EAAA,iBACa,MAAA;cAAA,MAAA;IAC1B,SAAA,EAAW,cAAA;EAAA;EAGR,cAAA,CAAgB,KAAA;IACnB,KAAA,EAAO,IAAA;IACP,GAAA,EAAK,IAAA;EAAA,IACJ,cAAA;IAAgB,KAAA,EAAO,IAAA;IAAM,GAAA,EAAK,IAAA;IAAM,KAAA,EAAO,GAAA,CAAI,MAAA,CAAO,SAAA;EAAA;AAAA;;;KCdvD,kBAAA;AAAA,KACA,mBAAA;AAAA,KACA,qBAAA;AAAA,KACA,qBAAA;AAAA,KAEA,cAAA,MAAoB,kBAAA,QAA0B,mBAAA,QAA2B,qBAAA,QAA6B,qBAAA;;;KCS7G,eAAA;EACD,SAAA,EAAW,cAAA;EACX,YAAA,EAAc,gBAAA;EACd,cAAA,EAAgB,aAAA;EAChB,QAAA;IRGI;AAAA;;;IQEA,QAAA,EAAU,cAAA;IRCU;;;;IQKpB,OAAA,EAAS,cAAA;IRIT;;AAAQ;AAAA;IQER,aAAA,EAAe,cAAA;EAAA;AAAA;AAAA,cAIF,SAAA;EAAA,iBAUI,gBAAA;EAAA,iBACA,MAAA;EAAA,OATd,UAAA,CAAY,eAAA,EAAiB,eAAA,GAAmB,SAAA;cAQlC,gBAAA,EAAkB,gBAAA,EAClB,MAAA,EAAQ,IAAA,CAAK,eAAA;EAG3B,KAAA,CAAO,KAAA,EAAO,QAAA,GAAY,cAAA,CAAe,oBAAA;;;APrDjC;AACnB;;EOiFU,QAAA,CAAU,MAAA,EAAQ,cAAA,GAAkB,OAAA,CAAQ,cAAA;AAAA;;;UCjFrC,QAAA;;EAEb,IAAA;;EAEA,KAAA;ETLmB;ESOnB,OAAA;ETPmB;ESSnB,OAAA;AAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,247 @@
1
+ import { fetchCalendarObjects, getBasicAuthHeaders } from "tsdav";
2
+ import { parseString } from "@filecage/ical/parser";
3
+ import { getDateFromDateTime, getEventEndDateTime } from "@filecage/ical/Getters";
4
+ import { addSeconds, endOfDay, startOfDay } from "date-fns";
5
+ //#region src/CalendarProvider.ts
6
+ var CalendarProvider = class {
7
+ config;
8
+ constructor(config) {
9
+ this.config = config;
10
+ }
11
+ async *queryAllEvents(query) {
12
+ const queries = Promise.all(this.config.calendars.map((calendarConfig) => {
13
+ const headers = getBasicAuthHeaders(calendarConfig.auth);
14
+ return calendarConfig.uris.map((calendarUri) => fetchCalendarObjects({
15
+ calendar: { url: new URL(calendarUri, `https://${calendarConfig.server}`).toString() },
16
+ timeRange: {
17
+ start: query.start.toISOString(),
18
+ end: query.end.toISOString()
19
+ },
20
+ expand: true,
21
+ headers
22
+ }));
23
+ }).flat());
24
+ for (const query of await queries) for (const object of query) try {
25
+ const calendars = parseString(object.data);
26
+ for (const calendar of calendars.VCALENDAR) for (const event of calendar.VEVENT ?? []) {
27
+ if (event.TRANSP?.value === "TRANSPARENT") continue;
28
+ yield {
29
+ start: getDateFromDateTime(event.DTSTART.value, calendar.VTIMEZONE || []),
30
+ end: getDateFromDateTime(getEventEndDateTime(event), calendar.VTIMEZONE || []),
31
+ event
32
+ };
33
+ }
34
+ } catch (e) {
35
+ console.error(`Error: '${typeof e === "object" && e && "message" in e && e.message}' for object ${object.url}`);
36
+ console.error(`Object data:\n${object.data}\n`);
37
+ console.error(e);
38
+ }
39
+ }
40
+ };
41
+ //#endregion
42
+ //#region src/util/normaliseAvailability.ts
43
+ const WEEKDAYS = [
44
+ "sunday",
45
+ "monday",
46
+ "tuesday",
47
+ "wednesday",
48
+ "thursday",
49
+ "friday",
50
+ "saturday"
51
+ ];
52
+ /**
53
+ * Normalises multi- and single availability rules.
54
+ * CAUTION: This function *does not* validate each rule itself.
55
+ *
56
+ * @param {AvailabilityRule[]} rules
57
+ */
58
+ function* normaliseAvailability(...rules) {
59
+ for (const rule of rules) {
60
+ if ("days" in rule && rule.days !== void 0) {
61
+ yield rule;
62
+ continue;
63
+ }
64
+ const [startingWeekday, start] = rule.start.split(" ");
65
+ const [endingWeekday, end] = rule.end.split(" ");
66
+ if (!start || !end) {
67
+ yield {
68
+ start: startingWeekday,
69
+ end: endingWeekday,
70
+ days: WEEKDAYS
71
+ };
72
+ continue;
73
+ }
74
+ yield {
75
+ start,
76
+ end,
77
+ days: startingWeekday === endingWeekday ? WEEKDAYS : (() => {
78
+ const weekdays = [...WEEKDAYS.slice(WEEKDAYS.indexOf(startingWeekday)), ...WEEKDAYS.slice(0, WEEKDAYS.indexOf(startingWeekday))];
79
+ return weekdays.slice(0, weekdays.indexOf(endingWeekday) + 1);
80
+ })()
81
+ };
82
+ }
83
+ }
84
+ //#endregion
85
+ //#region src/util/generateTimeslotsByAvailabilityRules.ts
86
+ /**
87
+ * This function generates timeslots for the given availability rules.
88
+ */
89
+ function* generateTimeslotsByAvailabilityRules(between, availabilityRules) {
90
+ if (between.start >= between.end || availabilityRules.length === 0) return;
91
+ const timeslots = [];
92
+ const rules = [...normaliseAvailability(...availabilityRules)].sort((a, b) => hourMinuteToMinutes(a.start) - hourMinuteToMinutes(b.start));
93
+ const cursor = startOfDay(between.start);
94
+ const end = endOfDay(between.end);
95
+ while (cursor < end) {
96
+ const weekday = WEEKDAYS[cursor.getDay()];
97
+ for (const rule of rules) {
98
+ if (!rule.days.includes(weekday)) continue;
99
+ const start = atTime(cursor, rule.start);
100
+ const end = atTime(cursor, rule.end);
101
+ if (end <= start) end.setDate(end.getDate() + 1);
102
+ if (start >= between.start && end <= between.end) timeslots.push({
103
+ start,
104
+ end
105
+ });
106
+ }
107
+ cursor.setDate(cursor.getDate() + 1);
108
+ }
109
+ if (!timeslots.length) return;
110
+ let currentSlot = timeslots[0];
111
+ for (let i = 1; i < timeslots.length; i++) {
112
+ const timeslot = timeslots[i];
113
+ if (timeslot.start <= currentSlot.end) currentSlot.end.setTime(Math.max(timeslot.end.getTime(), currentSlot.end.getTime()));
114
+ else {
115
+ yield currentSlot;
116
+ currentSlot = timeslot;
117
+ }
118
+ }
119
+ yield currentSlot;
120
+ }
121
+ function hourMinuteToMinutes(hm) {
122
+ const { hours, minutes } = parseHourMinute(hm);
123
+ return hours * 60 + minutes;
124
+ }
125
+ function parseHourMinute(hm) {
126
+ const [h, m] = hm.split(":").map(Number);
127
+ return {
128
+ hours: h,
129
+ minutes: m
130
+ };
131
+ }
132
+ function atTime(date, hm) {
133
+ const { hours, minutes } = parseHourMinute(hm);
134
+ const d = new Date(date);
135
+ d.setHours(hours, minutes, 0, 0);
136
+ return d;
137
+ }
138
+ //#endregion
139
+ //#region src/util/chunkTimeslot.ts
140
+ function* chunkTimeslot(timeslot, chunkLengthInSeconds, paddingInSeconds = 0) {
141
+ if (chunkLengthInSeconds === 0 && paddingInSeconds === 0) throw new Error(`Can not chunk timeslot if chunk length and padding is zero`);
142
+ const cursor = new Date(timeslot.start);
143
+ while (cursor < timeslot.end) {
144
+ const start = new Date(cursor);
145
+ const end = addSeconds(cursor, chunkLengthInSeconds);
146
+ if (end <= timeslot.end) yield {
147
+ start,
148
+ end
149
+ };
150
+ cursor.setTime(cursor.getTime() + (chunkLengthInSeconds + paddingInSeconds) * 1e3);
151
+ }
152
+ }
153
+ //#endregion
154
+ //#region src/util/parseDurationString.ts
155
+ const DURATION_STRING_REGEX = /* @__PURE__ */ new RegExp(/^(?<days>\d+d)?(?<hours>[0-9]+h)?(?<minutes>[0-9]+m)?(?<seconds>[0-9]+s)?$/);
156
+ function parseDurationString(duration) {
157
+ const matches = duration.match(DURATION_STRING_REGEX);
158
+ if (matches === null) throw new Error(`Invalid duration string: '${duration}'`);
159
+ return {
160
+ days: parseInt(matches.groups?.days || "0"),
161
+ hours: parseInt(matches.groups?.hours || "0"),
162
+ minutes: parseInt(matches.groups?.minutes || "0"),
163
+ seconds: parseInt(matches.groups?.seconds || "0")
164
+ };
165
+ }
166
+ //#endregion
167
+ //#region src/util/durationToSeconds.ts
168
+ function durationToSeconds(duration) {
169
+ const { days = 0, hours = 0, minutes = 0, seconds = 0 } = duration;
170
+ return days * 86400 + hours * 3600 + minutes * 60 + seconds;
171
+ }
172
+ //#endregion
173
+ //#region src/util/iterateTimeslotAvailability.ts
174
+ /**
175
+ * Applies a sweeping line algorithm to mark available timeslots.
176
+ * WARNING: BOTH ARRAYS NEED TO BE SORTED BEFOREHAND!
177
+ *
178
+ * @param timeslots
179
+ * @param busyTimes
180
+ * @param options
181
+ */
182
+ function* iterateTimeslotAvailability(timeslots, busyTimes, options) {
183
+ let index = 0;
184
+ let timeslot;
185
+ busyTimesLoop: for (const busyTime of busyTimes) while (index < timeslots.length) {
186
+ let available = true;
187
+ timeslot = timeslots[index];
188
+ const busyTimeStart = /* @__PURE__ */ new Date(busyTime.start.getTime() - options.busyThresholdSecondsBefore * 1e3);
189
+ const busyTimeEnd = new Date(busyTime.end.getTime() + options.busyThresholdSecondsAfter * 1e3);
190
+ if (busyTimeEnd < timeslot.start) continue busyTimesLoop;
191
+ else if (busyTimeStart <= timeslot.start && timeslot.start < busyTimeEnd || busyTimeStart < timeslot.end && timeslot.end < busyTimeEnd || timeslot.start < busyTimeStart && busyTimeEnd < timeslot.end) available = false;
192
+ yield {
193
+ ...timeslot,
194
+ available
195
+ };
196
+ index++;
197
+ }
198
+ for (const timeslot of timeslots.slice(index)) yield {
199
+ ...timeslot,
200
+ available: true
201
+ };
202
+ }
203
+ //#endregion
204
+ //#region src/Scheduler.ts
205
+ var Scheduler = class Scheduler {
206
+ calendarProvider;
207
+ config;
208
+ static fromConfig(schedulerConfig) {
209
+ const { calendars, ...config } = schedulerConfig;
210
+ return new Scheduler(new CalendarProvider({ calendars }), config);
211
+ }
212
+ constructor(calendarProvider, config) {
213
+ this.calendarProvider = calendarProvider;
214
+ this.config = config;
215
+ }
216
+ async *query(query) {
217
+ const timeslots = [];
218
+ const calendarEvents = this.calendarProvider.queryAllEvents(query);
219
+ const timeslotLengthInSeconds = durationToSeconds(parseDurationString(this.config.defaults.duration));
220
+ const timeslotPaddingInSeconds = durationToSeconds(parseDurationString(this.config.defaults.padding));
221
+ for (const availabilitySlot of generateTimeslotsByAvailabilityRules(query, this.config.availability)) for (const timeslotCandidate of chunkTimeslot(availabilitySlot, timeslotLengthInSeconds, timeslotPaddingInSeconds)) timeslots.push({
222
+ ...timeslotCandidate,
223
+ available: true
224
+ });
225
+ const busyTimeslots = [];
226
+ for await (const { start, end } of calendarEvents) busyTimeslots.push({
227
+ start,
228
+ end
229
+ });
230
+ yield* iterateTimeslotAvailability(timeslots, busyTimeslots.sort((a, b) => a.start.getTime() - b.start.getTime()), {
231
+ busyThresholdSecondsBefore: timeslotPaddingInSeconds,
232
+ busyThresholdSecondsAfter: timeslotPaddingInSeconds
233
+ });
234
+ }
235
+ /**
236
+ *
237
+ * @param {ScheduleIntent} intent
238
+ * @throws SchedulingError
239
+ */
240
+ async schedule(intent) {
241
+ throw new Error(`Not implemented`);
242
+ }
243
+ };
244
+ //#endregion
245
+ export { CalendarProvider, Scheduler };
246
+
247
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/CalendarProvider.ts","../src/util/normaliseAvailability.ts","../src/util/generateTimeslotsByAvailabilityRules.ts","../src/util/chunkTimeslot.ts","../src/util/parseDurationString.ts","../src/util/durationToSeconds.ts","../src/util/iterateTimeslotAvailability.ts","../src/Scheduler.ts"],"sourcesContent":["import CalendarConfig from \"@/zeitmeister/types/CalendarConfig\";\nimport {DAVCalendar, fetchCalendarObjects, getBasicAuthHeaders} from \"tsdav\";\nimport {ICS} from \"@filecage/ical\";\nimport {parseString} from \"@filecage/ical/parser\";\nimport {getDateFromDateTime, getEventEndDateTime} from \"@filecage/ical/Getters\";\n\nexport default class CalendarProvider {\n constructor (private readonly config: {\n calendars: CalendarConfig[]\n }) {}\n\n async *queryAllEvents (query: {\n start: Date,\n end: Date,\n }) : AsyncGenerator<{start: Date, end: Date, event: ICS.VEVENT.Published}> {\n const queries = Promise.all(this.config.calendars.map(calendarConfig => {\n const headers = getBasicAuthHeaders(calendarConfig.auth);\n\n return calendarConfig.uris.map(calendarUri => fetchCalendarObjects({\n calendar: {url: new URL(calendarUri, `https://${calendarConfig.server}`).toString()} satisfies DAVCalendar,\n timeRange: {start: query.start.toISOString(), end: query.end.toISOString()},\n expand: true,\n headers,\n }));\n }).flat());\n\n for (const query of await queries) {\n for (const object of query) {\n try {\n const calendars = parseString(object.data);\n for (const calendar of calendars.VCALENDAR) {\n for (const event of calendar.VEVENT ?? []) {\n // Skip non-busy events\n if (event.TRANSP?.value === 'TRANSPARENT') {\n continue;\n }\n\n yield {\n start: getDateFromDateTime(event.DTSTART.value, calendar.VTIMEZONE || []),\n end: getDateFromDateTime(getEventEndDateTime(event), calendar.VTIMEZONE || []),\n event,\n };\n }\n }\n\n } catch (e) {\n // TODO: Correct error handling, throw an exception/error to the consumer\n console.error(`Error: '${typeof e === 'object' && e && 'message' in e && e.message}' for object ${object.url}`);\n console.error(`Object data:\\n${object.data}\\n`);\n console.error(e);\n }\n }\n }\n }\n\n}","import AvailabilityRule, {AvailabilityRuleMultiple, HourMinute, Weekday} from \"@/zeitmeister/types/AvailabilityRule\";\n\nexport const WEEKDAYS: Weekday[] = [\n 'sunday',\n 'monday',\n 'tuesday',\n 'wednesday',\n 'thursday',\n 'friday',\n 'saturday'\n] as const;\n\n/**\n * Normalises multi- and single availability rules.\n * CAUTION: This function *does not* validate each rule itself.\n *\n * @param {AvailabilityRule[]} rules\n */\nexport function *normaliseAvailability (...rules: AvailabilityRule[]) : Generator<AvailabilityRuleMultiple & {days: Weekday[]}> {\n for (const rule of rules) {\n if ('days' in rule && rule.days !== undefined) {\n yield rule as AvailabilityRuleMultiple & {days: Weekday[]};\n continue;\n }\n\n const [startingWeekday, start] = rule.start.split(' ') as [Weekday, HourMinute];\n const [endingWeekday, end] = rule.end.split(' ') as [Weekday, HourMinute];\n\n // if start/end does not consist of two parts, it's only time data\n // assume all weekdays then\n if (!start || !end) {\n yield {\n start: startingWeekday as HourMinute,\n end: endingWeekday as HourMinute,\n days: WEEKDAYS,\n };\n\n continue;\n }\n\n // Re-order weekdays so that we can iterate from start to end\n // If start and end match, it's the full week\n const days = startingWeekday === endingWeekday\n ? WEEKDAYS\n : (() => {\n const weekdays = [\n ...WEEKDAYS.slice(WEEKDAYS.indexOf(startingWeekday)),\n ...WEEKDAYS.slice(0, WEEKDAYS.indexOf(startingWeekday))\n ];\n\n return weekdays.slice(0, weekdays.indexOf(endingWeekday) + 1);\n })()\n ;\n\n yield {start, end, days};\n }\n}","import {endOfDay, startOfDay} from \"date-fns\";\nimport AvailabilityRule, {HourMinute} from \"@/zeitmeister/types/AvailabilityRule\";\nimport Timeslot from \"@/zeitmeister/types/Timeslot\";\nimport {normaliseAvailability, WEEKDAYS} from \"./normaliseAvailability\";\n\n/**\n * This function generates timeslots for the given availability rules.\n */\nexport function *generateTimeslotsByAvailabilityRules (between: {start: Date, end: Date}, availabilityRules: AvailabilityRule[]) : Generator<Timeslot> {\n if (between.start >= between.end || availabilityRules.length === 0) {\n return;\n }\n\n const timeslots: Timeslot[] = [];\n const rules = [...normaliseAvailability(...availabilityRules)]\n // Availability rules are sorted beforehand, so the generated slots are also sorted correctly (this way we have to sort less items)\n .sort((a, b) => hourMinuteToMinutes(a.start) - hourMinuteToMinutes(b.start));\n\n\n // For each day, we generate *all* possible timeslots, regardless of the query time\n // If a slot does not match the query, it will be marked as unavailable later\n const cursor = startOfDay(between.start);\n const end = endOfDay(between.end);\n\n while (cursor < end) {\n const weekday = WEEKDAYS[cursor.getDay()];\n\n for (const rule of rules) {\n if (!rule.days.includes(weekday)) {\n continue;\n }\n\n const start = atTime(cursor, rule.start);\n const end = atTime(cursor, rule.end);\n if (end <= start) {\n // If end time is lower than start time, we have to skip to the next day (for example: 23:00-01:00)\n end.setDate(end.getDate() + 1);\n }\n\n if (start >= between.start && end <= between.end) {\n timeslots.push({start, end});\n }\n }\n\n // Move cursor to next day\n cursor.setDate(cursor.getDate() + 1);\n }\n\n if (!timeslots.length) {\n return;\n }\n\n // Next, we merge all slots that overlap\n let currentSlot = timeslots[0];\n for (let i = 1; i < timeslots.length; i++) {\n const timeslot = timeslots[i];\n\n if (timeslot.start <= currentSlot.end) {\n // Set current slot end time to latest slot's end time if they overlap\n currentSlot.end.setTime(Math.max(timeslot.end.getTime(), currentSlot.end.getTime()));\n } else {\n // If they don't overlap, emit the current slot and set this slot to the current slot\n yield currentSlot;\n currentSlot = timeslot;\n }\n }\n\n // Emit last slot when loop has run (or hasn't if there was only one item)\n yield currentSlot;\n}\n\nfunction hourMinuteToMinutes (hm: HourMinute) : number {\n const {hours, minutes} = parseHourMinute(hm);\n\n return hours * 60 + minutes;\n}\n\nfunction parseHourMinute(hm: HourMinute): { hours: number; minutes: number } {\n const [h, m] = hm.split(':').map(Number);\n return { hours: h, minutes: m };\n}\n\nfunction atTime(date: Date, hm: HourMinute): Date {\n const {hours, minutes} = parseHourMinute(hm);\n const d = new Date(date);\n d.setHours(hours, minutes, 0, 0);\n\n return d;\n}","import Timeslot from \"@/zeitmeister/types/Timeslot\";\nimport {addSeconds} from \"date-fns\";\n\nexport function *chunkTimeslot (timeslot: Timeslot, chunkLengthInSeconds: number, paddingInSeconds: number = 0) : Generator<Timeslot> {\n if (chunkLengthInSeconds === 0 && paddingInSeconds === 0) {\n throw new Error(`Can not chunk timeslot if chunk length and padding is zero`);\n }\n\n const cursor = new Date(timeslot.start);\n\n while (cursor < timeslot.end) {\n const start = new Date(cursor);\n const end = addSeconds(cursor, chunkLengthInSeconds);\n\n if (end <= timeslot.end) {\n yield {start, end};\n }\n\n cursor.setTime(cursor.getTime() + ((chunkLengthInSeconds + paddingInSeconds) * 1000));\n }\n}","import {Duration} from \"@/zeitmeister/types/Duration\";\nimport {DurationString} from \"@/zeitmeister/types/DurationString\";\n\nconst DURATION_STRING_REGEX = new RegExp(/^(?<days>\\d+d)?(?<hours>[0-9]+h)?(?<minutes>[0-9]+m)?(?<seconds>[0-9]+s)?$/);\n\nexport function parseDurationString (duration: DurationString) : Duration {\n const matches = duration.match(DURATION_STRING_REGEX);\n if (matches === null) {\n throw new Error(`Invalid duration string: '${duration}'`);\n }\n\n return {\n days: parseInt(matches.groups?.days || '0'),\n hours: parseInt(matches.groups?.hours || '0'),\n minutes: parseInt(matches.groups?.minutes || '0'),\n seconds: parseInt(matches.groups?.seconds || '0'),\n };\n}\n\nexport function isDurationString (duration: string) : duration is DurationString {\n return DURATION_STRING_REGEX.test(duration);\n}","import {Duration} from \"@/zeitmeister/types/Duration\";\n\nexport function durationToSeconds (duration: Duration) : number {\n const { days = 0, hours = 0, minutes = 0, seconds = 0 } = duration;\n return days * 86400 + hours * 3600 + minutes * 60 + seconds;\n}","import Timeslot, {AvailabilityTimeslot} from \"@/zeitmeister/types/Timeslot\";\n\n/**\n * Applies a sweeping line algorithm to mark available timeslots.\n * WARNING: BOTH ARRAYS NEED TO BE SORTED BEFOREHAND!\n *\n * @param timeslots\n * @param busyTimes\n * @param options\n */\nexport function *iterateTimeslotAvailability (timeslots: Timeslot[], busyTimes: Timeslot[], options: {busyThresholdSecondsBefore: number, busyThresholdSecondsAfter: number}) : Generator<AvailabilityTimeslot> {\n let index = 0;\n let timeslot: Timeslot;\n\n busyTimesLoop: for (const busyTime of busyTimes) {\n while (index < timeslots.length) {\n let available = true;\n timeslot = timeslots[index];\n\n // Apply busy threshold\n const busyTimeStart = new Date(busyTime.start.getTime() - options.busyThresholdSecondsBefore * 1000);\n const busyTimeEnd = new Date(busyTime.end.getTime() + options.busyThresholdSecondsAfter * 1000);\n\n if (busyTimeEnd < timeslot.start) {\n // if busy time occurs before our timeslot, we skip to the next busy time\n continue busyTimesLoop;\n } else if (\n (busyTimeStart <= timeslot.start && timeslot.start < busyTimeEnd) // timeslot starts during busyTime\n || (busyTimeStart < timeslot.end && timeslot.end < busyTimeEnd) // timeslot ends during busyTime\n || (timeslot.start < busyTimeStart && busyTimeEnd < timeslot.end) // busyTime is within timeslot\n ) {\n // if busy time overlaps, we mark this timeslot as unavailable\n available = false;\n }\n\n // emit the timeslot and go to the next one\n yield {...timeslot, available};\n index++;\n }\n }\n\n // After we went through all busy times, we still have to emit the remaining timeslots as available\n for (const timeslot of timeslots.slice(index)) {\n yield {...timeslot, available: true};\n }\n}","import CalendarConfig from \"@/zeitmeister/types/CalendarConfig\";\nimport AvailabilityRule from \"@/zeitmeister/types/AvailabilityRule\";\nimport ScheduleActor from \"@/zeitmeister/types/ScheduleActor\";\nimport Timeslot, {AvailabilityTimeslot} from \"@/zeitmeister/types/Timeslot\";\nimport ScheduleIntent from \"@/zeitmeister/types/ScheduleIntent\";\nimport ScheduleResult from \"@/zeitmeister/types/ScheduleResult\";\nimport CalendarProvider from \"./CalendarProvider\";\nimport {DurationString} from \"@/zeitmeister/types/DurationString\";\nimport {generateTimeslotsByAvailabilityRules} from \"./util/generateTimeslotsByAvailabilityRules\";\nimport {chunkTimeslot} from \"./util/chunkTimeslot\";\nimport {parseDurationString} from \"./util/parseDurationString\";\nimport {durationToSeconds} from \"./util/durationToSeconds\";\nimport {iterateTimeslotAvailability} from \"./util/iterateTimeslotAvailability\";\n\ntype SchedulerConfig = {\n calendars: CalendarConfig[],\n availability: AvailabilityRule[],\n scheduleActors: ScheduleActor[],\n defaults: {\n /**\n * The default duration of an event in the format of an interval string\n * Example values: 30m, 1h30m\n */\n duration: DurationString,\n\n /**\n * The default padding / duration between events in the format of an interval string\n * Example values: 10m, 30m\n */\n padding: DurationString,\n\n /**\n * The default maximum time we're allowing users to plan ahead\n * Example values: 14d, 30d\n */\n planningAhead: DurationString,\n }\n};\n\nexport default class Scheduler {\n\n static fromConfig (schedulerConfig: SchedulerConfig) : Scheduler {\n const {calendars, ...config} = schedulerConfig;\n const provider = new CalendarProvider({calendars});\n\n return new Scheduler(provider, config);\n }\n\n constructor (\n private readonly calendarProvider: CalendarProvider,\n private readonly config: Omit<SchedulerConfig, 'calendars'>\n ) {}\n\n async *query (query: Timeslot) : AsyncGenerator<AvailabilityTimeslot> {\n const timeslots: AvailabilityTimeslot[] = [];\n const calendarEvents = this.calendarProvider.queryAllEvents(query);\n const timeslotLengthInSeconds = durationToSeconds(parseDurationString(this.config.defaults.duration));\n const timeslotPaddingInSeconds = durationToSeconds(parseDurationString(this.config.defaults.padding));\n\n for (const availabilitySlot of generateTimeslotsByAvailabilityRules(query, this.config.availability)) {\n for (const timeslotCandidate of chunkTimeslot(availabilitySlot, timeslotLengthInSeconds, timeslotPaddingInSeconds)) {\n timeslots.push({...timeslotCandidate, available: true});\n }\n }\n\n const busyTimeslots: Timeslot[] = [];\n for await (const {start, end} of calendarEvents) {\n busyTimeslots.push({start, end});\n }\n\n yield *iterateTimeslotAvailability(timeslots, busyTimeslots.sort((a, b) => a.start.getTime() - b.start.getTime()), {\n busyThresholdSecondsBefore: timeslotPaddingInSeconds,\n busyThresholdSecondsAfter: timeslotPaddingInSeconds\n });\n }\n\n /**\n *\n * @param {ScheduleIntent} intent\n * @throws SchedulingError\n */\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n async schedule (intent: ScheduleIntent) : Promise<ScheduleResult> {\n // TODO: Implement me!\n throw new Error(`Not implemented`);\n }\n}"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@filecage/zeitmeister",
3
+ "version": "0.1.0",
4
+ "description": "zeitmeister is an open source library for scheduling appointments over an unlimited number of calendars.",
5
+ "type": "module",
6
+ "author": "David Beuchert <github-public@dbeuchert.com>",
7
+ "license": "UNLICENSED",
8
+ "dependencies": {
9
+ "@filecage/ical": "^0.5.1",
10
+ "date-fns": "^4.4",
11
+ "tsdav": "^2.3.0"
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "exports": {
17
+ ".": "./dist/index.mjs",
18
+ "./package.json": "./package.json"
19
+ }
20
+ }