@forcecalendar/core 2.4.0 → 2.5.1
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 +191 -23
- package/core/events/Event.js +106 -14
- package/core/events/EventStore.js +482 -63
- package/core/events/RRuleParser.js +19 -3
- package/core/events/RecurrenceEngine.js +427 -31
- package/core/events/RecurrenceEngineV2.js +497 -88
- package/core/index.js +1 -1
- package/core/integration/EnhancedCalendar.js +126 -14
- package/core/timezone/TimezoneManager.js +45 -9
- package/core/types.js +61 -0
- package/package.json +1 -1
- package/types/calendar/Calendar.d.ts +123 -9
- package/types/events/Event.d.ts +50 -0
- package/types/events/EventStore.d.ts +235 -13
- package/types/events/RRuleParser.d.ts +10 -1
- package/types/events/RecurrenceEngine.d.ts +159 -1
- package/types/events/RecurrenceEngineV2.d.ts +178 -15
- package/types/index.d.ts +1 -1
- package/types/integration/EnhancedCalendar.d.ts +73 -1
- package/types/types.d.ts +201 -0
package/core/index.js
CHANGED
|
@@ -32,7 +32,7 @@ export { ConflictDetector } from './conflicts/ConflictDetector.js';
|
|
|
32
32
|
export { EnhancedCalendar } from './integration/EnhancedCalendar.js';
|
|
33
33
|
|
|
34
34
|
// Version — keep in sync with package.json
|
|
35
|
-
export const VERSION = '2.
|
|
35
|
+
export const VERSION = '2.5.1';
|
|
36
36
|
|
|
37
37
|
// Default export
|
|
38
38
|
export { Calendar as default } from './calendar/Calendar.js';
|
|
@@ -24,6 +24,40 @@ export class EnhancedCalendar extends Calendar {
|
|
|
24
24
|
|
|
25
25
|
// Setup event listeners for real-time indexing
|
|
26
26
|
this.setupRealtimeIndexing();
|
|
27
|
+
|
|
28
|
+
// The enhanced engine keeps its own expansion cache, so drop the entries
|
|
29
|
+
// of every series the store changes
|
|
30
|
+
this._unsubscribeCacheInvalidation = this.eventStore.subscribe(change =>
|
|
31
|
+
this._invalidateOccurrenceCache(change)
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Invalidate the enhanced engine's cached expansions for a store change
|
|
37
|
+
* @param {import('../types.js').EventStoreChange} change - Store change
|
|
38
|
+
* @private
|
|
39
|
+
*/
|
|
40
|
+
_invalidateOccurrenceCache(change) {
|
|
41
|
+
const engine = this.recurrenceEngine;
|
|
42
|
+
if (!engine || !change) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
switch (change.type) {
|
|
46
|
+
case 'add':
|
|
47
|
+
case 'update':
|
|
48
|
+
case 'remove':
|
|
49
|
+
if (change.event) {
|
|
50
|
+
engine.clearEventCache(change.event.id);
|
|
51
|
+
}
|
|
52
|
+
break;
|
|
53
|
+
case 'batch':
|
|
54
|
+
for (const entry of change.changes || []) {
|
|
55
|
+
this._invalidateOccurrenceCache(entry);
|
|
56
|
+
}
|
|
57
|
+
break;
|
|
58
|
+
default:
|
|
59
|
+
engine.occurrenceCache.clear();
|
|
60
|
+
}
|
|
27
61
|
}
|
|
28
62
|
|
|
29
63
|
/**
|
|
@@ -56,29 +90,38 @@ export class EnhancedCalendar extends Calendar {
|
|
|
56
90
|
|
|
57
91
|
/**
|
|
58
92
|
* Get events with enhanced recurrence expansion
|
|
93
|
+
*
|
|
94
|
+
* Regular events overlapping the range are returned as stored. Every
|
|
95
|
+
* recurring series in the store is expanded with this calendar's
|
|
96
|
+
* RecurrenceEngineV2 (so instance modifications and cancellations apply),
|
|
97
|
+
* including series that started before the range. Occurrences are the
|
|
98
|
+
* engine's plain occurrence objects, with the id `<masterId>_<startMs>`
|
|
99
|
+
* (see `Event.occurrenceId`), `recurringEventId`, `isOccurrence: true` and
|
|
100
|
+
* `occurrenceStart`.
|
|
59
101
|
*/
|
|
60
102
|
getEventsInRange(startDate, endDate, options = {}) {
|
|
61
103
|
const startTime = performance.now();
|
|
104
|
+
const rangeStart = new Date(startDate);
|
|
105
|
+
const rangeEnd = new Date(endDate);
|
|
62
106
|
|
|
63
|
-
|
|
64
|
-
const
|
|
107
|
+
// Recurring masters are represented by their occurrences below
|
|
108
|
+
const regularEvents = this.eventStore
|
|
109
|
+
.getEventsInRange(rangeStart, rangeEnd, false)
|
|
110
|
+
.filter(event => !event.recurring);
|
|
65
111
|
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
for (const event of allEvents) {
|
|
70
|
-
if (event.recurring) {
|
|
71
|
-
recurringEvents.push(event);
|
|
72
|
-
} else {
|
|
73
|
-
regularEvents.push(event);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
112
|
+
// A series that started before the range can still occur inside it, so
|
|
113
|
+
// every recurring series is a candidate; the engine selects by range.
|
|
114
|
+
const recurringEvents = this.eventStore.queryEvents({ recurring: true });
|
|
76
115
|
|
|
77
116
|
// Expand recurring events with enhanced engine
|
|
78
117
|
const expandedOccurrences = [];
|
|
79
118
|
|
|
80
119
|
for (const event of recurringEvents) {
|
|
81
|
-
|
|
120
|
+
// Look back one event duration so occurrences that began before the
|
|
121
|
+
// range but overlap it are found
|
|
122
|
+
const duration = Math.max(0, event.end - event.start);
|
|
123
|
+
const expandStart = new Date(rangeStart.getTime() - duration);
|
|
124
|
+
const occurrences = this.recurrenceEngine.expandEvent(event, expandStart, rangeEnd, {
|
|
82
125
|
maxOccurrences: options.maxOccurrences || 365,
|
|
83
126
|
includeModified: options.includeModified !== false,
|
|
84
127
|
includeCancelled: options.includeCancelled || false,
|
|
@@ -86,7 +129,16 @@ export class EnhancedCalendar extends Calendar {
|
|
|
86
129
|
handleDST: options.handleDST !== false
|
|
87
130
|
});
|
|
88
131
|
|
|
89
|
-
|
|
132
|
+
for (const occurrence of occurrences) {
|
|
133
|
+
if (occurrence.end < rangeStart || occurrence.start > rangeEnd) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
expandedOccurrences.push({
|
|
137
|
+
...occurrence,
|
|
138
|
+
isOccurrence: true,
|
|
139
|
+
occurrenceStart: new Date(occurrence.start)
|
|
140
|
+
});
|
|
141
|
+
}
|
|
90
142
|
}
|
|
91
143
|
|
|
92
144
|
const endTime = performance.now();
|
|
@@ -129,6 +181,62 @@ export class EnhancedCalendar extends Calendar {
|
|
|
129
181
|
});
|
|
130
182
|
}
|
|
131
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Lazily iterate the occurrences of an event through the enhanced
|
|
186
|
+
* engine, so occurrences changed with modifyOccurrence or cancelled with
|
|
187
|
+
* cancelOccurrence are reflected. Same semantics as
|
|
188
|
+
* Calendar#iterateOccurrences.
|
|
189
|
+
* @param {string} eventId - The event ID
|
|
190
|
+
* @param {import('../types.js').ExpandedOccurrenceIteratorOptions} [options={}] - Window and expansion options
|
|
191
|
+
* @returns {Generator<import('../types.js').ExpandedOccurrence, void, undefined>} Occurrences in chronological order
|
|
192
|
+
* @throws {Error} If no event with the ID exists
|
|
193
|
+
*/
|
|
194
|
+
iterateOccurrences(eventId, options = {}) {
|
|
195
|
+
const query = this._occurrenceQuery(eventId, options);
|
|
196
|
+
return this.recurrenceEngine.iterateOccurrences(query.event, query.options);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* First occurrence of an event after an instant through the enhanced
|
|
201
|
+
* engine, or null. Same semantics as Calendar#getNextOccurrence.
|
|
202
|
+
* @param {string} eventId - The event ID
|
|
203
|
+
* @param {Date|number} [after=null] - Instant to search from (defaults to the series start)
|
|
204
|
+
* @param {import('../types.js').ExpandedOccurrenceIteratorOptions} [options={}] - Further options
|
|
205
|
+
* @returns {import('../types.js').ExpandedOccurrence|null} The next occurrence, or null
|
|
206
|
+
* @throws {Error} If no event with the ID exists
|
|
207
|
+
*/
|
|
208
|
+
getNextOccurrence(eventId, after = null, options = {}) {
|
|
209
|
+
const query = this._occurrenceQuery(eventId, options);
|
|
210
|
+
return this.recurrenceEngine.nextOccurrence(query.event, after, query.options);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The first `count` occurrences of an event through the enhanced
|
|
215
|
+
* engine. Same semantics as Calendar#takeOccurrences.
|
|
216
|
+
* @param {string} eventId - The event ID
|
|
217
|
+
* @param {number} count - Maximum number of occurrences to return
|
|
218
|
+
* @param {import('../types.js').ExpandedOccurrenceIteratorOptions} [options={}] - Window and expansion options
|
|
219
|
+
* @returns {import('../types.js').ExpandedOccurrence[]} Up to `count` occurrences in chronological order
|
|
220
|
+
* @throws {Error} If no event with the ID exists
|
|
221
|
+
*/
|
|
222
|
+
takeOccurrences(eventId, count, options = {}) {
|
|
223
|
+
const query = this._occurrenceQuery(eventId, options);
|
|
224
|
+
return this.recurrenceEngine.takeOccurrences(query.event, count, query.options);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Resolve an occurrence query to the stored event and its options, with
|
|
229
|
+
* the timezone defaulted as getEventsInRange does
|
|
230
|
+
* @private
|
|
231
|
+
*/
|
|
232
|
+
_occurrenceQuery(eventId, options) {
|
|
233
|
+
const event = this.eventStore.getEvent(eventId);
|
|
234
|
+
if (!event) {
|
|
235
|
+
throw new Error(`Event with id ${eventId} not found`);
|
|
236
|
+
}
|
|
237
|
+
return { event, options: { ...options, timezone: options.timezone || event.timeZone } };
|
|
238
|
+
}
|
|
239
|
+
|
|
132
240
|
/**
|
|
133
241
|
* Bulk operations for recurring events
|
|
134
242
|
*/
|
|
@@ -366,6 +474,10 @@ export class EnhancedCalendar extends Calendar {
|
|
|
366
474
|
this._clearReindexTimeout();
|
|
367
475
|
this._clearReindexTimeout = null;
|
|
368
476
|
}
|
|
477
|
+
if (typeof this._unsubscribeCacheInvalidation === 'function') {
|
|
478
|
+
this._unsubscribeCacheInvalidation();
|
|
479
|
+
this._unsubscribeCacheInvalidation = null;
|
|
480
|
+
}
|
|
369
481
|
|
|
370
482
|
// Clean up worker
|
|
371
483
|
if (this.searchManager) {
|
|
@@ -10,6 +10,11 @@ import { TimezoneDatabase } from './TimezoneDatabase.js';
|
|
|
10
10
|
// Singleton instance for shared use across the application
|
|
11
11
|
let sharedInstance = null;
|
|
12
12
|
|
|
13
|
+
// Timezone databases know only local mean time before the 19th century, so
|
|
14
|
+
// transition scans check a span that ends before this instant at its ends
|
|
15
|
+
// rather than probing it week by week
|
|
16
|
+
const PRE_TZDATA_FLOOR_MS = Date.UTC(1800, 0, 1);
|
|
17
|
+
|
|
13
18
|
export class TimezoneManager {
|
|
14
19
|
/**
|
|
15
20
|
* Get the shared singleton instance of TimezoneManager
|
|
@@ -215,19 +220,43 @@ export class TimezoneManager {
|
|
|
215
220
|
}
|
|
216
221
|
timezone = this.database.resolveAlias(timezone);
|
|
217
222
|
let cached = this.transitionCache.get(timezone);
|
|
218
|
-
if (!cached
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
223
|
+
if (!cached) {
|
|
224
|
+
cached = {
|
|
225
|
+
from: fromMs,
|
|
226
|
+
to: toMs,
|
|
227
|
+
transitions: this._scanTransitions(timezone, fromMs, toMs)
|
|
228
|
+
};
|
|
224
229
|
this.transitionCache.set(timezone, cached);
|
|
230
|
+
} else {
|
|
231
|
+
// Extend coverage incrementally so only the uncovered span is scanned
|
|
232
|
+
if (fromMs < cached.from) {
|
|
233
|
+
cached.transitions = this._scanTransitions(timezone, fromMs, cached.from).concat(
|
|
234
|
+
cached.transitions
|
|
235
|
+
);
|
|
236
|
+
cached.from = fromMs;
|
|
237
|
+
}
|
|
238
|
+
if (toMs > cached.to) {
|
|
239
|
+
cached.transitions = cached.transitions.concat(
|
|
240
|
+
this._scanTransitions(timezone, cached.to, toMs)
|
|
241
|
+
);
|
|
242
|
+
cached.to = toMs;
|
|
243
|
+
}
|
|
225
244
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
245
|
+
// First transition after fromMs; the list is sorted
|
|
246
|
+
const transitions = cached.transitions;
|
|
247
|
+
let lo = 0;
|
|
248
|
+
let hi = transitions.length;
|
|
249
|
+
while (lo < hi) {
|
|
250
|
+
const mid = (lo + hi) >>> 1;
|
|
251
|
+
if (transitions[mid] > fromMs) {
|
|
252
|
+
hi = mid;
|
|
253
|
+
} else {
|
|
254
|
+
lo = mid + 1;
|
|
229
255
|
}
|
|
230
256
|
}
|
|
257
|
+
if (lo < transitions.length && transitions[lo] <= toMs) {
|
|
258
|
+
return transitions[lo];
|
|
259
|
+
}
|
|
231
260
|
return Infinity;
|
|
232
261
|
}
|
|
233
262
|
|
|
@@ -247,6 +276,13 @@ export class TimezoneManager {
|
|
|
247
276
|
const offsetAt = ms => this.getTimezoneOffset(new Date(ms), timezone);
|
|
248
277
|
let lo = fromMs;
|
|
249
278
|
let loOffset = offsetAt(lo);
|
|
279
|
+
// Timezone databases know only local mean time before the 19th century:
|
|
280
|
+
// that era has a single offset and is skipped in one step (probing it
|
|
281
|
+
// week by week could take minutes for a very old DTSTART)
|
|
282
|
+
if (lo < PRE_TZDATA_FLOOR_MS) {
|
|
283
|
+
lo = Math.min(toMs, PRE_TZDATA_FLOOR_MS);
|
|
284
|
+
loOffset = offsetAt(lo);
|
|
285
|
+
}
|
|
250
286
|
while (lo < toMs) {
|
|
251
287
|
const hi = Math.min(lo + WEEK, toMs);
|
|
252
288
|
const hiOffset = offsetAt(hi);
|
package/core/types.js
CHANGED
|
@@ -324,6 +324,15 @@
|
|
|
324
324
|
* @property {import('./events/Event.js').Event[]} unchanged - Events left untouched
|
|
325
325
|
*/
|
|
326
326
|
|
|
327
|
+
/**
|
|
328
|
+
* Payload of the Calendar `eventSelect` event
|
|
329
|
+
* @typedef {Object} EventSelectPayload
|
|
330
|
+
* @property {import('./events/Event.js').Event} event - The stored event (the master for an occurrence id)
|
|
331
|
+
* @property {string} eventId - Id of the stored event, as kept in the state's selectedEventId
|
|
332
|
+
* @property {string|null} occurrenceId - The occurrence id that was selected, or null for a stored event's id
|
|
333
|
+
* @property {import('./events/Event.js').Event|null} occurrence - The selected occurrence as in view data, or null
|
|
334
|
+
*/
|
|
335
|
+
|
|
327
336
|
/**
|
|
328
337
|
* @typedef {Object} QueryFilters
|
|
329
338
|
* @property {Date} [start] - Start date for range query
|
|
@@ -346,6 +355,58 @@
|
|
|
346
355
|
* @property {Date} start - Occurrence start date
|
|
347
356
|
* @property {Date} end - Occurrence end date
|
|
348
357
|
* @property {string} recurringEventId - ID of the parent recurring event
|
|
358
|
+
* @property {string} [timezone] - Timezone the occurrence was expanded in
|
|
359
|
+
* @property {Date} [originalStart] - Start of the series (DTSTART)
|
|
360
|
+
*/
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Window for lazy occurrence iteration. Both bounds are exclusive unless
|
|
364
|
+
* `inclusive` is set: an occurrence starting exactly at `after` or `before`
|
|
365
|
+
* is skipped by default, so iterating from a known occurrence's start
|
|
366
|
+
* continues the series without repeating it. Omit a bound to leave that
|
|
367
|
+
* end of the window open.
|
|
368
|
+
* @typedef {Object} OccurrenceIteratorOptions
|
|
369
|
+
* @property {Date|number} [after] - Only occurrences starting after this instant (Date or timestamp)
|
|
370
|
+
* @property {Date|number} [before] - Only occurrences starting before this instant (Date or timestamp)
|
|
371
|
+
* @property {boolean} [inclusive=false] - Treat `after` and `before` as closed bounds
|
|
372
|
+
* @property {string} [timezone] - Timezone for expansion (defaults to the event's)
|
|
373
|
+
*/
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Options for lazy occurrence iteration through RecurrenceEngineV2,
|
|
377
|
+
* EventStore and Calendar: the OccurrenceIteratorOptions window plus the
|
|
378
|
+
* expansion switches RecurrenceEngineV2.expandEvent accepts.
|
|
379
|
+
* @typedef {Object} ExpandedOccurrenceIteratorOptions
|
|
380
|
+
* @property {Date|number} [after] - Only occurrences starting after this instant (Date or timestamp)
|
|
381
|
+
* @property {Date|number} [before] - Only occurrences starting before this instant (Date or timestamp)
|
|
382
|
+
* @property {boolean} [inclusive=false] - Treat `after` and `before` as closed bounds
|
|
383
|
+
* @property {string} [timezone] - Timezone for expansion (defaults to the event's)
|
|
384
|
+
* @property {boolean} [includeModified=true] - Apply stored instance modifications
|
|
385
|
+
* @property {boolean} [includeCancelled=false] - Yield exception dates as cancelled occurrences
|
|
386
|
+
* @property {boolean} [handleDST=true] - Adjust occurrences across DST transitions
|
|
387
|
+
*/
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Occurrence produced by RecurrenceEngineV2 (and therefore by EventStore
|
|
391
|
+
* and Calendar occurrence queries)
|
|
392
|
+
* @typedef {Object} ExpandedOccurrence
|
|
393
|
+
* @property {string} id - Occurrence ID (`<eventId>_<startTimestamp>` for recurring events)
|
|
394
|
+
* @property {string} [recurringEventId] - ID of the parent recurring event
|
|
395
|
+
* @property {string} title - Event title
|
|
396
|
+
* @property {Date} start - Occurrence start date
|
|
397
|
+
* @property {Date} end - Occurrence end date
|
|
398
|
+
* @property {Date} [startUTC] - Occurrence start in UTC
|
|
399
|
+
* @property {Date} [endUTC] - Occurrence end in UTC
|
|
400
|
+
* @property {string} timezone - Timezone the occurrence was expanded in
|
|
401
|
+
* @property {Date} [originalStart] - Start of the series (DTSTART)
|
|
402
|
+
* @property {boolean} allDay - Whether the event is all-day
|
|
403
|
+
* @property {string} [description] - Event description
|
|
404
|
+
* @property {string} [location] - Event location
|
|
405
|
+
* @property {string[]} [categories] - Event categories
|
|
406
|
+
* @property {EventStatus} [status] - 'confirmed', or 'cancelled' for exception dates yielded with includeCancelled
|
|
407
|
+
* @property {string} [cancellationReason] - Reason recorded for a cancelled occurrence
|
|
408
|
+
* @property {boolean} isRecurring - Whether the occurrence belongs to a recurring series
|
|
409
|
+
* @property {boolean} [isModified] - Whether a stored instance modification was applied
|
|
349
410
|
*/
|
|
350
411
|
|
|
351
412
|
/**
|
package/package.json
CHANGED
|
@@ -80,14 +80,20 @@ export declare class Calendar {
|
|
|
80
80
|
addEvent(eventData: import('../events/Event.js').Event | import('../types.js').EventData): import('../events/Event.js').Event;
|
|
81
81
|
/**
|
|
82
82
|
* Update an event
|
|
83
|
-
*
|
|
83
|
+
*
|
|
84
|
+
* An occurrence id taken from view data (see {@link Event.occurrenceId})
|
|
85
|
+
* updates the recurring master, i.e. the whole series.
|
|
86
|
+
* @param {string} eventId - Event id or occurrence id
|
|
84
87
|
* @param {Object} updates - Properties to update
|
|
85
|
-
* @returns {Event} The updated event
|
|
88
|
+
* @returns {Event} The updated event (the master for an occurrence id)
|
|
86
89
|
*/
|
|
87
90
|
updateEvent(eventId: string, updates: Object): Event;
|
|
88
91
|
/**
|
|
89
92
|
* Remove an event
|
|
90
|
-
*
|
|
93
|
+
*
|
|
94
|
+
* An occurrence id taken from view data (see {@link Event.occurrenceId})
|
|
95
|
+
* removes the recurring master, i.e. the whole series.
|
|
96
|
+
* @param {string} eventId - Event id or occurrence id
|
|
91
97
|
* @returns {boolean} True if removed
|
|
92
98
|
*/
|
|
93
99
|
removeEvent(eventId: string): boolean;
|
|
@@ -99,12 +105,39 @@ export declare class Calendar {
|
|
|
99
105
|
deleteEvent(eventId: string): boolean;
|
|
100
106
|
/**
|
|
101
107
|
* Get an event by ID
|
|
102
|
-
*
|
|
103
|
-
*
|
|
108
|
+
*
|
|
109
|
+
* Occurrence ids taken from view data (`<masterId>_<startMs>`, see
|
|
110
|
+
* {@link Event.occurrenceId}) resolve to the stored recurring master, so
|
|
111
|
+
* every id a renderer hands back can be looked up here.
|
|
112
|
+
* @param {string} eventId - Event id or occurrence id
|
|
113
|
+
* @returns {Event|null} The stored event (the master for an occurrence id) or null
|
|
104
114
|
*/
|
|
105
115
|
getEvent(eventId: string): Event | null;
|
|
106
116
|
/**
|
|
107
|
-
*
|
|
117
|
+
* Resolve an event id or occurrence id to the id of the stored event it
|
|
118
|
+
* refers to: the id itself for a stored event, the master's id for an
|
|
119
|
+
* occurrence id taken from view data, `null` when nothing stored matches.
|
|
120
|
+
* See `EventStore.resolveEventId`.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* calendar.resolveEventId('standup_1750028400000'); // 'standup'
|
|
124
|
+
* calendar.resolveEventId('unknown'); // null
|
|
125
|
+
*
|
|
126
|
+
* @param {string} id - Event id or occurrence id
|
|
127
|
+
* @returns {string|null} Id of the stored event, or null
|
|
128
|
+
*/
|
|
129
|
+
resolveEventId(id: string): string | null;
|
|
130
|
+
/**
|
|
131
|
+
* Get the occurrence an occurrence id from view data stands for, as an
|
|
132
|
+
* {@link Event} like the ones the views hold, or `null` when the id is
|
|
133
|
+
* not an occurrence of a stored recurring series. See
|
|
134
|
+
* `EventStore.getOccurrence`.
|
|
135
|
+
* @param {string} occurrenceId - Occurrence id (`<masterId>_<startMs>`)
|
|
136
|
+
* @returns {Event|null} The occurrence, or null
|
|
137
|
+
*/
|
|
138
|
+
getOccurrence(occurrenceId: string): Event | null;
|
|
139
|
+
/**
|
|
140
|
+
* Get all stored events (recurring masters, never their occurrences)
|
|
108
141
|
* @returns {Event[]}
|
|
109
142
|
*/
|
|
110
143
|
getEvents(): Event[];
|
|
@@ -156,9 +189,16 @@ export declare class Calendar {
|
|
|
156
189
|
* @param {Array<import('../events/Event.js').Event|import('../types.js').EventData>} events - Complete snapshot of events
|
|
157
190
|
* @param {import('../types.js').ReconcileOptions} [options={}] - Reconcile options
|
|
158
191
|
* @returns {import('../types.js').EventsSetPayload} Resulting events and the applied change set
|
|
159
|
-
* @throws {Error} If an entry fails validation or two entries share an id
|
|
192
|
+
* @throws {Error} If `events` is not iterable, an entry fails validation or two entries share an id
|
|
160
193
|
*/
|
|
161
194
|
reconcileEvents(events: Array<import('../events/Event.js').Event | import('../types.js').EventData>, options?: import('../types.js').ReconcileOptions): import('../types.js').EventsSetPayload;
|
|
195
|
+
/**
|
|
196
|
+
* Throw a clear error when a snapshot is not iterable
|
|
197
|
+
* @param {*} events - Candidate snapshot
|
|
198
|
+
* @param {string} method - Calling method, for the message
|
|
199
|
+
* @private
|
|
200
|
+
*/
|
|
201
|
+
private _assertIterable;
|
|
162
202
|
/**
|
|
163
203
|
* Get the event store's change counter
|
|
164
204
|
*
|
|
@@ -177,12 +217,23 @@ export declare class Calendar {
|
|
|
177
217
|
*/
|
|
178
218
|
queryEvents(filters: Object): Event[];
|
|
179
219
|
/**
|
|
180
|
-
* Get events for a specific date
|
|
220
|
+
* Get events for a specific date, with recurring series expanded into occurrences
|
|
181
221
|
* @param {Date} date - The date
|
|
182
222
|
* @param {string} [timezone] - Timezone for the query (defaults to calendar timezone)
|
|
183
223
|
* @returns {Event[]}
|
|
184
224
|
*/
|
|
185
225
|
getEventsForDate(date: Date, timezone?: string): Event[];
|
|
226
|
+
/**
|
|
227
|
+
* Get the events for every day in a range, keyed by local date (YYYY-MM-DD)
|
|
228
|
+
*
|
|
229
|
+
* Recurring series are expanded once for the whole range; this is what the
|
|
230
|
+
* month and week views use. See `EventStore.getEventsByDate`.
|
|
231
|
+
* @param {Date} start - First day of the range
|
|
232
|
+
* @param {Date} end - Last day of the range
|
|
233
|
+
* @param {string} [timezone] - Timezone for the query (defaults to calendar timezone)
|
|
234
|
+
* @returns {Map<string, Event[]>} Local date string -> events on that day
|
|
235
|
+
*/
|
|
236
|
+
getEventsByDate(start: Date, end: Date, timezone?: string): Map<string, Event[]>;
|
|
186
237
|
/**
|
|
187
238
|
* Get events in a date range
|
|
188
239
|
* @param {Date} start - Start date
|
|
@@ -191,6 +242,55 @@ export declare class Calendar {
|
|
|
191
242
|
* @returns {Event[]}
|
|
192
243
|
*/
|
|
193
244
|
getEventsInRange(start: Date, end: Date, timezone?: string): Event[];
|
|
245
|
+
/**
|
|
246
|
+
* Lazily iterate the occurrences of an event in chronological order.
|
|
247
|
+
*
|
|
248
|
+
* Occurrences are produced one at a time, so taking the next few of an
|
|
249
|
+
* open-ended series does not expand the series. `after` and `before`
|
|
250
|
+
* are exclusive unless `inclusive` is set; see
|
|
251
|
+
* RecurrenceEngineV2.iterateOccurrences for the full semantics.
|
|
252
|
+
*
|
|
253
|
+
* @example
|
|
254
|
+
* for (const occurrence of calendar.iterateOccurrences('standup', { after: new Date() })) {
|
|
255
|
+
* if (occurrence.start > deadline) break;
|
|
256
|
+
* remind(occurrence);
|
|
257
|
+
* }
|
|
258
|
+
*
|
|
259
|
+
* @param {string} eventId - Event id or occurrence id (resolved to its master)
|
|
260
|
+
* @param {import('../types.js').ExpandedOccurrenceIteratorOptions} [options={}] - Window and expansion options
|
|
261
|
+
* @returns {Generator<import('../types.js').ExpandedOccurrence, void, undefined>} Occurrences in chronological order
|
|
262
|
+
* @throws {Error} If no event with the ID exists
|
|
263
|
+
*/
|
|
264
|
+
iterateOccurrences(eventId: string, options?: import('../types.js').ExpandedOccurrenceIteratorOptions): Generator<import('../types.js').ExpandedOccurrence, void, undefined>;
|
|
265
|
+
/**
|
|
266
|
+
* First occurrence of an event after an instant, or null when the
|
|
267
|
+
* series has no occurrence after it. `after` is exclusive unless
|
|
268
|
+
* `options.inclusive` is set.
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* const upcoming = calendar.getNextOccurrence('standup', new Date());
|
|
272
|
+
*
|
|
273
|
+
* @param {string} eventId - Event id or occurrence id (resolved to its master)
|
|
274
|
+
* @param {Date|number} [after=null] - Instant to search from (defaults to the series start)
|
|
275
|
+
* @param {import('../types.js').ExpandedOccurrenceIteratorOptions} [options={}] - Further options
|
|
276
|
+
* @returns {import('../types.js').ExpandedOccurrence|null} The next occurrence, or null
|
|
277
|
+
* @throws {Error} If no event with the ID exists
|
|
278
|
+
*/
|
|
279
|
+
getNextOccurrence(eventId: string, after?: Date | number, options?: import('../types.js').ExpandedOccurrenceIteratorOptions): import('../types.js').ExpandedOccurrence | null;
|
|
280
|
+
/**
|
|
281
|
+
* The first `count` occurrences of an event inside a window, generated
|
|
282
|
+
* lazily. `count` is capped at the engine's MAX_OCCURRENCES_HARD_LIMIT.
|
|
283
|
+
*
|
|
284
|
+
* @example
|
|
285
|
+
* const nextFive = calendar.takeOccurrences('standup', 5, { after: new Date() });
|
|
286
|
+
*
|
|
287
|
+
* @param {string} eventId - Event id or occurrence id (resolved to its master)
|
|
288
|
+
* @param {number} count - Maximum number of occurrences to return (fractions are floored)
|
|
289
|
+
* @param {import('../types.js').ExpandedOccurrenceIteratorOptions} [options={}] - Window and expansion options
|
|
290
|
+
* @returns {import('../types.js').ExpandedOccurrence[]} Up to `count` occurrences in chronological order
|
|
291
|
+
* @throws {Error} If no event with the ID exists
|
|
292
|
+
*/
|
|
293
|
+
takeOccurrences(eventId: string, count: number, options?: import('../types.js').ExpandedOccurrenceIteratorOptions): import('../types.js').ExpandedOccurrence[];
|
|
194
294
|
/**
|
|
195
295
|
* Set the calendar's timezone
|
|
196
296
|
* @param {string} timezone - IANA timezone identifier
|
|
@@ -293,7 +393,21 @@ export declare class Calendar {
|
|
|
293
393
|
private _getListViewData;
|
|
294
394
|
/**
|
|
295
395
|
* Select an event
|
|
296
|
-
*
|
|
396
|
+
*
|
|
397
|
+
* Accepts a stored event's id or an occurrence id taken from view data
|
|
398
|
+
* (see {@link Event.occurrenceId}). Either way the stored event is what
|
|
399
|
+
* gets selected: `selectedEventId` in the state holds its id, so it can
|
|
400
|
+
* always be looked up with {@link Calendar#getEvent}. The `eventSelect`
|
|
401
|
+
* payload carries the stored `event`, its `eventId`, and for an
|
|
402
|
+
* occurrence id also `occurrenceId` and the `occurrence` itself (an
|
|
403
|
+
* {@link Event} as in view data, or null when the series has no
|
|
404
|
+
* occurrence at that instant). Nothing happens for an unknown id.
|
|
405
|
+
*
|
|
406
|
+
* @example
|
|
407
|
+
* calendar.on('eventSelect', ({ event, occurrence }) => open(occurrence || event));
|
|
408
|
+
* calendar.selectEvent(chip.dataset.eventId);
|
|
409
|
+
*
|
|
410
|
+
* @param {string} eventId - Event id or occurrence id to select
|
|
297
411
|
*/
|
|
298
412
|
selectEvent(eventId: string): void;
|
|
299
413
|
/**
|
package/types/events/Event.d.ts
CHANGED
|
@@ -23,6 +23,12 @@ export declare class Event {
|
|
|
23
23
|
textColor: string | undefined;
|
|
24
24
|
recurring: boolean | undefined;
|
|
25
25
|
recurrenceRule: string | import("../types.js").RecurrenceRule | undefined;
|
|
26
|
+
/** @type {boolean} True when this instance is one occurrence of a recurring series */
|
|
27
|
+
isOccurrence: boolean;
|
|
28
|
+
/** @type {string|null} Id of the recurring master this occurrence belongs to */
|
|
29
|
+
recurringEventId: string | null;
|
|
30
|
+
/** @type {Date|null} Start of this occurrence as generated by the recurrence rule */
|
|
31
|
+
occurrenceStart: Date | null;
|
|
26
32
|
_originalTimeZone: string | null;
|
|
27
33
|
status: import("../types.js").EventStatus | undefined;
|
|
28
34
|
visibility: import("../types.js").EventVisibility | undefined;
|
|
@@ -161,6 +167,8 @@ export declare class Event {
|
|
|
161
167
|
* Scalars are compared with strict equality, dates by timestamp and
|
|
162
168
|
* structured fields (recurrence rule, organizer, attendees, reminders,
|
|
163
169
|
* categories, attachments, conference data, metadata) structurally.
|
|
170
|
+
* The `color` shorthand is not listed: normalization copies it into
|
|
171
|
+
* `backgroundColor` and `borderColor`, which are compared instead.
|
|
164
172
|
* @type {ReadonlyArray<string>}
|
|
165
173
|
*/
|
|
166
174
|
static EQUIVALENCE_FIELDS: ReadonlyArray<string>;
|
|
@@ -179,6 +187,14 @@ export declare class Event {
|
|
|
179
187
|
* describe the same event. Two events with different ids are never
|
|
180
188
|
* equivalent.
|
|
181
189
|
*
|
|
190
|
+
* Two things to know when building snapshots for `reconcile()`:
|
|
191
|
+
* - `attendees`, `reminders`, `categories` and `attachments` are compared
|
|
192
|
+
* in order, so the same attendees listed in a different order count as
|
|
193
|
+
* a change.
|
|
194
|
+
* - Only the top-level dates are normalized. Values inside `metadata` are
|
|
195
|
+
* compared as given, so a `Date` and its ISO string are not equivalent
|
|
196
|
+
* there; keep metadata in one representation.
|
|
197
|
+
*
|
|
182
198
|
* This is the default comparator used by `EventStore.reconcile()` to decide
|
|
183
199
|
* whether an incoming snapshot entry replaces the stored event.
|
|
184
200
|
*
|
|
@@ -192,6 +208,40 @@ export declare class Event {
|
|
|
192
208
|
* @throws {Error} If raw event data fails {@link Event.validate}
|
|
193
209
|
*/
|
|
194
210
|
static isEquivalent(a: Event | import('../types.js').EventData, b: Event | import('../types.js').EventData): boolean;
|
|
211
|
+
/**
|
|
212
|
+
* Build the id of one occurrence of a recurring series.
|
|
213
|
+
*
|
|
214
|
+
* The id is `<recurringEventId>_<startMs>` where `startMs` is the
|
|
215
|
+
* occurrence start as returned by `Date.prototype.getTime()`. It is
|
|
216
|
+
* deterministic, so the same occurrence gets the same id no matter which
|
|
217
|
+
* range it was expanded for, and it matches the ids generated by
|
|
218
|
+
* `RecurrenceEngineV2`. Use {@link Event.parseOccurrenceId} to get the
|
|
219
|
+
* master id back.
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* Event.occurrenceId('standup', new Date(2025, 5, 16, 9)); // 'standup_1750028400000'
|
|
223
|
+
*
|
|
224
|
+
* @param {string} recurringEventId - Id of the recurring master event
|
|
225
|
+
* @param {Date|number|string} occurrenceStart - Start of the occurrence
|
|
226
|
+
* @returns {string} Occurrence id
|
|
227
|
+
* @throws {TypeError} If occurrenceStart is not a valid date
|
|
228
|
+
*/
|
|
229
|
+
static occurrenceId(recurringEventId: string, occurrenceStart: Date | number | string): string;
|
|
230
|
+
/**
|
|
231
|
+
* Split an occurrence id built by {@link Event.occurrenceId} into the master
|
|
232
|
+
* id and the occurrence start.
|
|
233
|
+
*
|
|
234
|
+
* Returns `null` for ids that do not have the `<id>_<startMs>` shape. A
|
|
235
|
+
* positive result only means the id is well-formed; whether the master
|
|
236
|
+
* exists is for the caller (see `EventStore.getEvent`) to check.
|
|
237
|
+
*
|
|
238
|
+
* @param {string} id - Candidate occurrence id
|
|
239
|
+
* @returns {{recurringEventId: string, occurrenceStart: Date}|null} Parsed parts or null
|
|
240
|
+
*/
|
|
241
|
+
static parseOccurrenceId(id: string): {
|
|
242
|
+
recurringEventId: string;
|
|
243
|
+
occurrenceStart: Date;
|
|
244
|
+
} | null;
|
|
195
245
|
/**
|
|
196
246
|
* Add an attendee to the event
|
|
197
247
|
* @param {import('../types.js').Attendee} attendee - Attendee to add
|