@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
|
@@ -4,43 +4,43 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export class SearchWorkerManager {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
7
|
+
constructor(eventStore) {
|
|
8
|
+
this.eventStore = eventStore;
|
|
9
|
+
this.workerSupported = typeof Worker !== 'undefined';
|
|
10
|
+
this.worker = null;
|
|
11
|
+
this.indexReady = false;
|
|
12
|
+
this.pendingSearches = [];
|
|
13
|
+
|
|
14
|
+
// Fallback to main thread if workers not available
|
|
15
|
+
this.fallbackIndex = null;
|
|
16
|
+
|
|
17
|
+
// Configuration
|
|
18
|
+
this.config = {
|
|
19
|
+
chunkSize: 100, // Events per indexing batch
|
|
20
|
+
maxWorkers: 4, // Max parallel workers
|
|
21
|
+
indexThreshold: 1000, // Use workers above this event count
|
|
22
|
+
cacheSize: 50 // LRU cache for search results
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Search result cache
|
|
26
|
+
this.searchCache = new Map();
|
|
27
|
+
this.cacheOrder = [];
|
|
28
|
+
|
|
29
|
+
this.initializeWorker();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Initialize the search worker
|
|
34
|
+
*/
|
|
35
|
+
initializeWorker() {
|
|
36
|
+
if (!this.workerSupported) {
|
|
37
|
+
// Use InvertedIndex as fallback
|
|
38
|
+
this.fallbackIndex = new InvertedIndex();
|
|
39
|
+
return;
|
|
30
40
|
}
|
|
31
41
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
*/
|
|
35
|
-
initializeWorker() {
|
|
36
|
-
if (!this.workerSupported) {
|
|
37
|
-
// Use InvertedIndex as fallback
|
|
38
|
-
this.fallbackIndex = new InvertedIndex();
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Create worker from inline code to avoid separate file requirement
|
|
43
|
-
const workerCode = `
|
|
42
|
+
// Create worker from inline code to avoid separate file requirement
|
|
43
|
+
const workerCode = `
|
|
44
44
|
let index = {};
|
|
45
45
|
let events = {};
|
|
46
46
|
let config = {};
|
|
@@ -163,236 +163,236 @@ export class SearchWorkerManager {
|
|
|
163
163
|
};
|
|
164
164
|
`;
|
|
165
165
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
166
|
+
// Create worker from blob
|
|
167
|
+
const blob = new Blob([workerCode], { type: 'application/javascript' });
|
|
168
|
+
const workerUrl = URL.createObjectURL(blob);
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
this.worker = new Worker(workerUrl);
|
|
172
|
+
this.setupWorkerHandlers();
|
|
173
|
+
|
|
174
|
+
// Initialize worker
|
|
175
|
+
this.worker.postMessage({
|
|
176
|
+
type: 'init',
|
|
177
|
+
data: { config: this.config }
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Clean up blob URL
|
|
181
|
+
URL.revokeObjectURL(workerUrl);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
console.warn('Worker creation failed, falling back to main thread:', error);
|
|
184
|
+
this.workerSupported = false;
|
|
185
|
+
this.fallbackIndex = new InvertedIndex();
|
|
187
186
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Setup worker message handlers
|
|
191
|
+
*/
|
|
192
|
+
setupWorkerHandlers() {
|
|
193
|
+
this.worker.onmessage = e => {
|
|
194
|
+
const { type, data } = e.data;
|
|
195
|
+
|
|
196
|
+
switch (type) {
|
|
197
|
+
case 'ready':
|
|
198
|
+
this.indexReady = true;
|
|
199
|
+
this.indexEvents();
|
|
200
|
+
break;
|
|
201
|
+
|
|
202
|
+
case 'indexed':
|
|
203
|
+
// Process pending searches
|
|
204
|
+
this.processPendingSearches();
|
|
205
|
+
break;
|
|
206
|
+
|
|
207
|
+
case 'results':
|
|
208
|
+
this.handleSearchResults(e.data);
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
this.worker.onerror = error => {
|
|
214
|
+
console.error('Worker error:', error);
|
|
215
|
+
// Fallback to main thread
|
|
216
|
+
this.workerSupported = false;
|
|
217
|
+
this.fallbackIndex = new InvertedIndex();
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Index all events
|
|
223
|
+
*/
|
|
224
|
+
async indexEvents() {
|
|
225
|
+
const events = this.eventStore.getAllEvents();
|
|
226
|
+
|
|
227
|
+
// Use main thread for small datasets
|
|
228
|
+
if (events.length < this.config.indexThreshold) {
|
|
229
|
+
if (this.fallbackIndex) {
|
|
230
|
+
this.fallbackIndex.buildIndex(events);
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
219
233
|
}
|
|
220
234
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
this.fallbackIndex.buildIndex(events);
|
|
231
|
-
}
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Chunk events for worker
|
|
236
|
-
if (this.worker && this.indexReady) {
|
|
237
|
-
for (let i = 0; i < events.length; i += this.config.chunkSize) {
|
|
238
|
-
const chunk = events.slice(i, i + this.config.chunkSize);
|
|
239
|
-
this.worker.postMessage({
|
|
240
|
-
type: 'index',
|
|
241
|
-
data: { events: chunk }
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
}
|
|
235
|
+
// Chunk events for worker
|
|
236
|
+
if (this.worker && this.indexReady) {
|
|
237
|
+
for (let i = 0; i < events.length; i += this.config.chunkSize) {
|
|
238
|
+
const chunk = events.slice(i, i + this.config.chunkSize);
|
|
239
|
+
this.worker.postMessage({
|
|
240
|
+
type: 'index',
|
|
241
|
+
data: { events: chunk }
|
|
242
|
+
});
|
|
243
|
+
}
|
|
245
244
|
}
|
|
245
|
+
}
|
|
246
246
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
247
|
+
/**
|
|
248
|
+
* Search with caching and worker support
|
|
249
|
+
*/
|
|
250
|
+
async search(query, options = {}) {
|
|
251
|
+
const cacheKey = JSON.stringify({ query, options });
|
|
252
252
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Use appropriate search method
|
|
259
|
-
let results;
|
|
260
|
-
if (this.worker && this.indexReady) {
|
|
261
|
-
results = await this.workerSearch(query, options);
|
|
262
|
-
} else if (this.fallbackIndex) {
|
|
263
|
-
results = this.fallbackIndex.search(query, options);
|
|
264
|
-
} else {
|
|
265
|
-
// Direct search as last resort
|
|
266
|
-
results = this.directSearch(query, options);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Cache results
|
|
270
|
-
this.cacheResults(cacheKey, results);
|
|
271
|
-
|
|
272
|
-
return results;
|
|
253
|
+
// Check cache
|
|
254
|
+
if (this.searchCache.has(cacheKey)) {
|
|
255
|
+
return this.searchCache.get(cacheKey);
|
|
273
256
|
}
|
|
274
257
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
resolve
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
this.worker.postMessage({
|
|
288
|
-
type: 'search',
|
|
289
|
-
data: {
|
|
290
|
-
id: searchId,
|
|
291
|
-
query,
|
|
292
|
-
options
|
|
293
|
-
}
|
|
294
|
-
});
|
|
295
|
-
});
|
|
258
|
+
// Use appropriate search method
|
|
259
|
+
let results;
|
|
260
|
+
if (this.worker && this.indexReady) {
|
|
261
|
+
results = await this.workerSearch(query, options);
|
|
262
|
+
} else if (this.fallbackIndex) {
|
|
263
|
+
results = this.fallbackIndex.search(query, options);
|
|
264
|
+
} else {
|
|
265
|
+
// Direct search as last resort
|
|
266
|
+
results = this.directSearch(query, options);
|
|
296
267
|
}
|
|
297
268
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
results.push({ event, score });
|
|
323
|
-
}
|
|
269
|
+
// Cache results
|
|
270
|
+
this.cacheResults(cacheKey, results);
|
|
271
|
+
|
|
272
|
+
return results;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Search using worker
|
|
277
|
+
*/
|
|
278
|
+
workerSearch(query, options) {
|
|
279
|
+
return new Promise(resolve => {
|
|
280
|
+
const searchId = Date.now() + Math.random();
|
|
281
|
+
|
|
282
|
+
this.pendingSearches.push({
|
|
283
|
+
id: searchId,
|
|
284
|
+
resolve
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
this.worker.postMessage({
|
|
288
|
+
type: 'search',
|
|
289
|
+
data: {
|
|
290
|
+
id: searchId,
|
|
291
|
+
query,
|
|
292
|
+
options
|
|
324
293
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Direct search without worker
|
|
300
|
+
*/
|
|
301
|
+
directSearch(query, options) {
|
|
302
|
+
const events = this.eventStore.getAllEvents();
|
|
303
|
+
const queryLower = query.toLowerCase();
|
|
304
|
+
const results = [];
|
|
305
|
+
|
|
306
|
+
for (const event of events) {
|
|
307
|
+
let score = 0;
|
|
308
|
+
|
|
309
|
+
// Check each field
|
|
310
|
+
const fields = options.fields || ['title', 'description', 'location'];
|
|
311
|
+
for (const field of fields) {
|
|
312
|
+
const value = event[field];
|
|
313
|
+
if (!value) continue;
|
|
314
|
+
|
|
315
|
+
const valueLower = value.toLowerCase();
|
|
316
|
+
if (valueLower.includes(queryLower)) {
|
|
317
|
+
score += field === 'title' ? 20 : 10;
|
|
330
318
|
}
|
|
319
|
+
}
|
|
331
320
|
|
|
332
|
-
|
|
321
|
+
if (score > 0) {
|
|
322
|
+
results.push({ event, score });
|
|
323
|
+
}
|
|
333
324
|
}
|
|
334
325
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const pending = this.pendingSearches.find(s => s.id === data.id);
|
|
340
|
-
if (pending) {
|
|
341
|
-
pending.resolve(data.results);
|
|
342
|
-
this.pendingSearches = this.pendingSearches.filter(s => s.id !== data.id);
|
|
343
|
-
}
|
|
326
|
+
// Sort and limit
|
|
327
|
+
results.sort((a, b) => b.score - a.score);
|
|
328
|
+
if (options.limit) {
|
|
329
|
+
return results.slice(0, options.limit);
|
|
344
330
|
}
|
|
345
331
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
332
|
+
return results;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Handle search results from worker
|
|
337
|
+
*/
|
|
338
|
+
handleSearchResults(data) {
|
|
339
|
+
const pending = this.pendingSearches.find(s => s.id === data.id);
|
|
340
|
+
if (pending) {
|
|
341
|
+
pending.resolve(data.results);
|
|
342
|
+
this.pendingSearches = this.pendingSearches.filter(s => s.id !== data.id);
|
|
354
343
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Process any pending searches
|
|
348
|
+
*/
|
|
349
|
+
processPendingSearches() {
|
|
350
|
+
// Re-trigger pending searches after indexing
|
|
351
|
+
for (const search of this.pendingSearches) {
|
|
352
|
+
// Will be handled by worker
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Cache search results with LRU eviction
|
|
358
|
+
*/
|
|
359
|
+
cacheResults(key, results) {
|
|
360
|
+
// Add to cache
|
|
361
|
+
this.searchCache.set(key, results);
|
|
362
|
+
this.cacheOrder.push(key);
|
|
363
|
+
|
|
364
|
+
// Evict old entries
|
|
365
|
+
while (this.cacheOrder.length > this.config.cacheSize) {
|
|
366
|
+
const oldKey = this.cacheOrder.shift();
|
|
367
|
+
this.searchCache.delete(oldKey);
|
|
369
368
|
}
|
|
369
|
+
}
|
|
370
370
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
371
|
+
/**
|
|
372
|
+
* Clear index and cache
|
|
373
|
+
*/
|
|
374
|
+
clear() {
|
|
375
|
+
this.searchCache.clear();
|
|
376
|
+
this.cacheOrder = [];
|
|
377
377
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
}
|
|
381
|
-
if (this.fallbackIndex) {
|
|
382
|
-
this.fallbackIndex.clear();
|
|
383
|
-
}
|
|
378
|
+
if (this.worker) {
|
|
379
|
+
this.worker.postMessage({ type: 'clear' });
|
|
384
380
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
* Destroy worker and clean up
|
|
388
|
-
*/
|
|
389
|
-
destroy() {
|
|
390
|
-
if (this.worker) {
|
|
391
|
-
this.worker.terminate();
|
|
392
|
-
this.worker = null;
|
|
393
|
-
}
|
|
394
|
-
this.clear();
|
|
381
|
+
if (this.fallbackIndex) {
|
|
382
|
+
this.fallbackIndex.clear();
|
|
395
383
|
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Destroy worker and clean up
|
|
388
|
+
*/
|
|
389
|
+
destroy() {
|
|
390
|
+
if (this.worker) {
|
|
391
|
+
this.worker.terminate();
|
|
392
|
+
this.worker = null;
|
|
393
|
+
}
|
|
394
|
+
this.clear();
|
|
395
|
+
}
|
|
396
396
|
}
|
|
397
397
|
|
|
398
398
|
/**
|
|
@@ -400,107 +400,107 @@ export class SearchWorkerManager {
|
|
|
400
400
|
* Used as fallback when Web Workers not available
|
|
401
401
|
*/
|
|
402
402
|
export class InvertedIndex {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
403
|
+
constructor() {
|
|
404
|
+
this.index = new Map(); // term -> Set of event IDs
|
|
405
|
+
this.events = new Map(); // event ID -> event
|
|
406
|
+
this.fieldBoosts = {
|
|
407
|
+
title: 2.0,
|
|
408
|
+
description: 1.0,
|
|
409
|
+
location: 1.5,
|
|
410
|
+
category: 1.5
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Build inverted index from events
|
|
416
|
+
*/
|
|
417
|
+
buildIndex(events) {
|
|
418
|
+
this.clear();
|
|
419
|
+
|
|
420
|
+
for (const event of events) {
|
|
421
|
+
this.events.set(event.id, event);
|
|
422
|
+
|
|
423
|
+
// Index each field with boost factors
|
|
424
|
+
for (const [field, boost] of Object.entries(this.fieldBoosts)) {
|
|
425
|
+
const value = event[field];
|
|
426
|
+
if (!value) continue;
|
|
427
|
+
|
|
428
|
+
const tokens = this.tokenize(value);
|
|
429
|
+
for (const token of tokens) {
|
|
430
|
+
if (!this.index.has(token)) {
|
|
431
|
+
this.index.set(token, new Map());
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const eventScores = this.index.get(token);
|
|
435
|
+
const currentScore = eventScores.get(event.id) || 0;
|
|
436
|
+
eventScores.set(event.id, currentScore + boost);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
412
439
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Tokenize text into searchable terms
|
|
444
|
+
*/
|
|
445
|
+
tokenize(text) {
|
|
446
|
+
return text
|
|
447
|
+
.toLowerCase()
|
|
448
|
+
.split(/\W+/)
|
|
449
|
+
.filter(token => token.length > 1);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Search the index
|
|
454
|
+
*/
|
|
455
|
+
search(query, options = {}) {
|
|
456
|
+
const queryTokens = this.tokenize(query);
|
|
457
|
+
const scores = new Map();
|
|
458
|
+
|
|
459
|
+
// Aggregate scores from all matching terms
|
|
460
|
+
for (const token of queryTokens) {
|
|
461
|
+
// Exact matches
|
|
462
|
+
if (this.index.has(token)) {
|
|
463
|
+
const eventScores = this.index.get(token);
|
|
464
|
+
for (const [eventId, tokenScore] of eventScores) {
|
|
465
|
+
const currentScore = scores.get(eventId) || 0;
|
|
466
|
+
scores.set(eventId, currentScore + tokenScore);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Prefix matches for autocomplete
|
|
471
|
+
if (options.prefixMatch) {
|
|
472
|
+
for (const [indexToken, eventScores] of this.index) {
|
|
473
|
+
if (indexToken.startsWith(token)) {
|
|
474
|
+
for (const [eventId, tokenScore] of eventScores) {
|
|
475
|
+
const currentScore = scores.get(eventId) || 0;
|
|
476
|
+
scores.set(eventId, currentScore + tokenScore * 0.5);
|
|
438
477
|
}
|
|
478
|
+
}
|
|
439
479
|
}
|
|
480
|
+
}
|
|
440
481
|
}
|
|
441
482
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
483
|
+
// Convert to results array
|
|
484
|
+
const results = Array.from(scores.entries())
|
|
485
|
+
.map(([eventId, score]) => ({
|
|
486
|
+
event: this.events.get(eventId),
|
|
487
|
+
score
|
|
488
|
+
}))
|
|
489
|
+
.sort((a, b) => b.score - a.score);
|
|
490
|
+
|
|
491
|
+
// Apply limit
|
|
492
|
+
if (options.limit) {
|
|
493
|
+
return results.slice(0, options.limit);
|
|
450
494
|
}
|
|
451
495
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
*/
|
|
455
|
-
search(query, options = {}) {
|
|
456
|
-
const queryTokens = this.tokenize(query);
|
|
457
|
-
const scores = new Map();
|
|
458
|
-
|
|
459
|
-
// Aggregate scores from all matching terms
|
|
460
|
-
for (const token of queryTokens) {
|
|
461
|
-
// Exact matches
|
|
462
|
-
if (this.index.has(token)) {
|
|
463
|
-
const eventScores = this.index.get(token);
|
|
464
|
-
for (const [eventId, tokenScore] of eventScores) {
|
|
465
|
-
const currentScore = scores.get(eventId) || 0;
|
|
466
|
-
scores.set(eventId, currentScore + tokenScore);
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// Prefix matches for autocomplete
|
|
471
|
-
if (options.prefixMatch) {
|
|
472
|
-
for (const [indexToken, eventScores] of this.index) {
|
|
473
|
-
if (indexToken.startsWith(token)) {
|
|
474
|
-
for (const [eventId, tokenScore] of eventScores) {
|
|
475
|
-
const currentScore = scores.get(eventId) || 0;
|
|
476
|
-
scores.set(eventId, currentScore + tokenScore * 0.5);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// Convert to results array
|
|
484
|
-
const results = Array.from(scores.entries())
|
|
485
|
-
.map(([eventId, score]) => ({
|
|
486
|
-
event: this.events.get(eventId),
|
|
487
|
-
score
|
|
488
|
-
}))
|
|
489
|
-
.sort((a, b) => b.score - a.score);
|
|
490
|
-
|
|
491
|
-
// Apply limit
|
|
492
|
-
if (options.limit) {
|
|
493
|
-
return results.slice(0, options.limit);
|
|
494
|
-
}
|
|
496
|
+
return results;
|
|
497
|
+
}
|
|
495
498
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
this.events.clear();
|
|
505
|
-
}
|
|
506
|
-
}
|
|
499
|
+
/**
|
|
500
|
+
* Clear the index
|
|
501
|
+
*/
|
|
502
|
+
clear() {
|
|
503
|
+
this.index.clear();
|
|
504
|
+
this.events.clear();
|
|
505
|
+
}
|
|
506
|
+
}
|