UNPKG

@gsb-core/core

Version:

GSB core services and classes for platform-independent web applications

770 lines 34.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.GsbCacheService = void 0; const gsb_entity_service_1 = require("../entity/gsb-entity.service"); const query_params_1 = require("../../types/query-params"); const query_1 = require("../../types/query"); const gsb_config_1 = require("../../config/gsb-config"); const core_1 = require("@gsb-core/core"); class GsbCacheService { constructor() { this.entityDefCache = new Map(); this.entityCache = new Map(); this.enumCache = new Map(); this.propertyDefCache = new Map(); this.genericCache = new Map(); this.CACHE_DURATION = Number.MAX_SAFE_INTEGER; // Infinite cache duration this.enumCacheTimeout = 30 * 60 * 1000; // 30 minutes this.allDefinitionsCached = false; this.allDefinitionsCachedTimestamp = 0; this.acquireQueue = new Map(); this.dbPromise = null; this.DB_NAME = 'gsb-cache-db'; this.OBJECT_STORE_NAME = 'gsb-cache-store'; this.entityService = gsb_entity_service_1.GsbEntityService.getInstance(); this.baseUrl = (0, gsb_config_1.getGsbBaseDomain)(); // Initialize tenant code const currentTenant = (0, gsb_config_1.getGsbTenantCode)(); if (currentTenant) { (0, gsb_config_1.setGsbTenantCode)(currentTenant); } } static getInstance() { if (!GsbCacheService.instance) { GsbCacheService.instance = new GsbCacheService(); } return GsbCacheService.instance; } isIndexedDbSupported() { return typeof window !== 'undefined' && 'indexedDB' in window; } getDb() { if (!this.isIndexedDbSupported()) { return Promise.reject('IndexedDB is not supported in this environment.'); } if (this.dbPromise) { return this.dbPromise; } this.dbPromise = new Promise((resolve, reject) => { const openRequest = indexedDB.open(this.DB_NAME, 1); openRequest.onupgradeneeded = () => { if (!openRequest.result.objectStoreNames.contains(this.OBJECT_STORE_NAME)) { openRequest.result.createObjectStore(this.OBJECT_STORE_NAME); } }; openRequest.onsuccess = () => { resolve(openRequest.result); }; openRequest.onerror = () => { this.dbPromise = null; // Reset promise on error reject(openRequest.error); }; }); return this.dbPromise; } async getIndexedDb(key) { const db = await this.getDb(); return new Promise((resolve, reject) => { const transaction = db.transaction(this.OBJECT_STORE_NAME, 'readonly'); const store = transaction.objectStore(this.OBJECT_STORE_NAME); const request = store.get(key); request.onsuccess = () => { resolve(request.result || null); }; request.onerror = () => { reject(request.error); }; }); } async setIndexedDb(key, value) { const db = await this.getDb(); return new Promise((resolve, reject) => { const transaction = db.transaction(this.OBJECT_STORE_NAME, 'readwrite'); const store = transaction.objectStore(this.OBJECT_STORE_NAME); const entry = { data: value, timestamp: Date.now() }; const request = store.put(entry, key); request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); }); } async removeIndexedDb(key) { const db = await this.getDb(); return new Promise((resolve, reject) => { const transaction = db.transaction(this.OBJECT_STORE_NAME, 'readwrite'); const store = transaction.objectStore(this.OBJECT_STORE_NAME); const request = store.delete(key); request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); }); } async clearIndexedDb() { if (!this.isIndexedDbSupported()) { return; } try { const db = await this.getDb(); return new Promise((resolve, reject) => { const transaction = db.transaction(this.OBJECT_STORE_NAME, 'readwrite'); const store = transaction.objectStore(this.OBJECT_STORE_NAME); const request = store.clear(); request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); }); } catch (error) { console.error('Error clearing IndexedDB:', error); } } async getAuthParams() { const token = await (0, gsb_config_1.getGsbToken)(); const tenantCode = (0, gsb_config_1.getGsbTenantCode)(); // Get sessionId from sessionStorage if available let sessionId = undefined; if (typeof window !== 'undefined') { sessionId = sessionStorage.getItem('gsb_session_id'); // If no sessionId exists, create a new one if (!sessionId) { // Create a simple sessionId using timestamp and random number sessionId = `session-${Date.now()}-${Math.floor(Math.random() * 1000000)}`; sessionStorage.setItem('gsb_session_id', sessionId); } } return { token, tenantCode, sessionId }; } async acquire(key, fetchFn) { // Check if there's already a pending request if (this.acquireQueue.has(key)) { return this.acquireQueue.get(key); } // Create new promise for the fetch operation const promise = fetchFn(); this.acquireQueue.set(key, promise); try { const result = await promise; return result; } finally { // Clean up the queue this.acquireQueue.delete(key); } } async getPropertyDefs(token, tenantCode) { const cacheKey = 'propertyDefs'; // Check cache first if (this.propertyDefCache.size > 0) { return Array.from(this.propertyDefCache.values()).map(entry => entry.data); } return this.acquire(cacheKey, async () => { // Get auth params - use provided token and tenantCode if available, otherwise get from auth let authToken = token; let authTenantCode = tenantCode; if (!authToken || !authTenantCode) { const authParams = await this.getAuthParams(); authToken = authToken || authParams.token; authTenantCode = authTenantCode || authParams.tenantCode; } let queryParams = new query_params_1.EntityQueryParams('GsbPropertyDef'); queryParams.queryType = query_params_1.QueryType.FullNonPersonal; const propertyDefResponse = await this.entityService.query(queryParams, authToken, authTenantCode); if (!propertyDefResponse.entities) { throw new Error('Property definitions not found'); } for (const prop of propertyDefResponse.entities) { // Cache the property definition this.propertyDefCache.set(prop.id, { data: prop, timestamp: Date.now() }); // Also store in generic cache with localStorage persistence this.setCache(`propDef_${prop.id}`, prop, this.CACHE_DURATION, `gsb_propDef_${prop.id}`); } return propertyDefResponse.entities; }); } /** * Get an entity definition from the cache * @param def The entity definition to get from the cache * @param token Optional custom token to use for this request * @param tenantCode Optional custom tenant code to use for this request * @returns The entity definition */ async getEntityDef(def, token, tenantCode) { try { if (!def) { throw new Error('Entity definition name is required'); } // Determine cache key based on available identifier const cacheKey = def.id ? `entityDefById:${def.id}` : def.name ? `entityDefByName:${def.name}` : null; // Also create localStorage key const localStorageKey = def.id ? `gsb_entityDef_id_${def.id}` : def.name ? `gsb_entityDef_name_${def.name}` : null; if (!cacheKey) { throw new Error('Entity definition must have either id or name'); } // Check cache first using generic cache function if (localStorageKey) { const cachedDef = this.getCache(cacheKey, localStorageKey, this.CACHE_DURATION); if (cachedDef) { return cachedDef; } } // If not in generic cache, check old cache maps if (def.name) { const cached = this.entityDefCache.get(def.name); if (cached && Date.now() - cached.timestamp < this.CACHE_DURATION) { return cached.data; } } else if (def.id) { const entries = Array.from(this.entityDefCache.values()); const cached = entries.find(entry => entry.data.id === def.id); if (cached && Date.now() - cached.timestamp < this.CACHE_DURATION) { return cached.data; } } return this.acquire(cacheKey, async () => { // Get auth params - use provided token and tenantCode if available, otherwise get from auth let authToken = token; let authTenantCode = tenantCode; if (!authToken || !authTenantCode) { const authParams = await this.getAuthParams(); authToken = authToken || authParams.token; authTenantCode = authTenantCode || authParams.tenantCode; } // Fetch entity definition const entityDefResponse = await this.entityService.getDefinition({ entityDef: def }, authToken, authTenantCode); if (!(entityDefResponse === null || entityDefResponse === void 0 ? void 0 : entityDefResponse.entityDef)) { return null; } const entityDef = entityDefResponse.entityDef; // Fetch property definitions for each property if (entityDef.properties) { const allPropertyDefs = await this.getPropertyDefs(authToken, authTenantCode); for (let prop of entityDef.properties) { // Check cache first const cachedDef = allPropertyDefs.find(p => p.id === prop.definition_id); if (cachedDef) { prop.definition = cachedDef; } else { throw new Error(`Property definition not found for ${prop.name}`); } } } // Cache the updated entity definition if (entityDef === null || entityDef === void 0 ? void 0 : entityDef.name) { this.entityDefCache.set(entityDef.name, { data: entityDef, timestamp: Date.now() }); // Also store in generic cache with localStorage persistence if (entityDef.id) { this.setCache(`entityDefById:${entityDef.id}`, entityDef, this.CACHE_DURATION, `gsb_entityDef_id_${entityDef.id}`); } this.setCache(`entityDefByName:${entityDef.name}`, entityDef, this.CACHE_DURATION, `gsb_entityDef_name_${entityDef.name}`); } return entityDef; }); } catch (error) { console.error('Error fetching entity definition:', error); throw error; } } /** * Set an entity definition in the cache * @param newEntityDef The entity definition to set in the cache * @param arg1 Optional argument 1 * @param arg2 Optional argument 2 */ setEntityDef(newEntityDef, token, tenantCode) { if (newEntityDef.id) { this.entityDefCache.set(newEntityDef.id, { data: newEntityDef, timestamp: Date.now() }); } else if (newEntityDef.name) { this.entityDefCache.set(newEntityDef.name, { data: newEntityDef, timestamp: Date.now() }); } } removeEntityDef(id, token, tenantCode) { return this.entityDefCache.delete(id); } /** * Get an enum definition from the cache * @param id The id of the enum definition to get from the cache * @param token Optional custom token to use for this request * @param tenantCode Optional custom tenant code to use for this request * @returns The enum definition or null if not found */ async getEnum(id, token, tenantCode) { try { const cacheKey = `enum:${id}`; const localStorageKey = `gsb_enum_${id}`; // Check generic cache first const cachedEnum = this.getCache(cacheKey, localStorageKey, this.enumCacheTimeout); if (cachedEnum) { return cachedEnum; } // Check old cache map const cached = this.enumCache.get(id); if (cached && Date.now() - cached.timestamp < this.enumCacheTimeout) { return cached.data; } // Get auth params - use provided token and tenantCode if available, otherwise get from auth let authToken = token; let authTenantCode = tenantCode; if (!authToken || !authTenantCode) { const authParams = await this.getAuthParams(); authToken = authToken || authParams.token; authTenantCode = authTenantCode || authParams.tenantCode; } const queryParams = new query_params_1.EntityQueryParams('GsbEnum'); queryParams.queryType = query_params_1.QueryType.FullNonPersonal; queryParams.entityId = id; queryParams.include('values'); // Fetch enum definition const enumResponse = await this.entityService.get(queryParams, authToken, authTenantCode); if (!(enumResponse === null || enumResponse === void 0 ? void 0 : enumResponse.entity)) { return null; } const enumDef = enumResponse.entity; // Cache the enum definition in both caches this.enumCache.set(id, { data: enumDef, timestamp: Date.now() }); // Also store in generic cache with localStorage persistence this.setCache(cacheKey, enumDef, this.enumCacheTimeout, localStorageKey); return enumDef; } catch (error) { console.error('Error fetching enum definition:', error); return null; } } async getEnums(enumIds, token, tenantCode) { try { const result = new Map(); const uncachedIds = enumIds.filter(id => { // Check generic cache first const cacheKey = `enum:${id}`; const localStorageKey = `gsb_enum_${id}`; const cachedEnum = this.getCache(cacheKey, localStorageKey, this.enumCacheTimeout); if (cachedEnum) { result.set(id, cachedEnum); return false; } // Then check old cache map const cached = this.enumCache.get(id); if (cached && Date.now() - cached.timestamp < this.enumCacheTimeout) { result.set(id, cached.data); return false; } return true; }); if (uncachedIds.length > 0) { // Get auth params - use provided token and tenantCode if available, otherwise get from auth let authToken = token; let authTenantCode = tenantCode; if (!authToken || !authTenantCode) { const authParams = await this.getAuthParams(); authToken = authToken || authParams.token; authTenantCode = authTenantCode || authParams.tenantCode; } const queryParams = new query_params_1.EntityQueryParams('GsbEnum'); queryParams .filter('id', uncachedIds, query_1.QueryFunction.In) .include('values'); const response = await this.entityService.query(queryParams, authToken, authTenantCode); if (response === null || response === void 0 ? void 0 : response.entities) { for (const entity of response.entities) { const enumData = entity; if (enumData.id) { // Cache the result in both caches this.enumCache.set(enumData.id, { data: enumData, timestamp: Date.now() }); // Also store in generic cache with localStorage persistence const cacheKey = `enum:${enumData.id}`; const localStorageKey = `gsb_enum_${enumData.id}`; this.setCache(cacheKey, enumData, this.enumCacheTimeout, localStorageKey); result.set(enumData.id, enumData); } } } } return result; } catch (error) { console.error('Error fetching enum definitions:', error); return new Map(); } } /** * Store entity definitions in cache * @param entityDefs Array of entity definitions to store in cache * @param token Optional custom token to use for this request * @param tenantCode Optional custom tenant code to use for this request */ async storeAllEntityDefinitions(entityDefs, token, tenantCode) { const now = Date.now(); // Get property definitions for the entities const propertyDefs = await this.getPropertyDefs(token, tenantCode); for (const def of entityDefs) { if (def.name) { // Set property definitions for each property if (def.properties && def.properties.length > 0) { for (let prop of def.properties) { // Find and set the property definition const propDef = propertyDefs.find(p => p.id === prop.definition_id); if (propDef) { prop.definition = propDef; } } } // Store in cache this.entityDefCache.set(def.name, { data: def, timestamp: now }); } } this.allDefinitionsCached = true; this.allDefinitionsCachedTimestamp = now; } /** * Check if all entity definitions are cached and not expired * @returns true if all definitions are cached and not expired */ areAllDefinitionsCached() { return this.allDefinitionsCached && (Date.now() - this.allDefinitionsCachedTimestamp < this.CACHE_DURATION); } /** * Get all cached entity definitions * @returns Array of all cached entity definitions */ getAllCachedEntityDefinitions() { if (!this.areAllDefinitionsCached()) { return []; } return Array.from(this.entityDefCache.values()).map(entry => entry.data); } /** * Get a value from the generic cache with optional localStorage fallback * @param key The cache key * @param localStorageKey Optional localStorage key for persistence * @param duration Optional cache duration in milliseconds (defaults to CACHE_DURATION) * @returns The cached value or null if not found or expired */ getCache(key, localStorageKey, duration) { const cacheTime = duration || this.CACHE_DURATION; // Check memory cache first const cached = this.genericCache.get(key); if (cached && Date.now() - cached.timestamp < cacheTime) { return cached.data; } // If not in memory cache but localStorage key is provided, try there if (localStorageKey && typeof window !== 'undefined') { try { // Use the getLocalStorage function from gsb-config const storedValue = (0, gsb_config_1.getLocalStorage)(localStorageKey); if (storedValue) { try { // Try to parse it as JSON const parsedData = JSON.parse(storedValue); // Store back in memory cache and return the data this.genericCache.set(key, { data: parsedData, timestamp: Date.now() }); return parsedData; } catch (e) { // If we can't parse as JSON, use it as a primitive value this.genericCache.set(key, { data: storedValue, timestamp: Date.now() }); return storedValue; } } } catch (error) { console.error(`Error retrieving data from localStorage with key ${localStorageKey}:`, error); } } return null; } /** * Set a value in the generic cache with optional localStorage persistence * @param key The cache key * @param value The value to store * @param duration Optional cache duration in milliseconds (defaults to CACHE_DURATION) * @param localStorageKey Optional localStorage key for persistence * @returns The stored value */ setCache(key, value, duration, localStorageKey) { const cacheTime = duration || this.CACHE_DURATION; // Store in memory cache this.genericCache.set(key, { data: value, timestamp: Date.now() }); // If localStorage key is provided, also store there if (localStorageKey && typeof window !== 'undefined') { try { if (typeof value === 'string') { // Use the setLocalStorage function from gsb-config for string values (0, gsb_config_1.setLocalStorage)(localStorageKey, value, cacheTime); } else { // For non-string values, stringify them first (0, gsb_config_1.setLocalStorage)(localStorageKey, JSON.stringify(value), cacheTime); } } catch (error) { console.error(`Error storing data in localStorage with key ${localStorageKey}:`, error); } } return value; } /** * Remove a value from both the generic cache and localStorage if specified * @param key The cache key * @param localStorageKey Optional localStorage key to also remove */ removeCache(key, localStorageKey) { this.genericCache.delete(key); if (localStorageKey && typeof window !== 'undefined') { try { localStorage.removeItem(localStorageKey); } catch (error) { console.error(`Error removing data from localStorage with key ${localStorageKey}:`, error); } } } clearCache() { this.entityDefCache.clear(); this.entityCache.clear(); this.enumCache.clear(); this.propertyDefCache.clear(); this.genericCache.clear(); this.allDefinitionsCached = false; // Also clear localStorage cache items if (typeof window !== 'undefined') { try { const localStorageKeys = Object.keys(localStorage); // Clear GSB-related cache items localStorageKeys.forEach(key => { if (key.startsWith('gsb_entityDef_') || key.startsWith('gsb_enum_') || key.startsWith('gsb_propDef_')) { localStorage.removeItem(key); } }); } catch (error) { console.error('Error clearing localStorage cache:', error); } } this.clearIndexedDb().then(); } /** * Get all entity definitions */ async getAllEntityDefs() { return { entityDefs: this.getAllCachedEntityDefinitions() }; } /** * Fetches entities for a given definition from cache if available or from server if not * @param entityDef The entity definition to fetch data for * @param cacheKey Optional custom cache key (defaults to entity name) * @param queryParams Optional query parameters (defaults to QueryType.Full) * @param expiry Optional cache expiry time in ms (defaults to CACHE_DURATION) * @param token Optional custom token to use for this request * @param tenantCode Optional custom tenant code to use for this request * @returns Query response with entities */ async fetchOrCached(entDefName, cacheKey, queryParams, expiry, token, tenantCode) { if (!entDefName) { throw new Error('Entity definition name is required'); } // Create cache key if not provided const cacheKeyToUse = cacheKey || `entities_${entDefName}`; const localStorageKey = `gsb_entities_${entDefName}`; const cacheDuration = expiry || this.CACHE_DURATION; // Check cache first const cachedData = await this.getCacheAsync(cacheKeyToUse, localStorageKey, cacheDuration, 'indexedDB'); if (cachedData) { return cachedData; } return this.acquire(cacheKeyToUse, async () => { // Get auth params - use provided token and tenantCode if available, otherwise get from auth let authToken = token; let authTenantCode = tenantCode; if (!authToken || !authTenantCode) { const authParams = await this.getAuthParams(); authToken = authToken || authParams.token; authTenantCode = authTenantCode || authParams.tenantCode; } // Create default query params if not provided const params = queryParams || new query_params_1.QueryParams(entDefName).type(query_params_1.QueryType.Full); if (!(0, core_1.isDefinitionSet)(params)) { params.entityDef = { name: entDefName }; } // Query the entity service const response = await this.entityService.query(params, authToken, authTenantCode); // Cache the response this.setCacheAsync(cacheKeyToUse, response, cacheDuration, localStorageKey, 'indexedDB'); return response; }); } /** * Get all entity definitions metadata with their properties * This method caches the results and returns from cache if available * @param token Optional custom token to use for this request * @param tenantCode Optional custom tenant code to use for this request * @returns Array of entity definitions with their properties */ async getAllDefinitions(token, tenantCode) { const cacheKey = 'allDefinitions'; // Check if all definitions are already cached and not expired if (this.areAllDefinitionsCached()) { return this.getAllCachedEntityDefinitions(); } return this.acquire(cacheKey, async () => { // Get auth params - use provided token and tenantCode if available, otherwise get from auth let authToken = token; let authTenantCode = tenantCode; if (!authToken || !authTenantCode) { const authParams = await this.getAuthParams(); authToken = authToken || authParams.token; authTenantCode = authTenantCode || authParams.tenantCode; } // If not in cache, fetch from server const query = new query_params_1.QueryParams('GsbEntityDef'); query.queryType = query_params_1.QueryType.FullNonPersonal; query.include(p => p.properties); // Get all entities without pagination query.startIndex = 0; query.count = 1000; // Adjust if needed query.calcTotalCount = true; const response = await this.entityService.query(query, authToken, authTenantCode); if (!response.entities) { return []; } // Store all definitions in cache with their property definitions await this.storeAllEntityDefinitions(response.entities, authToken, authTenantCode); return response.entities; }); } async getCacheAsync(key, persistenceKey, duration, persistence = 'localStorage') { const cacheTime = duration || this.CACHE_DURATION; // Check memory cache first const cached = this.genericCache.get(key); if (cached && Date.now() - cached.timestamp < cacheTime) { return cached.data; } // If not in memory cache but persistence key is provided, try there if (persistenceKey && typeof window !== 'undefined') { try { if (persistence === 'localStorage') { const storedValue = (0, gsb_config_1.getLocalStorage)(persistenceKey); if (storedValue) { try { const parsedData = JSON.parse(storedValue); this.genericCache.set(key, { data: parsedData, timestamp: Date.now() }); return parsedData; } catch (e) { this.genericCache.set(key, { data: storedValue, timestamp: Date.now() }); return storedValue; } } } else if (persistence === 'indexedDB' && this.isIndexedDbSupported()) { const entry = await this.getIndexedDb(persistenceKey); if (entry && (Date.now() - entry.timestamp < cacheTime)) { this.genericCache.set(key, { data: entry.data, timestamp: Date.now() }); return entry.data; } if (entry) { // expired await this.removeIndexedDb(persistenceKey); } } } catch (error) { console.error(`Error retrieving data with persistence '${persistence}' and key ${persistenceKey}:`, error); } } return null; } async setCacheAsync(key, value, duration, persistenceKey, persistence = 'localStorage') { // Store in memory cache this.genericCache.set(key, { data: value, timestamp: Date.now() }); // If persistence key is provided, also store there if (persistenceKey && typeof window !== 'undefined') { try { if (persistence === 'localStorage') { const cacheTime = duration || this.CACHE_DURATION; if (typeof value === 'string') { (0, gsb_config_1.setLocalStorage)(persistenceKey, value, cacheTime); } else { (0, gsb_config_1.setLocalStorage)(persistenceKey, JSON.stringify(value), cacheTime); } } else if (persistence === 'indexedDB' && this.isIndexedDbSupported()) { await this.setIndexedDb(persistenceKey, value); } } catch (error) { console.error(`Error storing data with persistence '${persistence}' and key ${persistenceKey}:`, error); } } return value; } async removeCacheAsync(key, persistenceKey, persistence = 'localStorage') { this.genericCache.delete(key); if (persistenceKey && typeof window !== 'undefined') { try { if (persistence === 'localStorage') { localStorage.removeItem(persistenceKey); } else if (persistence === 'indexedDB' && this.isIndexedDbSupported()) { await this.removeIndexedDb(persistenceKey); } } catch (error) { console.error(`Error removing data from with persistence '${persistence}' and key ${persistenceKey}:`, error); } } } } exports.GsbCacheService = GsbCacheService; //# sourceMappingURL=gsb-cache.service.js.map