@rrulenet/events
v0.1.8
Published
Event-set projection layer for contextual scheduling with @rrulenet/recurrence.
Maintainers
Readme
@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.
Install
npm install @rrulenet/events @rrulenet/recurrenceRole
The 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 Mondayor:
Every day at 09:00 from 10 days before Black Friday through 2 days after Cyber Mondayor:
Trigger each regional audience 7 days before a maintenance window at 09:00 local timeBoundary With @rrulenet/recurrence
@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
Data Boundary
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.
JSON Model
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.
Example
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()));Event-Relative Example
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]'
// ]Future VEVENT Relationship
@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.
Development
npm install
npm test