@courierkit/availability
v0.1.0
Published
A stateless, composable slot generation library for Node.js
Readme
@courierkit/availability
A stateless, composable slot generation library for Node.js. Given schedules, bookings, external calendar events, and event type configuration, it answers the question: "When can this happen?"
Installation
npm install @courierkit/availabilityQuick Start
import { getAvailableSlots } from '@courierkit/availability';
const slots = getAvailableSlots({
eventType: {
id: 'consultation',
length: 60 * 60 * 1000, // 1 hour
bufferAfter: 15 * 60 * 1000, // 15 min for notes
minimumNotice: 24 * 60 * 60 * 1000, // 24 hours advance booking
maxPerDay: 4,
},
hosts: [{
hostId: 'dr-smith',
schedules: {
default: {
id: 'default',
rules: [{
days: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'],
startTime: '09:00',
endTime: '17:00',
timezone: 'America/New_York',
}],
overrides: [
{ date: '2024-12-25', available: false }, // Holiday
],
},
},
}],
bookings: [
{
hostId: 'dr-smith',
start: new Date('2024-01-15T14:00:00Z'),
end: new Date('2024-01-15T15:00:00Z'),
eventTypeId: 'consultation',
},
],
range: {
start: new Date('2024-01-15T00:00:00Z'),
end: new Date('2024-01-22T00:00:00Z'),
},
});
// Returns: Slot[] sorted by start time
// [{ hostId: 'dr-smith', start: Date, end: Date, bufferAfter?: Interval }, ...]Adapter Engine (Optional)
If you don't want to assemble inputs on every request, use createAvailability with an adapter:
import { createAvailability } from '@courierkit/availability';
const availability = createAvailability({
adapter: {
async getEventType(eventTypeId) {
return db.eventTypes.findById(eventTypeId);
},
async getHosts({ hostIds }) {
return db.hosts.withSchedules(hostIds);
},
async getBookings({ hostIds, range }) {
return db.bookings.overlap(hostIds, range);
},
async getBlocks({ hostIds, range }) {
return db.blocks.overlap(hostIds, range);
},
async getEventTypeBuffers({ eventTypeIds }) {
return db.eventTypes.bufferMap(eventTypeIds);
},
},
});
const slots = await availability.getAvailableSlots({
eventTypeId: 'consultation',
hostIds: ['dr-smith'],
range: { start: new Date('2024-01-15'), end: new Date('2024-01-22') },
at: new Date(), // optional override for "now"
});Database Setup
At minimum, you'll need tables/collections for:
- Hosts and schedules (rules + overrides)
- Event types (length, buffers, limits)
- Bookings (with UTC start/end)
- Optional external blocks
For a concrete schema and query patterns, see the data model guide in the docs.
How It Works
Everything is an interval on a timeline. The engine layers intervals to produce available slots:
Availability (schedule) ████████████████████████████
− Bookings ████ ████
− External calendar blocks ███
− Buffer zones (derived) ▒█████▒ ▒████▒
− Minimum notice window ███
= Available slots ░░░░ ░░░░░░░Key Features
- Stateless: No side effects, no caching, no persistence. You own your data.
- Timezone-Aware: Schedules are defined in local time, everything else is UTC.
- Composable: Low-level interval arithmetic exposed for custom logic.
- Type-Safe: Full TypeScript support.
Core Concepts
Schedules
Define recurring availability with rules and overrides:
const schedule: Schedule = {
id: 'default',
rules: [
{
days: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'],
startTime: '09:00',
endTime: '17:00',
timezone: 'America/New_York',
},
],
overrides: [
{ date: '2024-12-25', available: false }, // Christmas off
{ date: '2024-01-20', available: true, startTime: '10:00', endTime: '14:00' }, // Special Saturday
],
};Event Types
Configure what's being booked with constraints:
const consultation: EventType = {
id: 'consultation',
length: 60 * 60 * 1000, // 1 hour
bufferBefore: 15 * 60 * 1000, // 15 min prep
bufferAfter: 15 * 60 * 1000, // 15 min notes
slotInterval: 30 * 60 * 1000, // 30 min grid
minimumNotice: 24 * 60 * 60 * 1000, // 24 hours
maxPerDay: 4,
maxPerWeek: 15,
// Per-host customization
hostOverrides: {
'dr-jones': { maxPerDay: 3 },
},
};Multiple Schedules per Host
const drSmith: HostSchedules = {
hostId: 'dr-smith',
schedules: {
default: officeSchedule,
telehealth: extendedHoursSchedule,
},
};
// Use scheduleKey to select which schedule
const telehealthVisit: EventType = {
id: 'telehealth',
length: 20 * 60 * 1000,
scheduleKey: 'telehealth', // Uses telehealth schedule
};Helpers
Google Calendar Integration
import { buildBlocksFromFreebusy } from '@courierkit/availability';
// Convert Google Calendar FreeBusy response to blocks
const blocks = buildBlocksFromFreebusy(freebusyResponse, 'dr-smith');Recurrence Expansion
import { expandRecurrence } from '@courierkit/availability';
const weeklyMeeting = {
frequency: 'weekly' as const,
days: ['monday', 'wednesday'] as const,
startTime: '09:00',
endTime: '10:00',
timezone: 'America/New_York',
};
const intervals = expandRecurrence(weeklyMeeting, dateRange);Interval Arithmetic
import { mergeIntervals, subtractIntervals, intersectIntervals } from '@courierkit/availability';
// Merge overlapping intervals
const merged = mergeIntervals(intervals);
// Remove busy time from available time
const free = subtractIntervals(available, busy);
// Find common availability (all must be free)
const overlap = intersectIntervals(aliceAvailability, bobAvailability);API Reference
getAvailableSlots(input, now?)
The main entry point. Returns available slots for the given configuration.
expandSchedule(schedule, range)
Converts a schedule to UTC intervals for a date range.
expandRecurrence(rule, range)
Expands a recurrence rule into concrete intervals.
buildBlocksFromFreebusy(freebusy, hostId)
Converts Google Calendar FreeBusy response to blocks.
mergeIntervals(intervals)
Combines overlapping or adjacent intervals.
subtractIntervals(from, subtract)
Removes intervals from another set.
intersectIntervals(a, b)
Finds time present in both sets.
Documentation
Full documentation with examples: courierkit.mintlify.app
License
MIT
