diff --git a/tx/ocl/cs-ocl.cjs b/tx/ocl/cs-ocl.cjs index 9722c63..ea7062b 100644 --- a/tx/ocl/cs-ocl.cjs +++ b/tx/ocl/cs-ocl.cjs @@ -48,6 +48,8 @@ class OCLCodeSystemProvider extends AbstractCodeSystemProvider { this._pendingChanges = null; this._initialized = false; this._initializePromise = null; + this._organizationIdsCache = null; + this._organizationIdsFetchPromise = null; } async initialize() { @@ -206,35 +208,54 @@ class OCLCodeSystemProvider extends AbstractCodeSystemProvider { } async #fetchOrganizationIds() { - const endpoint = '/orgs/'; - const orgs = await this.#fetchAllPages(endpoint); + // Return cached result if available + if (this._organizationIdsCache) { + return this._organizationIdsCache; + } - const ids = []; - const seen = new Set(); - for (const org of orgs || []) { - if (!org || typeof org !== 'object') { - continue; - } + // Deduplicate concurrent requests + if (this._organizationIdsFetchPromise) { + return this._organizationIdsFetchPromise; + } - const id = org.id || org.mnemonic || org.short_code || org.shortCode || org.name || null; - if (!id) { - continue; + this._organizationIdsFetchPromise = (async () => { + const endpoint = '/orgs/'; + const orgs = await this.#fetchAllPages(endpoint); + + const ids = []; + const seen = new Set(); + for (const org of orgs || []) { + if (!org || typeof org !== 'object') { + continue; + } + + const id = org.id || org.mnemonic || org.short_code || org.shortCode || org.name || null; + if (!id) { + continue; + } + + const normalized = String(id).trim(); + if (!normalized || seen.has(normalized)) { + continue; + } + + seen.add(normalized); + ids.push(normalized); } - const normalized = String(id).trim(); - if (!normalized || seen.has(normalized)) { - continue; + if (ids.length === 0 && this.org) { + ids.push(this.org); } - seen.add(normalized); - ids.push(normalized); - } + this._organizationIdsCache = ids; + return ids; + })(); - if (ids.length === 0 && this.org) { - ids.push(this.org); + try { + return await this._organizationIdsFetchPromise; + } finally { + this._organizationIdsFetchPromise = null; } - - return ids; } #sourceIdentity(source) { @@ -998,7 +1019,7 @@ class OCLSourceCodeSystemProvider extends CodeSystemProvider { const pending = (async () => { let response; try { - response = await this.httpClient.get(url); + response = await this.httpClient.get(url, { params: { verbose: true } }); } catch (error) { // Missing concept should be treated as not-found, not as an internal server failure. if (error && error.response && error.response.status === 404) { @@ -1302,7 +1323,6 @@ class OCLSourceCodeSystemFactory extends CodeSystemFactoryProvider { this.#applyConceptsToCodeSystemResource(this.meta?.codeSystem || null); - console.log(`[OCL] Loaded CodeSystem from cold cache: ${canonicalUrl} (${cached.concepts.length} concepts, fingerprint=${this.customFingerprint?.substring(0, 8)})`); } catch (error) { if (error.code !== 'ENOENT') { console.error(`[OCL] Failed to load cold cache for CodeSystem ${canonicalUrl}:`, error.message); @@ -1329,7 +1349,6 @@ class OCLSourceCodeSystemFactory extends CodeSystemFactoryProvider { }; await fs.writeFile(cacheFilePath, JSON.stringify(cacheData, null, 2), 'utf-8'); - console.log(`[OCL] Saved CodeSystem to cold cache: ${canonicalUrl} (${concepts.length} concepts, fingerprint=${fingerprint?.substring(0, 8)})`); return fingerprint; } catch (error) { @@ -1343,7 +1362,6 @@ class OCLSourceCodeSystemFactory extends CodeSystemFactoryProvider { const key = `${normalizedSystem}|${version || ''}`; const factory = OCLSourceCodeSystemFactory.#findFactory(normalizedSystem, version); if (!factory) { - console.log(`[OCL] CodeSystem load not scheduled (factory unavailable): ${key}`); return false; } factory.scheduleBackgroundLoad(reason); @@ -1412,7 +1430,6 @@ class OCLSourceCodeSystemFactory extends CodeSystemFactoryProvider { } if (cacheAgeMs != null && cacheAgeMs < COLD_CACHE_FRESHNESS_MS) { - console.log(`[OCL] Skipping warm-up for CodeSystem ${this.system()} (cold cache age: ${formatCacheAgeMinutes(cacheAgeMs)})`); return; } @@ -1420,12 +1437,10 @@ class OCLSourceCodeSystemFactory extends CodeSystemFactoryProvider { const jobKey = `cs:${key}`; if (OCLBackgroundJobQueue.isQueuedOrRunning(jobKey)) { - console.log(`[OCL] CodeSystem load already queued or running: ${key}`); return; } let queuedJobSize = null; - console.log(`[OCL] CodeSystem load enqueued: ${key} (${reason})`); OCLBackgroundJobQueue.enqueue( jobKey, 'CodeSystem load', @@ -1444,7 +1459,6 @@ class OCLSourceCodeSystemFactory extends CodeSystemFactoryProvider { } async #runBackgroundLoad(key, knownConceptCount = null) { - console.log(`[OCL] CodeSystem load started: ${key}`); try { this.backgroundLoadProgress = { processed: 0, total: null }; const resolvedTotal = Number.isFinite(knownConceptCount) && knownConceptCount >= 0 @@ -1470,16 +1484,7 @@ class OCLSourceCodeSystemFactory extends CodeSystemFactoryProvider { const allConcepts = Array.from(this.sharedConceptCache.values()); const newFingerprint = computeCodeSystemFingerprint(allConcepts); - if (this.customFingerprint && newFingerprint === this.customFingerprint) { - console.log(`[OCL] CodeSystem fingerprint unchanged: ${key} (fingerprint=${newFingerprint?.substring(0, 8)})`); - } else { - if (this.customFingerprint) { - console.log(`[OCL] CodeSystem fingerprint changed: ${key} (${this.customFingerprint?.substring(0, 8)} -> ${newFingerprint?.substring(0, 8)})`); - console.log(`[OCL] Replacing cold cache with new hot cache: ${key}`); - } else { - console.log(`[OCL] Computed fingerprint for CodeSystem: ${key} (fingerprint=${newFingerprint?.substring(0, 8)})`); - } - + if (!this.customFingerprint || newFingerprint !== this.customFingerprint) { // Save to cold cache const savedFingerprint = await this.#saveColdCache(allConcepts); if (savedFingerprint) { @@ -1487,10 +1492,7 @@ class OCLSourceCodeSystemFactory extends CodeSystemFactoryProvider { } } - console.log(`[OCL] CodeSystem load completed, marked content=complete: ${key}`); - const progress = OCLSourceCodeSystemFactory.loadProgress(); - console.log(`[OCL] CodeSystem load completed: ${this.system()}. Loaded ${progress.loaded}/${progress.total} CodeSystems (${progress.percentage.toFixed(2)}%)`); - console.log(`[OCL] CodeSystem now available in cache: ${key} (${count} concepts)`); + console.log(`[OCL] CodeSystem loaded: ${this.system()} (${count} concepts)`); } catch (error) { console.error(`[OCL] CodeSystem background load failed: ${key}: ${error.message}`); } diff --git a/tx/ocl/jobs/background-queue.cjs b/tx/ocl/jobs/background-queue.cjs index 5e323ab..e20628c 100644 --- a/tx/ocl/jobs/background-queue.cjs +++ b/tx/ocl/jobs/background-queue.cjs @@ -34,7 +34,6 @@ class OCLBackgroundJobQueue { }; this.#insertPendingJobOrdered(job); this.ensureHeartbeatRunning(); - console.log(`[OCL] ${jobType || 'Background job'} enqueued: ${jobKey} (size=${normalizedSize}, queue=${this.pendingJobs.length}, active=${this.activeCount})`); this.processNext(); }; @@ -77,7 +76,6 @@ class OCLBackgroundJobQueue { // Prioridade máxima para userRequested if (job.userRequested) { this.pendingJobs.unshift(job); - console.log(`[OCL] User-requested job prioritized: ${job.jobKey}`); return; } let index = this.pendingJobs.findIndex(existing => { @@ -179,13 +177,8 @@ class OCLBackgroundJobQueue { getProgress: job.getProgress || null, startedAt: Date.now() }); - console.log(`[OCL] Background job started: ${job.jobType} ${job.jobKey} (size=${job.jobSize}, queue=${this.pendingJobs.length}, active=${this.activeCount})`); - Promise.resolve() .then(() => job.runJob()) - .then(() => { - console.log(`[OCL] Background job completed: ${job.jobType} ${job.jobKey}`); - }) .catch((error) => { const message = error && error.message ? error.message : String(error); console.error(`[OCL] Background job failed: ${job.jobType} ${job.jobKey}: ${message}`); @@ -194,7 +187,6 @@ class OCLBackgroundJobQueue { this.activeCount -= 1; this.queuedOrRunningKeys.delete(job.jobKey); this.activeJobs.delete(job.jobKey); - console.log(`[OCL] Background queue status: queue=${this.pendingJobs.length}, active=${this.activeCount}`); this.processNext(); }); } diff --git a/tx/ocl/vs-ocl.cjs b/tx/ocl/vs-ocl.cjs index efdf15a..564da27 100644 --- a/tx/ocl/vs-ocl.cjs +++ b/tx/ocl/vs-ocl.cjs @@ -51,6 +51,9 @@ class OCLValueSetProvider extends AbstractValueSetProvider { this.pendingSourceCanonicalRequests = new Map(); this.collectionByCanonicalCache = new Map(); this.pendingCollectionByCanonicalRequests = new Map(); + this._organizationIdsCache = null; + this._organizationIdsFetchPromise = null; + this._negativeUrlCache = new Set(); this._composePromises = new Map(); this.backgroundExpansionCache = new Map(); this.backgroundExpansionProgress = new Map(); @@ -77,7 +80,13 @@ class OCLValueSetProvider extends AbstractValueSetProvider { const data = await fs.readFile(filePath, 'utf-8'); const cached = JSON.parse(data); - if (!cached || !cached.canonicalUrl || !cached.expansion) { + if (!cached || !cached.canonicalUrl) { + continue; + } + + // Support both old (expansion-based) and new (compose-based) cache formats + const compose = cached.compose || this.#composeFromExpansion(cached.expansion); + if (!compose || !Array.isArray(compose.include) || compose.include.length === 0) { continue; } @@ -88,37 +97,19 @@ class OCLValueSetProvider extends AbstractValueSetProvider { ); const createdAt = cached.timestamp ? new Date(cached.timestamp).getTime() : null; this.backgroundExpansionCache.set(cacheKey, { - expansion: cached.expansion, + compose, metadataSignature: cached.metadataSignature || null, dependencyChecksums: cached.dependencyChecksums || {}, createdAt: Number.isFinite(createdAt) ? createdAt : null }); - // Instancia ValueSet para garantir jsonObj - // Reconstrói compose.include se não existir - let compose = cached.expansion?.compose; - if (!compose || !Array.isArray(compose.include)) { - // Reconstrói a partir dos sistemas/códigos em expansion.contains - const systemConcepts = new Map(); - if (Array.isArray(cached.expansion?.contains)) { - for (const entry of cached.expansion.contains) { - if (!entry.system || !entry.code) continue; - if (!systemConcepts.has(entry.system)) { - systemConcepts.set(entry.system, []); - } - systemConcepts.get(entry.system).push({ code: entry.code }); - } - } - compose = { include: Array.from(systemConcepts.entries()).map(([system, concepts]) => ({ system, concept: concepts })) }; - } const valueSetObj = new ValueSet({ resourceType: 'ValueSet', url: cached.canonicalUrl, version: cached.version || null, - expansion: cached.expansion, compose, - id: cached.canonicalUrl // ou outro identificador se necessário + id: cached.canonicalUrl }, 'R5'); - this.#applyCachedExpansion(valueSetObj, paramsKey); + this.#applyCachedCompose(valueSetObj, paramsKey); // Indexa o ValueSet restaurado para torná-lo disponível via fetchValueSet this.valueSetMap.set(valueSetObj.url, valueSetObj); if (valueSetObj.version) { @@ -128,7 +119,6 @@ class OCLValueSetProvider extends AbstractValueSetProvider { this._idMap.set(valueSetObj.id, valueSetObj); this.valueSetFingerprints.set(cacheKey, cached.fingerprint || null); loadedCount++; - console.log(`[OCL-ValueSet] Loaded ValueSet from cold cache into memory: ${cached.canonicalUrl}`); } catch (error) { console.error(`[OCL-ValueSet] Failed to load cold cache file ${file}:`, error.message); } @@ -144,10 +134,10 @@ class OCLValueSetProvider extends AbstractValueSetProvider { } } - async #saveColdCacheForValueSet(vs, expansion, metadataSignature, dependencyChecksums, paramsKey = 'default') { + async #saveColdCacheForValueSet(vs, compose, metadataSignature, dependencyChecksums, paramsKey = 'default') { const canonicalUrl = vs?.url; const version = vs?.version || null; - if (!canonicalUrl || !expansion) { + if (!canonicalUrl || !compose) { return null; } @@ -156,22 +146,25 @@ class OCLValueSetProvider extends AbstractValueSetProvider { try { await ensureCacheDirectories(CACHE_VS_DIR); - const fingerprint = computeValueSetExpansionFingerprint(expansion); + const conceptCount = Array.isArray(compose.include) + ? compose.include.reduce((sum, inc) => sum + (Array.isArray(inc.concept) ? inc.concept.length : 0), 0) + : 0; + const fingerprint = computeValueSetExpansionFingerprint(compose); const cacheData = { canonicalUrl, version, paramsKey, fingerprint, timestamp: new Date().toISOString(), - conceptCount: expansion.contains?.length || 0, - expansion, + conceptCount, + compose, metadataSignature, dependencyChecksums }; await fs.writeFile(cacheFilePath, JSON.stringify(cacheData, null, 2), 'utf-8'); - console.log(`[OCL-ValueSet] Saved ValueSet expansion to cold cache: ${canonicalUrl} (${expansion.contains?.length || 0} concepts, fingerprint=${fingerprint?.substring(0, 8)})`); - + console.log(`[OCL-ValueSet] Saved ValueSet compose to cold cache: ${canonicalUrl} (${conceptCount} concepts, fingerprint=${fingerprint?.substring(0, 8)})`); + return fingerprint; } catch (error) { console.error(`[OCL-ValueSet] Failed to save cold cache for ValueSet ${canonicalUrl}:`, error.message); @@ -249,12 +242,15 @@ class OCLValueSetProvider extends AbstractValueSetProvider { async fetchValueSet(url, version) { this._validateFetchParams(url, version); + // Fast path: skip URLs already known to not be in OCL + if (this._negativeUrlCache.has(url)) { + return null; + } + let key = `${url}|${version}`; if (this.valueSetMap.has(key)) { const vs = this.valueSetMap.get(key); - // await this.#ensureComposeIncludes(vs); - this.#clearInlineExpansion(vs); - console.log(`[OCL-ValueSet] fetchValueSet cache hit for ${url} (version: ${version || 'none'})`); + await this.#ensureComposeIncludes(vs); this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset', userRequested: true }); return vs; } @@ -265,8 +261,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider { key = `${url}|${majorMinor}`; if (this.valueSetMap.has(key)) { const vs = this.valueSetMap.get(key); - // await this.#ensureComposeIncludes(vs); - this.#clearInlineExpansion(vs); + await this.#ensureComposeIncludes(vs); this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset-mm' }); return vs; } @@ -275,16 +270,14 @@ class OCLValueSetProvider extends AbstractValueSetProvider { if (this.valueSetMap.has(url)) { const vs = this.valueSetMap.get(url); - // await this.#ensureComposeIncludes(vs); - this.#clearInlineExpansion(vs); + await this.#ensureComposeIncludes(vs); this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset-url' }); return vs; } const resolved = await this.#resolveValueSetByCanonical(url, version); if (resolved) { - // await this.#ensureComposeIncludes(resolved); - this.#clearInlineExpansion(resolved); + await this.#ensureComposeIncludes(resolved); this.#scheduleBackgroundExpansion(resolved, { reason: 'fetch-valueset-resolved' }); return resolved; } @@ -294,8 +287,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider { key = `${url}|${version}`; if (this.valueSetMap.has(key)) { const vs = this.valueSetMap.get(key); - // await this.#ensureComposeIncludes(vs); - this.#clearInlineExpansion(vs); + await this.#ensureComposeIncludes(vs); this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset-init' }); return vs; } @@ -306,8 +298,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider { key = `${url}|${majorMinor}`; if (this.valueSetMap.has(key)) { const vs = this.valueSetMap.get(key); - // await this.#ensureComposeIncludes(vs); - this.#clearInlineExpansion(vs); + await this.#ensureComposeIncludes(vs); this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset-init-mm' }); return vs; } @@ -316,20 +307,20 @@ class OCLValueSetProvider extends AbstractValueSetProvider { if (this.valueSetMap.has(url)) { const vs = this.valueSetMap.get(url); - // await this.#ensureComposeIncludes(vs); - this.#clearInlineExpansion(vs); + await this.#ensureComposeIncludes(vs); this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset-init-url' }); return vs; } + // Remember that this URL is not in OCL to skip future lookups + this._negativeUrlCache.add(url); return null; } async fetchValueSetById(id) { const local = this.#getLocalValueSetById(id); if (local) { - // await this.#ensureComposeIncludes(local); - this.#clearInlineExpansion(local); + await this.#ensureComposeIncludes(local); this.#scheduleBackgroundExpansion(local, { reason: 'fetch-valueset-by-id' }); return local; } @@ -337,8 +328,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider { await this.initialize(); const vs = this.#getLocalValueSetById(id); - // await this.#ensureComposeIncludes(vs); - this.#clearInlineExpansion(vs); + await this.#ensureComposeIncludes(vs); this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset-by-id-init' }); return vs; } @@ -901,6 +891,24 @@ class OCLValueSetProvider extends AbstractValueSetProvider { } } + #composeFromExpansion(expansion) { + if (!expansion || !Array.isArray(expansion.contains)) { + return null; + } + const systemConcepts = new Map(); + for (const entry of expansion.contains) { + if (!entry.system || !entry.code) continue; + if (!systemConcepts.has(entry.system)) { + systemConcepts.set(entry.system, []); + } + systemConcepts.get(entry.system).push({ code: entry.code }); + } + if (systemConcepts.size === 0) { + return null; + } + return { include: Array.from(systemConcepts.entries()).map(([system, concepts]) => ({ system, concept: concepts })) }; + } + #expansionCacheKey(vs, paramsKey) { const base = this.#valueSetBaseKey(vs); if (!base) { @@ -924,7 +932,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider { } } - #applyCachedExpansion(vs, paramsKey) { + #applyCachedCompose(vs, paramsKey) { if (!vs || !vs.jsonObj) { return; } @@ -935,25 +943,20 @@ class OCLValueSetProvider extends AbstractValueSetProvider { } const cached = this.backgroundExpansionCache.get(cacheKey); - if (!cached || !cached.expansion) { + if (!cached || !cached.compose) { return; } if (!this.#isCachedExpansionValid(vs, cached)) { this.backgroundExpansionCache.delete(cacheKey); - if (vs.jsonObj.expansion) { - delete vs.jsonObj.expansion; - } - console.log(`[OCL-ValueSet] Cached ValueSet expansion invalidated: ${cacheKey}`); return; } - if (vs.jsonObj.expansion) { + if (vs.jsonObj.compose && Array.isArray(vs.jsonObj.compose.include) && vs.jsonObj.compose.include.length > 0) { return; } - vs.jsonObj.expansion = structuredClone(cached.expansion); - console.log(`[OCL-ValueSet] ValueSet expansion restored from cache: ${cacheKey}`); + vs.jsonObj.compose = structuredClone(cached.compose); } #scheduleBackgroundExpansion(vs, options = {}) { @@ -970,13 +973,10 @@ class OCLValueSetProvider extends AbstractValueSetProvider { const cached = this.backgroundExpansionCache.get(cacheKey); if (cached && !this.#isCachedExpansionValid(vs, cached)) { this.backgroundExpansionCache.delete(cacheKey); - if (vs.jsonObj.expansion) { - delete vs.jsonObj.expansion; - } - console.log(`[OCL-ValueSet] Cached ValueSet expansion invalidated: ${cacheKey}`); } - if (vs.jsonObj.expansion) { + // Already have a cached compose ready + if (cached && cached.compose) { return; } @@ -996,23 +996,15 @@ class OCLValueSetProvider extends AbstractValueSetProvider { : cacheAgeFromFileMs != null ? 'file' : 'metadata'; - console.log(`[OCL-ValueSet] Skipping warm-up for ValueSet ${vs.url} (cold cache age: ${formatCacheAgeMinutes(freshestCacheAgeMs)})`); - console.log(`[OCL-ValueSet] ValueSet cold cache is fresh, not enqueueing warm-up job (${cacheKey}, source=${freshnessSource})`); return; } const jobKey = `vs:${cacheKey}`; if (OCLBackgroundJobQueue.isQueuedOrRunning(jobKey)) { - console.log(`[OCL-ValueSet] ValueSet expansion already queued or running: ${cacheKey}`); return; } let queuedJobSize = null; - const warmupAgeText = freshestCacheAgeMs != null - ? formatCacheAgeMinutes(freshestCacheAgeMs) - : 'no cold cache'; - console.log(`[OCL-ValueSet] Enqueueing warm-up for ValueSet ${vs.url} (cold cache age: ${warmupAgeText})`); - console.log(`[OCL-ValueSet] ValueSet expansion enqueued: ${cacheKey}`); OCLBackgroundJobQueue.enqueue( jobKey, 'ValueSet expansion', @@ -1033,7 +1025,6 @@ class OCLValueSetProvider extends AbstractValueSetProvider { } async #runBackgroundExpansion(vs, cacheKey, paramsKey = 'default', knownConceptCount = null) { - console.log(`[OCL-ValueSet] ValueSet expansion started: ${cacheKey}`); const progressState = { processed: 0, total: null }; this.backgroundExpansionProgress.set(cacheKey, progressState); try { @@ -1053,51 +1044,46 @@ class OCLValueSetProvider extends AbstractValueSetProvider { ); } - const expansion = await this.#buildBackgroundExpansion(vs, progressState); - if (!expansion) { + const compose = await this.#buildBackgroundCompose(vs, progressState); + if (!compose) { return; } - progressState.processed = expansion.total || progressState.processed; + const conceptCount = Array.isArray(compose.include) + ? compose.include.reduce((sum, inc) => sum + (Array.isArray(inc.concept) ? inc.concept.length : 0), 0) + : 0; + progressState.processed = conceptCount || progressState.processed; if (typeof progressState.total !== 'number' || !Number.isFinite(progressState.total) || progressState.total <= 0) { - progressState.total = expansion.total || 0; + progressState.total = conceptCount || 0; } const metadataSignature = this.#valueSetMetadataSignature(vs); const dependencyChecksums = this.#valueSetDependencyChecksums(vs); - // Compute custom fingerprint and compare with cold cache - const newFingerprint = computeValueSetExpansionFingerprint(expansion); + // Compute fingerprint and compare with cold cache + const newFingerprint = computeValueSetExpansionFingerprint(compose); const oldFingerprint = this.valueSetFingerprints.get(cacheKey); - if (oldFingerprint && newFingerprint === oldFingerprint) { - console.log(`[OCL-ValueSet] ValueSet expansion fingerprint unchanged: ${cacheKey} (fingerprint=${newFingerprint?.substring(0, 8)})`); - } else { - if (oldFingerprint) { - console.log(`[OCL-ValueSet] ValueSet expansion fingerprint changed: ${cacheKey} (${oldFingerprint?.substring(0, 8)} -> ${newFingerprint?.substring(0, 8)})`); - console.log(`[OCL-ValueSet] Replacing cold cache with new hot cache: ${cacheKey}`); - } else { - console.log(`[OCL-ValueSet] Computed fingerprint for ValueSet expansion: ${cacheKey} (fingerprint=${newFingerprint?.substring(0, 8)})`); - } - - // Save to cold cache - const savedFingerprint = await this.#saveColdCacheForValueSet(vs, expansion, metadataSignature, dependencyChecksums, paramsKey); + if (!oldFingerprint || newFingerprint !== oldFingerprint) { + const savedFingerprint = await this.#saveColdCacheForValueSet(vs, compose, metadataSignature, dependencyChecksums, paramsKey); if (savedFingerprint) { this.valueSetFingerprints.set(cacheKey, savedFingerprint); } } this.backgroundExpansionCache.set(cacheKey, { - expansion, + compose, metadataSignature, dependencyChecksums, createdAt: Date.now() }); - // Keep expansions in provider-managed cache only. - // Inline expansion on ValueSet bypasses $expand filtering in worker pipeline. - console.log(`[OCL-ValueSet] ValueSet expansion completed and cached: ${cacheKey}`); - console.log(`[OCL-ValueSet] ValueSet now available in cache: ${cacheKey}`); + // Apply compose to the ValueSet so the expand engine can use it + vs.jsonObj.compose = structuredClone(compose); + // Ensure no stale inline expansion + delete vs.jsonObj.expansion; + + console.log(`[OCL-ValueSet] compose cached: ${vs.url} (${conceptCount} concepts)`); } catch (error) { console.error(`[OCL-ValueSet] ValueSet background expansion failed: ${cacheKey}: ${error.message}`); } finally { @@ -1105,17 +1091,15 @@ class OCLValueSetProvider extends AbstractValueSetProvider { } } - async #buildBackgroundExpansion(vs, progressState = null) { + async #buildBackgroundCompose(vs, progressState = null) { const meta = this.#getCollectionMeta(vs); if (!meta || !meta.conceptsUrl) { return null; } - const contains = []; - let offset = 0; // Moved this line up - // Agrupa conceitos por system + let offset = 0; const systemConcepts = new Map(); - // Removed duplicate offset declaration + let totalCount = 0; while (true) { const batch = await this.#fetchCollectionConcepts(meta, { count: CONCEPT_PAGE_SIZE, @@ -1132,55 +1116,30 @@ class OCLValueSetProvider extends AbstractValueSetProvider { if (!entry?.system || !entry?.code) { continue; } - const out = { - system: entry.system, - code: entry.code - }; - if (entry.display) { - out.display = entry.display; - } - if (entry.definition) { - out.definition = entry.definition; - } - if (entry.inactive === true) { - out.inactive = true; - } - if (Array.isArray(entry.designation) && entry.designation.length > 0) { - out.designation = entry.designation - .filter(d => d && d.value) - .map(d => ({ - language: d.language, - value: d.value - })); - } - contains.push(out); - // Agrupa por system if (!systemConcepts.has(entry.system)) { systemConcepts.set(entry.system, []); } systemConcepts.get(entry.system).push(entry.code); + totalCount++; } if (progressState) { - progressState.processed = contains.length; + progressState.processed = totalCount; } if (entries.length < CONCEPT_PAGE_SIZE) { break; } offset += entries.length; } - // Popular compose.include para cada system - if (!vs.jsonObj.compose) { - vs.jsonObj.compose = { include: [] }; + + if (systemConcepts.size === 0) { + return null; } - vs.jsonObj.compose.include = Array.from(systemConcepts.entries()).map(([system, codes]) => ({ - system, - concept: codes.map(code => ({ code })) - })); + return { - timestamp: new Date().toISOString(), - identifier: `urn:uuid:${crypto.randomUUID()}`, - total: contains.length, - contains + include: Array.from(systemConcepts.entries()).map(([system, codes]) => ({ + system, + concept: codes.map(code => ({ code })) + })) }; } @@ -1354,36 +1313,55 @@ class OCLValueSetProvider extends AbstractValueSetProvider { } async #fetchOrganizationIds() { - const endpoint = '/orgs/'; - console.log(`[OCL-ValueSet] Loading organizations from: ${this.baseUrl}${endpoint}`); - const orgs = await this.#fetchAllPages(endpoint); + // Return cached result if available + if (this._organizationIdsCache) { + return this._organizationIdsCache; + } - const ids = []; - const seen = new Set(); - for (const org of orgs || []) { - if (!org || typeof org !== 'object') { - continue; - } + // Deduplicate concurrent requests + if (this._organizationIdsFetchPromise) { + return this._organizationIdsFetchPromise; + } - const id = org.id || org.mnemonic || org.short_code || org.shortCode || org.name || null; - if (!id) { - continue; + this._organizationIdsFetchPromise = (async () => { + const endpoint = '/orgs/'; + console.log(`[OCL-ValueSet] Loading organizations from: ${this.baseUrl}${endpoint}`); + const orgs = await this.#fetchAllPages(endpoint); + + const ids = []; + const seen = new Set(); + for (const org of orgs || []) { + if (!org || typeof org !== 'object') { + continue; + } + + const id = org.id || org.mnemonic || org.short_code || org.shortCode || org.name || null; + if (!id) { + continue; + } + + const normalized = String(id).trim(); + if (!normalized || seen.has(normalized)) { + continue; + } + + seen.add(normalized); + ids.push(normalized); } - const normalized = String(id).trim(); - if (!normalized || seen.has(normalized)) { - continue; + if (ids.length === 0 && this.org) { + ids.push(this.org); } - seen.add(normalized); - ids.push(normalized); - } + this._organizationIdsCache = ids; + return ids; + })(); - if (ids.length === 0 && this.org) { - ids.push(this.org); + try { + return await this._organizationIdsFetchPromise; + } finally { + this._organizationIdsFetchPromise = null; } - - return ids; } #collectionIdentity(collection) {