@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.
@@ -4,43 +4,43 @@
4
4
  */
5
5
 
6
6
  export class SearchWorkerManager {
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();
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
- * Initialize the search worker
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
- // 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();
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
- * 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
- };
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
- * 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;
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
- * Search with caching and worker support
249
- */
250
- async search(query, options = {}) {
251
- const cacheKey = JSON.stringify({ query, options });
247
+ /**
248
+ * Search with caching and worker support
249
+ */
250
+ async search(query, options = {}) {
251
+ const cacheKey = JSON.stringify({ query, options });
252
252
 
253
- // Check cache
254
- if (this.searchCache.has(cacheKey)) {
255
- return this.searchCache.get(cacheKey);
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
- * 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
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
- * 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;
318
- }
319
- }
320
-
321
- if (score > 0) {
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
- // Sort and limit
327
- results.sort((a, b) => b.score - a.score);
328
- if (options.limit) {
329
- return results.slice(0, options.limit);
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
- return results;
321
+ if (score > 0) {
322
+ results.push({ event, score });
323
+ }
333
324
  }
334
325
 
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);
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
- * 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
- }
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
- * 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);
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
- * Clear index and cache
373
- */
374
- clear() {
375
- this.searchCache.clear();
376
- this.cacheOrder = [];
371
+ /**
372
+ * Clear index and cache
373
+ */
374
+ clear() {
375
+ this.searchCache.clear();
376
+ this.cacheOrder = [];
377
377
 
378
- if (this.worker) {
379
- this.worker.postMessage({ type: 'clear' });
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
- 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
- };
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
- * 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
- }
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
- * Tokenize text into searchable terms
444
- */
445
- tokenize(text) {
446
- return text
447
- .toLowerCase()
448
- .split(/\W+/)
449
- .filter(token => token.length > 1);
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
- * 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);
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
- return results;
497
- }
498
-
499
- /**
500
- * Clear the index
501
- */
502
- clear() {
503
- this.index.clear();
504
- this.events.clear();
505
- }
506
- }
499
+ /**
500
+ * Clear the index
501
+ */
502
+ clear() {
503
+ this.index.clear();
504
+ this.events.clear();
505
+ }
506
+ }