import type { Node3D } from '@cognite/sdk'; import type { DMInstanceRef } from '@cognite/reveal'; import { chunk, executeParallel } from '../utils/executeParallel'; const ASSET_MAPPING_CHUNK_SIZE = 1000; /** * Multi-level asset mapping cache with split-chunk strategy. * * Three-way indexing: * - By model+revision: Fast lookup for all mappings in a model * - By asset instance (space:externalId): Fast lookup for all nodes belonging to an asset * - By node ID: Fast lookup for individual node metadata */ export class AssetMappingCache { private byModelCache = new Map>(); private byAssetCache = new Map(); private byNodeCache = new Map(); async getOrFetch( modelId: number, revisionId: number, assetInstances: DMInstanceRef[], fetchFn: ( modelId: number, revisionId: number, instances: DMInstanceRef[] ) => Promise> ): Promise> { const modelKey = this.createModelKey(modelId, revisionId); const cachedModel = this.byModelCache.get(modelKey); if (cachedModel) { const { cached, uncached } = this.splitCachedAndMissing( assetInstances, cachedModel ); if (uncached.length === 0) { return cached; } const fetched = await this.fetchAndCache( modelId, revisionId, uncached, fetchFn ); return this.mergeMappings(cached, fetched); } return this.fetchAndCache(modelId, revisionId, assetInstances, fetchFn); } getCachedAssetMapping(instance: DMInstanceRef): Node3D[] | undefined { return this.byAssetCache.get(this.createAssetKey(instance)); } getCachedNode(modelId: number, revisionId: number, treeIndex: number): Node3D | undefined { const key = this.createNodeKey(modelId, revisionId, treeIndex); return this.byNodeCache.get(key); } clear(): void { this.byModelCache.clear(); this.byAssetCache.clear(); this.byNodeCache.clear(); } clearModel(modelId: number, revisionId: number): void { const modelKey = this.createModelKey(modelId, revisionId); this.byModelCache.delete(modelKey); } private createAssetKey(instance: DMInstanceRef): string { return `${instance.space}:${instance.externalId}`; } private createModelKey(modelId: number, revisionId: number): string { return `${modelId}/${revisionId}`; } private createNodeKey(modelId: number, revisionId: number, treeIndex: number): string { return `${modelId}/${revisionId}/${treeIndex}`; } private splitCachedAndMissing( assetInstances: DMInstanceRef[], cachedModel: Map ): { cached: Map; uncached: DMInstanceRef[]; } { const cached = new Map(); const uncached: DMInstanceRef[] = []; for (const instance of assetInstances) { const assetKey = this.createAssetKey(instance); const cachedNodes = cachedModel.get(assetKey); if (cachedNodes) { cached.set(assetKey, cachedNodes); } else { uncached.push(instance); } } return { cached, uncached }; } private async fetchAndCache( modelId: number, revisionId: number, assetInstances: DMInstanceRef[], fetchFn: ( modelId: number, revisionId: number, instances: DMInstanceRef[] ) => Promise> ): Promise> { const chunks = chunk(assetInstances, ASSET_MAPPING_CHUNK_SIZE); const results = await executeParallel( chunks.map((chunkInstances) => async () => { return fetchFn(modelId, revisionId, chunkInstances); }), 3 ); const merged = new Map(); for (const result of results) { if (result) { for (const [assetKey, nodes] of result.entries()) { merged.set(assetKey, nodes); } } } this.indexMappings(modelId, revisionId, merged); return merged; } private indexMappings( modelId: number, revisionId: number, mappings: Map ): void { const modelKey = this.createModelKey(modelId, revisionId); let modelCache = this.byModelCache.get(modelKey); if (!modelCache) { modelCache = new Map(); this.byModelCache.set(modelKey, modelCache); } for (const [assetKey, nodes] of mappings.entries()) { modelCache.set(assetKey, nodes); this.byAssetCache.set(assetKey, nodes); for (const node of nodes) { if (node.treeIndex !== undefined) { const nodeKey = this.createNodeKey(modelId, revisionId, node.treeIndex); this.byNodeCache.set(nodeKey, node); } } } } private mergeMappings( map1: Map, map2: Map ): Map { const merged = new Map(map1); for (const [key, value] of map2.entries()) { merged.set(key, value); } return merged; } }