@forcecalendar/core 2.1.0 → 2.1.2
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 +7 -9
- package/core/calendar/DateUtils.js +10 -9
- package/core/conflicts/ConflictDetector.js +24 -24
- package/core/events/Event.js +14 -20
- package/core/events/EventStore.js +70 -19
- package/core/events/RRuleParser.js +423 -394
- package/core/events/RecurrenceEngine.js +33 -21
- package/core/events/RecurrenceEngineV2.js +536 -562
- package/core/ics/ICSHandler.js +348 -348
- package/core/ics/ICSParser.js +433 -435
- package/core/index.js +1 -1
- package/core/integration/EnhancedCalendar.js +363 -398
- package/core/performance/AdaptiveMemoryManager.js +310 -308
- package/core/performance/LRUCache.js +3 -4
- package/core/performance/PerformanceOptimizer.js +4 -6
- package/core/search/EventSearch.js +409 -417
- package/core/search/SearchWorkerManager.js +338 -338
- package/core/state/StateManager.js +4 -2
- package/core/timezone/TimezoneDatabase.js +574 -271
- package/core/timezone/TimezoneManager.js +422 -402
- package/core/types.js +1 -1
- package/package.json +1 -1
|
@@ -8,456 +8,421 @@ import { SearchWorkerManager } from '../search/SearchWorkerManager.js';
|
|
|
8
8
|
import { RecurrenceEngineV2 } from '../events/RecurrenceEngineV2.js';
|
|
9
9
|
|
|
10
10
|
export class EnhancedCalendar extends Calendar {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
11
|
+
constructor(config) {
|
|
12
|
+
super(config);
|
|
13
|
+
|
|
14
|
+
// Initialize enhanced components
|
|
15
|
+
this.searchManager = new SearchWorkerManager(this.eventStore);
|
|
16
|
+
this.recurrenceEngine = new RecurrenceEngineV2();
|
|
17
|
+
|
|
18
|
+
// Performance monitoring
|
|
19
|
+
this.performanceMetrics = {
|
|
20
|
+
searchTime: [],
|
|
21
|
+
expansionTime: [],
|
|
22
|
+
renderTime: []
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Setup event listeners for real-time indexing
|
|
26
|
+
this.setupRealtimeIndexing();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Enhanced search with worker support
|
|
31
|
+
*/
|
|
32
|
+
async search(query, options = {}) {
|
|
33
|
+
const startTime = performance.now();
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
// Use enhanced search manager
|
|
37
|
+
const results = await this.searchManager.search(query, {
|
|
38
|
+
fields: options.fields || ['title', 'description', 'location', 'category'],
|
|
39
|
+
fuzzy: options.fuzzy !== false,
|
|
40
|
+
limit: options.limit || 50,
|
|
41
|
+
prefixMatch: options.autocomplete || false,
|
|
42
|
+
...options
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const endTime = performance.now();
|
|
46
|
+
this.recordMetric('searchTime', endTime - startTime);
|
|
47
|
+
|
|
48
|
+
// Transform results to match expected format
|
|
49
|
+
return results.map(r => r.event);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error('Search error:', error);
|
|
52
|
+
// Fallback to basic search
|
|
53
|
+
return super.search ? super.search(query, options) : [];
|
|
27
54
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// Transform results to match expected format
|
|
49
|
-
return results.map(r => r.event);
|
|
50
|
-
} catch (error) {
|
|
51
|
-
console.error('Search error:', error);
|
|
52
|
-
// Fallback to basic search
|
|
53
|
-
return super.search ? super.search(query, options) : [];
|
|
54
|
-
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get events with enhanced recurrence expansion
|
|
59
|
+
*/
|
|
60
|
+
async getEventsInRange(startDate, endDate, options = {}) {
|
|
61
|
+
const startTime = performance.now();
|
|
62
|
+
|
|
63
|
+
const regularEvents = [];
|
|
64
|
+
const recurringEvents = [];
|
|
65
|
+
|
|
66
|
+
// Separate regular and recurring events
|
|
67
|
+
const allEvents = this.eventStore.getEventsInDateRange(startDate, endDate);
|
|
68
|
+
|
|
69
|
+
for (const event of allEvents) {
|
|
70
|
+
if (event.recurring) {
|
|
71
|
+
recurringEvents.push(event);
|
|
72
|
+
} else {
|
|
73
|
+
regularEvents.push(event);
|
|
74
|
+
}
|
|
55
75
|
}
|
|
56
76
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
*/
|
|
60
|
-
async getEventsInRange(startDate, endDate, options = {}) {
|
|
61
|
-
const startTime = performance.now();
|
|
77
|
+
// Expand recurring events with enhanced engine
|
|
78
|
+
const expandedOccurrences = [];
|
|
62
79
|
|
|
63
|
-
|
|
64
|
-
|
|
80
|
+
for (const event of recurringEvents) {
|
|
81
|
+
const occurrences = this.recurrenceEngine.expandEvent(event, startDate, endDate, {
|
|
82
|
+
maxOccurrences: options.maxOccurrences || 365,
|
|
83
|
+
includeModified: options.includeModified !== false,
|
|
84
|
+
includeCancelled: options.includeCancelled || false,
|
|
85
|
+
timezone: options.timezone || event.timeZone,
|
|
86
|
+
handleDST: options.handleDST !== false
|
|
87
|
+
});
|
|
65
88
|
|
|
66
|
-
|
|
67
|
-
|
|
89
|
+
expandedOccurrences.push(...occurrences);
|
|
90
|
+
}
|
|
68
91
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
recurringEvents.push(event);
|
|
72
|
-
} else {
|
|
73
|
-
regularEvents.push(event);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
92
|
+
const endTime = performance.now();
|
|
93
|
+
this.recordMetric('expansionTime', endTime - startTime);
|
|
76
94
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
for (const event of recurringEvents) {
|
|
81
|
-
const occurrences = this.recurrenceEngine.expandEvent(
|
|
82
|
-
event,
|
|
83
|
-
startDate,
|
|
84
|
-
endDate,
|
|
85
|
-
{
|
|
86
|
-
maxOccurrences: options.maxOccurrences || 365,
|
|
87
|
-
includeModified: options.includeModified !== false,
|
|
88
|
-
includeCancelled: options.includeCancelled || false,
|
|
89
|
-
timezone: options.timezone || event.timeZone,
|
|
90
|
-
handleDST: options.handleDST !== false
|
|
91
|
-
}
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
expandedOccurrences.push(...occurrences);
|
|
95
|
-
}
|
|
95
|
+
// Combine and sort
|
|
96
|
+
const allEventsInRange = [...regularEvents, ...expandedOccurrences];
|
|
97
|
+
allEventsInRange.sort((a, b) => a.start - b.start);
|
|
96
98
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
return allEventsInRange;
|
|
100
|
+
}
|
|
99
101
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
/**
|
|
103
|
+
* Modify a single occurrence of a recurring event
|
|
104
|
+
*/
|
|
105
|
+
modifyOccurrence(eventId, occurrenceDate, modifications) {
|
|
106
|
+
// Add to modified instances
|
|
107
|
+
this.recurrenceEngine.addModifiedInstance(eventId, occurrenceDate, modifications);
|
|
103
108
|
|
|
104
|
-
|
|
105
|
-
|
|
109
|
+
// Emit change event
|
|
110
|
+
this.emit('occurrence:modified', {
|
|
111
|
+
eventId,
|
|
112
|
+
occurrenceDate,
|
|
113
|
+
modifications
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Trigger re-render if in view
|
|
117
|
+
this.refreshView();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Cancel a single occurrence of a recurring event
|
|
122
|
+
*/
|
|
123
|
+
cancelOccurrence(eventId, occurrenceDate, reason = 'Cancelled') {
|
|
124
|
+
// Add exception
|
|
125
|
+
this.recurrenceEngine.addException(eventId, occurrenceDate, reason);
|
|
126
|
+
|
|
127
|
+
// Emit change event
|
|
128
|
+
this.emit('occurrence:cancelled', {
|
|
129
|
+
eventId,
|
|
130
|
+
occurrenceDate,
|
|
131
|
+
reason
|
|
132
|
+
});
|
|
106
133
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
// Emit change event
|
|
119
|
-
this.emit('occurrence:modified', {
|
|
120
|
-
eventId,
|
|
121
|
-
occurrenceDate,
|
|
122
|
-
modifications
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
// Trigger re-render if in view
|
|
126
|
-
this.refreshView();
|
|
134
|
+
// Trigger re-render
|
|
135
|
+
this.refreshView();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Bulk operations for recurring events
|
|
140
|
+
*/
|
|
141
|
+
async bulkModifyOccurrences(eventId, dateRange, modifications) {
|
|
142
|
+
const event = this.eventStore.getEvent(eventId);
|
|
143
|
+
if (!event || !event.recurring) {
|
|
144
|
+
throw new Error('Event not found or not recurring');
|
|
127
145
|
}
|
|
128
146
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
// Emit change event
|
|
137
|
-
this.emit('occurrence:cancelled', {
|
|
138
|
-
eventId,
|
|
139
|
-
occurrenceDate,
|
|
140
|
-
reason
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
// Trigger re-render
|
|
144
|
-
this.refreshView();
|
|
147
|
+
// Get all occurrences in range
|
|
148
|
+
const occurrences = this.recurrenceEngine.expandEvent(event, dateRange.start, dateRange.end);
|
|
149
|
+
|
|
150
|
+
// Apply modifications to each
|
|
151
|
+
for (const occurrence of occurrences) {
|
|
152
|
+
this.recurrenceEngine.addModifiedInstance(eventId, occurrence.start, modifications);
|
|
145
153
|
}
|
|
146
154
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
throw new Error('Event not found or not recurring');
|
|
154
|
-
}
|
|
155
|
+
// Emit bulk change event
|
|
156
|
+
this.emit('occurrences:bulk-modified', {
|
|
157
|
+
eventId,
|
|
158
|
+
count: occurrences.length,
|
|
159
|
+
modifications
|
|
160
|
+
});
|
|
155
161
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
event,
|
|
159
|
-
dateRange.start,
|
|
160
|
-
dateRange.end
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
// Apply modifications to each
|
|
164
|
-
for (const occurrence of occurrences) {
|
|
165
|
-
this.recurrenceEngine.addModifiedInstance(
|
|
166
|
-
eventId,
|
|
167
|
-
occurrence.start,
|
|
168
|
-
modifications
|
|
169
|
-
);
|
|
170
|
-
}
|
|
162
|
+
this.refreshView();
|
|
163
|
+
}
|
|
171
164
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
165
|
+
/**
|
|
166
|
+
* Advanced search with filters and recurrence awareness
|
|
167
|
+
*/
|
|
168
|
+
async advancedSearch(query, filters = {}, options = {}) {
|
|
169
|
+
// First get search results
|
|
170
|
+
const searchResults = await this.search(query, options);
|
|
178
171
|
|
|
179
|
-
|
|
180
|
-
|
|
172
|
+
// Apply additional filters
|
|
173
|
+
let filtered = searchResults;
|
|
181
174
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
// Apply additional filters
|
|
190
|
-
let filtered = searchResults;
|
|
191
|
-
|
|
192
|
-
// Date range filter with recurrence expansion
|
|
193
|
-
if (filters.dateRange) {
|
|
194
|
-
const expandedEvents = await this.getEventsInRange(
|
|
195
|
-
filters.dateRange.start,
|
|
196
|
-
filters.dateRange.end,
|
|
197
|
-
{ includeModified: true }
|
|
198
|
-
);
|
|
199
|
-
|
|
200
|
-
const expandedIds = new Set(expandedEvents.map(e =>
|
|
201
|
-
e.recurringEventId || e.id
|
|
202
|
-
));
|
|
203
|
-
|
|
204
|
-
filtered = filtered.filter(e => expandedIds.has(e.id));
|
|
205
|
-
}
|
|
175
|
+
// Date range filter with recurrence expansion
|
|
176
|
+
if (filters.dateRange) {
|
|
177
|
+
const expandedEvents = await this.getEventsInRange(
|
|
178
|
+
filters.dateRange.start,
|
|
179
|
+
filters.dateRange.end,
|
|
180
|
+
{ includeModified: true }
|
|
181
|
+
);
|
|
206
182
|
|
|
207
|
-
|
|
208
|
-
if (filters.categories && filters.categories.length > 0) {
|
|
209
|
-
const categorySet = new Set(filters.categories);
|
|
210
|
-
filtered = filtered.filter(e =>
|
|
211
|
-
e.categories && e.categories.some(c => categorySet.has(c))
|
|
212
|
-
);
|
|
213
|
-
}
|
|
183
|
+
const expandedIds = new Set(expandedEvents.map(e => e.recurringEventId || e.id));
|
|
214
184
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
filtered = filtered.filter(e => e.status === filters.status);
|
|
218
|
-
}
|
|
185
|
+
filtered = filtered.filter(e => expandedIds.has(e.id));
|
|
186
|
+
}
|
|
219
187
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
});
|
|
226
|
-
}
|
|
188
|
+
// Category filter
|
|
189
|
+
if (filters.categories && filters.categories.length > 0) {
|
|
190
|
+
const categorySet = new Set(filters.categories);
|
|
191
|
+
filtered = filtered.filter(e => e.categories && e.categories.some(c => categorySet.has(c)));
|
|
192
|
+
}
|
|
227
193
|
|
|
228
|
-
|
|
194
|
+
// Status filter
|
|
195
|
+
if (filters.status) {
|
|
196
|
+
filtered = filtered.filter(e => e.status === filters.status);
|
|
229
197
|
}
|
|
230
198
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
this.searchManager.indexEvents();
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
// Re-index when events are modified
|
|
241
|
-
this.on('event:updated', (event) => {
|
|
242
|
-
this.searchManager.indexEvents();
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
// Re-index when events are removed
|
|
246
|
-
this.on('event:removed', (eventId) => {
|
|
247
|
-
this.searchManager.indexEvents();
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
// Batch re-indexing for bulk operations
|
|
251
|
-
let reindexTimeout;
|
|
252
|
-
this.on('events:bulk-operation', () => {
|
|
253
|
-
clearTimeout(reindexTimeout);
|
|
254
|
-
reindexTimeout = setTimeout(() => {
|
|
255
|
-
this.searchManager.indexEvents();
|
|
256
|
-
}, 100);
|
|
257
|
-
});
|
|
199
|
+
// Modified only filter
|
|
200
|
+
if (filters.modifiedOnly) {
|
|
201
|
+
filtered = filtered.filter(e => {
|
|
202
|
+
const modifications = this.recurrenceEngine.modifiedInstances.get(e.id);
|
|
203
|
+
return modifications && modifications.size > 0;
|
|
204
|
+
});
|
|
258
205
|
}
|
|
259
206
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
*/
|
|
263
|
-
async getSuggestions(partial, field = 'title') {
|
|
264
|
-
if (partial.length < 2) {
|
|
265
|
-
return [];
|
|
266
|
-
}
|
|
207
|
+
return filtered;
|
|
208
|
+
}
|
|
267
209
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const suggestions = new Set();
|
|
277
|
-
for (const result of results) {
|
|
278
|
-
const value = result.event[field];
|
|
279
|
-
if (value) {
|
|
280
|
-
suggestions.add(value);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
210
|
+
/**
|
|
211
|
+
* Setup real-time indexing for search
|
|
212
|
+
*/
|
|
213
|
+
setupRealtimeIndexing() {
|
|
214
|
+
// Re-index when events are added
|
|
215
|
+
this.on('event:added', event => {
|
|
216
|
+
this.searchManager.indexEvents();
|
|
217
|
+
});
|
|
283
218
|
|
|
284
|
-
|
|
285
|
-
|
|
219
|
+
// Re-index when events are modified
|
|
220
|
+
this.on('event:updated', event => {
|
|
221
|
+
this.searchManager.indexEvents();
|
|
222
|
+
});
|
|
286
223
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
this.performanceMetrics[type].push(value);
|
|
224
|
+
// Re-index when events are removed
|
|
225
|
+
this.on('event:removed', eventId => {
|
|
226
|
+
this.searchManager.indexEvents();
|
|
227
|
+
});
|
|
292
228
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
229
|
+
// Batch re-indexing for bulk operations
|
|
230
|
+
let reindexTimeout;
|
|
231
|
+
this.on('events:bulk-operation', () => {
|
|
232
|
+
clearTimeout(reindexTimeout);
|
|
233
|
+
reindexTimeout = setTimeout(() => {
|
|
234
|
+
this.searchManager.indexEvents();
|
|
235
|
+
}, 100);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Get search suggestions (autocomplete)
|
|
241
|
+
*/
|
|
242
|
+
async getSuggestions(partial, field = 'title') {
|
|
243
|
+
if (partial.length < 2) {
|
|
244
|
+
return [];
|
|
297
245
|
}
|
|
298
246
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
for (const [metric, values] of Object.entries(this.performanceMetrics)) {
|
|
306
|
-
if (values.length === 0) {
|
|
307
|
-
stats[metric] = { avg: 0, min: 0, max: 0, p95: 0 };
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const sorted = [...values].sort((a, b) => a - b);
|
|
312
|
-
const sum = sorted.reduce((a, b) => a + b, 0);
|
|
313
|
-
|
|
314
|
-
stats[metric] = {
|
|
315
|
-
avg: sum / sorted.length,
|
|
316
|
-
min: sorted[0],
|
|
317
|
-
max: sorted[sorted.length - 1],
|
|
318
|
-
p95: sorted[Math.floor(sorted.length * 0.95)]
|
|
319
|
-
};
|
|
320
|
-
}
|
|
247
|
+
// Use search with prefix matching
|
|
248
|
+
const results = await this.searchManager.search(partial, {
|
|
249
|
+
fields: [field],
|
|
250
|
+
prefixMatch: true,
|
|
251
|
+
limit: 10
|
|
252
|
+
});
|
|
321
253
|
|
|
322
|
-
|
|
254
|
+
// Extract unique values
|
|
255
|
+
const suggestions = new Set();
|
|
256
|
+
for (const result of results) {
|
|
257
|
+
const value = result.event[field];
|
|
258
|
+
if (value) {
|
|
259
|
+
suggestions.add(value);
|
|
260
|
+
}
|
|
323
261
|
}
|
|
324
262
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
*/
|
|
328
|
-
exportWithRecurrence(format = 'json') {
|
|
329
|
-
const data = {
|
|
330
|
-
events: this.eventStore.getAllEvents(),
|
|
331
|
-
modifiedInstances: {},
|
|
332
|
-
exceptions: {}
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
// Include modified instances
|
|
336
|
-
for (const [eventId, modifications] of this.recurrenceEngine.modifiedInstances) {
|
|
337
|
-
data.modifiedInstances[eventId] = Array.from(modifications.entries());
|
|
338
|
-
}
|
|
263
|
+
return Array.from(suggestions);
|
|
264
|
+
}
|
|
339
265
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
266
|
+
/**
|
|
267
|
+
* Performance monitoring
|
|
268
|
+
*/
|
|
269
|
+
recordMetric(type, value) {
|
|
270
|
+
this.performanceMetrics[type].push(value);
|
|
344
271
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
272
|
+
// Keep only last 100 measurements
|
|
273
|
+
if (this.performanceMetrics[type].length > 100) {
|
|
274
|
+
this.performanceMetrics[type].shift();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Get performance statistics
|
|
280
|
+
*/
|
|
281
|
+
getPerformanceStats() {
|
|
282
|
+
const stats = {};
|
|
283
|
+
|
|
284
|
+
for (const [metric, values] of Object.entries(this.performanceMetrics)) {
|
|
285
|
+
if (values.length === 0) {
|
|
286
|
+
stats[metric] = { avg: 0, min: 0, max: 0, p95: 0 };
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
291
|
+
const sum = sorted.reduce((a, b) => a + b, 0);
|
|
292
|
+
|
|
293
|
+
stats[metric] = {
|
|
294
|
+
avg: sum / sorted.length,
|
|
295
|
+
min: sorted[0],
|
|
296
|
+
max: sorted[sorted.length - 1],
|
|
297
|
+
p95: sorted[Math.floor(sorted.length * 0.95)]
|
|
298
|
+
};
|
|
299
|
+
}
|
|
348
300
|
|
|
349
|
-
|
|
350
|
-
|
|
301
|
+
return stats;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Export calendar with recurrence data
|
|
306
|
+
*/
|
|
307
|
+
exportWithRecurrence(format = 'json') {
|
|
308
|
+
const data = {
|
|
309
|
+
events: this.eventStore.getAllEvents(),
|
|
310
|
+
modifiedInstances: {},
|
|
311
|
+
exceptions: {}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// Include modified instances
|
|
315
|
+
for (const [eventId, modifications] of this.recurrenceEngine.modifiedInstances) {
|
|
316
|
+
data.modifiedInstances[eventId] = Array.from(modifications.entries());
|
|
351
317
|
}
|
|
352
318
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
importWithRecurrence(data, format = 'json') {
|
|
357
|
-
if (format === 'json') {
|
|
358
|
-
const parsed = typeof data === 'string' ? JSON.parse(data) : data;
|
|
359
|
-
|
|
360
|
-
// Import events
|
|
361
|
-
for (const event of parsed.events) {
|
|
362
|
-
this.addEvent(event);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// Import modified instances
|
|
366
|
-
if (parsed.modifiedInstances) {
|
|
367
|
-
for (const [eventId, modifications] of Object.entries(parsed.modifiedInstances)) {
|
|
368
|
-
for (const [dateKey, mods] of modifications) {
|
|
369
|
-
this.recurrenceEngine.addModifiedInstance(
|
|
370
|
-
eventId,
|
|
371
|
-
new Date(dateKey),
|
|
372
|
-
mods
|
|
373
|
-
);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// Import exceptions
|
|
379
|
-
if (parsed.exceptions) {
|
|
380
|
-
for (const [eventId, exceptions] of Object.entries(parsed.exceptions)) {
|
|
381
|
-
for (const [dateKey, reason] of exceptions) {
|
|
382
|
-
this.recurrenceEngine.addException(
|
|
383
|
-
eventId,
|
|
384
|
-
new Date(dateKey),
|
|
385
|
-
reason
|
|
386
|
-
);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
}
|
|
319
|
+
// Include exceptions
|
|
320
|
+
for (const [eventId, exceptions] of this.recurrenceEngine.exceptionStore) {
|
|
321
|
+
data.exceptions[eventId] = Array.from(exceptions.entries());
|
|
391
322
|
}
|
|
392
323
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
destroy() {
|
|
397
|
-
// Clean up worker
|
|
398
|
-
if (this.searchManager) {
|
|
399
|
-
this.searchManager.destroy();
|
|
400
|
-
}
|
|
324
|
+
if (format === 'json') {
|
|
325
|
+
return JSON.stringify(data, null, 2);
|
|
326
|
+
}
|
|
401
327
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
328
|
+
// Could add ICS export here
|
|
329
|
+
return data;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Import calendar with recurrence data
|
|
334
|
+
*/
|
|
335
|
+
importWithRecurrence(data, format = 'json') {
|
|
336
|
+
if (format === 'json') {
|
|
337
|
+
const parsed = typeof data === 'string' ? JSON.parse(data) : data;
|
|
338
|
+
|
|
339
|
+
// Import events
|
|
340
|
+
for (const event of parsed.events) {
|
|
341
|
+
this.addEvent(event);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Import modified instances
|
|
345
|
+
if (parsed.modifiedInstances) {
|
|
346
|
+
for (const [eventId, modifications] of Object.entries(parsed.modifiedInstances)) {
|
|
347
|
+
for (const [dateKey, mods] of modifications) {
|
|
348
|
+
this.recurrenceEngine.addModifiedInstance(eventId, new Date(dateKey), mods);
|
|
349
|
+
}
|
|
405
350
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Import exceptions
|
|
354
|
+
if (parsed.exceptions) {
|
|
355
|
+
for (const [eventId, exceptions] of Object.entries(parsed.exceptions)) {
|
|
356
|
+
for (const [dateKey, reason] of exceptions) {
|
|
357
|
+
this.recurrenceEngine.addException(eventId, new Date(dateKey), reason);
|
|
358
|
+
}
|
|
410
359
|
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Clean up resources
|
|
366
|
+
*/
|
|
367
|
+
destroy() {
|
|
368
|
+
// Clean up worker
|
|
369
|
+
if (this.searchManager) {
|
|
370
|
+
this.searchManager.destroy();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Clear caches
|
|
374
|
+
if (this.recurrenceEngine) {
|
|
375
|
+
this.recurrenceEngine.occurrenceCache.clear();
|
|
411
376
|
}
|
|
377
|
+
|
|
378
|
+
// Call parent destroy if exists
|
|
379
|
+
if (super.destroy) {
|
|
380
|
+
super.destroy();
|
|
381
|
+
}
|
|
382
|
+
}
|
|
412
383
|
}
|
|
413
384
|
|
|
414
385
|
// Usage Example
|
|
415
386
|
export function createEnhancedCalendar(config) {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
new Date('2024-01-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
end: new Date('2024-01-31')
|
|
453
|
-
},
|
|
454
|
-
categories: ['meetings'],
|
|
455
|
-
modifiedOnly: false
|
|
456
|
-
}).then(results => {
|
|
457
|
-
console.log('Search results:', results);
|
|
387
|
+
const calendar = new EnhancedCalendar(config);
|
|
388
|
+
|
|
389
|
+
// Example: Add a complex recurring event
|
|
390
|
+
calendar.addEvent({
|
|
391
|
+
id: 'meeting-1',
|
|
392
|
+
title: 'Weekly Team Standup',
|
|
393
|
+
start: new Date('2024-01-01T10:00:00'),
|
|
394
|
+
end: new Date('2024-01-01T10:30:00'),
|
|
395
|
+
recurring: true,
|
|
396
|
+
recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20241231T235959Z',
|
|
397
|
+
timeZone: 'America/New_York',
|
|
398
|
+
categories: ['meetings', 'team']
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// Example: Modify a single occurrence
|
|
402
|
+
calendar.modifyOccurrence('meeting-1', new Date('2024-01-08T10:00:00'), {
|
|
403
|
+
title: 'Extended Team Standup - Sprint Planning',
|
|
404
|
+
end: new Date('2024-01-08T11:30:00'),
|
|
405
|
+
location: 'Conference Room A'
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// Example: Cancel an occurrence
|
|
409
|
+
calendar.cancelOccurrence('meeting-1', new Date('2024-01-15T10:00:00'), 'Public Holiday');
|
|
410
|
+
|
|
411
|
+
// Example: Advanced search
|
|
412
|
+
calendar
|
|
413
|
+
.advancedSearch('standup', {
|
|
414
|
+
dateRange: {
|
|
415
|
+
start: new Date('2024-01-01'),
|
|
416
|
+
end: new Date('2024-01-31')
|
|
417
|
+
},
|
|
418
|
+
categories: ['meetings'],
|
|
419
|
+
modifiedOnly: false
|
|
420
|
+
})
|
|
421
|
+
.then(results => {
|
|
422
|
+
console.log('Search results:', results);
|
|
458
423
|
});
|
|
459
424
|
|
|
460
|
-
|
|
425
|
+
return calendar;
|
|
461
426
|
}
|
|
462
427
|
|
|
463
|
-
export default EnhancedCalendar;
|
|
428
|
+
export default EnhancedCalendar;
|