diff --git a/tx/ocl/jobs/background-queue.cjs b/tx/ocl/jobs/background-queue.cjs index 060b93b4..5e323abb 100644 --- a/tx/ocl/jobs/background-queue.cjs +++ b/tx/ocl/jobs/background-queue.cjs @@ -22,15 +22,17 @@ class OCLBackgroundJobQueue { const resolveAndEnqueue = async () => { const resolvedSize = await this.#resolveJobSize(options); const normalizedSize = this.#normalizeJobSize(resolvedSize); - this.#insertPendingJobOrdered({ + const job = { jobKey, jobType: jobType || 'background-job', jobId: options?.jobId || jobKey, jobSize: normalizedSize, getProgress: typeof options?.getProgress === 'function' ? options.getProgress : null, runJob, - enqueueOrder: this.enqueueSequence++ - }); + enqueueOrder: this.enqueueSequence++, + userRequested: !!options.userRequested + }; + this.#insertPendingJobOrdered(job); this.ensureHeartbeatRunning(); console.log(`[OCL] ${jobType || 'Background job'} enqueued: ${jobKey} (size=${normalizedSize}, queue=${this.pendingJobs.length}, active=${this.activeCount})`); this.processNext(); @@ -72,17 +74,21 @@ class OCLBackgroundJobQueue { } static #insertPendingJobOrdered(job) { + // 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 => { if (existing.jobSize === job.jobSize) { return existing.enqueueOrder > job.enqueueOrder; } return existing.jobSize > job.jobSize; }); - if (index < 0) { index = this.pendingJobs.length; } - this.pendingJobs.splice(index, 0, job); } diff --git a/tx/ocl/shared/patches.cjs b/tx/ocl/shared/patches.cjs index eae4750b..28220664 100644 --- a/tx/ocl/shared/patches.cjs +++ b/tx/ocl/shared/patches.cjs @@ -178,6 +178,43 @@ function patchValueSetExpandWholeSystemForOcl() { const originalIncludeCodes = proto.includeCodes; proto.includeCodes = async function patchedIncludeCodes(cset, path, vsSrc, compose, filter, expansion, excludeInactive, notClosed) { + // ...existing code... + // PATCH: Para OCL ValueSet, só expandir códigos explicitamente listados em compose.include.code + if (Array.isArray(compose?.include)) { + const explicitCodes = []; + for (const include of compose.include) { + // Normaliza o system para URL canônico + const canonicalSystem = typeof include.system === 'string' ? include.system.trim() : include.system; + if (Array.isArray(include.code)) { + for (const code of include.code) { + explicitCodes.push({ system: canonicalSystem, code }); + } + } + // Também verifica se há conceitos explícitos em include.concept + if (Array.isArray(include.concept)) { + for (const concept of include.concept) { + if (concept && concept.code) { + explicitCodes.push({ system: canonicalSystem, code: concept.code }); + } + } + } + } + if (explicitCodes.length > 0) { + // Filtra expansão para só retornar os códigos explicitamente referenciados + const filtered = []; + for (const { system, code } of explicitCodes) { + // Busca conceito no CodeSystem + const resources = await originalIncludeCodes.call(this, { system, code }, path, vsSrc, compose, filter, expansion, excludeInactive, notClosed); + if (Array.isArray(resources)) { + filtered.push(...resources); + } else if (resources) { + filtered.push(resources); + } + } + return filtered; + } + } + // Fallback para comportamento original try { return await originalIncludeCodes.call(this, cset, path, vsSrc, compose, filter, expansion, excludeInactive, notClosed); } catch (error) { diff --git a/tx/ocl/vs-ocl.cjs b/tx/ocl/vs-ocl.cjs index 27a7316e..efdf15aa 100644 --- a/tx/ocl/vs-ocl.cjs +++ b/tx/ocl/vs-ocl.cjs @@ -15,7 +15,7 @@ const { computeValueSetExpansionFingerprint } = require('./fingerprint/fingerpri const { ensureTxParametersHashIncludesFilter, patchValueSetExpandWholeSystemForOcl } = require('./shared/patches'); ensureTxParametersHashIncludesFilter(TxParameters); -patchValueSetExpandWholeSystemForOcl(); +//patchValueSetExpandWholeSystemForOcl(); function normalizeCanonicalSystem(system) { if (typeof system !== 'string') { @@ -93,10 +93,42 @@ class OCLValueSetProvider extends AbstractValueSetProvider { 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 + }, 'R5'); + this.#applyCachedExpansion(valueSetObj, paramsKey); + // Indexa o ValueSet restaurado para torná-lo disponível via fetchValueSet + this.valueSetMap.set(valueSetObj.url, valueSetObj); + if (valueSetObj.version) { + this.valueSetMap.set(`${valueSetObj.url}|${valueSetObj.version}`, valueSetObj); + } + this.valueSetMap.set(valueSetObj.id, valueSetObj); + this._idMap.set(valueSetObj.id, valueSetObj); this.valueSetFingerprints.set(cacheKey, cached.fingerprint || null); loadedCount++; - console.log(`[OCL-ValueSet] Loaded ValueSet from cold cache: ${cached.canonicalUrl}`); + 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); } @@ -220,9 +252,10 @@ class OCLValueSetProvider extends AbstractValueSetProvider { let key = `${url}|${version}`; if (this.valueSetMap.has(key)) { const vs = this.valueSetMap.get(key); - await this.#ensureComposeIncludes(vs); + // await this.#ensureComposeIncludes(vs); this.#clearInlineExpansion(vs); - this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset' }); + console.log(`[OCL-ValueSet] fetchValueSet cache hit for ${url} (version: ${version || 'none'})`); + this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset', userRequested: true }); return vs; } @@ -232,7 +265,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider { key = `${url}|${majorMinor}`; if (this.valueSetMap.has(key)) { const vs = this.valueSetMap.get(key); - await this.#ensureComposeIncludes(vs); + // await this.#ensureComposeIncludes(vs); this.#clearInlineExpansion(vs); this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset-mm' }); return vs; @@ -242,7 +275,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider { if (this.valueSetMap.has(url)) { const vs = this.valueSetMap.get(url); - await this.#ensureComposeIncludes(vs); + // await this.#ensureComposeIncludes(vs); this.#clearInlineExpansion(vs); this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset-url' }); return vs; @@ -250,7 +283,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider { const resolved = await this.#resolveValueSetByCanonical(url, version); if (resolved) { - await this.#ensureComposeIncludes(resolved); + // await this.#ensureComposeIncludes(resolved); this.#clearInlineExpansion(resolved); this.#scheduleBackgroundExpansion(resolved, { reason: 'fetch-valueset-resolved' }); return resolved; @@ -261,7 +294,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider { key = `${url}|${version}`; if (this.valueSetMap.has(key)) { const vs = this.valueSetMap.get(key); - await this.#ensureComposeIncludes(vs); + // await this.#ensureComposeIncludes(vs); this.#clearInlineExpansion(vs); this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset-init' }); return vs; @@ -273,7 +306,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider { key = `${url}|${majorMinor}`; if (this.valueSetMap.has(key)) { const vs = this.valueSetMap.get(key); - await this.#ensureComposeIncludes(vs); + // await this.#ensureComposeIncludes(vs); this.#clearInlineExpansion(vs); this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset-init-mm' }); return vs; @@ -283,7 +316,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider { if (this.valueSetMap.has(url)) { const vs = this.valueSetMap.get(url); - await this.#ensureComposeIncludes(vs); + // await this.#ensureComposeIncludes(vs); this.#clearInlineExpansion(vs); this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset-init-url' }); return vs; @@ -295,7 +328,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider { async fetchValueSetById(id) { const local = this.#getLocalValueSetById(id); if (local) { - await this.#ensureComposeIncludes(local); + // await this.#ensureComposeIncludes(local); this.#clearInlineExpansion(local); this.#scheduleBackgroundExpansion(local, { reason: 'fetch-valueset-by-id' }); return local; @@ -304,7 +337,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider { await this.initialize(); const vs = this.#getLocalValueSetById(id); - await this.#ensureComposeIncludes(vs); + // await this.#ensureComposeIncludes(vs); this.#clearInlineExpansion(vs); this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset-by-id-init' }); return vs; @@ -369,17 +402,15 @@ class OCLValueSetProvider extends AbstractValueSetProvider { || this._idMap.get(vs.id) || null; - // Preserve hydrated cold-cache expansions on first index; invalidate only on replacement. - if (existing && existing !== vs) { - this.#invalidateExpansionCache(vs); - } - - this.valueSetMap.set(vs.url, vs); - if (vs.version) { - this.valueSetMap.set(`${vs.url}|${vs.version}`, vs); + // Só indexa se não existe ou se for o mesmo objeto + if (!existing || existing === vs) { + this.valueSetMap.set(vs.url, vs); + if (vs.version) { + this.valueSetMap.set(`${vs.url}|${vs.version}`, vs); + } + this.valueSetMap.set(vs.id, vs); + this._idMap.set(vs.id, vs); } - this.valueSetMap.set(vs.id, vs); - this._idMap.set(vs.id, vs); } #toValueSet(collection) { @@ -414,11 +445,11 @@ class OCLValueSetProvider extends AbstractValueSetProvider { json.meta = { lastUpdated }; } - if (preferredSource) { - json.compose = { - include: [{ system: preferredSource }] - }; - } + // if (preferredSource) { + // json.compose = { + // include: [{ system: preferredSource }] + // }; + // } const conceptsUrl = this.#normalizePath( collection.concepts_url || collection.conceptsUrl || this.#buildCollectionConceptsPath(collection) @@ -995,7 +1026,8 @@ class OCLValueSetProvider extends AbstractValueSetProvider { const meta = this.#getCollectionMeta(vs); queuedJobSize = await this.#fetchConceptCountFromHeaders(meta?.conceptsUrl || null); return queuedJobSize; - } + }, + userRequested: !!options.userRequested } ); } @@ -1080,10 +1112,10 @@ class OCLValueSetProvider extends AbstractValueSetProvider { } const contains = []; - let offset = 0; - - // Pull all concepts in fixed-size pages until exhausted. - // eslint-disable-next-line no-constant-condition + let offset = 0; // Moved this line up + // Agrupa conceitos por system + const systemConcepts = new Map(); + // Removed duplicate offset declaration while (true) { const batch = await this.#fetchCollectionConcepts(meta, { count: CONCEPT_PAGE_SIZE, @@ -1092,17 +1124,14 @@ class OCLValueSetProvider extends AbstractValueSetProvider { filter: null, languageCodes: [] }); - const entries = Array.isArray(batch?.contains) ? batch.contains : []; if (entries.length === 0) { break; } - for (const entry of entries) { if (!entry?.system || !entry?.code) { continue; } - const out = { system: entry.system, code: entry.code @@ -1125,18 +1154,28 @@ class OCLValueSetProvider extends AbstractValueSetProvider { })); } contains.push(out); + // Agrupa por system + if (!systemConcepts.has(entry.system)) { + systemConcepts.set(entry.system, []); + } + systemConcepts.get(entry.system).push(entry.code); } - if (progressState) { progressState.processed = contains.length; } - if (entries.length < CONCEPT_PAGE_SIZE) { break; } offset += entries.length; } - + // Popular compose.include para cada system + if (!vs.jsonObj.compose) { + vs.jsonObj.compose = { include: [] }; + } + 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()}`,