@forcecalendar/core 0.3.0 → 0.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.
@@ -0,0 +1,463 @@
1
+ /**
2
+ * EnhancedCalendar - Integration of advanced search and recurrence features
3
+ * Demonstrates how to use the new scalable components
4
+ */
5
+
6
+ import { Calendar } from '../calendar/Calendar.js';
7
+ import { SearchWorkerManager } from '../search/SearchWorkerManager.js';
8
+ import { RecurrenceEngineV2 } from '../events/RecurrenceEngineV2.js';
9
+
10
+ export class EnhancedCalendar extends Calendar {
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) : [];
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
+ }
75
+ }
76
+
77
+ // Expand recurring events with enhanced engine
78
+ const expandedOccurrences = [];
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
+ }
96
+
97
+ const endTime = performance.now();
98
+ this.recordMetric('expansionTime', endTime - startTime);
99
+
100
+ // Combine and sort
101
+ const allEventsInRange = [...regularEvents, ...expandedOccurrences];
102
+ allEventsInRange.sort((a, b) => a.start - b.start);
103
+
104
+ return allEventsInRange;
105
+ }
106
+
107
+ /**
108
+ * Modify a single occurrence of a recurring event
109
+ */
110
+ modifyOccurrence(eventId, occurrenceDate, modifications) {
111
+ // Add to modified instances
112
+ this.recurrenceEngine.addModifiedInstance(
113
+ eventId,
114
+ occurrenceDate,
115
+ modifications
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();
127
+ }
128
+
129
+ /**
130
+ * Cancel a single occurrence of a recurring event
131
+ */
132
+ cancelOccurrence(eventId, occurrenceDate, reason = 'Cancelled') {
133
+ // Add exception
134
+ this.recurrenceEngine.addException(eventId, occurrenceDate, reason);
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();
145
+ }
146
+
147
+ /**
148
+ * Bulk operations for recurring events
149
+ */
150
+ async bulkModifyOccurrences(eventId, dateRange, modifications) {
151
+ const event = this.eventStore.getEvent(eventId);
152
+ if (!event || !event.recurring) {
153
+ throw new Error('Event not found or not recurring');
154
+ }
155
+
156
+ // Get all occurrences in range
157
+ const occurrences = this.recurrenceEngine.expandEvent(
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
+ }
171
+
172
+ // Emit bulk change event
173
+ this.emit('occurrences:bulk-modified', {
174
+ eventId,
175
+ count: occurrences.length,
176
+ modifications
177
+ });
178
+
179
+ this.refreshView();
180
+ }
181
+
182
+ /**
183
+ * Advanced search with filters and recurrence awareness
184
+ */
185
+ async advancedSearch(query, filters = {}, options = {}) {
186
+ // First get search results
187
+ const searchResults = await this.search(query, options);
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
+ }
206
+
207
+ // Category filter
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
+ }
214
+
215
+ // Status filter
216
+ if (filters.status) {
217
+ filtered = filtered.filter(e => e.status === filters.status);
218
+ }
219
+
220
+ // Modified only filter
221
+ if (filters.modifiedOnly) {
222
+ filtered = filtered.filter(e => {
223
+ const modifications = this.recurrenceEngine.modifiedInstances.get(e.id);
224
+ return modifications && modifications.size > 0;
225
+ });
226
+ }
227
+
228
+ return filtered;
229
+ }
230
+
231
+ /**
232
+ * Setup real-time indexing for search
233
+ */
234
+ setupRealtimeIndexing() {
235
+ // Re-index when events are added
236
+ this.on('event:added', (event) => {
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
+ });
258
+ }
259
+
260
+ /**
261
+ * Get search suggestions (autocomplete)
262
+ */
263
+ async getSuggestions(partial, field = 'title') {
264
+ if (partial.length < 2) {
265
+ return [];
266
+ }
267
+
268
+ // Use search with prefix matching
269
+ const results = await this.searchManager.search(partial, {
270
+ fields: [field],
271
+ prefixMatch: true,
272
+ limit: 10
273
+ });
274
+
275
+ // Extract unique values
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
+ }
283
+
284
+ return Array.from(suggestions);
285
+ }
286
+
287
+ /**
288
+ * Performance monitoring
289
+ */
290
+ recordMetric(type, value) {
291
+ this.performanceMetrics[type].push(value);
292
+
293
+ // Keep only last 100 measurements
294
+ if (this.performanceMetrics[type].length > 100) {
295
+ this.performanceMetrics[type].shift();
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Get performance statistics
301
+ */
302
+ getPerformanceStats() {
303
+ const stats = {};
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
+ }
321
+
322
+ return stats;
323
+ }
324
+
325
+ /**
326
+ * Export calendar with recurrence data
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
+ }
339
+
340
+ // Include exceptions
341
+ for (const [eventId, exceptions] of this.recurrenceEngine.exceptionStore) {
342
+ data.exceptions[eventId] = Array.from(exceptions.entries());
343
+ }
344
+
345
+ if (format === 'json') {
346
+ return JSON.stringify(data, null, 2);
347
+ }
348
+
349
+ // Could add ICS export here
350
+ return data;
351
+ }
352
+
353
+ /**
354
+ * Import calendar with recurrence data
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
+ }
391
+ }
392
+
393
+ /**
394
+ * Clean up resources
395
+ */
396
+ destroy() {
397
+ // Clean up worker
398
+ if (this.searchManager) {
399
+ this.searchManager.destroy();
400
+ }
401
+
402
+ // Clear caches
403
+ if (this.recurrenceEngine) {
404
+ this.recurrenceEngine.occurrenceCache.clear();
405
+ }
406
+
407
+ // Call parent destroy if exists
408
+ if (super.destroy) {
409
+ super.destroy();
410
+ }
411
+ }
412
+ }
413
+
414
+ // Usage Example
415
+ export function createEnhancedCalendar(config) {
416
+ const calendar = new EnhancedCalendar(config);
417
+
418
+ // Example: Add a complex recurring event
419
+ calendar.addEvent({
420
+ id: 'meeting-1',
421
+ title: 'Weekly Team Standup',
422
+ start: new Date('2024-01-01T10:00:00'),
423
+ end: new Date('2024-01-01T10:30:00'),
424
+ recurring: true,
425
+ recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20241231T235959Z',
426
+ timeZone: 'America/New_York',
427
+ categories: ['meetings', 'team']
428
+ });
429
+
430
+ // Example: Modify a single occurrence
431
+ calendar.modifyOccurrence(
432
+ 'meeting-1',
433
+ new Date('2024-01-08T10:00:00'),
434
+ {
435
+ title: 'Extended Team Standup - Sprint Planning',
436
+ end: new Date('2024-01-08T11:30:00'),
437
+ location: 'Conference Room A'
438
+ }
439
+ );
440
+
441
+ // Example: Cancel an occurrence
442
+ calendar.cancelOccurrence(
443
+ 'meeting-1',
444
+ new Date('2024-01-15T10:00:00'),
445
+ 'Public Holiday'
446
+ );
447
+
448
+ // Example: Advanced search
449
+ calendar.advancedSearch('standup', {
450
+ dateRange: {
451
+ start: new Date('2024-01-01'),
452
+ end: new Date('2024-01-31')
453
+ },
454
+ categories: ['meetings'],
455
+ modifiedOnly: false
456
+ }).then(results => {
457
+ console.log('Search results:', results);
458
+ });
459
+
460
+ return calendar;
461
+ }
462
+
463
+ export default EnhancedCalendar;