@forcecalendar/core 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/calendar/Calendar.js +198 -14
- package/core/events/Event.js +173 -0
- package/core/events/EventStore.js +484 -92
- package/core/events/RecurrenceEngine.js +586 -20
- package/core/events/RecurrenceEngineV2.js +384 -51
- package/core/index.js +1 -1
- package/core/integration/EnhancedCalendar.js +88 -14
- package/core/types.js +95 -1
- package/package.json +1 -1
- package/types/calendar/Calendar.d.ts +137 -10
- package/types/events/Event.d.ts +75 -0
- package/types/events/EventStore.d.ts +213 -6
- package/types/events/RecurrenceEngine.d.ts +216 -1
- package/types/events/RecurrenceEngineV2.d.ts +156 -9
- package/types/index.d.ts +1 -1
- package/types/integration/EnhancedCalendar.d.ts +66 -1
- package/types/types.d.ts +296 -2
|
@@ -64,24 +64,57 @@ export declare class EventStore {
|
|
|
64
64
|
addEvent(event: Event | import('../types.js').EventData): Event;
|
|
65
65
|
/**
|
|
66
66
|
* Update an existing event
|
|
67
|
-
*
|
|
67
|
+
*
|
|
68
|
+
* An occurrence id (see {@link Event.occurrenceId}) updates the recurring
|
|
69
|
+
* master the occurrence belongs to, i.e. the whole series.
|
|
70
|
+
* @param {string} eventId - Event id or occurrence id
|
|
68
71
|
* @param {Partial<import('../types.js').EventData>} updates - Properties to update
|
|
69
|
-
* @returns {Event} The updated event
|
|
72
|
+
* @returns {Event} The updated event (the master for an occurrence id)
|
|
70
73
|
* @throws {Error} If event not found
|
|
71
74
|
*/
|
|
72
75
|
updateEvent(eventId: string, updates: Partial<import('../types.js').EventData>): Event;
|
|
76
|
+
/**
|
|
77
|
+
* Swap a stored event for a new instance with the same id, keeping
|
|
78
|
+
* indices and caches in sync. Does not notify listeners.
|
|
79
|
+
* @param {Event} existingEvent - Event currently in the store
|
|
80
|
+
* @param {Event} replacement - Event instance that takes its place
|
|
81
|
+
* @private
|
|
82
|
+
*/
|
|
83
|
+
private _replaceEvent;
|
|
73
84
|
/**
|
|
74
85
|
* Remove an event from the store
|
|
75
|
-
*
|
|
86
|
+
*
|
|
87
|
+
* An occurrence id (see {@link Event.occurrenceId}) removes the recurring
|
|
88
|
+
* master the occurrence belongs to, i.e. the whole series.
|
|
89
|
+
* @param {string} eventId - Event id or occurrence id
|
|
76
90
|
* @returns {boolean} True if removed, false if not found
|
|
77
91
|
*/
|
|
78
92
|
removeEvent(eventId: string): boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Remove an event from storage, caches and indices. Does not notify listeners.
|
|
95
|
+
* @param {Event} event - Event currently in the store
|
|
96
|
+
* @private
|
|
97
|
+
*/
|
|
98
|
+
private _detachEvent;
|
|
79
99
|
/**
|
|
80
100
|
* Get an event by ID
|
|
81
|
-
*
|
|
82
|
-
*
|
|
101
|
+
*
|
|
102
|
+
* Occurrence ids produced by {@link EventStore#expandRecurringEvent}
|
|
103
|
+
* (`<masterId>_<startMs>`, see {@link Event.occurrenceId}) resolve to the
|
|
104
|
+
* stored recurring master they were derived from, so an id taken from view
|
|
105
|
+
* data can always be looked up. Occurrences themselves are never stored.
|
|
106
|
+
* @param {string} eventId - Event id or occurrence id
|
|
107
|
+
* @returns {Event|null} The stored event (the master for an occurrence id) or null
|
|
83
108
|
*/
|
|
84
109
|
getEvent(eventId: string): Event | null;
|
|
110
|
+
/**
|
|
111
|
+
* Resolve an occurrence id to the stored recurring master it belongs to.
|
|
112
|
+
* Not cached: the master's own cache entry is the one kept in sync.
|
|
113
|
+
* @param {string} eventId - Candidate occurrence id
|
|
114
|
+
* @returns {Event|null} The master event or null
|
|
115
|
+
* @private
|
|
116
|
+
*/
|
|
117
|
+
private _resolveOccurrenceMaster;
|
|
85
118
|
/**
|
|
86
119
|
* Get all events
|
|
87
120
|
* @returns {Event[]} Array of all events
|
|
@@ -95,11 +128,59 @@ export declare class EventStore {
|
|
|
95
128
|
queryEvents(filters?: import('../types.js').QueryFilters): Event[];
|
|
96
129
|
/**
|
|
97
130
|
* Get events for a specific date
|
|
131
|
+
*
|
|
132
|
+
* Recurring series are expanded for the day, so the result holds their
|
|
133
|
+
* occurrences (see {@link EventStore#expandRecurringEvent}) rather than the
|
|
134
|
+
* master events. When building a grid of days use
|
|
135
|
+
* {@link EventStore#getEventsByDate}, which expands once for the whole range.
|
|
98
136
|
* @param {Date} date - The date to query
|
|
99
137
|
* @param {string} [timezone] - Timezone for the query (defaults to store timezone)
|
|
100
138
|
* @returns {Event[]} Events occurring on the date, sorted by start time
|
|
101
139
|
*/
|
|
102
140
|
getEventsForDate(date: Date, timezone?: string): Event[];
|
|
141
|
+
/**
|
|
142
|
+
* Get the events for every day in a range, keyed by local date (YYYY-MM-DD)
|
|
143
|
+
*
|
|
144
|
+
* Recurring series are expanded once for the whole range rather than once
|
|
145
|
+
* per day, which is what a month or week grid needs. Every day in the range
|
|
146
|
+
* has an entry (an empty array when nothing occurs) and multi-day events
|
|
147
|
+
* appear under each day they span. Each array is sorted like
|
|
148
|
+
* {@link EventStore#getEventsForDate}.
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* const byDate = store.getEventsByDate(gridStart, gridEnd);
|
|
152
|
+
* const events = byDate.get(DateUtils.getLocalDateString(cellDate)) || [];
|
|
153
|
+
*
|
|
154
|
+
* @param {Date} start - First day of the range
|
|
155
|
+
* @param {Date} end - Last day of the range
|
|
156
|
+
* @param {string} [timezone] - Timezone deciding which day an event falls on (defaults to store timezone)
|
|
157
|
+
* @returns {Map<string, Event[]>} Local date string -> events on that day
|
|
158
|
+
*/
|
|
159
|
+
getEventsByDate(start: Date, end: Date, timezone?: string): Map<string, Event[]>;
|
|
160
|
+
/**
|
|
161
|
+
* Collect the ids of stored events that may occur on a date.
|
|
162
|
+
* @param {Date} date - The date to query
|
|
163
|
+
* @returns {Set<string>} Candidate event ids
|
|
164
|
+
* @private
|
|
165
|
+
*/
|
|
166
|
+
private _collectDateCandidateIds;
|
|
167
|
+
/**
|
|
168
|
+
* Keep the events that overlap a day in the given timezone, sorted by start.
|
|
169
|
+
* @param {Event[]} events - Candidate events
|
|
170
|
+
* @param {Date} dayStart - Start of the day
|
|
171
|
+
* @param {Date} dayEnd - End of the day
|
|
172
|
+
* @param {string} timezone - Timezone deciding whether an event falls on the day
|
|
173
|
+
* @returns {Event[]} Events on the day, sorted
|
|
174
|
+
* @private
|
|
175
|
+
*/
|
|
176
|
+
private _selectEventsForDay;
|
|
177
|
+
/**
|
|
178
|
+
* Comparator ordering events by start time in a timezone, longer events first.
|
|
179
|
+
* @param {string} timezone - Timezone used for the start comparison
|
|
180
|
+
* @returns {(a: Event, b: Event) => number} Comparator
|
|
181
|
+
* @private
|
|
182
|
+
*/
|
|
183
|
+
private _compareByStart;
|
|
103
184
|
/**
|
|
104
185
|
* Get events that overlap with a given time range
|
|
105
186
|
* @param {Date} start - Start time
|
|
@@ -124,6 +205,15 @@ export declare class EventStore {
|
|
|
124
205
|
* @returns {Array<Event[]>} Array of event groups that overlap
|
|
125
206
|
*/
|
|
126
207
|
getOverlapGroups(date: Date, timedOnly?: boolean): Array<Event[]>;
|
|
208
|
+
/**
|
|
209
|
+
* Group a list of events into clusters of overlapping time slots
|
|
210
|
+
* Same result as {@link EventStore#getOverlapGroups} for events already fetched
|
|
211
|
+
* (for example one day of {@link EventStore#getEventsByDate}).
|
|
212
|
+
* @param {Event[]} events - Events to group; the array is not modified
|
|
213
|
+
* @param {boolean} [timedOnly=true] - Only include timed events (not all-day)
|
|
214
|
+
* @returns {Array<Event[]>} Array of event groups that overlap
|
|
215
|
+
*/
|
|
216
|
+
groupOverlappingEvents(events: Event[], timedOnly?: boolean): Array<Event[]>;
|
|
127
217
|
/**
|
|
128
218
|
* Calculate positions for overlapping events (for rendering)
|
|
129
219
|
* @param {Event[]} events - Array of overlapping events
|
|
@@ -145,6 +235,16 @@ export declare class EventStore {
|
|
|
145
235
|
getEventsInRange(start: Date, end: Date, expandRecurringOrOptions?: boolean | Object, timezone?: string): Event[];
|
|
146
236
|
/**
|
|
147
237
|
* Expand a recurring event into individual occurrences
|
|
238
|
+
*
|
|
239
|
+
* Returns every occurrence that overlaps the range, including ones that
|
|
240
|
+
* start before it but run into it (multi-day series). Each occurrence is an
|
|
241
|
+
* {@link Event} cloned from the master with:
|
|
242
|
+
* - `id` from {@link Event.occurrenceId} (`<masterId>_<startMs>`), stable
|
|
243
|
+
* across ranges and resolvable with {@link EventStore#getEvent},
|
|
244
|
+
* - `isOccurrence: true`, `recurringEventId` and `occurrenceStart`,
|
|
245
|
+
* - `metadata.recurringEventId`, `metadata.occurrenceId` (same as `id`) and
|
|
246
|
+
* `metadata.occurrenceIndex` (position within this expansion).
|
|
247
|
+
* Non-recurring events are returned as-is in a one-element array.
|
|
148
248
|
* @param {Event} event - The recurring event
|
|
149
249
|
* @param {Date} rangeStart - Start of the expansion range
|
|
150
250
|
* @param {Date} rangeEnd - End of the expansion range
|
|
@@ -152,6 +252,78 @@ export declare class EventStore {
|
|
|
152
252
|
* @returns {Event[]} Array of event occurrences
|
|
153
253
|
*/
|
|
154
254
|
expandRecurringEvent(event: Event, rangeStart: Date, rangeEnd: Date, timezone?: string): Event[];
|
|
255
|
+
/**
|
|
256
|
+
* Build the Event instance for one occurrence of a recurring master.
|
|
257
|
+
* @param {Event} event - The recurring master
|
|
258
|
+
* @param {{start: Date, end: Date, timezone?: string}} occurrence - Engine occurrence
|
|
259
|
+
* @param {string} eventTimezone - Timezone the series was expanded in
|
|
260
|
+
* @param {number} index - Position within the current expansion
|
|
261
|
+
* @returns {Event} Occurrence event
|
|
262
|
+
* @private
|
|
263
|
+
*/
|
|
264
|
+
private _createOccurrence;
|
|
265
|
+
/**
|
|
266
|
+
* Lazily iterate the occurrences of a stored event in chronological order.
|
|
267
|
+
*
|
|
268
|
+
* Occurrences come one at a time from the store's recurrence engine
|
|
269
|
+
* (RecurrenceEngineV2 by default), so taking the next few occurrences of
|
|
270
|
+
* an open-ended series does not expand the series. `after` and `before`
|
|
271
|
+
* are exclusive unless `inclusive` is set; see
|
|
272
|
+
* RecurrenceEngineV2.iterateOccurrences for the full semantics. The
|
|
273
|
+
* expansion timezone defaults to the event's, then the store's.
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* for (const occurrence of store.iterateOccurrences('standup', { after: new Date() })) {
|
|
277
|
+
* if (occurrence.start > deadline) break;
|
|
278
|
+
* remind(occurrence);
|
|
279
|
+
* }
|
|
280
|
+
*
|
|
281
|
+
* @param {string} eventId - The event ID
|
|
282
|
+
* @param {import('../types.js').ExpandedOccurrenceIteratorOptions} [options={}] - Window and expansion options
|
|
283
|
+
* @returns {Generator<import('../types.js').ExpandedOccurrence, void, undefined>} Occurrences in chronological order
|
|
284
|
+
* @throws {Error} If no event with the ID exists
|
|
285
|
+
*/
|
|
286
|
+
iterateOccurrences(eventId: string, options?: import('../types.js').ExpandedOccurrenceIteratorOptions): Generator<import('../types.js').ExpandedOccurrence, void, undefined>;
|
|
287
|
+
/**
|
|
288
|
+
* First occurrence of a stored event after an instant, or null when the
|
|
289
|
+
* series has no occurrence after it. `after` is exclusive unless
|
|
290
|
+
* `options.inclusive` is set.
|
|
291
|
+
*
|
|
292
|
+
* @example
|
|
293
|
+
* const upcoming = store.getNextOccurrence('standup', new Date());
|
|
294
|
+
*
|
|
295
|
+
* @param {string} eventId - The event ID
|
|
296
|
+
* @param {Date|number} [after=null] - Instant to search from (defaults to the series start)
|
|
297
|
+
* @param {import('../types.js').ExpandedOccurrenceIteratorOptions} [options={}] - Further options
|
|
298
|
+
* @returns {import('../types.js').ExpandedOccurrence|null} The next occurrence, or null
|
|
299
|
+
* @throws {Error} If no event with the ID exists
|
|
300
|
+
*/
|
|
301
|
+
getNextOccurrence(eventId: string, after?: Date | number, options?: import('../types.js').ExpandedOccurrenceIteratorOptions): import('../types.js').ExpandedOccurrence | null;
|
|
302
|
+
/**
|
|
303
|
+
* The first `count` occurrences of a stored event inside a window,
|
|
304
|
+
* generated lazily. `count` is capped at the engine's
|
|
305
|
+
* MAX_OCCURRENCES_HARD_LIMIT.
|
|
306
|
+
*
|
|
307
|
+
* @example
|
|
308
|
+
* const nextFive = store.takeOccurrences('standup', 5, { after: new Date() });
|
|
309
|
+
*
|
|
310
|
+
* @param {string} eventId - The event ID
|
|
311
|
+
* @param {number} count - Maximum number of occurrences to return
|
|
312
|
+
* @param {import('../types.js').ExpandedOccurrenceIteratorOptions} [options={}] - Window and expansion options
|
|
313
|
+
* @returns {import('../types.js').ExpandedOccurrence[]} Up to `count` occurrences in chronological order
|
|
314
|
+
* @throws {Error} If no event with the ID exists
|
|
315
|
+
*/
|
|
316
|
+
takeOccurrences(eventId: string, count: number, options?: import('../types.js').ExpandedOccurrenceIteratorOptions): import('../types.js').ExpandedOccurrence[];
|
|
317
|
+
/**
|
|
318
|
+
* Resolve an occurrence query to the stored event and its options, with
|
|
319
|
+
* the timezone defaulted as expandRecurringEvent does
|
|
320
|
+
* @param {string} eventId - The event ID
|
|
321
|
+
* @param {Object} options - Caller options
|
|
322
|
+
* @returns {{ event: Event, options: Object }}
|
|
323
|
+
* @throws {Error} If no event with the ID exists
|
|
324
|
+
* @private
|
|
325
|
+
*/
|
|
326
|
+
private _occurrenceQuery;
|
|
155
327
|
/**
|
|
156
328
|
* Clear all events
|
|
157
329
|
*/
|
|
@@ -161,6 +333,35 @@ export declare class EventStore {
|
|
|
161
333
|
* @param {Event[]} events - Array of events or event data
|
|
162
334
|
*/
|
|
163
335
|
loadEvents(events: Event[]): void;
|
|
336
|
+
/**
|
|
337
|
+
* Reconcile the store with a snapshot of events, applying only the differences.
|
|
338
|
+
*
|
|
339
|
+
* Compared with {@link EventStore#loadEvents} (clear + re-add everything) this:
|
|
340
|
+
* - keeps the existing {@link Event} instance for every entry that is
|
|
341
|
+
* equivalent to the stored one (identity is preserved, no notification),
|
|
342
|
+
* - replaces stored events whose incoming data differs (`update` change),
|
|
343
|
+
* - adds events whose id is not in the store (`add` change),
|
|
344
|
+
* - removes stored events missing from the snapshot (`remove` change),
|
|
345
|
+
* unless `removeMissing` is `false`,
|
|
346
|
+
* - emits a single `batch` notification listing those changes, or nothing at
|
|
347
|
+
* all when the snapshot matches the store. When called while a batch is
|
|
348
|
+
* already open the changes are queued on that batch instead.
|
|
349
|
+
*
|
|
350
|
+
* Input is validated up front: invalid event data or duplicate ids throw
|
|
351
|
+
* before the store is modified. Any error raised while applying the diff
|
|
352
|
+
* rolls the store back to its previous state.
|
|
353
|
+
*
|
|
354
|
+
* @example
|
|
355
|
+
* // periodic server snapshot
|
|
356
|
+
* const { added, updated, removed } = store.reconcile(rowsFromServer);
|
|
357
|
+
* if (added.length || updated.length || removed.length) rerender();
|
|
358
|
+
*
|
|
359
|
+
* @param {Array<Event|import('../types.js').EventData>} events - Complete snapshot of events
|
|
360
|
+
* @param {import('../types.js').ReconcileOptions} [options={}] - Reconcile options
|
|
361
|
+
* @returns {import('../types.js').ReconcileResult} Events that were added, updated, removed and left untouched
|
|
362
|
+
* @throws {Error} If an entry fails validation or two entries share an id
|
|
363
|
+
*/
|
|
364
|
+
reconcile(events: Array<Event | import('../types.js').EventData>, options?: import('../types.js').ReconcileOptions): import('../types.js').ReconcileResult;
|
|
164
365
|
/**
|
|
165
366
|
* Subscribe to store changes
|
|
166
367
|
* @param {Function} callback - Callback function
|
|
@@ -201,7 +402,13 @@ export declare class EventStore {
|
|
|
201
402
|
* Notify listeners of changes
|
|
202
403
|
* @private
|
|
203
404
|
*/
|
|
204
|
-
|
|
405
|
+
/**
|
|
406
|
+
* Deliver a change now, or queue it when a batch is open
|
|
407
|
+
* @param {import('../types.js').EventStoreChange} change - Change to deliver
|
|
408
|
+
* @private
|
|
409
|
+
*/
|
|
410
|
+
private _queueChange;
|
|
411
|
+
_notifyChange(change: any): void;
|
|
205
412
|
/**
|
|
206
413
|
* Get store statistics
|
|
207
414
|
* @returns {Object}
|
|
@@ -5,18 +5,166 @@
|
|
|
5
5
|
export declare class RecurrenceEngine {
|
|
6
6
|
static _systemTransitions: any;
|
|
7
7
|
static MAX_OCCURRENCES_HARD_LIMIT: number;
|
|
8
|
+
static MAX_ITERATIONS_HARD_LIMIT: number;
|
|
8
9
|
static _ruleCache: Map<any, any>;
|
|
9
10
|
static _RULE_CACHE_MAX: number;
|
|
10
11
|
/**
|
|
11
12
|
* Expand a recurring event into individual occurrences
|
|
13
|
+
*
|
|
14
|
+
* Occurrences before rangeStart are skipped without being generated:
|
|
15
|
+
* daily, weekly and sub-daily rules seek straight to the range, so the
|
|
16
|
+
* cost of a query does not grow with the age of the series, and a series
|
|
17
|
+
* that started years before the queried window is still expanded.
|
|
18
|
+
*
|
|
12
19
|
* @param {import('./Event.js').Event} event - The recurring event
|
|
13
20
|
* @param {Date} rangeStart - Start of the expansion range
|
|
14
21
|
* @param {Date} rangeEnd - End of the expansion range
|
|
15
|
-
* @param {number} [maxOccurrences=365] - Maximum number of occurrences to
|
|
22
|
+
* @param {number} [maxOccurrences=365] - Maximum number of occurrences to return.
|
|
23
|
+
* Only occurrences inside the range count towards this limit; occurrences
|
|
24
|
+
* between the series start and rangeStart do not consume it.
|
|
16
25
|
* @param {string} [timezone] - Timezone for expansion (important for DST)
|
|
17
26
|
* @returns {import('../types.js').EventOccurrence[]} Array of occurrence objects with start/end dates
|
|
18
27
|
*/
|
|
19
28
|
static expandEvent(event: import('./Event.js').Event, rangeStart: Date, rangeEnd: Date, maxOccurrences?: number, timezone?: string): import('../types.js').EventOccurrence[];
|
|
29
|
+
/**
|
|
30
|
+
* Lazily iterate the occurrences of an event in chronological order.
|
|
31
|
+
*
|
|
32
|
+
* Yields the same occurrence objects, in the same order, that expandEvent
|
|
33
|
+
* returns for the window — but one at a time, so a caller can stop after
|
|
34
|
+
* any number of them without the rest of the series being generated.
|
|
35
|
+
* Daily, weekly and sub-daily rules are seeked to `after` arithmetically,
|
|
36
|
+
* so finding the first occurrence after a far-away instant does not step
|
|
37
|
+
* through the series from its start.
|
|
38
|
+
*
|
|
39
|
+
* Both bounds are exclusive unless `inclusive` is set: an occurrence that
|
|
40
|
+
* starts exactly at `after` or `before` is skipped by default, which lets
|
|
41
|
+
* `iterateOccurrences(event, { after: previous.start })` continue a series
|
|
42
|
+
* without repeating `previous`. With `inclusive: true` the window is
|
|
43
|
+
* closed on both ends, exactly like expandEvent's range. An omitted bound
|
|
44
|
+
* leaves that end of the window open.
|
|
45
|
+
*
|
|
46
|
+
* COUNT, UNTIL, INTERVAL, BYDAY/BYMONTHDAY/BYSETPOS and exception dates
|
|
47
|
+
* are honoured as in expandEvent; BYSETPOS rules are yielded one period at
|
|
48
|
+
* a time, since the set positions of a period are only known once it is
|
|
49
|
+
* complete. A non-recurring event yields its single occurrence when it
|
|
50
|
+
* falls inside the window. Iteration ends at COUNT or UNTIL, at `before`,
|
|
51
|
+
* or — as a guard for rules that produce no occurrences — after
|
|
52
|
+
* MAX_ITERATIONS_HARD_LIMIT consecutive steps without one.
|
|
53
|
+
*
|
|
54
|
+
* The generator is single-use; call this method again for a fresh one.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* for (const occurrence of RecurrenceEngine.iterateOccurrences(event, { after: new Date() })) {
|
|
58
|
+
* if (occurrence.start > deadline) break;
|
|
59
|
+
* schedule(occurrence);
|
|
60
|
+
* }
|
|
61
|
+
*
|
|
62
|
+
* @param {import('./Event.js').Event} event - The event to iterate
|
|
63
|
+
* @param {import('../types.js').OccurrenceIteratorOptions} [options={}] - Window and timezone
|
|
64
|
+
* @returns {Generator<import('../types.js').EventOccurrence, void, undefined>} Occurrences in chronological order
|
|
65
|
+
* @throws {TypeError} If `after` or `before` is not a valid Date or timestamp
|
|
66
|
+
*/
|
|
67
|
+
static iterateOccurrences(event: import('./Event.js').Event, options?: import('../types.js').OccurrenceIteratorOptions): Generator<import('../types.js').EventOccurrence, void, undefined>;
|
|
68
|
+
/**
|
|
69
|
+
* First occurrence of an event after an instant, or null when the series
|
|
70
|
+
* has no occurrence after it (past COUNT or UNTIL, or a non-recurring
|
|
71
|
+
* event that already started).
|
|
72
|
+
*
|
|
73
|
+
* `after` is exclusive unless `options.inclusive` is set, so passing the
|
|
74
|
+
* start of a known occurrence returns the one that follows it. Not to be
|
|
75
|
+
* confused with getNextOccurrence, which steps a parsed rule once
|
|
76
|
+
* without regard to COUNT, UNTIL or exceptions.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* const upcoming = RecurrenceEngine.nextOccurrence(event, new Date());
|
|
80
|
+
*
|
|
81
|
+
* @param {import('./Event.js').Event} event - The event to query
|
|
82
|
+
* @param {Date|number} [after=null] - Instant to search from (defaults to the series start)
|
|
83
|
+
* @param {import('../types.js').OccurrenceIteratorOptions} [options={}] - Further window options
|
|
84
|
+
* @returns {import('../types.js').EventOccurrence|null} The next occurrence, or null
|
|
85
|
+
*/
|
|
86
|
+
static nextOccurrence(event: import('./Event.js').Event, after?: Date | number, options?: import('../types.js').OccurrenceIteratorOptions): import('../types.js').EventOccurrence | null;
|
|
87
|
+
/**
|
|
88
|
+
* The first `count` occurrences of an event inside a window, generated
|
|
89
|
+
* lazily so an open-ended series costs only the occurrences taken.
|
|
90
|
+
* `count` is capped at MAX_OCCURRENCES_HARD_LIMIT; fewer are returned
|
|
91
|
+
* when the series or the window ends first.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* const nextFive = RecurrenceEngine.takeOccurrences(event, 5, { after: new Date() });
|
|
95
|
+
*
|
|
96
|
+
* @param {import('./Event.js').Event} event - The event to query
|
|
97
|
+
* @param {number} count - Maximum number of occurrences to return
|
|
98
|
+
* @param {import('../types.js').OccurrenceIteratorOptions} [options={}] - Window and timezone
|
|
99
|
+
* @returns {import('../types.js').EventOccurrence[]} Up to `count` occurrences in chronological order
|
|
100
|
+
*/
|
|
101
|
+
static takeOccurrences(event: import('./Event.js').Event, count: number, options?: import('../types.js').OccurrenceIteratorOptions): import('../types.js').EventOccurrence[];
|
|
102
|
+
/**
|
|
103
|
+
* Resolve iterator options into a closed window on numeric timestamps.
|
|
104
|
+
* Exclusive bounds are shifted by one millisecond, the resolution of
|
|
105
|
+
* Date, so the expansion loops only ever compare inclusively.
|
|
106
|
+
* @param {import('../types.js').OccurrenceIteratorOptions} options - Iterator options
|
|
107
|
+
* @returns {{ startMs: number, endMs: number }} Inclusive bounds (infinite when open)
|
|
108
|
+
* @throws {TypeError} If a bound is not a valid Date or timestamp
|
|
109
|
+
* @private
|
|
110
|
+
*/
|
|
111
|
+
private static _occurrenceWindow;
|
|
112
|
+
/**
|
|
113
|
+
* Timestamp of a window bound given as a Date or a number
|
|
114
|
+
* @param {Date|number} value - Bound to convert
|
|
115
|
+
* @param {string} name - Option name for the error message
|
|
116
|
+
* @returns {number} Timestamp in milliseconds
|
|
117
|
+
* @throws {TypeError} If the value is not a valid Date or timestamp
|
|
118
|
+
* @private
|
|
119
|
+
*/
|
|
120
|
+
private static _boundMs;
|
|
121
|
+
/**
|
|
122
|
+
* Yield a single occurrence if it starts inside the window
|
|
123
|
+
* @param {import('../types.js').EventOccurrence} occurrence - The occurrence
|
|
124
|
+
* @param {{ startMs: number, endMs: number }} window - Inclusive bounds
|
|
125
|
+
* @returns {Generator<import('../types.js').EventOccurrence, void, undefined>}
|
|
126
|
+
* @private
|
|
127
|
+
*/
|
|
128
|
+
private static _iterateSingle;
|
|
129
|
+
/**
|
|
130
|
+
* Lazy counterpart of the expansion loops: seeks to the window, then
|
|
131
|
+
* advances a Date cursor per step and yields each in-window occurrence,
|
|
132
|
+
* applying the same DST adjustment and exception filtering.
|
|
133
|
+
* @private
|
|
134
|
+
*/
|
|
135
|
+
private static _iterateRule;
|
|
136
|
+
/**
|
|
137
|
+
* Seek the iteration cursor to the last occurrence before rangeStartMs
|
|
138
|
+
* using the same arithmetic as expandEvent. The system-transition scan
|
|
139
|
+
* is bounded by the target itself, so an open-ended window costs no
|
|
140
|
+
* more than a closed one.
|
|
141
|
+
* @param {number} fromMs - Cursor position (DTSTART)
|
|
142
|
+
* @param {number} weekday - Weekday of the cursor
|
|
143
|
+
* @param {Object} rule - Parsed recurrence rule
|
|
144
|
+
* @param {number} rangeStartMs - Seek target
|
|
145
|
+
* @returns {{ ms: number, steps: number }|null} New cursor, or null for rules that cannot seek
|
|
146
|
+
* @private
|
|
147
|
+
*/
|
|
148
|
+
private static _seekToWindow;
|
|
149
|
+
/**
|
|
150
|
+
* Milliseconds per _advanceInPlace step for every rule whose step is a
|
|
151
|
+
* fixed duration between system-timezone transitions: the sub-daily
|
|
152
|
+
* frequencies, DAILY and plain WEEKLY
|
|
153
|
+
* @param {Object} rule - Parsed recurrence rule
|
|
154
|
+
* @returns {number} Step length in milliseconds, or 0 when not fixed
|
|
155
|
+
* @private
|
|
156
|
+
*/
|
|
157
|
+
private static _seekStepMs;
|
|
158
|
+
/**
|
|
159
|
+
* Streaming BYSETPOS filter: buffers the occurrences of one period and
|
|
160
|
+
* yields its selected positions once the next period begins. Periods
|
|
161
|
+
* arrive contiguously and in order, so the result matches _applyBySetPos.
|
|
162
|
+
* @param {Iterable<import('../types.js').EventOccurrence>} source - Occurrences in order
|
|
163
|
+
* @param {Object} rule - Rule with bySetPos
|
|
164
|
+
* @returns {Generator<import('../types.js').EventOccurrence, void, undefined>}
|
|
165
|
+
* @private
|
|
166
|
+
*/
|
|
167
|
+
private static _iterateBySetPos;
|
|
20
168
|
/**
|
|
21
169
|
* General expansion loop: advances a Date cursor per step. Handles every
|
|
22
170
|
* frequency and degenerate rules (non-advancing dates, invalid intervals).
|
|
@@ -39,6 +187,56 @@ export declare class RecurrenceEngine {
|
|
|
39
187
|
* @private
|
|
40
188
|
*/
|
|
41
189
|
private static _expandFast;
|
|
190
|
+
/**
|
|
191
|
+
* Milliseconds per step for rules whose step is a fixed duration while
|
|
192
|
+
* the system UTC offset is constant. Only the sub-daily frequencies are
|
|
193
|
+
* reported here: DAILY and WEEKLY have their own numeric loop, and the
|
|
194
|
+
* calendar-based frequencies take too few steps per year to need seeking.
|
|
195
|
+
* @param {Object} rule - Parsed recurrence rule
|
|
196
|
+
* @returns {number} Step length in milliseconds, or 0 when not fixed
|
|
197
|
+
* @private
|
|
198
|
+
*/
|
|
199
|
+
private static _fixedStepMs;
|
|
200
|
+
/**
|
|
201
|
+
* Skip the occurrences of a fixed-step rule that fall before
|
|
202
|
+
* rangeStartMs without visiting each one.
|
|
203
|
+
*
|
|
204
|
+
* While the system UTC offset is constant, a wall-clock step of the
|
|
205
|
+
* cursor is a constant number of milliseconds, so a whole run of steps
|
|
206
|
+
* collapses into one multiplication. The single step that crosses a
|
|
207
|
+
* system-timezone transition is taken with `advance` instead, so the
|
|
208
|
+
* cursor ends up exactly where stepping every occurrence would have put
|
|
209
|
+
* it. Stops at the last occurrence before rangeStartMs; the caller's loop
|
|
210
|
+
* takes the step into the range.
|
|
211
|
+
*
|
|
212
|
+
* @param {number} fromMs - Cursor position (an occurrence instant)
|
|
213
|
+
* @param {number} rangeStartMs - Seek target
|
|
214
|
+
* @param {number} rangeEndMs - Upper bound for transition lookup
|
|
215
|
+
* @param {number} stepMs - Step length while the UTC offset is constant
|
|
216
|
+
* @param {number} maxSteps - Steps still permitted under COUNT (Infinity if unbounded)
|
|
217
|
+
* @param {(cursor: Date) => void} advance - Wall-clock step, mutating the cursor
|
|
218
|
+
* @returns {{ ms: number, steps: number, nextSystemTransition: number }}
|
|
219
|
+
* Cursor position, steps taken and the next system transition after it
|
|
220
|
+
* @private
|
|
221
|
+
*/
|
|
222
|
+
private static _seekFixedStep;
|
|
223
|
+
/**
|
|
224
|
+
* Seek for WEEKLY BYDAY rules, whose step pattern repeats every week:
|
|
225
|
+
* whole weeks are skipped arithmetically from any weekday in the BYDAY
|
|
226
|
+
* set, and single steps (identical to the expansion loop's) are only
|
|
227
|
+
* taken to reach the set, around system-timezone transitions and in the
|
|
228
|
+
* last week before the range.
|
|
229
|
+
*
|
|
230
|
+
* @param {number} fromMs - Cursor position (an occurrence instant)
|
|
231
|
+
* @param {number} weekday - Weekday of the cursor (Date#getDay)
|
|
232
|
+
* @param {number} rangeStartMs - Seek target
|
|
233
|
+
* @param {number} rangeEndMs - Upper bound for transition lookup
|
|
234
|
+
* @param {Object} rule - Parsed rule with compiled _byDaySet/_byDayDeltas
|
|
235
|
+
* @param {number} maxSteps - Steps still permitted under COUNT (Infinity if unbounded)
|
|
236
|
+
* @returns {{ ms: number, steps: number, weekday: number, nextSystemTransition: number }}
|
|
237
|
+
* @private
|
|
238
|
+
*/
|
|
239
|
+
private static _seekWeekCycle;
|
|
42
240
|
/**
|
|
43
241
|
* Find the next system-timezone offset transition after fromMs.
|
|
44
242
|
* Cached module-wide: the system timezone is fixed for the process.
|
|
@@ -63,6 +261,23 @@ export declare class RecurrenceEngine {
|
|
|
63
261
|
* @private
|
|
64
262
|
*/
|
|
65
263
|
private static _applyBySetPos;
|
|
264
|
+
/**
|
|
265
|
+
* Key of the BYSETPOS period an occurrence belongs to
|
|
266
|
+
* @param {import('../types.js').EventOccurrence} occurrence - The occurrence
|
|
267
|
+
* @param {Object} rule - Recurrence rule
|
|
268
|
+
* @returns {string|number} Period key
|
|
269
|
+
* @private
|
|
270
|
+
*/
|
|
271
|
+
private static _bySetPosKey;
|
|
272
|
+
/**
|
|
273
|
+
* Occurrences of one period selected by the rule's BYSETPOS positions,
|
|
274
|
+
* in BYSETPOS order
|
|
275
|
+
* @param {Array} group - Occurrences of a single period, in order
|
|
276
|
+
* @param {Object} rule - Rule with bySetPos
|
|
277
|
+
* @returns {Array} Selected occurrences
|
|
278
|
+
* @private
|
|
279
|
+
*/
|
|
280
|
+
private static _selectBySetPos;
|
|
66
281
|
/**
|
|
67
282
|
* Parse an RRULE string into a rule object
|
|
68
283
|
* @param {string|import('../types.js').RecurrenceRule} ruleString - RRULE string (e.g., "FREQ=DAILY;INTERVAL=1;COUNT=10") or rule object
|