@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.
- package/core/events/EventStore.js +120 -58
- package/core/index.js +1 -1
- package/core/search/SearchWorkerManager.js +110 -25
- package/package.json +1 -1
|
@@ -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
|
-
|
|
413
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
204
|
-
|
|
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.
|
|
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
|
-
|
|
230
|
-
|
|
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.
|
|
261
|
-
|
|
262
|
-
|
|
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 || [
|
|
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
|
|
318
|
-
|
|
319
|
-
|
|
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
|
|
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());
|