@memberjunction/core 2.127.0 → 2.129.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.
Files changed (58) hide show
  1. package/dist/__tests__/mocks/TestMetadataProvider.d.ts +4 -2
  2. package/dist/__tests__/mocks/TestMetadataProvider.d.ts.map +1 -1
  3. package/dist/__tests__/mocks/TestMetadataProvider.js +9 -3
  4. package/dist/__tests__/mocks/TestMetadataProvider.js.map +1 -1
  5. package/dist/generic/RegisterForStartup.d.ts +228 -0
  6. package/dist/generic/RegisterForStartup.d.ts.map +1 -0
  7. package/dist/generic/RegisterForStartup.js +233 -0
  8. package/dist/generic/RegisterForStartup.js.map +1 -0
  9. package/dist/generic/baseEngine.d.ts +191 -8
  10. package/dist/generic/baseEngine.d.ts.map +1 -1
  11. package/dist/generic/baseEngine.js +360 -14
  12. package/dist/generic/baseEngine.js.map +1 -1
  13. package/dist/generic/baseEngineRegistry.d.ts +247 -0
  14. package/dist/generic/baseEngineRegistry.d.ts.map +1 -0
  15. package/dist/generic/baseEngineRegistry.js +470 -0
  16. package/dist/generic/baseEngineRegistry.js.map +1 -0
  17. package/dist/generic/entityInfo.d.ts +50 -0
  18. package/dist/generic/entityInfo.d.ts.map +1 -1
  19. package/dist/generic/entityInfo.js +56 -0
  20. package/dist/generic/entityInfo.js.map +1 -1
  21. package/dist/generic/graphqlTypeNames.d.ts +90 -0
  22. package/dist/generic/graphqlTypeNames.d.ts.map +1 -0
  23. package/dist/generic/graphqlTypeNames.js +119 -0
  24. package/dist/generic/graphqlTypeNames.js.map +1 -0
  25. package/dist/generic/interfaces.d.ts +234 -3
  26. package/dist/generic/interfaces.d.ts.map +1 -1
  27. package/dist/generic/interfaces.js.map +1 -1
  28. package/dist/generic/localCacheManager.d.ts +388 -0
  29. package/dist/generic/localCacheManager.d.ts.map +1 -0
  30. package/dist/generic/localCacheManager.js +856 -0
  31. package/dist/generic/localCacheManager.js.map +1 -0
  32. package/dist/generic/providerBase.d.ts +227 -13
  33. package/dist/generic/providerBase.d.ts.map +1 -1
  34. package/dist/generic/providerBase.js +751 -6
  35. package/dist/generic/providerBase.js.map +1 -1
  36. package/dist/generic/queryInfo.d.ts +18 -0
  37. package/dist/generic/queryInfo.d.ts.map +1 -1
  38. package/dist/generic/queryInfo.js +18 -0
  39. package/dist/generic/queryInfo.js.map +1 -1
  40. package/dist/generic/queryInfoInterfaces.d.ts +17 -0
  41. package/dist/generic/queryInfoInterfaces.d.ts.map +1 -1
  42. package/dist/generic/runQuery.d.ts +30 -0
  43. package/dist/generic/runQuery.d.ts.map +1 -1
  44. package/dist/generic/runQuery.js +13 -0
  45. package/dist/generic/runQuery.js.map +1 -1
  46. package/dist/generic/telemetryManager.d.ts +628 -0
  47. package/dist/generic/telemetryManager.d.ts.map +1 -0
  48. package/dist/generic/telemetryManager.js +1011 -0
  49. package/dist/generic/telemetryManager.js.map +1 -0
  50. package/dist/index.d.ts +5 -0
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +5 -0
  53. package/dist/index.js.map +1 -1
  54. package/dist/views/runView.d.ts +25 -0
  55. package/dist/views/runView.d.ts.map +1 -1
  56. package/dist/views/runView.js +4 -5
  57. package/dist/views/runView.js.map +1 -1
  58. package/package.json +2 -2
@@ -0,0 +1,856 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LocalCacheManager = exports.CacheCategory = void 0;
4
+ const global_1 = require("@memberjunction/global");
5
+ const logging_1 = require("./logging");
6
+ // ============================================================================
7
+ // DEFAULT CONFIGURATION
8
+ // ============================================================================
9
+ const DEFAULT_CONFIG = {
10
+ enabled: true,
11
+ maxSizeBytes: 50 * 1024 * 1024, // 50MB
12
+ maxEntries: 1000,
13
+ defaultTTLMs: 5 * 60 * 1000, // 5 minutes
14
+ evictionPolicy: 'lru'
15
+ };
16
+ // ============================================================================
17
+ // STORAGE CATEGORIES
18
+ // ============================================================================
19
+ /**
20
+ * Storage categories for organizing cache data.
21
+ * These map to IndexedDB object stores or localStorage key prefixes.
22
+ */
23
+ exports.CacheCategory = {
24
+ /** Cache for RunView results */
25
+ RunViewCache: 'RunViewCache',
26
+ /** Cache for RunQuery results */
27
+ RunQueryCache: 'RunQueryCache',
28
+ /** Cache for Dataset results */
29
+ DatasetCache: 'DatasetCache',
30
+ /** Cache for metadata */
31
+ Metadata: 'Metadata',
32
+ /** Default category for uncategorized data */
33
+ Default: 'default'
34
+ };
35
+ // ============================================================================
36
+ // LOCAL CACHE MANAGER
37
+ // ============================================================================
38
+ /**
39
+ * LocalCacheManager is a singleton that provides a unified caching abstraction
40
+ * for datasets, RunView results, and RunQuery results. It wraps ILocalStorageProvider
41
+ * for actual storage and maintains an internal registry of all cached items.
42
+ *
43
+ * Key features:
44
+ * - Typed methods for datasets, RunViews, and RunQueries
45
+ * - Automatic cache metadata tracking (timestamps, access counts, sizes)
46
+ * - Hit/miss statistics for performance monitoring
47
+ * - Eviction policies (LRU, LFU, FIFO) for memory management
48
+ * - Dashboard-friendly registry queries
49
+ *
50
+ * Usage:
51
+ * ```typescript
52
+ * // Initialize during app startup
53
+ * await LocalCacheManager.Instance.Initialize(storageProvider);
54
+ *
55
+ * // Cache a dataset
56
+ * await LocalCacheManager.Instance.SetDataset('MyDataset', filters, dataset, keyPrefix);
57
+ *
58
+ * // Retrieve cached data
59
+ * const cached = await LocalCacheManager.Instance.GetDataset('MyDataset', filters, keyPrefix);
60
+ * ```
61
+ */
62
+ class LocalCacheManager extends global_1.BaseSingleton {
63
+ /**
64
+ * Returns the singleton instance of LocalCacheManager
65
+ */
66
+ static get Instance() {
67
+ return super.getInstance();
68
+ }
69
+ constructor() {
70
+ super();
71
+ this._storageProvider = null;
72
+ this._registry = new Map();
73
+ this._initialized = false;
74
+ this._initializePromise = null;
75
+ this._stats = { hits: 0, misses: 0 };
76
+ this._config = { ...DEFAULT_CONFIG };
77
+ this.REGISTRY_KEY = '__MJ_CACHE_REGISTRY__';
78
+ this._persistTimeout = null;
79
+ }
80
+ // ========================================================================
81
+ // INITIALIZATION
82
+ // ========================================================================
83
+ /**
84
+ * Initialize the cache manager with a storage provider.
85
+ * This should be called during app startup after the storage provider is available.
86
+ *
87
+ * This method is safe to call multiple times - subsequent calls will return the same
88
+ * promise as the first caller, ensuring initialization only happens once.
89
+ *
90
+ * @param storageProvider - The local storage provider to use for persistence
91
+ * @param config - Optional configuration overrides
92
+ * @returns A promise that resolves when initialization is complete
93
+ */
94
+ Initialize(storageProvider, config) {
95
+ // If already initialized, return immediately
96
+ if (this._initialized) {
97
+ return Promise.resolve();
98
+ }
99
+ // If initialization is in progress, return the existing promise
100
+ // so all callers await the same initialization
101
+ if (this._initializePromise) {
102
+ return this._initializePromise;
103
+ }
104
+ // First caller - start initialization and store the promise
105
+ this._initializePromise = this.doInitialize(storageProvider, config);
106
+ return this._initializePromise;
107
+ }
108
+ /**
109
+ * Internal initialization logic - only called once by the first caller
110
+ */
111
+ async doInitialize(storageProvider, config) {
112
+ this._storageProvider = storageProvider;
113
+ if (config) {
114
+ this._config = { ...this._config, ...config };
115
+ }
116
+ await this.loadRegistry();
117
+ this._initialized = true;
118
+ }
119
+ /**
120
+ * Returns whether the cache manager has been initialized
121
+ */
122
+ get IsInitialized() {
123
+ return this._initialized;
124
+ }
125
+ /**
126
+ * Returns the current configuration
127
+ */
128
+ get Config() {
129
+ return { ...this._config };
130
+ }
131
+ /**
132
+ * Updates the configuration at runtime
133
+ */
134
+ UpdateConfig(config) {
135
+ this._config = { ...this._config, ...config };
136
+ }
137
+ // ========================================================================
138
+ // DATASET CACHING
139
+ // ========================================================================
140
+ /**
141
+ * Stores a dataset in the local cache.
142
+ *
143
+ * @param name - The dataset name
144
+ * @param itemFilters - Optional filters applied to the dataset
145
+ * @param dataset - The dataset result to cache
146
+ * @param keyPrefix - Prefix for the cache key (typically includes connection info)
147
+ */
148
+ async SetDataset(name, itemFilters, dataset, keyPrefix) {
149
+ if (!this._storageProvider || !this._config.enabled)
150
+ return;
151
+ const key = this.buildDatasetKey(name, itemFilters, keyPrefix);
152
+ const value = JSON.stringify(dataset);
153
+ const sizeBytes = this.estimateSize(value);
154
+ // Check if we need to evict entries
155
+ await this.evictIfNeeded(sizeBytes);
156
+ try {
157
+ await this._storageProvider.SetItem(key, value, exports.CacheCategory.DatasetCache);
158
+ await this._storageProvider.SetItem(key + '_date', dataset.LatestUpdateDate.toISOString(), exports.CacheCategory.DatasetCache);
159
+ this.registerEntry({
160
+ key,
161
+ type: 'dataset',
162
+ name,
163
+ params: itemFilters ? { itemFilters } : undefined,
164
+ cachedAt: Date.now(),
165
+ lastAccessedAt: Date.now(),
166
+ accessCount: 1,
167
+ sizeBytes,
168
+ maxUpdatedAt: dataset.LatestUpdateDate.toISOString()
169
+ });
170
+ }
171
+ catch (e) {
172
+ (0, logging_1.LogError)(`LocalCacheManager.SetDataset failed: ${e}`);
173
+ }
174
+ }
175
+ /**
176
+ * Retrieves a cached dataset.
177
+ *
178
+ * @param name - The dataset name
179
+ * @param itemFilters - Optional filters applied to the dataset
180
+ * @param keyPrefix - Prefix for the cache key
181
+ * @returns The cached dataset or null if not found
182
+ */
183
+ async GetDataset(name, itemFilters, keyPrefix) {
184
+ if (!this._storageProvider || !this._config.enabled)
185
+ return null;
186
+ const key = this.buildDatasetKey(name, itemFilters, keyPrefix);
187
+ try {
188
+ const value = await this._storageProvider.GetItem(key, exports.CacheCategory.DatasetCache);
189
+ if (value) {
190
+ this.recordAccess(key);
191
+ this._stats.hits++;
192
+ return JSON.parse(value);
193
+ }
194
+ }
195
+ catch (e) {
196
+ (0, logging_1.LogError)(`LocalCacheManager.GetDataset failed: ${e}`);
197
+ }
198
+ this._stats.misses++;
199
+ return null;
200
+ }
201
+ /**
202
+ * Gets the timestamp of a cached dataset.
203
+ *
204
+ * @param name - The dataset name
205
+ * @param itemFilters - Optional filters applied to the dataset
206
+ * @param keyPrefix - Prefix for the cache key
207
+ * @returns The cache timestamp or null if not found
208
+ */
209
+ async GetDatasetTimestamp(name, itemFilters, keyPrefix) {
210
+ if (!this._storageProvider)
211
+ return null;
212
+ const key = this.buildDatasetKey(name, itemFilters, keyPrefix);
213
+ try {
214
+ const dateStr = await this._storageProvider.GetItem(key + '_date', exports.CacheCategory.DatasetCache);
215
+ return dateStr ? new Date(dateStr) : null;
216
+ }
217
+ catch (e) {
218
+ return null;
219
+ }
220
+ }
221
+ /**
222
+ * Clears a cached dataset.
223
+ *
224
+ * @param name - The dataset name
225
+ * @param itemFilters - Optional filters applied to the dataset
226
+ * @param keyPrefix - Prefix for the cache key
227
+ */
228
+ async ClearDataset(name, itemFilters, keyPrefix) {
229
+ if (!this._storageProvider)
230
+ return;
231
+ const key = this.buildDatasetKey(name, itemFilters, keyPrefix);
232
+ try {
233
+ await this._storageProvider.Remove(key, exports.CacheCategory.DatasetCache);
234
+ await this._storageProvider.Remove(key + '_date', exports.CacheCategory.DatasetCache);
235
+ this.unregisterEntry(key);
236
+ }
237
+ catch (e) {
238
+ (0, logging_1.LogError)(`LocalCacheManager.ClearDataset failed: ${e}`);
239
+ }
240
+ }
241
+ /**
242
+ * Checks if a dataset is cached.
243
+ *
244
+ * @param name - The dataset name
245
+ * @param itemFilters - Optional filters applied to the dataset
246
+ * @param keyPrefix - Prefix for the cache key
247
+ * @returns True if the dataset is cached
248
+ */
249
+ async IsDatasetCached(name, itemFilters, keyPrefix) {
250
+ if (!this._storageProvider)
251
+ return false;
252
+ const key = this.buildDatasetKey(name, itemFilters, keyPrefix);
253
+ try {
254
+ const val = await this._storageProvider.GetItem(key, exports.CacheCategory.DatasetCache);
255
+ return val != null;
256
+ }
257
+ catch (e) {
258
+ return false;
259
+ }
260
+ }
261
+ // ========================================================================
262
+ // RUNVIEW CACHING
263
+ // ========================================================================
264
+ /**
265
+ * Generates a human-readable cache fingerprint for a RunView request.
266
+ * This fingerprint uniquely identifies the query based on its parameters and connection.
267
+ *
268
+ * Format: EntityName|filter|orderBy|resultType|maxRows|startRow|connection
269
+ * Example: Users|Active=1|Name ASC|simple|100|0|localhost
270
+ *
271
+ * @param params - The RunView parameters
272
+ * @param connectionPrefix - Prefix identifying the connection (e.g., server URL) to differentiate caches across connections
273
+ * @returns A unique, human-readable fingerprint string
274
+ */
275
+ GenerateRunViewFingerprint(params, connectionPrefix) {
276
+ const entity = params.EntityName?.trim() || 'Unknown';
277
+ const filter = (params.ExtraFilter || '').trim();
278
+ const orderBy = (params.OrderBy || '').trim();
279
+ const resultType = params.ResultType || 'simple';
280
+ const maxRows = params.MaxRows ?? -1;
281
+ const startRow = params.StartRow ?? 0;
282
+ const connection = connectionPrefix || '';
283
+ // Build human-readable fingerprint with pipe separators
284
+ // Format: Entity|Filter|OrderBy|ResultType|MaxRows|StartRow|Connection
285
+ const parts = [
286
+ entity,
287
+ filter || '_', // Use underscore for empty filter
288
+ orderBy || '_', // Use underscore for empty orderBy
289
+ resultType,
290
+ maxRows.toString(),
291
+ startRow.toString()
292
+ ];
293
+ // Only include connection if provided
294
+ if (connection) {
295
+ parts.push(connection);
296
+ }
297
+ return parts.join('|');
298
+ }
299
+ /**
300
+ * Stores a RunView result in the cache.
301
+ *
302
+ * @param fingerprint - The cache fingerprint (from GenerateRunViewFingerprint)
303
+ * @param params - The original RunView parameters
304
+ * @param results - The results to cache
305
+ * @param maxUpdatedAt - The latest __mj_UpdatedAt from the results
306
+ * @param rowCount - Optional row count (defaults to results.length if not provided)
307
+ */
308
+ async SetRunViewResult(fingerprint, params, results, maxUpdatedAt, rowCount) {
309
+ if (!this._storageProvider || !this._config.enabled)
310
+ return;
311
+ const actualRowCount = rowCount ?? results.length;
312
+ const value = JSON.stringify({ results, maxUpdatedAt, rowCount: actualRowCount });
313
+ const sizeBytes = this.estimateSize(value);
314
+ // Check if we need to evict entries
315
+ await this.evictIfNeeded(sizeBytes);
316
+ try {
317
+ await this._storageProvider.SetItem(fingerprint, value, exports.CacheCategory.RunViewCache);
318
+ this.registerEntry({
319
+ key: fingerprint,
320
+ type: 'runview',
321
+ name: params.EntityName || 'Unknown',
322
+ fingerprint,
323
+ params: {
324
+ EntityName: params.EntityName,
325
+ ExtraFilter: params.ExtraFilter,
326
+ OrderBy: params.OrderBy,
327
+ ResultType: params.ResultType,
328
+ MaxRows: params.MaxRows
329
+ },
330
+ cachedAt: Date.now(),
331
+ lastAccessedAt: Date.now(),
332
+ accessCount: 1,
333
+ sizeBytes,
334
+ maxUpdatedAt,
335
+ rowCount: actualRowCount
336
+ });
337
+ }
338
+ catch (e) {
339
+ (0, logging_1.LogError)(`LocalCacheManager.SetRunViewResult failed: ${e}`);
340
+ }
341
+ }
342
+ /**
343
+ * Retrieves a cached RunView result.
344
+ *
345
+ * @param fingerprint - The cache fingerprint
346
+ * @returns The cached results, maxUpdatedAt, and rowCount, or null if not found
347
+ */
348
+ async GetRunViewResult(fingerprint) {
349
+ if (!this._storageProvider || !this._config.enabled)
350
+ return null;
351
+ try {
352
+ const value = await this._storageProvider.GetItem(fingerprint, exports.CacheCategory.RunViewCache);
353
+ if (value) {
354
+ this.recordAccess(fingerprint);
355
+ this._stats.hits++;
356
+ const parsed = JSON.parse(value);
357
+ // Handle legacy entries that may not have rowCount
358
+ return {
359
+ results: parsed.results,
360
+ maxUpdatedAt: parsed.maxUpdatedAt,
361
+ rowCount: parsed.rowCount ?? parsed.results?.length ?? 0
362
+ };
363
+ }
364
+ }
365
+ catch (e) {
366
+ (0, logging_1.LogError)(`LocalCacheManager.GetRunViewResult failed: ${e}`);
367
+ }
368
+ this._stats.misses++;
369
+ return null;
370
+ }
371
+ /**
372
+ * Invalidates a cached RunView result.
373
+ *
374
+ * @param fingerprint - The cache fingerprint to invalidate
375
+ */
376
+ async InvalidateRunViewResult(fingerprint) {
377
+ if (!this._storageProvider)
378
+ return;
379
+ try {
380
+ await this._storageProvider.Remove(fingerprint, exports.CacheCategory.RunViewCache);
381
+ this.unregisterEntry(fingerprint);
382
+ }
383
+ catch (e) {
384
+ (0, logging_1.LogError)(`LocalCacheManager.InvalidateRunViewResult failed: ${e}`);
385
+ }
386
+ }
387
+ /**
388
+ * Invalidates all cached RunView results for a specific entity.
389
+ * Useful when an entity's data changes and all related caches should be cleared.
390
+ *
391
+ * @param entityName - The entity name to invalidate
392
+ */
393
+ async InvalidateEntityCaches(entityName) {
394
+ if (!this._storageProvider)
395
+ return;
396
+ const normalizedName = entityName.toLowerCase().trim();
397
+ const toRemove = [];
398
+ for (const [key, entry] of this._registry.entries()) {
399
+ if (entry.type === 'runview' && entry.name.toLowerCase().trim() === normalizedName) {
400
+ toRemove.push(key);
401
+ }
402
+ }
403
+ for (const key of toRemove) {
404
+ try {
405
+ await this._storageProvider.Remove(key, exports.CacheCategory.RunViewCache);
406
+ this._registry.delete(key);
407
+ }
408
+ catch (e) {
409
+ (0, logging_1.LogError)(`LocalCacheManager.InvalidateEntityCaches failed for key ${key}: ${e}`);
410
+ }
411
+ }
412
+ await this.persistRegistry();
413
+ }
414
+ // ========================================================================
415
+ // RUNQUERY CACHING
416
+ // ========================================================================
417
+ /**
418
+ * Generates a human-readable cache fingerprint for a RunQuery request.
419
+ *
420
+ * Format: QueryName|QueryID|params|connection
421
+ * Example: GetActiveUsers|abc123|{"status":"active"}|localhost
422
+ *
423
+ * @param queryId - The query ID
424
+ * @param queryName - The query name
425
+ * @param parameters - Optional query parameters
426
+ * @param connectionPrefix - Prefix identifying the connection (e.g., server URL) to differentiate caches across connections
427
+ * @returns A unique, human-readable fingerprint string
428
+ */
429
+ GenerateRunQueryFingerprint(queryId, queryName, parameters, connectionPrefix) {
430
+ const name = queryName?.trim() || 'Unknown';
431
+ const id = queryId || '_';
432
+ const params = parameters ? JSON.stringify(parameters) : '_';
433
+ const connection = connectionPrefix || '';
434
+ // Build human-readable fingerprint with pipe separators
435
+ // Format: QueryName|QueryID|Params|Connection
436
+ const parts = [name, id, params];
437
+ // Only include connection if provided
438
+ if (connection) {
439
+ parts.push(connection);
440
+ }
441
+ return parts.join('|');
442
+ }
443
+ /**
444
+ * Stores a RunQuery result in the cache.
445
+ *
446
+ * @param fingerprint - The cache fingerprint
447
+ * @param queryName - The query name for display
448
+ * @param results - The results to cache
449
+ * @param maxUpdatedAt - The latest update timestamp (for smart cache validation)
450
+ * @param rowCount - Optional row count (defaults to results.length if not provided)
451
+ * @param queryId - Optional query ID for reference
452
+ * @param ttlMs - Optional TTL in milliseconds (for cache expiry tracking)
453
+ */
454
+ async SetRunQueryResult(fingerprint, queryName, results, maxUpdatedAt, rowCount, queryId, ttlMs) {
455
+ if (!this._storageProvider || !this._config.enabled)
456
+ return;
457
+ const actualRowCount = rowCount ?? results.length;
458
+ const value = JSON.stringify({ results, maxUpdatedAt, rowCount: actualRowCount, queryId });
459
+ const sizeBytes = this.estimateSize(value);
460
+ // Check if we need to evict entries
461
+ await this.evictIfNeeded(sizeBytes);
462
+ const now = Date.now();
463
+ const expiresAt = ttlMs ? now + ttlMs : undefined;
464
+ try {
465
+ await this._storageProvider.SetItem(fingerprint, value, exports.CacheCategory.RunQueryCache);
466
+ this.registerEntry({
467
+ key: fingerprint,
468
+ type: 'runquery',
469
+ name: queryName,
470
+ fingerprint,
471
+ cachedAt: now,
472
+ lastAccessedAt: now,
473
+ accessCount: 1,
474
+ sizeBytes,
475
+ maxUpdatedAt,
476
+ rowCount: actualRowCount,
477
+ expiresAt
478
+ });
479
+ }
480
+ catch (e) {
481
+ (0, logging_1.LogError)(`LocalCacheManager.SetRunQueryResult failed: ${e}`);
482
+ }
483
+ }
484
+ /**
485
+ * Retrieves a cached RunQuery result.
486
+ *
487
+ * @param fingerprint - The cache fingerprint
488
+ * @returns The cached results, maxUpdatedAt, rowCount, and queryId, or null if not found
489
+ */
490
+ async GetRunQueryResult(fingerprint) {
491
+ if (!this._storageProvider || !this._config.enabled)
492
+ return null;
493
+ // Check if entry has expired
494
+ const entry = this._registry.get(fingerprint);
495
+ if (entry?.expiresAt && Date.now() > entry.expiresAt) {
496
+ // Entry has expired, invalidate it
497
+ await this.InvalidateRunQueryResult(fingerprint);
498
+ this._stats.misses++;
499
+ return null;
500
+ }
501
+ try {
502
+ const value = await this._storageProvider.GetItem(fingerprint, exports.CacheCategory.RunQueryCache);
503
+ if (value) {
504
+ this.recordAccess(fingerprint);
505
+ this._stats.hits++;
506
+ const parsed = JSON.parse(value);
507
+ // Handle legacy entries that may not have rowCount
508
+ return {
509
+ results: parsed.results,
510
+ maxUpdatedAt: parsed.maxUpdatedAt,
511
+ rowCount: parsed.rowCount ?? parsed.results?.length ?? 0,
512
+ queryId: parsed.queryId
513
+ };
514
+ }
515
+ }
516
+ catch (e) {
517
+ (0, logging_1.LogError)(`LocalCacheManager.GetRunQueryResult failed: ${e}`);
518
+ }
519
+ this._stats.misses++;
520
+ return null;
521
+ }
522
+ /**
523
+ * Invalidates a cached RunQuery result.
524
+ *
525
+ * @param fingerprint - The cache fingerprint to invalidate
526
+ */
527
+ async InvalidateRunQueryResult(fingerprint) {
528
+ if (!this._storageProvider)
529
+ return;
530
+ try {
531
+ await this._storageProvider.Remove(fingerprint, exports.CacheCategory.RunQueryCache);
532
+ this.unregisterEntry(fingerprint);
533
+ }
534
+ catch (e) {
535
+ (0, logging_1.LogError)(`LocalCacheManager.InvalidateRunQueryResult failed: ${e}`);
536
+ }
537
+ }
538
+ /**
539
+ * Invalidates all cached RunQuery results for a specific query.
540
+ * Useful when a query's underlying data changes and all related caches should be cleared.
541
+ *
542
+ * @param queryName - The query name to invalidate
543
+ */
544
+ async InvalidateQueryCaches(queryName) {
545
+ if (!this._storageProvider)
546
+ return;
547
+ const normalizedName = queryName.toLowerCase().trim();
548
+ const toRemove = [];
549
+ for (const [key, entry] of this._registry.entries()) {
550
+ if (entry.type === 'runquery' && entry.name.toLowerCase().trim() === normalizedName) {
551
+ toRemove.push(key);
552
+ }
553
+ }
554
+ for (const key of toRemove) {
555
+ try {
556
+ await this._storageProvider.Remove(key, exports.CacheCategory.RunQueryCache);
557
+ this._registry.delete(key);
558
+ }
559
+ catch (e) {
560
+ (0, logging_1.LogError)(`LocalCacheManager.InvalidateQueryCaches failed for key ${key}: ${e}`);
561
+ }
562
+ }
563
+ await this.persistRegistry();
564
+ }
565
+ /**
566
+ * Gets the cache status (fingerprint data) for a RunQuery result.
567
+ * Used for smart cache validation with the server.
568
+ *
569
+ * @param fingerprint - The cache fingerprint
570
+ * @returns The cache status with maxUpdatedAt and rowCount, or null if not found/expired
571
+ */
572
+ async GetRunQueryCacheStatus(fingerprint) {
573
+ const cached = await this.GetRunQueryResult(fingerprint);
574
+ if (!cached)
575
+ return null;
576
+ return {
577
+ maxUpdatedAt: cached.maxUpdatedAt,
578
+ rowCount: cached.rowCount
579
+ };
580
+ }
581
+ // ========================================================================
582
+ // REGISTRY QUERIES (FOR DASHBOARD)
583
+ // ========================================================================
584
+ /**
585
+ * Returns all cache entries for dashboard display.
586
+ */
587
+ GetAllEntries() {
588
+ return [...this._registry.values()];
589
+ }
590
+ /**
591
+ * Returns cache entries filtered by type.
592
+ *
593
+ * @param type - The cache entry type to filter by
594
+ */
595
+ GetEntriesByType(type) {
596
+ return this.GetAllEntries().filter(e => e.type === type);
597
+ }
598
+ /**
599
+ * Returns comprehensive cache statistics.
600
+ */
601
+ GetStats() {
602
+ const entries = this.GetAllEntries();
603
+ const byType = {
604
+ dataset: { count: 0, sizeBytes: 0 },
605
+ runview: { count: 0, sizeBytes: 0 },
606
+ runquery: { count: 0, sizeBytes: 0 }
607
+ };
608
+ for (const entry of entries) {
609
+ byType[entry.type].count++;
610
+ byType[entry.type].sizeBytes += entry.sizeBytes;
611
+ }
612
+ const timestamps = entries.map(e => e.cachedAt);
613
+ return {
614
+ totalEntries: entries.length,
615
+ totalSizeBytes: entries.reduce((sum, e) => sum + e.sizeBytes, 0),
616
+ byType,
617
+ oldestEntry: timestamps.length ? Math.min(...timestamps) : 0,
618
+ newestEntry: timestamps.length ? Math.max(...timestamps) : 0,
619
+ hits: this._stats.hits,
620
+ misses: this._stats.misses
621
+ };
622
+ }
623
+ /**
624
+ * Calculates the cache hit rate as a percentage.
625
+ */
626
+ GetHitRate() {
627
+ const total = this._stats.hits + this._stats.misses;
628
+ return total > 0 ? (this._stats.hits / total) * 100 : 0;
629
+ }
630
+ // ========================================================================
631
+ // BULK OPERATIONS
632
+ // ========================================================================
633
+ /**
634
+ * Clears all cache entries of a specific type.
635
+ *
636
+ * @param type - The cache entry type to clear
637
+ * @returns The number of entries cleared
638
+ */
639
+ async ClearByType(type) {
640
+ if (!this._storageProvider)
641
+ return 0;
642
+ const entries = this.GetEntriesByType(type);
643
+ const category = this.getCategoryForType(type);
644
+ for (const entry of entries) {
645
+ try {
646
+ await this._storageProvider.Remove(entry.key, category);
647
+ if (entry.type === 'dataset') {
648
+ await this._storageProvider.Remove(entry.key + '_date', category);
649
+ }
650
+ this._registry.delete(entry.key);
651
+ }
652
+ catch (e) {
653
+ (0, logging_1.LogError)(`LocalCacheManager.ClearByType failed for key ${entry.key}: ${e}`);
654
+ }
655
+ }
656
+ await this.persistRegistry();
657
+ return entries.length;
658
+ }
659
+ /**
660
+ * Clears all cache entries.
661
+ *
662
+ * @returns The number of entries cleared
663
+ */
664
+ async ClearAll() {
665
+ if (!this._storageProvider)
666
+ return 0;
667
+ const count = this._registry.size;
668
+ for (const entry of this._registry.values()) {
669
+ try {
670
+ const category = this.getCategoryForType(entry.type);
671
+ await this._storageProvider.Remove(entry.key, category);
672
+ if (entry.type === 'dataset') {
673
+ await this._storageProvider.Remove(entry.key + '_date', category);
674
+ }
675
+ }
676
+ catch (e) {
677
+ (0, logging_1.LogError)(`LocalCacheManager.ClearAll failed for key ${entry.key}: ${e}`);
678
+ }
679
+ }
680
+ this._registry.clear();
681
+ this._stats = { hits: 0, misses: 0 };
682
+ await this.persistRegistry();
683
+ return count;
684
+ }
685
+ /**
686
+ * Resets the hit/miss statistics.
687
+ */
688
+ ResetStats() {
689
+ this._stats = { hits: 0, misses: 0 };
690
+ }
691
+ // ========================================================================
692
+ // INTERNAL HELPERS
693
+ // ========================================================================
694
+ /**
695
+ * Maps a cache entry type to its storage category.
696
+ */
697
+ getCategoryForType(type) {
698
+ switch (type) {
699
+ case 'runview':
700
+ return exports.CacheCategory.RunViewCache;
701
+ case 'runquery':
702
+ return exports.CacheCategory.RunQueryCache;
703
+ case 'dataset':
704
+ return exports.CacheCategory.DatasetCache;
705
+ default:
706
+ return exports.CacheCategory.Default;
707
+ }
708
+ }
709
+ /**
710
+ * Builds a cache key for a dataset.
711
+ */
712
+ buildDatasetKey(name, itemFilters, keyPrefix) {
713
+ const filterKey = itemFilters
714
+ ? '{' + itemFilters.map(f => `"${f.ItemCode}":"${f.Filter}"`).join(',') + '}'
715
+ : '';
716
+ return keyPrefix + '__DATASET__' + name + filterKey;
717
+ }
718
+ /**
719
+ * Registers a cache entry in the registry.
720
+ */
721
+ registerEntry(entry) {
722
+ this._registry.set(entry.key, entry);
723
+ // Debounce registry persistence to avoid too many writes
724
+ this.debouncedPersistRegistry();
725
+ }
726
+ /**
727
+ * Unregisters a cache entry from the registry.
728
+ */
729
+ unregisterEntry(key) {
730
+ this._registry.delete(key);
731
+ this.debouncedPersistRegistry();
732
+ }
733
+ /**
734
+ * Records an access to a cache entry (updates lastAccessedAt and accessCount).
735
+ */
736
+ recordAccess(key) {
737
+ const entry = this._registry.get(key);
738
+ if (entry) {
739
+ entry.lastAccessedAt = Date.now();
740
+ entry.accessCount++;
741
+ // Don't persist on every access - too expensive
742
+ }
743
+ }
744
+ /**
745
+ * Loads the registry from storage.
746
+ */
747
+ async loadRegistry() {
748
+ if (!this._storageProvider)
749
+ return;
750
+ try {
751
+ const stored = await this._storageProvider.GetItem(this.REGISTRY_KEY, exports.CacheCategory.Metadata);
752
+ if (stored) {
753
+ const parsed = JSON.parse(stored);
754
+ this._registry = new Map(parsed.map(e => [e.key, e]));
755
+ }
756
+ }
757
+ catch (e) {
758
+ this._registry.clear();
759
+ }
760
+ }
761
+ /**
762
+ * Debounced registry persistence to avoid too many writes.
763
+ */
764
+ debouncedPersistRegistry() {
765
+ if (this._persistTimeout) {
766
+ clearTimeout(this._persistTimeout);
767
+ }
768
+ this._persistTimeout = setTimeout(() => {
769
+ this.persistRegistry();
770
+ }, 1000); // 1 second debounce
771
+ }
772
+ /**
773
+ * Persists the registry to storage.
774
+ */
775
+ async persistRegistry() {
776
+ if (!this._storageProvider)
777
+ return;
778
+ try {
779
+ const data = JSON.stringify(this.GetAllEntries());
780
+ await this._storageProvider.SetItem(this.REGISTRY_KEY, data, exports.CacheCategory.Metadata);
781
+ }
782
+ catch (e) {
783
+ // Ignore persistence errors - cache is still functional
784
+ }
785
+ }
786
+ /**
787
+ * Estimates the size of a string in bytes.
788
+ */
789
+ estimateSize(value) {
790
+ // Approximate size: UTF-16 strings are ~2 bytes per character
791
+ return value.length * 2;
792
+ }
793
+ /**
794
+ * Evicts entries if needed to make room for new data.
795
+ */
796
+ async evictIfNeeded(neededBytes) {
797
+ if (!this._storageProvider)
798
+ return;
799
+ const stats = this.GetStats();
800
+ const wouldExceedSize = (stats.totalSizeBytes + neededBytes) > this._config.maxSizeBytes;
801
+ const wouldExceedCount = stats.totalEntries >= this._config.maxEntries;
802
+ if (!wouldExceedSize && !wouldExceedCount)
803
+ return;
804
+ // Calculate how much to free
805
+ const targetFreeBytes = Math.max(neededBytes, this._config.maxSizeBytes * 0.1); // At least 10% of max
806
+ const targetFreeCount = Math.max(1, Math.floor(this._config.maxEntries * 0.1)); // At least 10% of max
807
+ await this.evict(targetFreeBytes, targetFreeCount);
808
+ }
809
+ /**
810
+ * Evicts entries based on the configured eviction policy.
811
+ */
812
+ async evict(targetBytes, targetCount) {
813
+ if (!this._storageProvider)
814
+ return;
815
+ const entries = this.GetAllEntries();
816
+ // Sort by eviction policy
817
+ switch (this._config.evictionPolicy) {
818
+ case 'lru':
819
+ entries.sort((a, b) => a.lastAccessedAt - b.lastAccessedAt);
820
+ break;
821
+ case 'lfu':
822
+ entries.sort((a, b) => a.accessCount - b.accessCount);
823
+ break;
824
+ case 'fifo':
825
+ entries.sort((a, b) => a.cachedAt - b.cachedAt);
826
+ break;
827
+ }
828
+ let freedBytes = 0;
829
+ let freedCount = 0;
830
+ const toDelete = [];
831
+ for (const entry of entries) {
832
+ if (freedBytes >= targetBytes && freedCount >= targetCount)
833
+ break;
834
+ toDelete.push(entry.key);
835
+ freedBytes += entry.sizeBytes;
836
+ freedCount++;
837
+ }
838
+ for (const key of toDelete) {
839
+ try {
840
+ const entry = this._registry.get(key);
841
+ const category = this.getCategoryForType(entry?.type);
842
+ await this._storageProvider.Remove(key, category);
843
+ if (entry?.type === 'dataset') {
844
+ await this._storageProvider.Remove(key + '_date', category);
845
+ }
846
+ this._registry.delete(key);
847
+ }
848
+ catch (e) {
849
+ // Continue evicting other entries
850
+ }
851
+ }
852
+ await this.persistRegistry();
853
+ }
854
+ }
855
+ exports.LocalCacheManager = LocalCacheManager;
856
+ //# sourceMappingURL=localCacheManager.js.map