@forcecalendar/core 2.3.0 → 2.4.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 +96 -4
- package/core/events/Event.js +112 -0
- package/core/events/EventStore.js +176 -39
- package/core/events/RecurrenceEngine.js +218 -3
- package/core/events/RecurrenceEngineV2.js +103 -3
- package/core/index.js +1 -1
- package/core/types.js +43 -1
- package/package.json +1 -1
- package/types/calendar/Calendar.d.ts +60 -3
- package/types/events/Event.d.ts +36 -0
- package/types/events/EventStore.d.ts +50 -1
- package/types/events/RecurrenceEngine.d.ts +60 -1
- package/types/events/RecurrenceEngineV2.d.ts +45 -2
- package/types/index.d.ts +1 -1
- package/types/types.d.ts +121 -2
|
@@ -5,14 +5,23 @@
|
|
|
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
|
*/
|
|
@@ -39,6 +48,56 @@ export declare class RecurrenceEngine {
|
|
|
39
48
|
* @private
|
|
40
49
|
*/
|
|
41
50
|
private static _expandFast;
|
|
51
|
+
/**
|
|
52
|
+
* Milliseconds per step for rules whose step is a fixed duration while
|
|
53
|
+
* the system UTC offset is constant. Only the sub-daily frequencies are
|
|
54
|
+
* reported here: DAILY and WEEKLY have their own numeric loop, and the
|
|
55
|
+
* calendar-based frequencies take too few steps per year to need seeking.
|
|
56
|
+
* @param {Object} rule - Parsed recurrence rule
|
|
57
|
+
* @returns {number} Step length in milliseconds, or 0 when not fixed
|
|
58
|
+
* @private
|
|
59
|
+
*/
|
|
60
|
+
private static _fixedStepMs;
|
|
61
|
+
/**
|
|
62
|
+
* Skip the occurrences of a fixed-step rule that fall before
|
|
63
|
+
* rangeStartMs without visiting each one.
|
|
64
|
+
*
|
|
65
|
+
* While the system UTC offset is constant, a wall-clock step of the
|
|
66
|
+
* cursor is a constant number of milliseconds, so a whole run of steps
|
|
67
|
+
* collapses into one multiplication. The single step that crosses a
|
|
68
|
+
* system-timezone transition is taken with `advance` instead, so the
|
|
69
|
+
* cursor ends up exactly where stepping every occurrence would have put
|
|
70
|
+
* it. Stops at the last occurrence before rangeStartMs; the caller's loop
|
|
71
|
+
* takes the step into the range.
|
|
72
|
+
*
|
|
73
|
+
* @param {number} fromMs - Cursor position (an occurrence instant)
|
|
74
|
+
* @param {number} rangeStartMs - Seek target
|
|
75
|
+
* @param {number} rangeEndMs - Upper bound for transition lookup
|
|
76
|
+
* @param {number} stepMs - Step length while the UTC offset is constant
|
|
77
|
+
* @param {number} maxSteps - Steps still permitted under COUNT (Infinity if unbounded)
|
|
78
|
+
* @param {(cursor: Date) => void} advance - Wall-clock step, mutating the cursor
|
|
79
|
+
* @returns {{ ms: number, steps: number, nextSystemTransition: number }}
|
|
80
|
+
* Cursor position, steps taken and the next system transition after it
|
|
81
|
+
* @private
|
|
82
|
+
*/
|
|
83
|
+
private static _seekFixedStep;
|
|
84
|
+
/**
|
|
85
|
+
* Seek for WEEKLY BYDAY rules, whose step pattern repeats every week:
|
|
86
|
+
* whole weeks are skipped arithmetically from any weekday in the BYDAY
|
|
87
|
+
* set, and single steps (identical to the expansion loop's) are only
|
|
88
|
+
* taken to reach the set, around system-timezone transitions and in the
|
|
89
|
+
* last week before the range.
|
|
90
|
+
*
|
|
91
|
+
* @param {number} fromMs - Cursor position (an occurrence instant)
|
|
92
|
+
* @param {number} weekday - Weekday of the cursor (Date#getDay)
|
|
93
|
+
* @param {number} rangeStartMs - Seek target
|
|
94
|
+
* @param {number} rangeEndMs - Upper bound for transition lookup
|
|
95
|
+
* @param {Object} rule - Parsed rule with compiled _byDaySet/_byDayDeltas
|
|
96
|
+
* @param {number} maxSteps - Steps still permitted under COUNT (Infinity if unbounded)
|
|
97
|
+
* @returns {{ ms: number, steps: number, weekday: number, nextSystemTransition: number }}
|
|
98
|
+
* @private
|
|
99
|
+
*/
|
|
100
|
+
private static _seekWeekCycle;
|
|
42
101
|
/**
|
|
43
102
|
* Find the next system-timezone offset transition after fromMs.
|
|
44
103
|
* Cached module-wide: the system timezone is fixed for the process.
|
|
@@ -10,16 +10,59 @@ export declare class RecurrenceEngineV2 {
|
|
|
10
10
|
modifiedInstances: Map<any, any>;
|
|
11
11
|
exceptionStore: Map<any, any>;
|
|
12
12
|
static MAX_OCCURRENCES_HARD_LIMIT: number;
|
|
13
|
+
static MAX_ITERATIONS_HARD_LIMIT: number;
|
|
13
14
|
constructor();
|
|
14
15
|
/**
|
|
15
16
|
* Expand recurring event with advanced handling
|
|
16
|
-
*
|
|
17
|
+
*
|
|
18
|
+
* Occurrences before rangeStart are skipped without being generated:
|
|
19
|
+
* daily, weekly, hourly and minutely rules seek straight to the range, so
|
|
20
|
+
* a series that started years before the queried window is expanded at
|
|
21
|
+
* the same cost as one that started yesterday.
|
|
22
|
+
*
|
|
23
|
+
* @param {import('./Event.js').Event} event - Recurring event
|
|
17
24
|
* @param {Date} rangeStart - Start of expansion range
|
|
18
25
|
* @param {Date} rangeEnd - End of expansion range
|
|
19
26
|
* @param {Object} options - Expansion options
|
|
27
|
+
* @param {number} [options.maxOccurrences=365] - Maximum number of occurrences to
|
|
28
|
+
* return. Only occurrences inside the range count towards this limit.
|
|
29
|
+
* @param {boolean} [options.includeModified=true] - Apply stored instance modifications
|
|
30
|
+
* @param {boolean} [options.includeCancelled=false] - Return exception dates as cancelled occurrences
|
|
31
|
+
* @param {string} [options.timezone] - Timezone for expansion (defaults to the event's)
|
|
32
|
+
* @param {boolean} [options.handleDST=true] - Adjust occurrences across DST transitions
|
|
20
33
|
* @returns {Array} Expanded occurrences
|
|
21
34
|
*/
|
|
22
|
-
expandEvent(event: Event, rangeStart: Date, rangeEnd: Date, options?:
|
|
35
|
+
expandEvent(event: import('./Event.js').Event, rangeStart: Date, rangeEnd: Date, options?: {
|
|
36
|
+
maxOccurrences?: number;
|
|
37
|
+
includeModified?: boolean;
|
|
38
|
+
includeCancelled?: boolean;
|
|
39
|
+
timezone?: string;
|
|
40
|
+
handleDST?: boolean;
|
|
41
|
+
}): any[];
|
|
42
|
+
/**
|
|
43
|
+
* Move the expansion cursor to the last occurrence before the range
|
|
44
|
+
* without stepping through every occurrence in between.
|
|
45
|
+
*
|
|
46
|
+
* Applies to rules whose step is a fixed duration between system-timezone
|
|
47
|
+
* transitions (plain DAILY and WEEKLY, HOURLY, MINUTELY); the step that
|
|
48
|
+
* crosses a transition is taken with getNextDate so the result is exactly
|
|
49
|
+
* what stepping from DTSTART would produce. Never seeks past UNTIL, and
|
|
50
|
+
* counts skipped steps against COUNT.
|
|
51
|
+
*
|
|
52
|
+
* @param {Object} state - Expansion state (currentDate and count are updated)
|
|
53
|
+
* @param {Object} rule - Parsed recurrence rule
|
|
54
|
+
* @param {Date} rangeStart - Start of expansion range
|
|
55
|
+
* @param {Date} rangeEnd - End of expansion range
|
|
56
|
+
* @param {string} timezone - Expansion timezone
|
|
57
|
+
*/
|
|
58
|
+
seekToRange(state: Object, rule: Object, rangeStart: Date, rangeEnd: Date, timezone: string): void;
|
|
59
|
+
/**
|
|
60
|
+
* Milliseconds per step for rules getNextDate advances by a fixed
|
|
61
|
+
* duration while the system UTC offset is constant
|
|
62
|
+
* @param {Object} rule - Parsed recurrence rule
|
|
63
|
+
* @returns {number} Step length in milliseconds, or 0 when not fixed
|
|
64
|
+
*/
|
|
65
|
+
getFixedStepMs(rule: Object): number;
|
|
23
66
|
/**
|
|
24
67
|
* Generate a single occurrence with timezone handling
|
|
25
68
|
*/
|
package/types/index.d.ts
CHANGED
|
@@ -18,5 +18,5 @@ export { RRuleParser } from './events/RRuleParser.js';
|
|
|
18
18
|
export { TimezoneManager } from './timezone/TimezoneManager.js';
|
|
19
19
|
export { ConflictDetector } from './conflicts/ConflictDetector.js';
|
|
20
20
|
export { EnhancedCalendar } from './integration/EnhancedCalendar.js';
|
|
21
|
-
export declare const VERSION = "2.
|
|
21
|
+
export declare const VERSION = "2.4.0";
|
|
22
22
|
export { Calendar as default } from './calendar/Calendar.js';
|
package/types/types.d.ts
CHANGED
|
@@ -688,7 +688,7 @@ export type EventStoreChange = {
|
|
|
688
688
|
/**
|
|
689
689
|
* - Type of change
|
|
690
690
|
*/
|
|
691
|
-
type: ('add' | 'update' | 'remove' | 'clear');
|
|
691
|
+
type: ('add' | 'update' | 'remove' | 'clear' | 'batch');
|
|
692
692
|
/**
|
|
693
693
|
* - Affected event
|
|
694
694
|
*/
|
|
@@ -701,11 +701,94 @@ export type EventStoreChange = {
|
|
|
701
701
|
* - Previous events (for clear)
|
|
702
702
|
*/
|
|
703
703
|
oldEvents?: import('./events/Event.js').Event[];
|
|
704
|
+
/**
|
|
705
|
+
* - Individual changes (for batch)
|
|
706
|
+
*/
|
|
707
|
+
changes?: EventStoreChange[];
|
|
708
|
+
/**
|
|
709
|
+
* - Number of individual changes (for batch)
|
|
710
|
+
*/
|
|
711
|
+
count?: number;
|
|
704
712
|
/**
|
|
705
713
|
* - Store version number
|
|
706
714
|
*/
|
|
707
715
|
version: number;
|
|
708
716
|
};
|
|
717
|
+
export type EventEquivalenceFn = (a: import('./events/Event.js').Event, b: import('./events/Event.js').Event) => boolean;
|
|
718
|
+
export type ReconcileOptions = {
|
|
719
|
+
/**
|
|
720
|
+
* - Remove stored events that are absent from the snapshot
|
|
721
|
+
*/
|
|
722
|
+
removeMissing?: boolean;
|
|
723
|
+
/**
|
|
724
|
+
* - Comparator deciding whether a stored event is unchanged (defaults to Event.isEquivalent)
|
|
725
|
+
*/
|
|
726
|
+
isEquivalent?: EventEquivalenceFn;
|
|
727
|
+
};
|
|
728
|
+
export type ReconciledUpdate = {
|
|
729
|
+
/**
|
|
730
|
+
* - Event now in the store
|
|
731
|
+
*/
|
|
732
|
+
event: import('./events/Event.js').Event;
|
|
733
|
+
/**
|
|
734
|
+
* - Event it replaced
|
|
735
|
+
*/
|
|
736
|
+
oldEvent: import('./events/Event.js').Event;
|
|
737
|
+
};
|
|
738
|
+
export type ReconcileResult = {
|
|
739
|
+
/**
|
|
740
|
+
* - Events that were not in the store before
|
|
741
|
+
*/
|
|
742
|
+
added: import('./events/Event.js').Event[];
|
|
743
|
+
/**
|
|
744
|
+
* - Events whose data changed
|
|
745
|
+
*/
|
|
746
|
+
updated: ReconciledUpdate[];
|
|
747
|
+
/**
|
|
748
|
+
* - Events removed from the store
|
|
749
|
+
*/
|
|
750
|
+
removed: import('./events/Event.js').Event[];
|
|
751
|
+
/**
|
|
752
|
+
* - Stored events left untouched (same instances)
|
|
753
|
+
*/
|
|
754
|
+
unchanged: import('./events/Event.js').Event[];
|
|
755
|
+
};
|
|
756
|
+
export type SetEventsOptions = {
|
|
757
|
+
/**
|
|
758
|
+
* - Apply only the differences instead of clearing and re-adding
|
|
759
|
+
*/
|
|
760
|
+
reconcile?: boolean;
|
|
761
|
+
/**
|
|
762
|
+
* - Reconcile only: remove stored events absent from the snapshot
|
|
763
|
+
*/
|
|
764
|
+
removeMissing?: boolean;
|
|
765
|
+
/**
|
|
766
|
+
* - Reconcile only: custom equivalence comparator
|
|
767
|
+
*/
|
|
768
|
+
isEquivalent?: EventEquivalenceFn;
|
|
769
|
+
};
|
|
770
|
+
export type EventsSetPayload = {
|
|
771
|
+
/**
|
|
772
|
+
* - All events after the operation
|
|
773
|
+
*/
|
|
774
|
+
events: import('./events/Event.js').Event[];
|
|
775
|
+
/**
|
|
776
|
+
* - Events added by the operation
|
|
777
|
+
*/
|
|
778
|
+
added: import('./events/Event.js').Event[];
|
|
779
|
+
/**
|
|
780
|
+
* - Events replaced by the operation
|
|
781
|
+
*/
|
|
782
|
+
updated: ReconciledUpdate[];
|
|
783
|
+
/**
|
|
784
|
+
* - Events removed by the operation
|
|
785
|
+
*/
|
|
786
|
+
removed: import('./events/Event.js').Event[];
|
|
787
|
+
/**
|
|
788
|
+
* - Events left untouched
|
|
789
|
+
*/
|
|
790
|
+
unchanged: import('./events/Event.js').Event[];
|
|
791
|
+
};
|
|
709
792
|
export type QueryFilters = {
|
|
710
793
|
/**
|
|
711
794
|
* - Start date for range query
|
|
@@ -1142,12 +1225,48 @@ export type ConflictSummary = {
|
|
|
1142
1225
|
*/
|
|
1143
1226
|
/**
|
|
1144
1227
|
* @typedef {Object} EventStoreChange
|
|
1145
|
-
* @property {('add'|'update'|'remove'|'clear')} type - Type of change
|
|
1228
|
+
* @property {('add'|'update'|'remove'|'clear'|'batch')} type - Type of change
|
|
1146
1229
|
* @property {import('./events/Event.js').Event} [event] - Affected event
|
|
1147
1230
|
* @property {import('./events/Event.js').Event} [oldEvent] - Previous event state (for updates)
|
|
1148
1231
|
* @property {import('./events/Event.js').Event[]} [oldEvents] - Previous events (for clear)
|
|
1232
|
+
* @property {EventStoreChange[]} [changes] - Individual changes (for batch)
|
|
1233
|
+
* @property {number} [count] - Number of individual changes (for batch)
|
|
1149
1234
|
* @property {number} version - Store version number
|
|
1150
1235
|
*/
|
|
1236
|
+
/**
|
|
1237
|
+
* @typedef {(a: import('./events/Event.js').Event, b: import('./events/Event.js').Event) => boolean} EventEquivalenceFn
|
|
1238
|
+
*/
|
|
1239
|
+
/**
|
|
1240
|
+
* @typedef {Object} ReconcileOptions
|
|
1241
|
+
* @property {boolean} [removeMissing=true] - Remove stored events that are absent from the snapshot
|
|
1242
|
+
* @property {EventEquivalenceFn} [isEquivalent] - Comparator deciding whether a stored event is unchanged (defaults to Event.isEquivalent)
|
|
1243
|
+
*/
|
|
1244
|
+
/**
|
|
1245
|
+
* @typedef {Object} ReconciledUpdate
|
|
1246
|
+
* @property {import('./events/Event.js').Event} event - Event now in the store
|
|
1247
|
+
* @property {import('./events/Event.js').Event} oldEvent - Event it replaced
|
|
1248
|
+
*/
|
|
1249
|
+
/**
|
|
1250
|
+
* @typedef {Object} ReconcileResult
|
|
1251
|
+
* @property {import('./events/Event.js').Event[]} added - Events that were not in the store before
|
|
1252
|
+
* @property {ReconciledUpdate[]} updated - Events whose data changed
|
|
1253
|
+
* @property {import('./events/Event.js').Event[]} removed - Events removed from the store
|
|
1254
|
+
* @property {import('./events/Event.js').Event[]} unchanged - Stored events left untouched (same instances)
|
|
1255
|
+
*/
|
|
1256
|
+
/**
|
|
1257
|
+
* @typedef {Object} SetEventsOptions
|
|
1258
|
+
* @property {boolean} [reconcile=false] - Apply only the differences instead of clearing and re-adding
|
|
1259
|
+
* @property {boolean} [removeMissing=true] - Reconcile only: remove stored events absent from the snapshot
|
|
1260
|
+
* @property {EventEquivalenceFn} [isEquivalent] - Reconcile only: custom equivalence comparator
|
|
1261
|
+
*/
|
|
1262
|
+
/**
|
|
1263
|
+
* @typedef {Object} EventsSetPayload
|
|
1264
|
+
* @property {import('./events/Event.js').Event[]} events - All events after the operation
|
|
1265
|
+
* @property {import('./events/Event.js').Event[]} added - Events added by the operation
|
|
1266
|
+
* @property {ReconciledUpdate[]} updated - Events replaced by the operation
|
|
1267
|
+
* @property {import('./events/Event.js').Event[]} removed - Events removed by the operation
|
|
1268
|
+
* @property {import('./events/Event.js').Event[]} unchanged - Events left untouched
|
|
1269
|
+
*/
|
|
1151
1270
|
/**
|
|
1152
1271
|
* @typedef {Object} QueryFilters
|
|
1153
1272
|
* @property {Date} [start] - Start date for range query
|