@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.
@@ -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
- * @param {string} eventId - The event ID
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
- * @param {string} eventId - The event ID to remove
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
- * @param {string} eventId - The event ID
82
- * @returns {Event|null} The event or null if not found
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
- private _notifyChange;
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 generate
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