@memberjunction/react-runtime 5.40.2 → 5.42.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.
@@ -9,8 +9,9 @@ export interface CacheOptions {
9
9
  defaultTTL?: number;
10
10
  cleanupInterval?: number;
11
11
  }
12
- export declare class CacheManager<T = any> {
12
+ export declare class CacheManager<T = unknown> {
13
13
  private cache;
14
+ private timers;
14
15
  private memoryUsage;
15
16
  private cleanupTimer?;
16
17
  private readonly options;
@@ -1 +1 @@
1
- {"version":3,"file":"cache-manager.d.ts","sourceRoot":"","sources":["../../src/utilities/cache-manager.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,KAAK,EAAE,CAAC,CAAC;IACT,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAMD,qBAAa,YAAY,CAAC,CAAC,GAAG,GAAG;IAC/B,OAAO,CAAC,KAAK,CAAoC;IACjD,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,YAAY,CAAC,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;gBAErC,OAAO,GAAE,YAAiB;IAgBtC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI;IAqC9C,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAkB/B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAezB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAY5B,KAAK,IAAI,IAAI;IAQb,QAAQ,IAAI;QACV,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;KACnB;IAYD,OAAO,IAAI,MAAM;IAiBjB,OAAO,IAAI,IAAI;IAQf,OAAO,CAAC,SAAS;IASjB,OAAO,CAAC,QAAQ;IAmBhB,OAAO,CAAC,aAAa;IAerB,OAAO,CAAC,YAAY;IAkBpB,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,gBAAgB;CAMzB"}
1
+ {"version":3,"file":"cache-manager.d.ts","sourceRoot":"","sources":["../../src/utilities/cache-manager.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,KAAK,EAAE,CAAC,CAAC;IACT,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAMD,qBAAa,YAAY,CAAC,CAAC,GAAG,OAAO;IACnC,OAAO,CAAC,KAAK,CAAoC;IACjD,OAAO,CAAC,MAAM,CAAoD;IAClE,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,YAAY,CAAC,CAAiC;IACtD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyB;gBAErC,OAAO,GAAE,YAAiB;IAgBtC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI;IAiD9C,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAkB/B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAezB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAiB5B,KAAK,IAAI,IAAI;IAYb,QAAQ,IAAI;QACV,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;KACnB;IAYD,OAAO,IAAI,MAAM;IAiBjB,OAAO,IAAI,IAAI;IAQf,OAAO,CAAC,SAAS;IASjB,OAAO,CAAC,QAAQ;IAmBhB,OAAO,CAAC,aAAa;IAerB,OAAO,CAAC,YAAY;IAkBpB,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,gBAAgB;CAMzB"}
@@ -1,6 +1,7 @@
1
1
  export class CacheManager {
2
2
  constructor(options = {}) {
3
3
  this.cache = new Map();
4
+ this.timers = new Map();
4
5
  this.memoryUsage = 0;
5
6
  this.options = {
6
7
  maxSize: options.maxSize || 1000,
@@ -29,11 +30,20 @@ export class CacheManager {
29
30
  if (oldEntry) {
30
31
  this.memoryUsage -= oldEntry.size || 0;
31
32
  }
33
+ const oldTimer = this.timers.get(key);
34
+ if (oldTimer !== undefined) {
35
+ clearTimeout(oldTimer);
36
+ this.timers.delete(key);
37
+ }
32
38
  this.cache.set(key, entry);
33
39
  this.memoryUsage += size;
34
40
  if (ttl || this.options.defaultTTL) {
35
41
  const timeout = ttl || this.options.defaultTTL;
36
- setTimeout(() => this.delete(key), timeout);
42
+ const timerId = setTimeout(() => {
43
+ this.timers.delete(key);
44
+ this.delete(key);
45
+ }, timeout);
46
+ this.timers.set(key, timerId);
37
47
  }
38
48
  }
39
49
  get(key) {
@@ -58,6 +68,11 @@ export class CacheManager {
58
68
  return true;
59
69
  }
60
70
  delete(key) {
71
+ const timer = this.timers.get(key);
72
+ if (timer !== undefined) {
73
+ clearTimeout(timer);
74
+ this.timers.delete(key);
75
+ }
61
76
  const entry = this.cache.get(key);
62
77
  if (entry) {
63
78
  this.memoryUsage -= entry.size || 0;
@@ -66,6 +81,10 @@ export class CacheManager {
66
81
  return false;
67
82
  }
68
83
  clear() {
84
+ for (const timer of this.timers.values()) {
85
+ clearTimeout(timer);
86
+ }
87
+ this.timers.clear();
69
88
  this.cache.clear();
70
89
  this.memoryUsage = 0;
71
90
  }
@@ -139,13 +158,13 @@ export class CacheManager {
139
158
  }
140
159
  }
141
160
  startCleanupTimer() {
142
- this.cleanupTimer = window.setInterval(() => {
161
+ this.cleanupTimer = setInterval(() => {
143
162
  this.cleanup();
144
163
  }, this.options.cleanupInterval);
145
164
  }
146
165
  stopCleanupTimer() {
147
166
  if (this.cleanupTimer) {
148
- window.clearInterval(this.cleanupTimer);
167
+ clearInterval(this.cleanupTimer);
149
168
  this.cleanupTimer = undefined;
150
169
  }
151
170
  }
@@ -1 +1 @@
1
- {"version":3,"file":"cache-manager.js","sourceRoot":"","sources":["../../src/utilities/cache-manager.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview Cache management with TTL and size limits\n * @module @memberjunction/react-runtime/utilities\n */\n\nexport interface CacheEntry<T> {\n value: T;\n timestamp: number;\n size?: number;\n}\n\nexport interface CacheOptions {\n maxSize?: number; // Maximum number of entries\n maxMemory?: number; // Maximum memory in bytes (estimated)\n defaultTTL?: number; // Default TTL in milliseconds\n cleanupInterval?: number; // Cleanup interval in milliseconds\n}\n\n/**\n * Cache manager with TTL and size limits.\n * Provides automatic cleanup and memory management.\n */\nexport class CacheManager<T = any> {\n private cache = new Map<string, CacheEntry<T>>();\n private memoryUsage = 0;\n private cleanupTimer?: number;\n private readonly options: Required<CacheOptions>;\n\n constructor(options: CacheOptions = {}) {\n this.options = {\n maxSize: options.maxSize || 1000,\n maxMemory: options.maxMemory || 50 * 1024 * 1024, // 50MB default\n defaultTTL: options.defaultTTL || 5 * 60 * 1000, // 5 minutes default\n cleanupInterval: options.cleanupInterval || 60 * 1000 // 1 minute default\n };\n\n if (this.options.cleanupInterval > 0) {\n this.startCleanupTimer();\n }\n }\n\n /**\n * Set a value in the cache\n */\n set(key: string, value: T, ttl?: number): void {\n const size = this.estimateSize(value);\n const entry: CacheEntry<T> = {\n value,\n timestamp: Date.now(),\n size\n };\n\n // Check if we need to evict entries\n if (this.cache.size >= this.options.maxSize) {\n this.evictLRU();\n }\n\n // Check memory usage\n if (this.memoryUsage + size > this.options.maxMemory) {\n this.evictByMemory(size);\n }\n\n // Remove old entry if exists\n const oldEntry = this.cache.get(key);\n if (oldEntry) {\n this.memoryUsage -= oldEntry.size || 0;\n }\n\n this.cache.set(key, entry);\n this.memoryUsage += size;\n\n // Schedule removal if TTL is set\n if (ttl || this.options.defaultTTL) {\n const timeout = ttl || this.options.defaultTTL;\n setTimeout(() => this.delete(key), timeout);\n }\n }\n\n /**\n * Get a value from the cache\n */\n get(key: string): T | undefined {\n const entry = this.cache.get(key);\n if (!entry) return undefined;\n\n // Check if expired\n if (this.isExpired(entry)) {\n this.delete(key);\n return undefined;\n }\n\n // Update timestamp for LRU\n entry.timestamp = Date.now();\n return entry.value;\n }\n\n /**\n * Check if a key exists and is not expired\n */\n has(key: string): boolean {\n const entry = this.cache.get(key);\n if (!entry) return false;\n\n if (this.isExpired(entry)) {\n this.delete(key);\n return false;\n }\n\n return true;\n }\n\n /**\n * Delete a key from the cache\n */\n delete(key: string): boolean {\n const entry = this.cache.get(key);\n if (entry) {\n this.memoryUsage -= entry.size || 0;\n return this.cache.delete(key);\n }\n return false;\n }\n\n /**\n * Clear all entries\n */\n clear(): void {\n this.cache.clear();\n this.memoryUsage = 0;\n }\n\n /**\n * Get cache statistics\n */\n getStats(): {\n size: number;\n memoryUsage: number;\n maxSize: number;\n maxMemory: number;\n } {\n return {\n size: this.cache.size,\n memoryUsage: this.memoryUsage,\n maxSize: this.options.maxSize,\n maxMemory: this.options.maxMemory\n };\n }\n\n /**\n * Manually trigger cleanup\n */\n cleanup(): number {\n let removed = 0;\n const now = Date.now();\n\n for (const [key, entry] of this.cache) {\n if (this.isExpired(entry, now)) {\n this.delete(key);\n removed++;\n }\n }\n\n return removed;\n }\n\n /**\n * Destroy the cache and stop cleanup timer\n */\n destroy(): void {\n this.stopCleanupTimer();\n this.clear();\n }\n\n /**\n * Check if an entry is expired\n */\n private isExpired(entry: CacheEntry<T>, now?: number): boolean {\n if (!this.options.defaultTTL) return false;\n const currentTime = now || Date.now();\n return currentTime - entry.timestamp > this.options.defaultTTL;\n }\n\n /**\n * Evict least recently used entry\n */\n private evictLRU(): void {\n let lruKey: string | undefined;\n let lruTime = Infinity;\n\n for (const [key, entry] of this.cache) {\n if (entry.timestamp < lruTime) {\n lruTime = entry.timestamp;\n lruKey = key;\n }\n }\n\n if (lruKey) {\n this.delete(lruKey);\n }\n }\n\n /**\n * Evict entries to make room for new memory\n */\n private evictByMemory(requiredSize: number): void {\n const entries = Array.from(this.cache.entries())\n .sort((a, b) => a[1].timestamp - b[1].timestamp);\n\n let freedMemory = 0;\n for (const [key, entry] of entries) {\n if (freedMemory >= requiredSize) break;\n freedMemory += entry.size || 0;\n this.delete(key);\n }\n }\n\n /**\n * Estimate size of a value\n */\n private estimateSize(value: T): number {\n if (typeof value === 'string') {\n return value.length * 2; // 2 bytes per character\n } else if (typeof value === 'object' && value !== null) {\n // Rough estimation for objects\n try {\n return JSON.stringify(value).length * 2;\n } catch {\n return 1024; // Default 1KB for objects that can't be stringified\n }\n } else {\n return 8; // Default for primitives\n }\n }\n\n /**\n * Start the cleanup timer\n */\n private startCleanupTimer(): void {\n this.cleanupTimer = window.setInterval(() => {\n this.cleanup();\n }, this.options.cleanupInterval);\n }\n\n /**\n * Stop the cleanup timer\n */\n private stopCleanupTimer(): void {\n if (this.cleanupTimer) {\n window.clearInterval(this.cleanupTimer);\n this.cleanupTimer = undefined;\n }\n }\n}"]}
1
+ {"version":3,"file":"cache-manager.js","sourceRoot":"","sources":["../../src/utilities/cache-manager.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview Cache management with TTL and size limits\n * @module @memberjunction/react-runtime/utilities\n */\n\nexport interface CacheEntry<T> {\n value: T;\n timestamp: number;\n size?: number;\n}\n\nexport interface CacheOptions {\n maxSize?: number; // Maximum number of entries\n maxMemory?: number; // Maximum memory in bytes (estimated)\n defaultTTL?: number; // Default TTL in milliseconds\n cleanupInterval?: number; // Cleanup interval in milliseconds\n}\n\n/**\n * Cache manager with TTL and size limits.\n * Provides automatic cleanup and memory management.\n */\nexport class CacheManager<T = unknown> {\n private cache = new Map<string, CacheEntry<T>>();\n private timers = new Map<string, ReturnType<typeof setTimeout>>();\n private memoryUsage = 0;\n private cleanupTimer?: ReturnType<typeof setInterval>;\n private readonly options: Required<CacheOptions>;\n\n constructor(options: CacheOptions = {}) {\n this.options = {\n maxSize: options.maxSize || 1000,\n maxMemory: options.maxMemory || 50 * 1024 * 1024, // 50MB default\n defaultTTL: options.defaultTTL || 5 * 60 * 1000, // 5 minutes default\n cleanupInterval: options.cleanupInterval || 60 * 1000 // 1 minute default\n };\n\n if (this.options.cleanupInterval > 0) {\n this.startCleanupTimer();\n }\n }\n\n /**\n * Set a value in the cache\n */\n set(key: string, value: T, ttl?: number): void {\n const size = this.estimateSize(value);\n const entry: CacheEntry<T> = {\n value,\n timestamp: Date.now(),\n size\n };\n\n // Check if we need to evict entries\n if (this.cache.size >= this.options.maxSize) {\n this.evictLRU();\n }\n\n // Check memory usage\n if (this.memoryUsage + size > this.options.maxMemory) {\n this.evictByMemory(size);\n }\n\n // Remove old entry if exists\n const oldEntry = this.cache.get(key);\n if (oldEntry) {\n this.memoryUsage -= oldEntry.size || 0;\n }\n\n // Cancel any existing TTL timer for this key so overwriting an entry doesn't\n // leave a dangling timer that deletes the new value when it fires.\n const oldTimer = this.timers.get(key);\n if (oldTimer !== undefined) {\n clearTimeout(oldTimer);\n this.timers.delete(key);\n }\n\n this.cache.set(key, entry);\n this.memoryUsage += size;\n\n // Schedule removal if TTL is set\n if (ttl || this.options.defaultTTL) {\n const timeout = ttl || this.options.defaultTTL;\n const timerId = setTimeout(() => {\n this.timers.delete(key);\n this.delete(key);\n }, timeout);\n this.timers.set(key, timerId);\n }\n }\n\n /**\n * Get a value from the cache\n */\n get(key: string): T | undefined {\n const entry = this.cache.get(key);\n if (!entry) return undefined;\n\n // Check if expired\n if (this.isExpired(entry)) {\n this.delete(key);\n return undefined;\n }\n\n // Update timestamp for LRU\n entry.timestamp = Date.now();\n return entry.value;\n }\n\n /**\n * Check if a key exists and is not expired\n */\n has(key: string): boolean {\n const entry = this.cache.get(key);\n if (!entry) return false;\n\n if (this.isExpired(entry)) {\n this.delete(key);\n return false;\n }\n\n return true;\n }\n\n /**\n * Delete a key from the cache\n */\n delete(key: string): boolean {\n const timer = this.timers.get(key);\n if (timer !== undefined) {\n clearTimeout(timer);\n this.timers.delete(key);\n }\n const entry = this.cache.get(key);\n if (entry) {\n this.memoryUsage -= entry.size || 0;\n return this.cache.delete(key);\n }\n return false;\n }\n\n /**\n * Clear all entries\n */\n clear(): void {\n for (const timer of this.timers.values()) {\n clearTimeout(timer);\n }\n this.timers.clear();\n this.cache.clear();\n this.memoryUsage = 0;\n }\n\n /**\n * Get cache statistics\n */\n getStats(): {\n size: number;\n memoryUsage: number;\n maxSize: number;\n maxMemory: number;\n } {\n return {\n size: this.cache.size,\n memoryUsage: this.memoryUsage,\n maxSize: this.options.maxSize,\n maxMemory: this.options.maxMemory\n };\n }\n\n /**\n * Manually trigger cleanup\n */\n cleanup(): number {\n let removed = 0;\n const now = Date.now();\n\n for (const [key, entry] of this.cache) {\n if (this.isExpired(entry, now)) {\n this.delete(key);\n removed++;\n }\n }\n\n return removed;\n }\n\n /**\n * Destroy the cache, cancel all entry timers, and stop the cleanup timer.\n */\n destroy(): void {\n this.stopCleanupTimer();\n this.clear();\n }\n\n /**\n * Check if an entry is expired\n */\n private isExpired(entry: CacheEntry<T>, now?: number): boolean {\n if (!this.options.defaultTTL) return false;\n const currentTime = now || Date.now();\n return currentTime - entry.timestamp > this.options.defaultTTL;\n }\n\n /**\n * Evict least recently used entry\n */\n private evictLRU(): void {\n let lruKey: string | undefined;\n let lruTime = Infinity;\n\n for (const [key, entry] of this.cache) {\n if (entry.timestamp < lruTime) {\n lruTime = entry.timestamp;\n lruKey = key;\n }\n }\n\n if (lruKey) {\n this.delete(lruKey);\n }\n }\n\n /**\n * Evict entries to make room for new memory\n */\n private evictByMemory(requiredSize: number): void {\n const entries = Array.from(this.cache.entries())\n .sort((a, b) => a[1].timestamp - b[1].timestamp);\n\n let freedMemory = 0;\n for (const [key, entry] of entries) {\n if (freedMemory >= requiredSize) break;\n freedMemory += entry.size || 0;\n this.delete(key);\n }\n }\n\n /**\n * Estimate size of a value\n */\n private estimateSize(value: T): number {\n if (typeof value === 'string') {\n return value.length * 2; // 2 bytes per character\n } else if (typeof value === 'object' && value !== null) {\n // Rough estimation for objects\n try {\n return JSON.stringify(value).length * 2;\n } catch {\n return 1024; // Default 1KB for objects that can't be stringified\n }\n } else {\n return 8; // Default for primitives\n }\n }\n\n /**\n * Start the cleanup timer\n */\n private startCleanupTimer(): void {\n this.cleanupTimer = setInterval(() => {\n this.cleanup();\n }, this.options.cleanupInterval);\n }\n\n /**\n * Stop the cleanup timer\n */\n private stopCleanupTimer(): void {\n if (this.cleanupTimer) {\n clearInterval(this.cleanupTimer);\n this.cleanupTimer = undefined;\n }\n }\n}"]}
@@ -0,0 +1,7 @@
1
+ export declare const USER_STATE_KEY_PREFIX = "InteractiveComponents_UserState_Root/";
2
+ export declare function resolveUserStateScope(explicitScope: string | undefined | null, namespace: string | undefined | null, name: string | undefined | null): string | null;
3
+ export declare function userStateStorageKey(scope: string | null): string | null;
4
+ export declare function parseStoredUserSettings(raw: string | undefined | null): Record<string, unknown>;
5
+ export declare function mergeUserSettings(hostDefaults: Record<string, unknown> | undefined | null, stored: Record<string, unknown> | undefined | null): Record<string, unknown>;
6
+ export declare function applyUserSettingsUpdate(current: Record<string, unknown> | undefined | null, incoming: Record<string, unknown> | undefined | null): Record<string, unknown>;
7
+ //# sourceMappingURL=user-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-state.d.ts","sourceRoot":"","sources":["../../src/utilities/user-state.ts"],"names":[],"mappings":"AAoBA,eAAO,MAAM,qBAAqB,0CAA0C,CAAC;AAiB7E,wBAAgB,qBAAqB,CACnC,aAAa,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EACxC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EACpC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAC9B,MAAM,GAAG,IAAI,CAYf;AAMD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAEvE;AAOD,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAa/F;AAOD,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG,IAAI,EACxD,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG,IAAI,GACjD,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEzB;AAsBD,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG,IAAI,EACnD,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG,IAAI,GACnD,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAUzB"}
@@ -0,0 +1,48 @@
1
+ export const USER_STATE_KEY_PREFIX = 'InteractiveComponents_UserState_Root/';
2
+ export function resolveUserStateScope(explicitScope, namespace, name) {
3
+ const explicit = explicitScope?.trim();
4
+ if (explicit) {
5
+ return explicit.toLowerCase();
6
+ }
7
+ const cleanName = name?.trim();
8
+ if (!cleanName) {
9
+ return null;
10
+ }
11
+ const cleanNamespace = namespace?.trim();
12
+ const scope = cleanNamespace ? `${cleanNamespace}/${cleanName}` : cleanName;
13
+ return scope.toLowerCase();
14
+ }
15
+ export function userStateStorageKey(scope) {
16
+ return scope ? `${USER_STATE_KEY_PREFIX}${scope}` : null;
17
+ }
18
+ export function parseStoredUserSettings(raw) {
19
+ if (!raw) {
20
+ return {};
21
+ }
22
+ try {
23
+ const parsed = JSON.parse(raw);
24
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
25
+ return parsed;
26
+ }
27
+ return {};
28
+ }
29
+ catch {
30
+ return {};
31
+ }
32
+ }
33
+ export function mergeUserSettings(hostDefaults, stored) {
34
+ return { ...(hostDefaults ?? {}), ...(stored ?? {}) };
35
+ }
36
+ export function applyUserSettingsUpdate(current, incoming) {
37
+ const next = { ...(current ?? {}) };
38
+ for (const [key, value] of Object.entries(incoming ?? {})) {
39
+ if (value === null || value === undefined) {
40
+ delete next[key];
41
+ }
42
+ else {
43
+ next[key] = value;
44
+ }
45
+ }
46
+ return next;
47
+ }
48
+ //# sourceMappingURL=user-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-state.js","sourceRoot":"","sources":["../../src/utilities/user-state.ts"],"names":[],"mappings":"AAoBA,MAAM,CAAC,MAAM,qBAAqB,GAAG,uCAAuC,CAAC;AAiB7E,MAAM,UAAU,qBAAqB,CACnC,aAAwC,EACxC,SAAoC,EACpC,IAA+B;IAE/B,MAAM,QAAQ,GAAG,aAAa,EAAE,IAAI,EAAE,CAAC;IACvC,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,QAAQ,CAAC,WAAW,EAAE,CAAC;IAChC,CAAC;IACD,MAAM,SAAS,GAAG,IAAI,EAAE,IAAI,EAAE,CAAC;IAC/B,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,cAAc,GAAG,SAAS,EAAE,IAAI,EAAE,CAAC;IACzC,MAAM,KAAK,GAAG,cAAc,CAAC,CAAC,CAAC,GAAG,cAAc,IAAI,SAAS,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IAC5E,OAAO,KAAK,CAAC,WAAW,EAAE,CAAC;AAC7B,CAAC;AAMD,MAAM,UAAU,mBAAmB,CAAC,KAAoB;IACtD,OAAO,KAAK,CAAC,CAAC,CAAC,GAAG,qBAAqB,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAC3D,CAAC;AAOD,MAAM,UAAU,uBAAuB,CAAC,GAA8B;IACpE,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YACnE,OAAO,MAAiC,CAAC;QAC3C,CAAC;QACD,OAAO,EAAE,CAAC;IACZ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAOD,MAAM,UAAU,iBAAiB,CAC/B,YAAwD,EACxD,MAAkD;IAElD,OAAO,EAAE,GAAG,CAAC,YAAY,IAAI,EAAE,CAAC,EAAE,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,CAAC;AACxD,CAAC;AAsBD,MAAM,UAAU,uBAAuB,CACrC,OAAmD,EACnD,QAAoD;IAEpD,MAAM,IAAI,GAA4B,EAAE,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC,EAAE,CAAC;IAC7D,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC,EAAE,CAAC;QAC1D,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YAC1C,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;QACnB,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;QACpB,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["/**\n * @fileoverview Helpers for the per-user interactive-component settings contract\n * (`savedUserSettings` in / `onSaveUserSettings` out).\n *\n * These helpers are intentionally **pure and framework-agnostic** so the Angular\n * host bridge and the Node test harness share one implementation and the scoping /\n * serialization logic can be unit tested in isolation. The host wires these to a\n * durable per-user store (e.g. `UserInfoEngine`); the runtime itself never persists.\n *\n * @module @memberjunction/react-runtime\n */\n\n/**\n * Prefix applied to every interactive-component user-settings storage key. The\n * full key is `InteractiveComponents_UserState_Root/<scope>`, where `<scope>` is\n * resolved by {@link resolveUserStateScope}. It is deliberately long and unique\n * to avoid collisions with other namespaced keys in the shared\n * `MJ: User Settings` keyspace. Keeping the prefix in one place also avoids\n * stringly-typed drift between the seed (read) and persist (write) paths.\n */\nexport const USER_STATE_KEY_PREFIX = 'InteractiveComponents_UserState_Root/';\n\n/**\n * Resolve the stable per-component scope used to namespace a component's\n * persisted user settings.\n *\n * An explicit, host-supplied scope always wins (used when a host needs to\n * differentiate two instances of the same component spec — e.g. scope a form's\n * settings by the entity it edits). Otherwise the scope is derived from the\n * component spec's `namespace` + `name`.\n *\n * The result is lowercased to avoid case-variant duplicate rows in the settings\n * store (per MJ's user-settings key convention).\n *\n * @returns the resolved scope, or `null` when no stable scope can be derived\n * (e.g. an unnamed component) — signaling the caller to skip persistence.\n */\nexport function resolveUserStateScope(\n explicitScope: string | undefined | null,\n namespace: string | undefined | null,\n name: string | undefined | null\n): string | null {\n const explicit = explicitScope?.trim();\n if (explicit) {\n return explicit.toLowerCase();\n }\n const cleanName = name?.trim();\n if (!cleanName) {\n return null;\n }\n const cleanNamespace = namespace?.trim();\n const scope = cleanNamespace ? `${cleanNamespace}/${cleanName}` : cleanName;\n return scope.toLowerCase();\n}\n\n/**\n * Build the full storage key for a resolved scope, or `null` when the scope is\n * `null` (persistence should be skipped).\n */\nexport function userStateStorageKey(scope: string | null): string | null {\n return scope ? `${USER_STATE_KEY_PREFIX}${scope}` : null;\n}\n\n/**\n * Safely parse a stored settings blob into a plain object. Returns `{}` for\n * null/empty input, invalid JSON, or any non-object JSON (arrays, primitives) —\n * persisted user settings are always a flat key/value object.\n */\nexport function parseStoredUserSettings(raw: string | undefined | null): Record<string, unknown> {\n if (!raw) {\n return {};\n }\n try {\n const parsed = JSON.parse(raw);\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>;\n }\n return {};\n } catch {\n return {};\n }\n}\n\n/**\n * Merge host-provided defaults with the stored per-user settings. Stored values\n * win, so a returning user sees their saved preferences while a host can still\n * seed sensible first-run defaults that fill any gaps.\n */\nexport function mergeUserSettings(\n hostDefaults: Record<string, unknown> | undefined | null,\n stored: Record<string, unknown> | undefined | null\n): Record<string, unknown> {\n return { ...(hostDefaults ?? {}), ...(stored ?? {}) };\n}\n\n/**\n * Apply an `onSaveUserSettings` payload to the host's current settings snapshot.\n *\n * **Merge, not replace.** The contract asks components to pass the complete\n * settings object, but the host must be resilient to a component (especially an\n * AI-generated one) passing only the changed keys — and to the stale-prop case\n * where a component spreads the mount-time `savedUserSettings` prop, which the\n * host deliberately never refreshes mid-session (no re-render on save). Under\n * full-replace semantics either slip silently wipes every other saved\n * preference; under merge the worst case is a no-op.\n *\n * **Removing a key requires explicit intent**: set its value to `null` and the\n * key is deleted from the snapshot (reads fall back to defaults via the\n * documented `savedUserSettings?.key ?? fallback` pattern). `undefined` values\n * are treated the same way, since `JSON.stringify` would drop them from the\n * persisted blob anyway and the in-memory snapshot must stay consistent with\n * what is stored.\n *\n * @returns a new object — neither input is mutated.\n */\nexport function applyUserSettingsUpdate(\n current: Record<string, unknown> | undefined | null,\n incoming: Record<string, unknown> | undefined | null\n): Record<string, unknown> {\n const next: Record<string, unknown> = { ...(current ?? {}) };\n for (const [key, value] of Object.entries(incoming ?? {})) {\n if (value === null || value === undefined) {\n delete next[key];\n } else {\n next[key] = value;\n }\n }\n return next;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memberjunction/react-runtime",
3
- "version": "5.40.2",
3
+ "version": "5.42.0",
4
4
  "description": "Platform-agnostic React component runtime for MemberJunction. Provides core compilation, registry, and execution capabilities for React components in any JavaScript environment.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -29,11 +29,11 @@
29
29
  },
30
30
  "homepage": "https://github.com/MemberJunction/MJ#readme",
31
31
  "dependencies": {
32
- "@memberjunction/core": "5.40.2",
33
- "@memberjunction/global": "5.40.2",
34
- "@memberjunction/interactive-component-types": "5.40.2",
35
- "@memberjunction/core-entities": "5.40.2",
36
- "@memberjunction/graphql-dataprovider": "5.40.2",
32
+ "@memberjunction/core": "5.42.0",
33
+ "@memberjunction/global": "5.42.0",
34
+ "@memberjunction/interactive-component-types": "5.42.0",
35
+ "@memberjunction/core-entities": "5.42.0",
36
+ "@memberjunction/graphql-dataprovider": "5.42.0",
37
37
  "@babel/standalone": "^7.29.1",
38
38
  "rxjs": "^7.8.2"
39
39
  },
@@ -159,4 +159,68 @@ describe('CacheManager', () => {
159
159
  expect(cache.getStats().size).toBe(0);
160
160
  });
161
161
  });
162
+
163
+ describe('timer cancellation on overwrite', () => {
164
+ it('should cancel the old TTL timer when a key is overwritten', () => {
165
+ // Store a value with a short TTL so its timer fires at +5s
166
+ cache.set('key1', 'originalValue', 5000);
167
+
168
+ // Advance 3 seconds — timer not yet fired
169
+ vi.advanceTimersByTime(3000);
170
+ expect(cache.get('key1')).toBe('originalValue');
171
+
172
+ // Overwrite the key with a fresh 10s TTL
173
+ cache.set('key1', 'updatedValue', 10000);
174
+
175
+ // Advance another 3 seconds — original timer would have fired at +5s (2s into overwrite)
176
+ // but since it was cancelled, the key should still be present
177
+ vi.advanceTimersByTime(3000);
178
+ expect(cache.get('key1')).toBe('updatedValue');
179
+ });
180
+
181
+ it('should not delete new value when the old TTL would have expired', () => {
182
+ cache.set('key1', 'first', 2000); // expires in 2s
183
+
184
+ vi.advanceTimersByTime(1000); // advance 1s
185
+
186
+ cache.set('key1', 'second', 10000); // overwrite with 10s TTL
187
+
188
+ // Advance 2 more seconds — original timer (now cancelled) would have fired here
189
+ vi.advanceTimersByTime(2000);
190
+
191
+ // Value should still be 'second', not deleted by the cancelled original timer
192
+ expect(cache.get('key1')).toBe('second');
193
+ });
194
+ });
195
+
196
+ describe('timer cancellation on delete', () => {
197
+ it('should cancel TTL timer when entry is explicitly deleted', () => {
198
+ cache.set('key1', 'value1', 5000);
199
+ cache.delete('key1');
200
+
201
+ // Advance past original TTL — timer fires should be no-ops
202
+ vi.advanceTimersByTime(6000);
203
+
204
+ // Nothing should blow up and the entry stays deleted
205
+ expect(cache.has('key1')).toBe(false);
206
+ expect(cache.getStats().size).toBe(0);
207
+ });
208
+ });
209
+
210
+ describe('timer cancellation on clear', () => {
211
+ it('should cancel all pending TTL timers when clear() is called', () => {
212
+ cache.set('key1', 'value1', 2000);
213
+ cache.set('key2', 'value2', 3000);
214
+ cache.set('key3', 'value3', 4000);
215
+
216
+ cache.clear();
217
+
218
+ // Advance past all TTLs — pending timer callbacks should be no-ops
219
+ vi.advanceTimersByTime(5000);
220
+
221
+ // Cache remains empty and memory usage is 0
222
+ expect(cache.getStats().size).toBe(0);
223
+ expect(cache.getStats().memoryUsage).toBe(0);
224
+ });
225
+ });
162
226
  });
@@ -0,0 +1,155 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ USER_STATE_KEY_PREFIX,
4
+ resolveUserStateScope,
5
+ userStateStorageKey,
6
+ parseStoredUserSettings,
7
+ mergeUserSettings,
8
+ applyUserSettingsUpdate
9
+ } from '../utilities/user-state';
10
+
11
+ describe('user-state helpers', () => {
12
+ describe('resolveUserStateScope', () => {
13
+ it('prefers an explicit scope over namespace/name', () => {
14
+ expect(resolveUserStateScope('Form/Contacts', 'crm', 'ContactForm')).toBe('form/contacts');
15
+ });
16
+
17
+ it('lowercases the explicit scope', () => {
18
+ expect(resolveUserStateScope('MyDashboard', null, null)).toBe('mydashboard');
19
+ });
20
+
21
+ it('trims surrounding whitespace from the explicit scope', () => {
22
+ expect(resolveUserStateScope(' Spaced ', null, null)).toBe('spaced');
23
+ });
24
+
25
+ it('combines namespace and name when no explicit scope is given', () => {
26
+ expect(resolveUserStateScope(undefined, 'CRM/Analytics', 'AccountDashboard'))
27
+ .toBe('crm/analytics/accountdashboard');
28
+ });
29
+
30
+ it('uses just the name when there is no namespace', () => {
31
+ expect(resolveUserStateScope(undefined, undefined, 'SalesReport')).toBe('salesreport');
32
+ expect(resolveUserStateScope(null, ' ', 'SalesReport')).toBe('salesreport');
33
+ });
34
+
35
+ it('returns null when no stable scope can be derived', () => {
36
+ expect(resolveUserStateScope(undefined, undefined, undefined)).toBeNull();
37
+ expect(resolveUserStateScope('', '', '')).toBeNull();
38
+ expect(resolveUserStateScope(' ', 'crm', ' ')).toBeNull();
39
+ });
40
+ });
41
+
42
+ describe('userStateStorageKey', () => {
43
+ it('prefixes a resolved scope with the unique root namespace', () => {
44
+ expect(userStateStorageKey('form/contacts')).toBe(`${USER_STATE_KEY_PREFIX}form/contacts`);
45
+ expect(userStateStorageKey('form/contacts')).toBe('InteractiveComponents_UserState_Root/form/contacts');
46
+ });
47
+
48
+ it('returns null for a null scope', () => {
49
+ expect(userStateStorageKey(null)).toBeNull();
50
+ });
51
+ });
52
+
53
+ describe('parseStoredUserSettings', () => {
54
+ it('parses a valid JSON object', () => {
55
+ expect(parseStoredUserSettings('{"sortBy":"Name","page":2}')).toEqual({ sortBy: 'Name', page: 2 });
56
+ });
57
+
58
+ it('returns an empty object for null/undefined/empty input', () => {
59
+ expect(parseStoredUserSettings(null)).toEqual({});
60
+ expect(parseStoredUserSettings(undefined)).toEqual({});
61
+ expect(parseStoredUserSettings('')).toEqual({});
62
+ });
63
+
64
+ it('returns an empty object for invalid JSON instead of throwing', () => {
65
+ expect(parseStoredUserSettings('{not valid json')).toEqual({});
66
+ });
67
+
68
+ it('returns an empty object for non-object JSON (arrays, primitives)', () => {
69
+ expect(parseStoredUserSettings('[1,2,3]')).toEqual({});
70
+ expect(parseStoredUserSettings('42')).toEqual({});
71
+ expect(parseStoredUserSettings('"hello"')).toEqual({});
72
+ expect(parseStoredUserSettings('null')).toEqual({});
73
+ });
74
+ });
75
+
76
+ describe('mergeUserSettings', () => {
77
+ it('lets stored values win over host defaults', () => {
78
+ const merged = mergeUserSettings({ sortBy: 'CloseDate', viewMode: 'grid' }, { sortBy: 'Name' });
79
+ expect(merged).toEqual({ sortBy: 'Name', viewMode: 'grid' });
80
+ });
81
+
82
+ it('keeps host defaults that the stored settings do not override', () => {
83
+ expect(mergeUserSettings({ a: 1, b: 2 }, { b: 3 })).toEqual({ a: 1, b: 3 });
84
+ });
85
+
86
+ it('handles null/undefined on either side', () => {
87
+ expect(mergeUserSettings(null, { x: 1 })).toEqual({ x: 1 });
88
+ expect(mergeUserSettings({ x: 1 }, null)).toEqual({ x: 1 });
89
+ expect(mergeUserSettings(null, null)).toEqual({});
90
+ expect(mergeUserSettings(undefined, undefined)).toEqual({});
91
+ });
92
+
93
+ it('returns a new object (does not mutate inputs)', () => {
94
+ const defaults = { a: 1 };
95
+ const stored = { b: 2 };
96
+ const merged = mergeUserSettings(defaults, stored);
97
+ expect(merged).not.toBe(defaults);
98
+ expect(merged).not.toBe(stored);
99
+ expect(defaults).toEqual({ a: 1 });
100
+ expect(stored).toEqual({ b: 2 });
101
+ });
102
+ });
103
+
104
+ describe('applyUserSettingsUpdate', () => {
105
+ it('overlays incoming keys onto the current snapshot', () => {
106
+ expect(applyUserSettingsUpdate({ viewMode: 'grid', sortBy: 'CloseDate' }, { sortBy: 'Name' }))
107
+ .toEqual({ viewMode: 'grid', sortBy: 'Name' });
108
+ });
109
+
110
+ it('preserves untouched keys when a component passes only a delta', () => {
111
+ // The AI-forgot-the-spread case: a delta must not wipe other preferences.
112
+ expect(applyUserSettingsUpdate({ viewMode: 'grid', page: 3 }, { sortBy: 'Name' }))
113
+ .toEqual({ viewMode: 'grid', page: 3, sortBy: 'Name' });
114
+ });
115
+
116
+ it('survives the stale-prop spread (two sequential saves keep both changes)', () => {
117
+ // Component spreads the mount-time prop both times; the host snapshot
118
+ // must still accumulate both changes.
119
+ const mountProp = { viewMode: 'grid' };
120
+ let snapshot = applyUserSettingsUpdate(mountProp, { ...mountProp, sortBy: 'Name' });
121
+ snapshot = applyUserSettingsUpdate(snapshot, { ...mountProp, viewMode: 'list' });
122
+ expect(snapshot).toEqual({ viewMode: 'list', sortBy: 'Name' });
123
+ });
124
+
125
+ it('removes a key when its incoming value is explicitly null', () => {
126
+ expect(applyUserSettingsUpdate({ sortBy: 'Name', viewMode: 'grid' }, { sortBy: null }))
127
+ .toEqual({ viewMode: 'grid' });
128
+ });
129
+
130
+ it('removes a key when its incoming value is undefined', () => {
131
+ expect(applyUserSettingsUpdate({ sortBy: 'Name', viewMode: 'grid' }, { sortBy: undefined }))
132
+ .toEqual({ viewMode: 'grid' });
133
+ });
134
+
135
+ it('treats a full-object payload as a superset (replace-equivalent)', () => {
136
+ expect(applyUserSettingsUpdate({ a: 1, b: 2 }, { a: 10, b: 20 })).toEqual({ a: 10, b: 20 });
137
+ });
138
+
139
+ it('handles null/undefined on either side', () => {
140
+ expect(applyUserSettingsUpdate(null, { x: 1 })).toEqual({ x: 1 });
141
+ expect(applyUserSettingsUpdate({ x: 1 }, null)).toEqual({ x: 1 });
142
+ expect(applyUserSettingsUpdate(null, null)).toEqual({});
143
+ });
144
+
145
+ it('returns a new object (does not mutate inputs)', () => {
146
+ const current = { a: 1 };
147
+ const incoming = { b: 2 };
148
+ const next = applyUserSettingsUpdate(current, incoming);
149
+ expect(next).not.toBe(current);
150
+ expect(next).not.toBe(incoming);
151
+ expect(current).toEqual({ a: 1 });
152
+ expect(incoming).toEqual({ b: 2 });
153
+ });
154
+ });
155
+ });
package/src/index.ts CHANGED
@@ -140,6 +140,15 @@ export {
140
140
  unwrapAllComponents
141
141
  } from './utilities/component-unwrapper';
142
142
 
143
+ export {
144
+ USER_STATE_KEY_PREFIX,
145
+ resolveUserStateScope,
146
+ userStateStorageKey,
147
+ parseStoredUserSettings,
148
+ mergeUserSettings,
149
+ applyUserSettingsUpdate
150
+ } from './utilities/user-state';
151
+
143
152
  // Version information
144
153
  export const VERSION = '2.69.1';
145
154
 
@@ -20,10 +20,11 @@ export interface CacheOptions {
20
20
  * Cache manager with TTL and size limits.
21
21
  * Provides automatic cleanup and memory management.
22
22
  */
23
- export class CacheManager<T = any> {
23
+ export class CacheManager<T = unknown> {
24
24
  private cache = new Map<string, CacheEntry<T>>();
25
+ private timers = new Map<string, ReturnType<typeof setTimeout>>();
25
26
  private memoryUsage = 0;
26
- private cleanupTimer?: number;
27
+ private cleanupTimer?: ReturnType<typeof setInterval>;
27
28
  private readonly options: Required<CacheOptions>;
28
29
 
29
30
  constructor(options: CacheOptions = {}) {
@@ -66,13 +67,25 @@ export class CacheManager<T = any> {
66
67
  this.memoryUsage -= oldEntry.size || 0;
67
68
  }
68
69
 
70
+ // Cancel any existing TTL timer for this key so overwriting an entry doesn't
71
+ // leave a dangling timer that deletes the new value when it fires.
72
+ const oldTimer = this.timers.get(key);
73
+ if (oldTimer !== undefined) {
74
+ clearTimeout(oldTimer);
75
+ this.timers.delete(key);
76
+ }
77
+
69
78
  this.cache.set(key, entry);
70
79
  this.memoryUsage += size;
71
80
 
72
81
  // Schedule removal if TTL is set
73
82
  if (ttl || this.options.defaultTTL) {
74
83
  const timeout = ttl || this.options.defaultTTL;
75
- setTimeout(() => this.delete(key), timeout);
84
+ const timerId = setTimeout(() => {
85
+ this.timers.delete(key);
86
+ this.delete(key);
87
+ }, timeout);
88
+ this.timers.set(key, timerId);
76
89
  }
77
90
  }
78
91
 
@@ -113,6 +126,11 @@ export class CacheManager<T = any> {
113
126
  * Delete a key from the cache
114
127
  */
115
128
  delete(key: string): boolean {
129
+ const timer = this.timers.get(key);
130
+ if (timer !== undefined) {
131
+ clearTimeout(timer);
132
+ this.timers.delete(key);
133
+ }
116
134
  const entry = this.cache.get(key);
117
135
  if (entry) {
118
136
  this.memoryUsage -= entry.size || 0;
@@ -125,6 +143,10 @@ export class CacheManager<T = any> {
125
143
  * Clear all entries
126
144
  */
127
145
  clear(): void {
146
+ for (const timer of this.timers.values()) {
147
+ clearTimeout(timer);
148
+ }
149
+ this.timers.clear();
128
150
  this.cache.clear();
129
151
  this.memoryUsage = 0;
130
152
  }
@@ -164,7 +186,7 @@ export class CacheManager<T = any> {
164
186
  }
165
187
 
166
188
  /**
167
- * Destroy the cache and stop cleanup timer
189
+ * Destroy the cache, cancel all entry timers, and stop the cleanup timer.
168
190
  */
169
191
  destroy(): void {
170
192
  this.stopCleanupTimer();
@@ -236,7 +258,7 @@ export class CacheManager<T = any> {
236
258
  * Start the cleanup timer
237
259
  */
238
260
  private startCleanupTimer(): void {
239
- this.cleanupTimer = window.setInterval(() => {
261
+ this.cleanupTimer = setInterval(() => {
240
262
  this.cleanup();
241
263
  }, this.options.cleanupInterval);
242
264
  }
@@ -246,7 +268,7 @@ export class CacheManager<T = any> {
246
268
  */
247
269
  private stopCleanupTimer(): void {
248
270
  if (this.cleanupTimer) {
249
- window.clearInterval(this.cleanupTimer);
271
+ clearInterval(this.cleanupTimer);
250
272
  this.cleanupTimer = undefined;
251
273
  }
252
274
  }