@kalendio/scheduler
v1.0.0
Published
Headless, UI-agnostic scheduling for events and tasks.
Maintainers
Readme
@kalendio/scheduler
Headless, UI-agnostic scheduler for events and tasks. Holds items in-memory with O(1) day-bucket indexing, detects overlaps on the half-open interval [start, end), and feeds @kalendio/core's Calendar.getMeta slot through a thin adapter.
Two item kinds: event ({ start, end }, occupies a time slot) and task ({ date }, all-day, never overlaps).
Requirements
- Node.js
>=22.0.0(when running in Node) - pnpm
>=10.0.0(when consuming via pnpm)
Peer dependencies
| Package | Version | Notes |
| ------------------ | ------------- | -------------------------------------------------------------------- |
| @kalendio/shared | workspace:* | Pulled in automatically as a normal dependency — listed for clarity. |
| @kalendio/core | * | Only required if you use bindToCalendar to feed a Calendar. |
Installation
# pnpm
pnpm add @kalendio/scheduler
# npm
npm install @kalendio/scheduler
# yarn
yarn add @kalendio/scheduler
# bun
bun add @kalendio/schedulerQuick Start
import { createScheduler } from "@kalendio/scheduler";
const scheduler = createScheduler({
items: [
{ start: "2026-06-16T09:00", end: "2026-06-16T10:00" },
{ date: "2026-06-16" }, // all-day task
],
onConflict: ({ item, with: hits }) => {
console.warn(`${item.id} overlaps ${hits.length} item(s)`);
},
});
scheduler.add({ start: "2026-06-16T09:30", end: "2026-06-16T11:00" });
scheduler.getByDay(new Date(2026, 5, 16)); // → ScheduleItem[]
scheduler.findOverlaps(); // → ConflictPair[]Pairing with @kalendio/core
import { createCalendar } from "@kalendio/core";
import { createScheduler, bindToCalendar } from "@kalendio/scheduler";
const scheduler = createScheduler({ items: [...] });
const calendar = createCalendar({
view: "month",
getMeta: bindToCalendar(scheduler),
});Each day cell receives the scheduler's items for that day, or undefined when empty.
Design notes
- Zone-unaware. Every
DateLikeinput is interpreted in the consumer's local time. Bring your own timezone layer (e.g.@kalendio/shared'sadjustDateTimeZone) when this is not desired. - Half-open intervals. Two events overlap iff
a.start < b.end && b.start < a.end. Back-to-back events (one ending exactly when the next begins) do not overlap. - Tasks never conflict. They are timeless anchors on a day; only events participate in overlap detection.
- Items are cloned on read.
get,getByDay,getAll,findOverlaps, andbindToCalendarreturn freshDateobjects. Mutating them does not leak into storage. addrejects duplicate ids. Useupdateto replace an existing item — this prevents the day-bucket index from going inconsistent when an item's day changes.updatecan change the discriminant. Removingstart/endand addingdateturns an event into a task, and vice versa. Thetypefield is re-inferred from the merged shape.
API
createScheduler<T>(options?: SchedulerOptions<T>): Scheduler<T>
| Option | Type | Description |
| ------------ | ---------------------------- | ---------------------------------------------------------------------------------------------------------- |
| items | ScheduleInput<T>[] | Initial items — each runs through add, so conflicts are reported as usual. |
| onConflict | ConflictHandler<T> \| null | Callback invoked when an added/updated event overlaps with siblings on the same day. |
| generateId | IdGenerator | Override the default id generator. Defaults to globalThis.crypto.randomUUID() with a timestamp fallback. |
Scheduler<T>
| Method | Returns | Description |
| --------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| add(input) | ScheduleItem<T> | Adds an item, indexes it by day, fires onConflict. Throws INVALID_ITEM on bad shape, duplicate id, or unparseable date; INVALID_RANGE when end <= start. |
| addMany(inputs[]) | ScheduleItem<T>[] | Batches add over an array. |
| update(id, patch) | ScheduleItem<T> \| null | Merges patch onto the existing item. null on a field deletes it; undefined leaves it untouched. Returns null when id is unknown. |
| remove(id) | boolean | true when an item was removed. |
| clear() | void | Drops every item. |
| get(id) / has(id) / count() | ScheduleItem<T> \| undefined / boolean / number | Basic lookups. |
| getByDay(date) | ScheduleItem<T>[] | Items on the given calendar day, sorted chronologically (tasks first on ties, ascending id on further ties). |
| getAll() | ScheduleItem<T>[] | Every item, unspecified order. |
| findOverlaps(date?) | ConflictPair<T>[] | Overlapping event pairs. Omit date to scan every populated day; supply a date to scan that day only. |
| setConflictHandler(handler) | void | Replace the conflict handler at runtime. Pass null to silence. |
bindToCalendar<T>(scheduler): (date, ctx?) => ScheduleItem<T>[] \| undefined
Adapter wrapping any object exposing getByDay (typically a full Scheduler) into a CalendarConfig.getMeta callback for @kalendio/core.
Domain helpers
| Export | Description |
| ------------------------------------------------ | --------------------------------------------------------------------------------------------------- |
| isEvent(item): item is EventItem | Type predicate narrowing to the event variant. |
| eventsOverlap(a, b): boolean | Half-open interval overlap. |
| overlapsAmong(focal, pool): EventItem[] | Every event in pool that overlaps focal, excluding the focal one by id and ignoring tasks. |
| normalizeItem(input, generateId): ScheduleItem | Raw input → resolved item (infers type, coerces dates, validates the range invariant, stamps id). |
Errors
| Export | Description |
| ---------------------------- | ----------------------------------------------------------- |
| SchedulerErrorCode | Frozen { INVALID_ITEM, INVALID_RANGE } enum. |
| invalidItemError(message) | Factory returning a KalendioError tagged INVALID_ITEM. |
| invalidRangeError(message) | Factory returning a KalendioError tagged INVALID_RANGE. |
Constants
| Export | Description |
| ----------- | ------------------------------------------------------------------ |
| ITEM_TYPE | Frozen { EVENT: "event", TASK: "task" } discriminator constants. |
| ItemType | Union of ITEM_TYPE string values. |
Types
Scheduler, SchedulerOptions, EventInput, TaskInput, ScheduleInput, EventItem, TaskItem, ScheduleItem, UpdatePatch, Conflict, ConflictPair, ConflictHandler, IdGenerator, ObjectShape, EmptyObject.
Typed extras
The generic T slot threads consumer-defined fields through every input and output:
interface Meta extends Record<string, unknown> {
title: string;
color: string;
}
const scheduler = createScheduler<Meta>({
items: [{ start: "2026-06-16T09:00", title: "Standup", color: "red" }],
});
const item = scheduler.add({ start: "2026-06-16T11:00", title: "Sync", color: "blue" });
item.title; // typed as stringLicense
MIT
