Event-set projection layer for contextual scheduling with @rrulenet/recurrence.
rrule.net • @rrulenet ecosystem
@rrulenet/rrule ·
@rrulenet/recurrence ·
@rrulenet/core ·
@rrulenet/events ·
@rrulenet/cli
@rrulenet/events is a small event-set projection layer. It combines explicit event sets supplied by an application with transforms that either project a Recurrence from @rrulenet/recurrence during or around those events, or generate event-relative triggers from those events.
It is not an event catalogue, a data client, or an event-discovery service. Applications own event sourcing, persistence, metadata, permissions, and product-specific availability rules.
npm install @rrulenet/events @rrulenet/recurrenceThe package answers this question:
given explicit event dates/windows + a transform,
which occurrences should run?
For example:
Every day at 09:00 during Black Friday and Cyber Monday
or:
Every day at 09:00 from 10 days before Black Friday through 2 days after Cyber Monday
or:
Trigger each regional audience 7 days before a maintenance window at 09:00 local time
@rrulenet/recurrence remains the recurrence engine. It owns recurrence parsing, recurrence JSON, point occurrence generation, and recurrence algebra.
@rrulenet/events does not extend that API. It depends on @rrulenet/recurrence and uses Recurrence as the engine for during and window transforms. Event-relative transforms are not recurrence filters; they generate explicit occurrences from event anchors.
The event layer owns only:
- event-set JSON validation
- date, point, and interval member semantics
- projection of recurrence occurrences onto event dates or event windows
- event-relative trigger occurrences in a target timezone
- deterministic serialization of event schedules
Runtime event data does not live in this package.
Applications pass explicit EventSetJson objects into the library. Event ids, versions, sources, metadata, curation status, persistence, permissions, and availability rules belong to the application layer.
This package does not fetch event data and does not ship a runtime event catalogue.
Tests include deterministic fixtures such as black_friday_2026 and cyber_monday_2026. They are fixtures, not bundled catalogue data.
EventSetJson contains explicit members:
type EventSetMemberJson =
| { kind: 'date'; id: string; date: '2026-11-27' }
| { kind: 'point'; id: string; at: '2026-11-27T09:00:00Z' }
| { kind: 'interval'; id: string; start: string; end: string };For recurrence projection, the schedule combines events, recurrence, and transform:
type EventScheduleJson = {
kind: 'event-schedule';
timezone: string;
events: EventSetJson;
recurrence: RecurrenceJson;
transform:
| { kind: 'during' }
| { kind: 'window'; before?: { days?: number }; after?: { days?: number } };
};For event-relative triggers, recurrence is intentionally absent:
type EventScheduleJson = {
kind: 'event-schedule';
timezone: string;
events: EventSetJson;
transform: {
kind: 'event-relative';
anchor?: 'start';
triggers: Array<
| { before: { days: number }; time: string }
| { after: { days: number }; time: string }
| { before: ElapsedDuration }
| { after: ElapsedDuration }
| { at: 'start' | 'end' }
>;
};
};
type ElapsedDuration =
| { hours: number; minutes?: number }
| { hours?: number; minutes: number };Date members are calendar dates. They are projected into the schedule timezone as half-open local-day windows:
[YYYY-MM-DDT00:00, next local midnight)
Point members are exact temporal points. Interval members are half-open windows:
[start, end)
Event-relative transforms deliberately distinguish calendar projection from elapsed-time arithmetic.
Calendar triggers use integer days plus a required local time:
- date members anchor on their date
- point members anchor on the point instant converted to the schedule timezone
- interval
beforetriggers anchor on the interval start instant converted to the schedule timezone - interval
aftertriggers anchor on the interval end instant converted to the schedule timezone
They apply calendar days to the local anchor date, then combine the resulting date with the supplied wall-clock time. The local time therefore remains stable across daylight-saving transitions.
Exact elapsed triggers use non-negative integer hours, minutes, or both,
with a positive total duration, and do not accept time. They subtract from the
start instant for before, and add to the interval end or point instant for
after. Exact { at: 'start' } and { at: 'end' } triggers project the
corresponding instant directly.
Date members support only calendar triggers because a date does not identify an
exact instant. Point members support elapsed triggers and at: 'start'.
Interval members support every trigger, including at: 'end'. Every trigger
must contain exactly one of before, after, or at; calendar days cannot be
mixed with elapsed hours or minutes.
import { Temporal } from 'temporal-polyfill';
import { Recurrence } from '@rrulenet/recurrence';
import { EventSchedule } from '@rrulenet/events';
const recurrence = Recurrence.rule({
freq: 'DAILY',
byHour: [9],
start: Temporal.ZonedDateTime.from('2026-11-01T09:00:00[America/New_York]'),
until: Temporal.ZonedDateTime.from('2026-12-10T09:00:00[America/New_York]'),
});
const schedule = EventSchedule.fromJSON({
kind: 'event-schedule',
timezone: 'America/New_York',
events: {
kind: 'event-set',
id: 'campaign_events_2026',
version: 'app-supplied-v1',
members: [
{ kind: 'date', id: 'black_friday_2026', date: '2026-11-27' },
{ kind: 'date', id: 'cyber_monday_2026', date: '2026-11-30' },
],
},
recurrence: recurrence.toJSON(),
transform: {
kind: 'window',
before: { days: 10 },
after: { days: 2 },
},
});
console.log(schedule.occurrences().map((value) => value.toString()));import { EventSchedule } from '@rrulenet/events';
const schedule = EventSchedule.fromJSON({
kind: 'event-schedule',
timezone: 'Australia/Perth',
events: {
kind: 'event-set',
id: 'inline.maintenance.2026-09-15',
version: 'user-snapshot-1',
source: 'inline',
timezone: 'UTC',
members: [
{
kind: 'interval',
id: 'maintenance-window',
start: '2026-09-15T02:00:00Z',
end: '2026-09-15T04:00:00Z',
},
],
},
transform: {
kind: 'event-relative',
triggers: [
{ before: { days: 7 }, time: '09:00' },
{ before: { hours: 2 } },
{ before: { minutes: 15 } },
{ at: 'start' },
{ at: 'end' },
{ after: { minutes: 30 } },
{ after: { days: 2 }, time: '09:00' },
],
},
});
console.log(schedule.occurrences().map((value) => value.toString()));
// [
// '2026-09-08T09:00:00+08:00[Australia/Perth]',
// '2026-09-15T08:00:00+08:00[Australia/Perth]',
// '2026-09-15T09:45:00+08:00[Australia/Perth]',
// '2026-09-15T10:00:00+08:00[Australia/Perth]',
// '2026-09-15T12:00:00+08:00[Australia/Perth]',
// '2026-09-15T12:30:00+08:00[Australia/Perth]',
// '2026-09-17T09:00:00+08:00[Australia/Perth]'
// ]@rrulenet/vevent is intentionally out of scope for this package.
This package keeps date sets, point sets, and interval/window semantics explicit, so a future VEVENT-oriented package can map richer event components into event sets later without forcing VEVENT concepts into this smaller projection layer.
npm install
npm test