Events and adapters
How Time reads your event records, and what happens when one is invalid.
Time has no event class of its own. You pass an array of your own records to Root, and an
adapter tells Time how to read each one: its ID, title, time range, and the resources it belongs
to.
import { createCalendar, defineEventAdapter, type CalendarDiagnostic } from "@moltenship/calendar";
interface Booking {
readonly id: `booking-${number}`;
readonly roomIds: readonly `room-${number}`[];
readonly summary: string;
readonly startsAt: Temporal.Instant;
readonly endsAt: Temporal.Instant;
readonly updatedAt: string;
readonly locked: boolean;
}
interface Room {
readonly id: `room-${number}`;
readonly name: string;
}
const bookingAdapter = defineEventAdapter<Booking>()({
getId: (booking) => booking.id,
getTitle: (booking) => booking.summary,
getInterval: (booking) => ({ kind: "timed", start: booking.startsAt, end: booking.endsAt }),
getResourceIds: (booking) => booking.roomIds,
getRevision: (booking) => booking.updatedAt,
isReadOnly: (booking) => booking.locked,
});
export const BookingCalendar = createCalendar<Booking, Room>()({ eventAdapter: bookingAdapter });defineEventAdapter<Booking>() is called twice on purpose. The first call fixes the record type,
and the second infers the ID types from your accessors. Here booking-${number} stays
booking-${number} everywhere Time hands an ID back to you, so you never cast.
getRevision and isReadOnly are optional. The adapter reference
lists every field.
Time ranges
getInterval returns one of three kinds of range. Pick the one that matches what the event means:
kind | start and end | Use it for |
|---|---|---|
"timed" | Temporal.Instant | Meetings and calls: exact moments, shown in the calendar's zone |
"all-day" | Temporal.PlainDate | Holidays and vacations: whole dates, the same in every zone |
"floating" | Temporal.PlainDateTime | Wall-clock times with no zone, like "gym at 7:00" |
The end is always exclusive. A one-day all-day event on October 8 has end set to October 9.
Time and time zones covers this in detail.
IDs
Event and resource IDs are non-empty strings or finite numbers. To check untrusted input before
it reaches the calendar, use isEventId and isResourceId:
import { isEventId } from "@moltenship/calendar";
isEventId("booking-12"); // true
isEventId(""); // false
isEventId(Number.NaN); // falseResources
Resources are what events are assigned to: rooms, people, equipment. Pass your own resource records
(anything with an id) to Root as resources, and return their IDs from getResourceIds. An
event can belong to no resource, one, or several.
Views can then filter by resource with snapshot.eventsForResource(roomId) or
snapshot.queryTimed(range, { resourceIds: [roomId] }). See snapshot queries.
Changing events
Time never edits, saves, or fetches your events. When your data changes, update your own state and
pass a new array. To fetch only the dates on screen, load the loadRange that
onVisibleRangeChange reports.
Treat your records as immutable: change a record by replacing it with a new object. Time compares
the array item by item, so a new array with the same records in the same order counts as unchanged,
even if you build it inline with filter or map. When records do change, Time re-reads only the ones that are new objects, and skips
those whose getRevision value is unchanged. The performance guide
explains how to make the most of that.
Invalid records
By default, an invalid record is skipped and reported. The rest of the calendar keeps working. Reasons include a missing or duplicate ID, an end that isn't after the start, a range of the wrong Temporal type, or an adapter function that throws.
Reports show up in meta.diagnostics and in the onDiagnosticsChange callback on Root:
export function reportDiagnostics(
diagnostics: readonly CalendarDiagnostic<Booking, Booking["id"]>[],
) {
for (const diagnostic of diagnostics) {
if (diagnostic.code === "invalid-event") {
console.warn(`Skipped event #${diagnostic.issue.index}: ${diagnostic.issue.code}`);
}
}
}
Pass reportDiagnostics as onDiagnosticsChange. Time calls it after mount and again whenever the
list changes; an inline function works too.
To reject bad input instead, pass invalidEventPolicy: "throw" to createCalendar.
Time then throws an InvalidCalendarEventSnapshotError during render, with every problem listed on
its issues property. Catch it with an error boundary.
Records in Time's own shape
If you're starting from scratch, you can store events in Time's built-in shape (CalendarEvent: an
id, title, kind, start, end, optional resourceIds, and your own data) and use
createCanonicalEventAdapter() instead of writing an adapter.
Last updated on