@forcecalendar/core 2.1.64 → 2.1.66

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.
@@ -29,6 +29,7 @@ export class EventStore {
29
29
  /** @type {Map<string, Set<string>>} Status -> Set of event IDs */
30
30
  byStatus: new Map()
31
31
  };
32
+ this.eventIndexRefs = new Map();
32
33
 
33
34
  // Timezone manager for conversions (use singleton to share cache)
34
35
  this.timezoneManager = TimezoneManager.getInstance();
@@ -334,9 +335,6 @@ export class EventStore {
334
335
  getEventsForDate(date, timezone = null) {
335
336
  timezone = timezone || this.defaultTimezone;
336
337
 
337
- // Use local date string for the query date (in the calendar's timezone)
338
- const dateStr = DateUtils.getLocalDateString(date);
339
-
340
338
  // Collect candidate event IDs from indices
341
339
  const candidateIds = new Set();
342
340
 
@@ -409,23 +407,40 @@ export class EventStore {
409
407
 
410
408
  // Collect all events from those dates
411
409
  const checkedIds = new Set();
412
- dates.forEach(date => {
413
- // Use getLocalDateString to match the index key format (YYYY-MM-DD)
414
- const dateStr = DateUtils.getLocalDateString(date);
415
- const eventIds = this.indices.byDate.get(dateStr) || new Set();
410
+ const addCandidateIds = eventIds => {
411
+ if (!eventIds) return;
416
412
 
417
413
  eventIds.forEach(id => {
418
414
  if (!checkedIds.has(id) && id !== excludeId) {
419
415
  checkedIds.add(id);
420
- const event = this.events.get(id);
421
-
422
- if (event && event.overlaps({ start, end })) {
423
- overlapping.push(event);
424
- }
425
416
  }
426
417
  });
418
+ };
419
+
420
+ dates.forEach(date => {
421
+ // Use getLocalDateString to match the index key format (YYYY-MM-DD)
422
+ const dateStr = DateUtils.getLocalDateString(date);
423
+ addCandidateIds(this.indices.byDate.get(dateStr));
427
424
  });
428
425
 
426
+ // Lazy-indexed long events may not have every day in byDate. Use month
427
+ // buckets as candidates and rely on precise overlap filtering below.
428
+ const currentMonth = new Date(startDate.getFullYear(), startDate.getMonth(), 1);
429
+ const endMonth = new Date(endDate.getFullYear(), endDate.getMonth(), 1);
430
+ while (currentMonth <= endMonth) {
431
+ const monthKey = `${currentMonth.getFullYear()}-${String(currentMonth.getMonth() + 1).padStart(2, '0')}`;
432
+ addCandidateIds(this.indices.byMonth.get(monthKey));
433
+ currentMonth.setMonth(currentMonth.getMonth() + 1);
434
+ }
435
+
436
+ for (const id of checkedIds) {
437
+ const event = this.events.get(id);
438
+
439
+ if (event && event.overlaps({ start, end })) {
440
+ overlapping.push(event);
441
+ }
442
+ }
443
+
429
444
  return overlapping.sort((a, b) => a.start - b.start);
430
445
  }
431
446
 
@@ -647,6 +662,7 @@ export class EventStore {
647
662
  this.indices.recurring.clear();
648
663
  this.indices.byCategory.clear();
649
664
  this.indices.byStatus.clear();
665
+ this.eventIndexRefs.clear();
650
666
 
651
667
  this._notifyChange({
652
668
  type: 'clear',
@@ -687,6 +703,8 @@ export class EventStore {
687
703
  * @private
688
704
  */
689
705
  _indexEvent(event) {
706
+ this._createIndexRefs(event.id);
707
+
690
708
  // Check if should use lazy indexing for large date ranges
691
709
  if (this.optimizer.shouldUseLazyIndexing(event)) {
692
710
  this._indexEventLazy(event);
@@ -706,26 +724,14 @@ export class EventStore {
706
724
 
707
725
  dates.forEach(date => {
708
726
  const dateStr = DateUtils.getLocalDateString(date);
709
-
710
- if (!this.indices.byDate.has(dateStr)) {
711
- this.indices.byDate.set(dateStr, new Set());
712
- }
713
- this.indices.byDate.get(dateStr).add(event.id);
727
+ this._addToKeyedIndex('byDate', dateStr, event.id);
714
728
  });
715
729
 
716
- // Index by month(s) using UTC
717
- const startMonth = `${startDate.getFullYear()}-${String(startDate.getMonth() + 1).padStart(2, '0')}`;
718
- const endMonth = `${endDate.getFullYear()}-${String(endDate.getMonth() + 1).padStart(2, '0')}`;
719
-
720
730
  // Add to all months the event spans
721
731
  const currentMonth = new Date(startDate.getFullYear(), startDate.getMonth(), 1);
722
732
  while (currentMonth <= endDate) {
723
733
  const monthKey = `${currentMonth.getFullYear()}-${String(currentMonth.getMonth() + 1).padStart(2, '0')}`;
724
-
725
- if (!this.indices.byMonth.has(monthKey)) {
726
- this.indices.byMonth.set(monthKey, new Set());
727
- }
728
- this.indices.byMonth.get(monthKey).add(event.id);
734
+ this._addToKeyedIndex('byMonth', monthKey, event.id);
729
735
 
730
736
  currentMonth.setMonth(currentMonth.getMonth() + 1);
731
737
  }
@@ -733,24 +739,19 @@ export class EventStore {
733
739
  // Index by categories
734
740
  if (event.categories && event.categories.length > 0) {
735
741
  event.categories.forEach(category => {
736
- if (!this.indices.byCategory.has(category)) {
737
- this.indices.byCategory.set(category, new Set());
738
- }
739
- this.indices.byCategory.get(category).add(event.id);
742
+ this._addToKeyedIndex('byCategory', category, event.id);
740
743
  });
741
744
  }
742
745
 
743
746
  // Index by status
744
747
  if (event.status) {
745
- if (!this.indices.byStatus.has(event.status)) {
746
- this.indices.byStatus.set(event.status, new Set());
747
- }
748
- this.indices.byStatus.get(event.status).add(event.id);
748
+ this._addToKeyedIndex('byStatus', event.status, event.id);
749
749
  }
750
750
 
751
751
  // Index recurring events
752
752
  if (event.recurring) {
753
753
  this.indices.recurring.add(event.id);
754
+ this.eventIndexRefs.get(event.id).recurring = true;
754
755
  }
755
756
  }
756
757
 
@@ -759,8 +760,7 @@ export class EventStore {
759
760
  * @private
760
761
  */
761
762
  _indexEventLazy(event) {
762
- // Create lazy index markers
763
- const markers = this.optimizer.createLazyIndexMarkers(event);
763
+ this.optimizer.createLazyIndexMarkers(event);
764
764
 
765
765
  // Index only the boundaries initially (in event's local timezone)
766
766
  const eventStartLocal = event.getStartInTimezone(event.timeZone);
@@ -779,10 +779,7 @@ export class EventStore {
779
779
 
780
780
  firstWeekDates.forEach(date => {
781
781
  const dateStr = DateUtils.getLocalDateString(date);
782
- if (!this.indices.byDate.has(dateStr)) {
783
- this.indices.byDate.set(dateStr, new Set());
784
- }
785
- this.indices.byDate.get(dateStr).add(event.id);
782
+ this._addToKeyedIndex('byDate', dateStr, event.id);
786
783
  });
787
784
 
788
785
  // Index last week if different from first
@@ -796,10 +793,7 @@ export class EventStore {
796
793
 
797
794
  lastWeekDates.forEach(date => {
798
795
  const dateStr = DateUtils.getLocalDateString(date);
799
- if (!this.indices.byDate.has(dateStr)) {
800
- this.indices.byDate.set(dateStr, new Set());
801
- }
802
- this.indices.byDate.get(dateStr).add(event.id);
796
+ this._addToKeyedIndex('byDate', dateStr, event.id);
803
797
  });
804
798
  }
805
799
 
@@ -807,33 +801,53 @@ export class EventStore {
807
801
  const currentMonth = new Date(startDate.getFullYear(), startDate.getMonth(), 1);
808
802
  while (currentMonth <= endDate) {
809
803
  const monthKey = `${currentMonth.getFullYear()}-${String(currentMonth.getMonth() + 1).padStart(2, '0')}`;
810
- if (!this.indices.byMonth.has(monthKey)) {
811
- this.indices.byMonth.set(monthKey, new Set());
812
- }
813
- this.indices.byMonth.get(monthKey).add(event.id);
804
+ this._addToKeyedIndex('byMonth', monthKey, event.id);
814
805
  currentMonth.setMonth(currentMonth.getMonth() + 1);
815
806
  }
816
807
 
817
808
  // Index other properties normally
818
809
  if (event.categories && event.categories.length > 0) {
819
810
  event.categories.forEach(category => {
820
- if (!this.indices.byCategory.has(category)) {
821
- this.indices.byCategory.set(category, new Set());
822
- }
823
- this.indices.byCategory.get(category).add(event.id);
811
+ this._addToKeyedIndex('byCategory', category, event.id);
824
812
  });
825
813
  }
826
814
 
827
815
  if (event.status) {
828
- if (!this.indices.byStatus.has(event.status)) {
829
- this.indices.byStatus.set(event.status, new Set());
830
- }
831
- this.indices.byStatus.get(event.status).add(event.id);
816
+ this._addToKeyedIndex('byStatus', event.status, event.id);
832
817
  }
833
818
 
834
819
  if (event.recurring) {
835
820
  this.indices.recurring.add(event.id);
821
+ this.eventIndexRefs.get(event.id).recurring = true;
822
+ }
823
+ }
824
+
825
+ /**
826
+ * Create reverse index references for an event.
827
+ * @private
828
+ */
829
+ _createIndexRefs(eventId) {
830
+ this.eventIndexRefs.set(eventId, {
831
+ byDate: new Set(),
832
+ byMonth: new Set(),
833
+ byCategory: new Set(),
834
+ byStatus: new Set(),
835
+ recurring: false
836
+ });
837
+ }
838
+
839
+ /**
840
+ * Add an event to a keyed index and record the reverse reference.
841
+ * @private
842
+ */
843
+ _addToKeyedIndex(indexName, key, eventId) {
844
+ const index = this.indices[indexName];
845
+ if (!index.has(key)) {
846
+ index.set(key, new Set());
836
847
  }
848
+
849
+ index.get(key).add(eventId);
850
+ this.eventIndexRefs.get(eventId)?.[indexName].add(key);
837
851
  }
838
852
 
839
853
  /**
@@ -841,6 +855,22 @@ export class EventStore {
841
855
  * @private
842
856
  */
843
857
  _unindexEvent(event) {
858
+ const refs = this.eventIndexRefs.get(event.id);
859
+
860
+ if (refs) {
861
+ this._removeFromReferencedIndex('byDate', refs.byDate, event.id);
862
+ this._removeFromReferencedIndex('byMonth', refs.byMonth, event.id);
863
+ this._removeFromReferencedIndex('byCategory', refs.byCategory, event.id);
864
+ this._removeFromReferencedIndex('byStatus', refs.byStatus, event.id);
865
+
866
+ if (refs.recurring) {
867
+ this.indices.recurring.delete(event.id);
868
+ }
869
+
870
+ this.eventIndexRefs.delete(event.id);
871
+ return;
872
+ }
873
+
844
874
  // Remove from date indices
845
875
  for (const [dateStr, eventIds] of this.indices.byDate) {
846
876
  eventIds.delete(event.id);
@@ -877,6 +907,24 @@ export class EventStore {
877
907
  this.indices.recurring.delete(event.id);
878
908
  }
879
909
 
910
+ /**
911
+ * Remove an event from only the keys it was indexed into.
912
+ * @private
913
+ */
914
+ _removeFromReferencedIndex(indexName, keys, eventId) {
915
+ const index = this.indices[indexName];
916
+
917
+ for (const key of keys) {
918
+ const eventIds = index.get(key);
919
+ if (!eventIds) continue;
920
+
921
+ eventIds.delete(eventId);
922
+ if (eventIds.size === 0) {
923
+ index.delete(key);
924
+ }
925
+ }
926
+ }
927
+
880
928
  /**
881
929
  * Notify listeners of changes
882
930
  * @private
@@ -938,6 +986,18 @@ export class EventStore {
938
986
  Array.from(this.indices.byStatus.entries()).map(([k, v]) => [k, new Set(v)])
939
987
  )
940
988
  },
989
+ eventIndexRefs: new Map(
990
+ Array.from(this.eventIndexRefs.entries()).map(([eventId, refs]) => [
991
+ eventId,
992
+ {
993
+ byDate: new Set(refs.byDate),
994
+ byMonth: new Set(refs.byMonth),
995
+ byCategory: new Set(refs.byCategory),
996
+ byStatus: new Set(refs.byStatus),
997
+ recurring: refs.recurring
998
+ }
999
+ ])
1000
+ ),
941
1001
  version: this.version
942
1002
  };
943
1003
  }
@@ -981,6 +1041,7 @@ export class EventStore {
981
1041
  if (this.batchBackup) {
982
1042
  this.events = this.batchBackup.events;
983
1043
  this.indices = this.batchBackup.indices;
1044
+ this.eventIndexRefs = this.batchBackup.eventIndexRefs;
984
1045
  this.version = this.batchBackup.version;
985
1046
  this.batchBackup = null;
986
1047
 
@@ -1154,7 +1215,6 @@ export class EventStore {
1154
1215
  cutoffDate.setMonth(cutoffDate.getMonth() - 6); // Default: 6 months ago
1155
1216
  }
1156
1217
 
1157
- const cutoffStr = cutoffDate.toDateString();
1158
1218
  let removed = 0;
1159
1219
 
1160
1220
  // Clean up date indices
@@ -1172,13 +1232,15 @@ export class EventStore {
1172
1232
  }
1173
1233
 
1174
1234
  if (!stillNeeded) {
1235
+ for (const eventId of eventIds) {
1236
+ this.eventIndexRefs.get(eventId)?.byDate.delete(dateStr);
1237
+ }
1175
1238
  this.indices.byDate.delete(dateStr);
1176
1239
  removed++;
1177
1240
  }
1178
1241
  }
1179
1242
  }
1180
1243
 
1181
- console.log(`Optimized indices: removed ${removed} old date entries`);
1182
1244
  return removed;
1183
1245
  }
1184
1246
 
package/core/index.js CHANGED
@@ -28,7 +28,7 @@ export { RRuleParser } from './events/RRuleParser.js';
28
28
  export { EnhancedCalendar } from './integration/EnhancedCalendar.js';
29
29
 
30
30
  // Version — keep in sync with package.json
31
- export const VERSION = '2.1.64';
31
+ export const VERSION = '2.1.66';
32
32
 
33
33
  // Default export
34
34
  export { Calendar as default } from './calendar/Calendar.js';
@@ -9,6 +9,8 @@ export class SearchWorkerManager {
9
9
  this.workerSupported = typeof Worker !== 'undefined';
10
10
  this.worker = null;
11
11
  this.indexReady = false;
12
+ this.indexMode = 'none';
13
+ this.workerExpectedCount = 0;
12
14
  this.pendingSearches = [];
13
15
 
14
16
  // Fallback to main thread if workers not available
@@ -19,7 +21,8 @@ export class SearchWorkerManager {
19
21
  chunkSize: 100, // Events per indexing batch
20
22
  maxWorkers: 4, // Max parallel workers
21
23
  indexThreshold: 1000, // Use workers above this event count
22
- cacheSize: 50 // LRU cache for search results
24
+ cacheSize: 50, // LRU cache for search results
25
+ searchTimeout: 10000 // Worker search timeout in milliseconds
23
26
  };
24
27
 
25
28
  // Search result cache
@@ -35,7 +38,7 @@ export class SearchWorkerManager {
35
38
  initializeWorker() {
36
39
  if (!this.workerSupported) {
37
40
  // Use InvertedIndex as fallback
38
- this.fallbackIndex = new InvertedIndex();
41
+ this.ensureFallbackIndex();
39
42
  return;
40
43
  }
41
44
 
@@ -51,13 +54,14 @@ export class SearchWorkerManager {
51
54
  events[event.id] = event;
52
55
 
53
56
  // Index each field
54
- const fields = ['title', 'description', 'location', 'category'];
57
+ const fields = ['title', 'description', 'location', 'category', 'categories'];
55
58
  for (const field of fields) {
56
59
  const value = event[field];
57
60
  if (!value) continue;
58
61
 
59
62
  // Tokenize and index
60
- const tokens = tokenize(value.toLowerCase());
63
+ const values = Array.isArray(value) ? value : [value];
64
+ const tokens = values.flatMap(item => tokenize(String(item).toLowerCase()));
61
65
  for (const token of tokens) {
62
66
  if (!index[token]) {
63
67
  index[token] = new Set();
@@ -163,11 +167,19 @@ export class SearchWorkerManager {
163
167
  };
164
168
  `;
165
169
 
166
- // Create worker from blob
167
- const blob = new Blob([workerCode], { type: 'application/javascript' });
168
- const workerUrl = URL.createObjectURL(blob);
169
-
170
+ let workerUrl = null;
170
171
  try {
172
+ if (
173
+ typeof Blob === 'undefined' ||
174
+ typeof URL === 'undefined' ||
175
+ typeof URL.createObjectURL !== 'function'
176
+ ) {
177
+ throw new Error('Blob workers are not supported in this environment');
178
+ }
179
+
180
+ // Create worker from blob
181
+ const blob = new Blob([workerCode], { type: 'application/javascript' });
182
+ workerUrl = URL.createObjectURL(blob);
171
183
  this.worker = new Worker(workerUrl);
172
184
  this.setupWorkerHandlers();
173
185
 
@@ -180,10 +192,24 @@ export class SearchWorkerManager {
180
192
  // Clean up blob URL
181
193
  URL.revokeObjectURL(workerUrl);
182
194
  } catch (error) {
195
+ if (workerUrl) {
196
+ URL.revokeObjectURL(workerUrl);
197
+ }
183
198
  console.warn('Worker creation failed, falling back to main thread:', error);
184
199
  this.workerSupported = false;
200
+ this.ensureFallbackIndex();
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Ensure a fallback index exists.
206
+ * @private
207
+ */
208
+ ensureFallbackIndex() {
209
+ if (!this.fallbackIndex) {
185
210
  this.fallbackIndex = new InvertedIndex();
186
211
  }
212
+ return this.fallbackIndex;
187
213
  }
188
214
 
189
215
  /**
@@ -191,7 +217,7 @@ export class SearchWorkerManager {
191
217
  */
192
218
  setupWorkerHandlers() {
193
219
  this.worker.onmessage = e => {
194
- const { type, data } = e.data;
220
+ const { type } = e.data;
195
221
 
196
222
  switch (type) {
197
223
  case 'ready':
@@ -200,8 +226,10 @@ export class SearchWorkerManager {
200
226
  break;
201
227
 
202
228
  case 'indexed':
203
- // Process pending searches
204
- this.processPendingSearches();
229
+ if (e.data.count >= this.workerExpectedCount) {
230
+ this.indexMode = 'worker';
231
+ this.processPendingSearches();
232
+ }
205
233
  break;
206
234
 
207
235
  case 'results':
@@ -212,9 +240,12 @@ export class SearchWorkerManager {
212
240
 
213
241
  this.worker.onerror = error => {
214
242
  console.error('Worker error:', error);
243
+ this.rejectPendingSearches(new Error('Search worker failed'));
215
244
  // Fallback to main thread
216
245
  this.workerSupported = false;
217
- this.fallbackIndex = new InvertedIndex();
246
+ this.worker = null;
247
+ this.ensureFallbackIndex().buildIndex(this.eventStore.getAllEvents());
248
+ this.indexMode = 'fallback';
218
249
  };
219
250
  }
220
251
 
@@ -223,17 +254,26 @@ export class SearchWorkerManager {
223
254
  */
224
255
  async indexEvents() {
225
256
  const events = this.eventStore.getAllEvents();
257
+ this.searchCache.clear();
258
+ this.cacheOrder = [];
226
259
 
227
260
  // Use main thread for small datasets
228
261
  if (events.length < this.config.indexThreshold) {
229
- if (this.fallbackIndex) {
230
- this.fallbackIndex.buildIndex(events);
262
+ this.ensureFallbackIndex().buildIndex(events);
263
+ this.indexMode = 'fallback';
264
+
265
+ if (this.worker && this.indexReady) {
266
+ this.worker.postMessage({ type: 'clear' });
231
267
  }
232
268
  return;
233
269
  }
234
270
 
235
271
  // Chunk events for worker
236
272
  if (this.worker && this.indexReady) {
273
+ this.indexMode = 'worker-indexing';
274
+ this.workerExpectedCount = events.length;
275
+ this.worker.postMessage({ type: 'clear' });
276
+
237
277
  for (let i = 0; i < events.length; i += this.config.chunkSize) {
238
278
  const chunk = events.slice(i, i + this.config.chunkSize);
239
279
  this.worker.postMessage({
@@ -241,7 +281,11 @@ export class SearchWorkerManager {
241
281
  data: { events: chunk }
242
282
  });
243
283
  }
284
+ return;
244
285
  }
286
+
287
+ this.ensureFallbackIndex().buildIndex(events);
288
+ this.indexMode = 'fallback';
245
289
  }
246
290
 
247
291
  /**
@@ -257,9 +301,13 @@ export class SearchWorkerManager {
257
301
 
258
302
  // Use appropriate search method
259
303
  let results;
260
- if (this.worker && this.indexReady) {
261
- results = await this.workerSearch(query, options);
262
- } else if (this.fallbackIndex) {
304
+ if (this.worker && this.indexMode === 'worker') {
305
+ try {
306
+ results = await this.workerSearch(query, options);
307
+ } catch {
308
+ results = this.directSearch(query, options);
309
+ }
310
+ } else if (this.fallbackIndex && this.indexMode === 'fallback') {
263
311
  results = this.fallbackIndex.search(query, options);
264
312
  } else {
265
313
  // Direct search as last resort
@@ -276,14 +324,20 @@ export class SearchWorkerManager {
276
324
  * Search using worker
277
325
  */
278
326
  workerSearch(query, options) {
279
- return new Promise(resolve => {
327
+ return new Promise((resolve, reject) => {
280
328
  const searchId = Date.now() + Math.random();
329
+ const timeoutId = setTimeout(() => {
330
+ this.pendingSearches = this.pendingSearches.filter(search => search.id !== searchId);
331
+ reject(new Error('Search worker timed out'));
332
+ }, options.timeout || this.config.searchTimeout);
281
333
 
282
334
  this.pendingSearches.push({
283
335
  id: searchId,
284
336
  query,
285
337
  options,
286
- resolve
338
+ resolve,
339
+ reject,
340
+ timeoutId
287
341
  });
288
342
 
289
343
  this.worker.postMessage({
@@ -309,14 +363,23 @@ export class SearchWorkerManager {
309
363
  let score = 0;
310
364
 
311
365
  // Check each field
312
- const fields = options.fields || ['title', 'description', 'location'];
366
+ const fields = options.fields || [
367
+ 'title',
368
+ 'description',
369
+ 'location',
370
+ 'category',
371
+ 'categories'
372
+ ];
313
373
  for (const field of fields) {
314
374
  const value = event[field];
315
375
  if (!value) continue;
316
376
 
317
- const valueLower = value.toLowerCase();
318
- if (valueLower.includes(queryLower)) {
319
- score += field === 'title' ? 20 : 10;
377
+ const values = Array.isArray(value) ? value : [value];
378
+ for (const item of values) {
379
+ const valueLower = String(item).toLowerCase();
380
+ if (valueLower.includes(queryLower)) {
381
+ score += field === 'title' ? 20 : 10;
382
+ }
320
383
  }
321
384
  }
322
385
 
@@ -340,6 +403,7 @@ export class SearchWorkerManager {
340
403
  handleSearchResults(data) {
341
404
  const pending = this.pendingSearches.find(s => s.id === data.id);
342
405
  if (pending) {
406
+ clearTimeout(pending.timeoutId);
343
407
  pending.resolve(data.results);
344
408
  this.pendingSearches = this.pendingSearches.filter(s => s.id !== data.id);
345
409
  }
@@ -366,6 +430,10 @@ export class SearchWorkerManager {
366
430
  * Cache search results with LRU eviction
367
431
  */
368
432
  cacheResults(key, results) {
433
+ if (this.searchCache.has(key)) {
434
+ this.cacheOrder = this.cacheOrder.filter(existingKey => existingKey !== key);
435
+ }
436
+
369
437
  // Add to cache
370
438
  this.searchCache.set(key, results);
371
439
  this.cacheOrder.push(key);
@@ -383,6 +451,9 @@ export class SearchWorkerManager {
383
451
  clear() {
384
452
  this.searchCache.clear();
385
453
  this.cacheOrder = [];
454
+ this.indexMode = 'none';
455
+ this.workerExpectedCount = 0;
456
+ this.rejectPendingSearches(new Error('Search index was cleared'));
386
457
 
387
458
  if (this.worker) {
388
459
  this.worker.postMessage({ type: 'clear' });
@@ -402,6 +473,18 @@ export class SearchWorkerManager {
402
473
  }
403
474
  this.clear();
404
475
  }
476
+
477
+ /**
478
+ * Reject all pending worker searches.
479
+ * @private
480
+ */
481
+ rejectPendingSearches(error) {
482
+ for (const search of this.pendingSearches) {
483
+ clearTimeout(search.timeoutId);
484
+ search.reject(error);
485
+ }
486
+ this.pendingSearches = [];
487
+ }
405
488
  }
406
489
 
407
490
  /**
@@ -416,7 +499,8 @@ export class InvertedIndex {
416
499
  title: 2.0,
417
500
  description: 1.0,
418
501
  location: 1.5,
419
- category: 1.5
502
+ category: 1.5,
503
+ categories: 1.5
420
504
  };
421
505
  }
422
506
 
@@ -434,7 +518,8 @@ export class InvertedIndex {
434
518
  const value = event[field];
435
519
  if (!value) continue;
436
520
 
437
- const tokens = this.tokenize(value);
521
+ const values = Array.isArray(value) ? value : [value];
522
+ const tokens = values.flatMap(item => this.tokenize(String(item)));
438
523
  for (const token of tokens) {
439
524
  if (!this.index.has(token)) {
440
525
  this.index.set(token, new Map());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forcecalendar/core",
3
- "version": "2.1.64",
3
+ "version": "2.1.66",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "A modern, lightweight, framework-agnostic calendar engine optimized for Salesforce",