fhirsmith 0.7.6 → 0.8.2
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.
- package/CHANGELOG.md +48 -0
- package/README.md +5 -1
- package/library/languages.js +10 -0
- package/package.json +1 -1
- package/packages/package-crawler.js +2 -2
- package/publisher/publisher.js +1 -1
- package/registry/registry.js +2 -2
- package/root-bare-template.html +1 -2
- package/security.md +3 -0
- package/server.js +100 -70
- package/stats.js +37 -6
- package/tx/cs/cs-api.js +8 -4
- package/tx/cs/cs-loinc.js +14 -2
- package/tx/cs/cs-omop.js +5 -3
- package/tx/cs/cs-rxnorm.js +18 -16
- package/tx/cs/cs-snomed.js +279 -6
- package/tx/data/cpt-fragment.db +0 -0
- package/tx/data/cs-de.json +186 -0
- package/tx/data/cs-extensions.json +92 -0
- package/tx/data/cs-simple.json +130 -0
- package/tx/data/cs-supplement.json +78 -0
- package/tx/data/lang.dat +49180 -0
- package/tx/data/languages.csv +191 -0
- package/tx/data/loinc-subset.txt +75 -0
- package/tx/data/omop-fragment.db +0 -0
- package/tx/data/readme.md +43 -0
- package/tx/data/regions.csv +273 -0
- package/tx/data/rxnorm-subset.txt +22 -0
- package/tx/data/snomed-subset.txt +47 -0
- package/tx/data/ucum-essence.xml +2059 -0
- package/tx/html/dash-metrics.liquid +147 -0
- package/tx/importers/import-rxnorm.module.js +4 -30
- package/tx/library/canonical-resource.js +8 -0
- package/tx/library/conceptmap.js +29 -1
- package/tx/library/designations.js +4 -8
- package/tx/library/extensions.js +4 -3
- package/tx/library/renderer.js +9 -9
- package/tx/ocl/cm-ocl.cjs +185 -65
- package/tx/ocl/cs-ocl.cjs +69 -50
- package/tx/ocl/jobs/background-queue.cjs +0 -8
- package/tx/ocl/mappers/concept-mapper.cjs +13 -3
- package/tx/ocl/shared/patches.cjs +1 -0
- package/tx/ocl/vs-ocl.cjs +137 -157
- package/tx/operation-context.js +3 -3
- package/tx/params.js +2 -2
- package/tx/provider.js +6 -3
- package/tx/sct/structures.js +6 -1
- package/tx/tx.fhir.org.yml +1 -1
- package/tx/vs/vs-database.js +107 -23
- package/tx/vs/vs-vsac.js +66 -19
- package/tx/workers/expand.js +10 -10
- package/tx/workers/related.js +2 -2
- package/tx/workers/search.js +2 -1
- package/tx/workers/translate.js +222 -33
- package/tx/workers/validate.js +13 -13
- package/tx/xversion/xv-parameters.js +54 -1
- package/xig/xig.js +171 -9
package/tx/ocl/vs-ocl.cjs
CHANGED
|
@@ -14,6 +14,7 @@ const { ensureCacheDirectories, getColdCacheAgeMs, formatCacheAgeMinutes } = req
|
|
|
14
14
|
const { computeValueSetExpansionFingerprint } = require('./fingerprint/fingerprint');
|
|
15
15
|
const { ensureTxParametersHashIncludesFilter, patchValueSetExpandWholeSystemForOcl } = require('./shared/patches');
|
|
16
16
|
|
|
17
|
+
|
|
17
18
|
ensureTxParametersHashIncludesFilter(TxParameters);
|
|
18
19
|
//patchValueSetExpandWholeSystemForOcl();
|
|
19
20
|
|
|
@@ -31,6 +32,7 @@ function normalizeCanonicalSystem(system) {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
class OCLValueSetProvider extends AbstractValueSetProvider {
|
|
35
|
+
|
|
34
36
|
constructor(config = {}) {
|
|
35
37
|
super();
|
|
36
38
|
const options = typeof config === 'string' ? { baseUrl: config } : (config || {});
|
|
@@ -51,6 +53,9 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
|
|
|
51
53
|
this.pendingSourceCanonicalRequests = new Map();
|
|
52
54
|
this.collectionByCanonicalCache = new Map();
|
|
53
55
|
this.pendingCollectionByCanonicalRequests = new Map();
|
|
56
|
+
this._organizationIdsCache = null;
|
|
57
|
+
this._organizationIdsFetchPromise = null;
|
|
58
|
+
this._negativeUrlCache = new Set();
|
|
54
59
|
this._composePromises = new Map();
|
|
55
60
|
this.backgroundExpansionCache = new Map();
|
|
56
61
|
this.backgroundExpansionProgress = new Map();
|
|
@@ -77,7 +82,13 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
|
|
|
77
82
|
const data = await fs.readFile(filePath, 'utf-8');
|
|
78
83
|
const cached = JSON.parse(data);
|
|
79
84
|
|
|
80
|
-
if (!cached || !cached.canonicalUrl
|
|
85
|
+
if (!cached || !cached.canonicalUrl) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Support both old (expansion-based) and new (compose-based) cache formats
|
|
90
|
+
const compose = cached.compose || this.#composeFromExpansion(cached.expansion);
|
|
91
|
+
if (!compose || !Array.isArray(compose.include) || compose.include.length === 0) {
|
|
81
92
|
continue;
|
|
82
93
|
}
|
|
83
94
|
|
|
@@ -88,37 +99,19 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
|
|
|
88
99
|
);
|
|
89
100
|
const createdAt = cached.timestamp ? new Date(cached.timestamp).getTime() : null;
|
|
90
101
|
this.backgroundExpansionCache.set(cacheKey, {
|
|
91
|
-
|
|
102
|
+
compose,
|
|
92
103
|
metadataSignature: cached.metadataSignature || null,
|
|
93
104
|
dependencyChecksums: cached.dependencyChecksums || {},
|
|
94
105
|
createdAt: Number.isFinite(createdAt) ? createdAt : null
|
|
95
106
|
});
|
|
96
|
-
// Instancia ValueSet para garantir jsonObj
|
|
97
|
-
// Reconstrói compose.include se não existir
|
|
98
|
-
let compose = cached.expansion?.compose;
|
|
99
|
-
if (!compose || !Array.isArray(compose.include)) {
|
|
100
|
-
// Reconstrói a partir dos sistemas/códigos em expansion.contains
|
|
101
|
-
const systemConcepts = new Map();
|
|
102
|
-
if (Array.isArray(cached.expansion?.contains)) {
|
|
103
|
-
for (const entry of cached.expansion.contains) {
|
|
104
|
-
if (!entry.system || !entry.code) continue;
|
|
105
|
-
if (!systemConcepts.has(entry.system)) {
|
|
106
|
-
systemConcepts.set(entry.system, []);
|
|
107
|
-
}
|
|
108
|
-
systemConcepts.get(entry.system).push({ code: entry.code });
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
compose = { include: Array.from(systemConcepts.entries()).map(([system, concepts]) => ({ system, concept: concepts })) };
|
|
112
|
-
}
|
|
113
107
|
const valueSetObj = new ValueSet({
|
|
114
108
|
resourceType: 'ValueSet',
|
|
115
109
|
url: cached.canonicalUrl,
|
|
116
110
|
version: cached.version || null,
|
|
117
|
-
expansion: cached.expansion,
|
|
118
111
|
compose,
|
|
119
|
-
id: cached.canonicalUrl
|
|
112
|
+
id: cached.canonicalUrl
|
|
120
113
|
}, 'R5');
|
|
121
|
-
this.#
|
|
114
|
+
this.#applyCachedCompose(valueSetObj, paramsKey);
|
|
122
115
|
// Indexa o ValueSet restaurado para torná-lo disponível via fetchValueSet
|
|
123
116
|
this.valueSetMap.set(valueSetObj.url, valueSetObj);
|
|
124
117
|
if (valueSetObj.version) {
|
|
@@ -128,7 +121,6 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
|
|
|
128
121
|
this._idMap.set(valueSetObj.id, valueSetObj);
|
|
129
122
|
this.valueSetFingerprints.set(cacheKey, cached.fingerprint || null);
|
|
130
123
|
loadedCount++;
|
|
131
|
-
console.log(`[OCL-ValueSet] Loaded ValueSet from cold cache into memory: ${cached.canonicalUrl}`);
|
|
132
124
|
} catch (error) {
|
|
133
125
|
console.error(`[OCL-ValueSet] Failed to load cold cache file ${file}:`, error.message);
|
|
134
126
|
}
|
|
@@ -144,10 +136,10 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
|
|
|
144
136
|
}
|
|
145
137
|
}
|
|
146
138
|
|
|
147
|
-
async #saveColdCacheForValueSet(vs,
|
|
139
|
+
async #saveColdCacheForValueSet(vs, compose, metadataSignature, dependencyChecksums, paramsKey = 'default') {
|
|
148
140
|
const canonicalUrl = vs?.url;
|
|
149
141
|
const version = vs?.version || null;
|
|
150
|
-
if (!canonicalUrl || !
|
|
142
|
+
if (!canonicalUrl || !compose) {
|
|
151
143
|
return null;
|
|
152
144
|
}
|
|
153
145
|
|
|
@@ -156,22 +148,25 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
|
|
|
156
148
|
try {
|
|
157
149
|
await ensureCacheDirectories(CACHE_VS_DIR);
|
|
158
150
|
|
|
159
|
-
const
|
|
151
|
+
const conceptCount = Array.isArray(compose.include)
|
|
152
|
+
? compose.include.reduce((sum, inc) => sum + (Array.isArray(inc.concept) ? inc.concept.length : 0), 0)
|
|
153
|
+
: 0;
|
|
154
|
+
const fingerprint = computeValueSetExpansionFingerprint(compose);
|
|
160
155
|
const cacheData = {
|
|
161
156
|
canonicalUrl,
|
|
162
157
|
version,
|
|
163
158
|
paramsKey,
|
|
164
159
|
fingerprint,
|
|
165
160
|
timestamp: new Date().toISOString(),
|
|
166
|
-
conceptCount
|
|
167
|
-
|
|
161
|
+
conceptCount,
|
|
162
|
+
compose,
|
|
168
163
|
metadataSignature,
|
|
169
164
|
dependencyChecksums
|
|
170
165
|
};
|
|
171
166
|
|
|
172
167
|
await fs.writeFile(cacheFilePath, JSON.stringify(cacheData, null, 2), 'utf-8');
|
|
173
|
-
console.log(`[OCL-ValueSet] Saved ValueSet
|
|
174
|
-
|
|
168
|
+
console.log(`[OCL-ValueSet] Saved ValueSet compose to cold cache: ${canonicalUrl} (${conceptCount} concepts, fingerprint=${fingerprint?.substring(0, 8)})`);
|
|
169
|
+
|
|
175
170
|
return fingerprint;
|
|
176
171
|
} catch (error) {
|
|
177
172
|
console.error(`[OCL-ValueSet] Failed to save cold cache for ValueSet ${canonicalUrl}:`, error.message);
|
|
@@ -249,12 +244,15 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
|
|
|
249
244
|
async fetchValueSet(url, version) {
|
|
250
245
|
this._validateFetchParams(url, version);
|
|
251
246
|
|
|
247
|
+
// Fast path: skip URLs already known to not be in OCL
|
|
248
|
+
if (this._negativeUrlCache.has(url)) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
252
|
let key = `${url}|${version}`;
|
|
253
253
|
if (this.valueSetMap.has(key)) {
|
|
254
254
|
const vs = this.valueSetMap.get(key);
|
|
255
|
-
|
|
256
|
-
this.#clearInlineExpansion(vs);
|
|
257
|
-
console.log(`[OCL-ValueSet] fetchValueSet cache hit for ${url} (version: ${version || 'none'})`);
|
|
255
|
+
await this.#ensureComposeIncludes(vs);
|
|
258
256
|
this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset', userRequested: true });
|
|
259
257
|
return vs;
|
|
260
258
|
}
|
|
@@ -265,8 +263,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
|
|
|
265
263
|
key = `${url}|${majorMinor}`;
|
|
266
264
|
if (this.valueSetMap.has(key)) {
|
|
267
265
|
const vs = this.valueSetMap.get(key);
|
|
268
|
-
|
|
269
|
-
this.#clearInlineExpansion(vs);
|
|
266
|
+
await this.#ensureComposeIncludes(vs);
|
|
270
267
|
this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset-mm' });
|
|
271
268
|
return vs;
|
|
272
269
|
}
|
|
@@ -275,16 +272,14 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
|
|
|
275
272
|
|
|
276
273
|
if (this.valueSetMap.has(url)) {
|
|
277
274
|
const vs = this.valueSetMap.get(url);
|
|
278
|
-
|
|
279
|
-
this.#clearInlineExpansion(vs);
|
|
275
|
+
await this.#ensureComposeIncludes(vs);
|
|
280
276
|
this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset-url' });
|
|
281
277
|
return vs;
|
|
282
278
|
}
|
|
283
279
|
|
|
284
280
|
const resolved = await this.#resolveValueSetByCanonical(url, version);
|
|
285
281
|
if (resolved) {
|
|
286
|
-
|
|
287
|
-
this.#clearInlineExpansion(resolved);
|
|
282
|
+
await this.#ensureComposeIncludes(resolved);
|
|
288
283
|
this.#scheduleBackgroundExpansion(resolved, { reason: 'fetch-valueset-resolved' });
|
|
289
284
|
return resolved;
|
|
290
285
|
}
|
|
@@ -294,8 +289,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
|
|
|
294
289
|
key = `${url}|${version}`;
|
|
295
290
|
if (this.valueSetMap.has(key)) {
|
|
296
291
|
const vs = this.valueSetMap.get(key);
|
|
297
|
-
|
|
298
|
-
this.#clearInlineExpansion(vs);
|
|
292
|
+
await this.#ensureComposeIncludes(vs);
|
|
299
293
|
this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset-init' });
|
|
300
294
|
return vs;
|
|
301
295
|
}
|
|
@@ -306,8 +300,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
|
|
|
306
300
|
key = `${url}|${majorMinor}`;
|
|
307
301
|
if (this.valueSetMap.has(key)) {
|
|
308
302
|
const vs = this.valueSetMap.get(key);
|
|
309
|
-
|
|
310
|
-
this.#clearInlineExpansion(vs);
|
|
303
|
+
await this.#ensureComposeIncludes(vs);
|
|
311
304
|
this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset-init-mm' });
|
|
312
305
|
return vs;
|
|
313
306
|
}
|
|
@@ -316,20 +309,20 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
|
|
|
316
309
|
|
|
317
310
|
if (this.valueSetMap.has(url)) {
|
|
318
311
|
const vs = this.valueSetMap.get(url);
|
|
319
|
-
|
|
320
|
-
this.#clearInlineExpansion(vs);
|
|
312
|
+
await this.#ensureComposeIncludes(vs);
|
|
321
313
|
this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset-init-url' });
|
|
322
314
|
return vs;
|
|
323
315
|
}
|
|
324
316
|
|
|
317
|
+
// Remember that this URL is not in OCL to skip future lookups
|
|
318
|
+
this._negativeUrlCache.add(url);
|
|
325
319
|
return null;
|
|
326
320
|
}
|
|
327
321
|
|
|
328
322
|
async fetchValueSetById(id) {
|
|
329
323
|
const local = this.#getLocalValueSetById(id);
|
|
330
324
|
if (local) {
|
|
331
|
-
|
|
332
|
-
this.#clearInlineExpansion(local);
|
|
325
|
+
await this.#ensureComposeIncludes(local);
|
|
333
326
|
this.#scheduleBackgroundExpansion(local, { reason: 'fetch-valueset-by-id' });
|
|
334
327
|
return local;
|
|
335
328
|
}
|
|
@@ -337,8 +330,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
|
|
|
337
330
|
await this.initialize();
|
|
338
331
|
|
|
339
332
|
const vs = this.#getLocalValueSetById(id);
|
|
340
|
-
|
|
341
|
-
this.#clearInlineExpansion(vs);
|
|
333
|
+
await this.#ensureComposeIncludes(vs);
|
|
342
334
|
this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset-by-id-init' });
|
|
343
335
|
return vs;
|
|
344
336
|
}
|
|
@@ -901,6 +893,24 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
|
|
|
901
893
|
}
|
|
902
894
|
}
|
|
903
895
|
|
|
896
|
+
#composeFromExpansion(expansion) {
|
|
897
|
+
if (!expansion || !Array.isArray(expansion.contains)) {
|
|
898
|
+
return null;
|
|
899
|
+
}
|
|
900
|
+
const systemConcepts = new Map();
|
|
901
|
+
for (const entry of expansion.contains) {
|
|
902
|
+
if (!entry.system || !entry.code) continue;
|
|
903
|
+
if (!systemConcepts.has(entry.system)) {
|
|
904
|
+
systemConcepts.set(entry.system, []);
|
|
905
|
+
}
|
|
906
|
+
systemConcepts.get(entry.system).push({ code: entry.code });
|
|
907
|
+
}
|
|
908
|
+
if (systemConcepts.size === 0) {
|
|
909
|
+
return null;
|
|
910
|
+
}
|
|
911
|
+
return { include: Array.from(systemConcepts.entries()).map(([system, concepts]) => ({ system, concept: concepts })) };
|
|
912
|
+
}
|
|
913
|
+
|
|
904
914
|
#expansionCacheKey(vs, paramsKey) {
|
|
905
915
|
const base = this.#valueSetBaseKey(vs);
|
|
906
916
|
if (!base) {
|
|
@@ -924,7 +934,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
|
|
|
924
934
|
}
|
|
925
935
|
}
|
|
926
936
|
|
|
927
|
-
#
|
|
937
|
+
#applyCachedCompose(vs, paramsKey) {
|
|
928
938
|
if (!vs || !vs.jsonObj) {
|
|
929
939
|
return;
|
|
930
940
|
}
|
|
@@ -935,25 +945,20 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
|
|
|
935
945
|
}
|
|
936
946
|
|
|
937
947
|
const cached = this.backgroundExpansionCache.get(cacheKey);
|
|
938
|
-
if (!cached || !cached.
|
|
948
|
+
if (!cached || !cached.compose) {
|
|
939
949
|
return;
|
|
940
950
|
}
|
|
941
951
|
|
|
942
952
|
if (!this.#isCachedExpansionValid(vs, cached)) {
|
|
943
953
|
this.backgroundExpansionCache.delete(cacheKey);
|
|
944
|
-
if (vs.jsonObj.expansion) {
|
|
945
|
-
delete vs.jsonObj.expansion;
|
|
946
|
-
}
|
|
947
|
-
console.log(`[OCL-ValueSet] Cached ValueSet expansion invalidated: ${cacheKey}`);
|
|
948
954
|
return;
|
|
949
955
|
}
|
|
950
956
|
|
|
951
|
-
if (vs.jsonObj.
|
|
957
|
+
if (vs.jsonObj.compose && Array.isArray(vs.jsonObj.compose.include) && vs.jsonObj.compose.include.length > 0) {
|
|
952
958
|
return;
|
|
953
959
|
}
|
|
954
960
|
|
|
955
|
-
vs.jsonObj.
|
|
956
|
-
console.log(`[OCL-ValueSet] ValueSet expansion restored from cache: ${cacheKey}`);
|
|
961
|
+
vs.jsonObj.compose = structuredClone(cached.compose);
|
|
957
962
|
}
|
|
958
963
|
|
|
959
964
|
#scheduleBackgroundExpansion(vs, options = {}) {
|
|
@@ -970,13 +975,10 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
|
|
|
970
975
|
const cached = this.backgroundExpansionCache.get(cacheKey);
|
|
971
976
|
if (cached && !this.#isCachedExpansionValid(vs, cached)) {
|
|
972
977
|
this.backgroundExpansionCache.delete(cacheKey);
|
|
973
|
-
if (vs.jsonObj.expansion) {
|
|
974
|
-
delete vs.jsonObj.expansion;
|
|
975
|
-
}
|
|
976
|
-
console.log(`[OCL-ValueSet] Cached ValueSet expansion invalidated: ${cacheKey}`);
|
|
977
978
|
}
|
|
978
979
|
|
|
979
|
-
|
|
980
|
+
// Already have a cached compose ready
|
|
981
|
+
if (cached && cached.compose) {
|
|
980
982
|
return;
|
|
981
983
|
}
|
|
982
984
|
|
|
@@ -996,23 +998,15 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
|
|
|
996
998
|
: cacheAgeFromFileMs != null
|
|
997
999
|
? 'file'
|
|
998
1000
|
: 'metadata';
|
|
999
|
-
console.log(`[OCL-ValueSet] Skipping warm-up for ValueSet ${vs.url} (cold cache age: ${formatCacheAgeMinutes(freshestCacheAgeMs)})`);
|
|
1000
|
-
console.log(`[OCL-ValueSet] ValueSet cold cache is fresh, not enqueueing warm-up job (${cacheKey}, source=${freshnessSource})`);
|
|
1001
1001
|
return;
|
|
1002
1002
|
}
|
|
1003
1003
|
|
|
1004
1004
|
const jobKey = `vs:${cacheKey}`;
|
|
1005
1005
|
if (OCLBackgroundJobQueue.isQueuedOrRunning(jobKey)) {
|
|
1006
|
-
console.log(`[OCL-ValueSet] ValueSet expansion already queued or running: ${cacheKey}`);
|
|
1007
1006
|
return;
|
|
1008
1007
|
}
|
|
1009
1008
|
|
|
1010
1009
|
let queuedJobSize = null;
|
|
1011
|
-
const warmupAgeText = freshestCacheAgeMs != null
|
|
1012
|
-
? formatCacheAgeMinutes(freshestCacheAgeMs)
|
|
1013
|
-
: 'no cold cache';
|
|
1014
|
-
console.log(`[OCL-ValueSet] Enqueueing warm-up for ValueSet ${vs.url} (cold cache age: ${warmupAgeText})`);
|
|
1015
|
-
console.log(`[OCL-ValueSet] ValueSet expansion enqueued: ${cacheKey}`);
|
|
1016
1010
|
OCLBackgroundJobQueue.enqueue(
|
|
1017
1011
|
jobKey,
|
|
1018
1012
|
'ValueSet expansion',
|
|
@@ -1033,7 +1027,6 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
|
|
|
1033
1027
|
}
|
|
1034
1028
|
|
|
1035
1029
|
async #runBackgroundExpansion(vs, cacheKey, paramsKey = 'default', knownConceptCount = null) {
|
|
1036
|
-
console.log(`[OCL-ValueSet] ValueSet expansion started: ${cacheKey}`);
|
|
1037
1030
|
const progressState = { processed: 0, total: null };
|
|
1038
1031
|
this.backgroundExpansionProgress.set(cacheKey, progressState);
|
|
1039
1032
|
try {
|
|
@@ -1053,51 +1046,46 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
|
|
|
1053
1046
|
);
|
|
1054
1047
|
}
|
|
1055
1048
|
|
|
1056
|
-
const
|
|
1057
|
-
if (!
|
|
1049
|
+
const compose = await this.#buildBackgroundCompose(vs, progressState);
|
|
1050
|
+
if (!compose) {
|
|
1058
1051
|
return;
|
|
1059
1052
|
}
|
|
1060
1053
|
|
|
1061
|
-
|
|
1054
|
+
const conceptCount = Array.isArray(compose.include)
|
|
1055
|
+
? compose.include.reduce((sum, inc) => sum + (Array.isArray(inc.concept) ? inc.concept.length : 0), 0)
|
|
1056
|
+
: 0;
|
|
1057
|
+
progressState.processed = conceptCount || progressState.processed;
|
|
1062
1058
|
if (typeof progressState.total !== 'number' || !Number.isFinite(progressState.total) || progressState.total <= 0) {
|
|
1063
|
-
progressState.total =
|
|
1059
|
+
progressState.total = conceptCount || 0;
|
|
1064
1060
|
}
|
|
1065
1061
|
|
|
1066
1062
|
const metadataSignature = this.#valueSetMetadataSignature(vs);
|
|
1067
1063
|
const dependencyChecksums = this.#valueSetDependencyChecksums(vs);
|
|
1068
1064
|
|
|
1069
|
-
// Compute
|
|
1070
|
-
const newFingerprint = computeValueSetExpansionFingerprint(
|
|
1065
|
+
// Compute fingerprint and compare with cold cache
|
|
1066
|
+
const newFingerprint = computeValueSetExpansionFingerprint(compose);
|
|
1071
1067
|
const oldFingerprint = this.valueSetFingerprints.get(cacheKey);
|
|
1072
1068
|
|
|
1073
|
-
if (oldFingerprint
|
|
1074
|
-
|
|
1075
|
-
} else {
|
|
1076
|
-
if (oldFingerprint) {
|
|
1077
|
-
console.log(`[OCL-ValueSet] ValueSet expansion fingerprint changed: ${cacheKey} (${oldFingerprint?.substring(0, 8)} -> ${newFingerprint?.substring(0, 8)})`);
|
|
1078
|
-
console.log(`[OCL-ValueSet] Replacing cold cache with new hot cache: ${cacheKey}`);
|
|
1079
|
-
} else {
|
|
1080
|
-
console.log(`[OCL-ValueSet] Computed fingerprint for ValueSet expansion: ${cacheKey} (fingerprint=${newFingerprint?.substring(0, 8)})`);
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
// Save to cold cache
|
|
1084
|
-
const savedFingerprint = await this.#saveColdCacheForValueSet(vs, expansion, metadataSignature, dependencyChecksums, paramsKey);
|
|
1069
|
+
if (!oldFingerprint || newFingerprint !== oldFingerprint) {
|
|
1070
|
+
const savedFingerprint = await this.#saveColdCacheForValueSet(vs, compose, metadataSignature, dependencyChecksums, paramsKey);
|
|
1085
1071
|
if (savedFingerprint) {
|
|
1086
1072
|
this.valueSetFingerprints.set(cacheKey, savedFingerprint);
|
|
1087
1073
|
}
|
|
1088
1074
|
}
|
|
1089
1075
|
|
|
1090
1076
|
this.backgroundExpansionCache.set(cacheKey, {
|
|
1091
|
-
|
|
1077
|
+
compose,
|
|
1092
1078
|
metadataSignature,
|
|
1093
1079
|
dependencyChecksums,
|
|
1094
1080
|
createdAt: Date.now()
|
|
1095
1081
|
});
|
|
1096
|
-
// Keep expansions in provider-managed cache only.
|
|
1097
|
-
// Inline expansion on ValueSet bypasses $expand filtering in worker pipeline.
|
|
1098
1082
|
|
|
1099
|
-
|
|
1100
|
-
|
|
1083
|
+
// Apply compose to the ValueSet so the expand engine can use it
|
|
1084
|
+
vs.jsonObj.compose = structuredClone(compose);
|
|
1085
|
+
// Ensure no stale inline expansion
|
|
1086
|
+
delete vs.jsonObj.expansion;
|
|
1087
|
+
|
|
1088
|
+
console.log(`[OCL-ValueSet] compose cached: ${vs.url} (${conceptCount} concepts)`);
|
|
1101
1089
|
} catch (error) {
|
|
1102
1090
|
console.error(`[OCL-ValueSet] ValueSet background expansion failed: ${cacheKey}: ${error.message}`);
|
|
1103
1091
|
} finally {
|
|
@@ -1105,17 +1093,15 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
|
|
|
1105
1093
|
}
|
|
1106
1094
|
}
|
|
1107
1095
|
|
|
1108
|
-
async #
|
|
1096
|
+
async #buildBackgroundCompose(vs, progressState = null) {
|
|
1109
1097
|
const meta = this.#getCollectionMeta(vs);
|
|
1110
1098
|
if (!meta || !meta.conceptsUrl) {
|
|
1111
1099
|
return null;
|
|
1112
1100
|
}
|
|
1113
1101
|
|
|
1114
|
-
|
|
1115
|
-
let offset = 0; // Moved this line up
|
|
1116
|
-
// Agrupa conceitos por system
|
|
1102
|
+
let offset = 0;
|
|
1117
1103
|
const systemConcepts = new Map();
|
|
1118
|
-
|
|
1104
|
+
let totalCount = 0;
|
|
1119
1105
|
while (true) {
|
|
1120
1106
|
const batch = await this.#fetchCollectionConcepts(meta, {
|
|
1121
1107
|
count: CONCEPT_PAGE_SIZE,
|
|
@@ -1132,55 +1118,30 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
|
|
|
1132
1118
|
if (!entry?.system || !entry?.code) {
|
|
1133
1119
|
continue;
|
|
1134
1120
|
}
|
|
1135
|
-
const out = {
|
|
1136
|
-
system: entry.system,
|
|
1137
|
-
code: entry.code
|
|
1138
|
-
};
|
|
1139
|
-
if (entry.display) {
|
|
1140
|
-
out.display = entry.display;
|
|
1141
|
-
}
|
|
1142
|
-
if (entry.definition) {
|
|
1143
|
-
out.definition = entry.definition;
|
|
1144
|
-
}
|
|
1145
|
-
if (entry.inactive === true) {
|
|
1146
|
-
out.inactive = true;
|
|
1147
|
-
}
|
|
1148
|
-
if (Array.isArray(entry.designation) && entry.designation.length > 0) {
|
|
1149
|
-
out.designation = entry.designation
|
|
1150
|
-
.filter(d => d && d.value)
|
|
1151
|
-
.map(d => ({
|
|
1152
|
-
language: d.language,
|
|
1153
|
-
value: d.value
|
|
1154
|
-
}));
|
|
1155
|
-
}
|
|
1156
|
-
contains.push(out);
|
|
1157
|
-
// Agrupa por system
|
|
1158
1121
|
if (!systemConcepts.has(entry.system)) {
|
|
1159
1122
|
systemConcepts.set(entry.system, []);
|
|
1160
1123
|
}
|
|
1161
1124
|
systemConcepts.get(entry.system).push(entry.code);
|
|
1125
|
+
totalCount++;
|
|
1162
1126
|
}
|
|
1163
1127
|
if (progressState) {
|
|
1164
|
-
progressState.processed =
|
|
1128
|
+
progressState.processed = totalCount;
|
|
1165
1129
|
}
|
|
1166
1130
|
if (entries.length < CONCEPT_PAGE_SIZE) {
|
|
1167
1131
|
break;
|
|
1168
1132
|
}
|
|
1169
1133
|
offset += entries.length;
|
|
1170
1134
|
}
|
|
1171
|
-
|
|
1172
|
-
if (
|
|
1173
|
-
|
|
1135
|
+
|
|
1136
|
+
if (systemConcepts.size === 0) {
|
|
1137
|
+
return null;
|
|
1174
1138
|
}
|
|
1175
|
-
|
|
1176
|
-
system,
|
|
1177
|
-
concept: codes.map(code => ({ code }))
|
|
1178
|
-
}));
|
|
1139
|
+
|
|
1179
1140
|
return {
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1141
|
+
include: Array.from(systemConcepts.entries()).map(([system, codes]) => ({
|
|
1142
|
+
system,
|
|
1143
|
+
concept: codes.map(code => ({ code }))
|
|
1144
|
+
}))
|
|
1184
1145
|
};
|
|
1185
1146
|
}
|
|
1186
1147
|
|
|
@@ -1354,36 +1315,55 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
|
|
|
1354
1315
|
}
|
|
1355
1316
|
|
|
1356
1317
|
async #fetchOrganizationIds() {
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1318
|
+
// Return cached result if available
|
|
1319
|
+
if (this._organizationIdsCache) {
|
|
1320
|
+
return this._organizationIdsCache;
|
|
1321
|
+
}
|
|
1360
1322
|
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
continue;
|
|
1366
|
-
}
|
|
1323
|
+
// Deduplicate concurrent requests
|
|
1324
|
+
if (this._organizationIdsFetchPromise) {
|
|
1325
|
+
return this._organizationIdsFetchPromise;
|
|
1326
|
+
}
|
|
1367
1327
|
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1328
|
+
this._organizationIdsFetchPromise = (async () => {
|
|
1329
|
+
const endpoint = '/orgs/';
|
|
1330
|
+
console.log(`[OCL-ValueSet] Loading organizations from: ${this.baseUrl}${endpoint}`);
|
|
1331
|
+
const orgs = await this.#fetchAllPages(endpoint);
|
|
1332
|
+
|
|
1333
|
+
const ids = [];
|
|
1334
|
+
const seen = new Set();
|
|
1335
|
+
for (const org of orgs || []) {
|
|
1336
|
+
if (!org || typeof org !== 'object') {
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
const id = org.id || org.mnemonic || org.short_code || org.shortCode || org.name || null;
|
|
1341
|
+
if (!id) {
|
|
1342
|
+
continue;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
const normalized = String(id).trim();
|
|
1346
|
+
if (!normalized || seen.has(normalized)) {
|
|
1347
|
+
continue;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
seen.add(normalized);
|
|
1351
|
+
ids.push(normalized);
|
|
1371
1352
|
}
|
|
1372
1353
|
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
continue;
|
|
1354
|
+
if (ids.length === 0 && this.org) {
|
|
1355
|
+
ids.push(this.org);
|
|
1376
1356
|
}
|
|
1377
1357
|
|
|
1378
|
-
|
|
1379
|
-
ids
|
|
1380
|
-
}
|
|
1358
|
+
this._organizationIdsCache = ids;
|
|
1359
|
+
return ids;
|
|
1360
|
+
})();
|
|
1381
1361
|
|
|
1382
|
-
|
|
1383
|
-
|
|
1362
|
+
try {
|
|
1363
|
+
return await this._organizationIdsFetchPromise;
|
|
1364
|
+
} finally {
|
|
1365
|
+
this._organizationIdsFetchPromise = null;
|
|
1384
1366
|
}
|
|
1385
|
-
|
|
1386
|
-
return ids;
|
|
1387
1367
|
}
|
|
1388
1368
|
|
|
1389
1369
|
#collectionIdentity(collection) {
|
package/tx/operation-context.js
CHANGED
|
@@ -154,7 +154,7 @@ class ResourceCache {
|
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
if (this.stats) {
|
|
157
|
-
this.stats.
|
|
157
|
+
this.stats.taskDone("Client Cache", `Pruned ${i} of ${this.cache.size} entries`);
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
160
|
|
|
@@ -347,12 +347,12 @@ class ExpansionCache {
|
|
|
347
347
|
if (heapUsed > this.memoryThresholdBytes) {
|
|
348
348
|
const i = this.evictOldestHalf();
|
|
349
349
|
if (this.stats) {
|
|
350
|
-
this.stats.
|
|
350
|
+
this.stats.taskDone('Expansion Cache', `Checked Memory Pressure: evicted half (${i} entries)`);
|
|
351
351
|
}
|
|
352
352
|
return true;
|
|
353
353
|
}
|
|
354
354
|
if (this.stats) {
|
|
355
|
-
this.stats.
|
|
355
|
+
this.stats.taskDone('Expansion Cache', `Checked Memory Pressure - OK (${this.cache.size} entries)`);
|
|
356
356
|
}
|
|
357
357
|
return false;
|
|
358
358
|
}
|
package/tx/params.js
CHANGED
|
@@ -114,11 +114,11 @@ class TxParameters {
|
|
|
114
114
|
break;
|
|
115
115
|
}
|
|
116
116
|
case 'force-valueset-version': {
|
|
117
|
-
this.seeVersionRule(
|
|
117
|
+
this.seeVersionRule(getValuePrimitive(p), true, 'override');
|
|
118
118
|
break;
|
|
119
119
|
}
|
|
120
120
|
case 'check-valueset-version': {
|
|
121
|
-
this.seeVersionRule(
|
|
121
|
+
this.seeVersionRule(getValuePrimitive(p), true, 'check');
|
|
122
122
|
break;
|
|
123
123
|
}
|
|
124
124
|
|
package/tx/provider.js
CHANGED
|
@@ -228,7 +228,10 @@ class Provider {
|
|
|
228
228
|
for (let csp of this.codeSystemFactories.values()) {
|
|
229
229
|
if (!uris.has(csp.system())) {
|
|
230
230
|
uris.add(csp.system());
|
|
231
|
-
await csp.findImplicitConceptMap(url, version);
|
|
231
|
+
let cm = await csp.findImplicitConceptMap(url, version);
|
|
232
|
+
if (cm) {
|
|
233
|
+
return cm;
|
|
234
|
+
}
|
|
232
235
|
}
|
|
233
236
|
}
|
|
234
237
|
}
|
|
@@ -269,9 +272,9 @@ class Provider {
|
|
|
269
272
|
|
|
270
273
|
|
|
271
274
|
|
|
272
|
-
async findConceptMapForTranslation(opContext, conceptMaps, sourceSystem, sourceScope, targetScope, targetSystem) {
|
|
275
|
+
async findConceptMapForTranslation(opContext, conceptMaps, sourceSystem, sourceScope, targetScope, targetSystem, sourceCode = null) {
|
|
273
276
|
for (let cmp of this.conceptMapProviders) {
|
|
274
|
-
await cmp.findConceptMapForTranslation(opContext, conceptMaps, sourceSystem, sourceScope, targetScope, targetSystem);
|
|
277
|
+
await cmp.findConceptMapForTranslation(opContext, conceptMaps, sourceSystem, sourceScope, targetScope, targetSystem, sourceCode);
|
|
275
278
|
}
|
|
276
279
|
if (sourceSystem && targetSystem) {
|
|
277
280
|
let uris = new Set();
|
package/tx/sct/structures.js
CHANGED
|
@@ -581,10 +581,15 @@ class SnomedConceptList {
|
|
|
581
581
|
return { found: result, index };
|
|
582
582
|
}
|
|
583
583
|
|
|
584
|
+
getConceptByCount(index) {
|
|
585
|
+
return this.getConcept(index * SnomedConceptList.CONCEPT_SIZE);
|
|
586
|
+
}
|
|
587
|
+
|
|
584
588
|
getConcept(index) {
|
|
585
589
|
this.#checkPostBuildAccess(index);
|
|
586
590
|
|
|
587
591
|
return {
|
|
592
|
+
index : index,
|
|
588
593
|
identity: this.master.readBigUInt64LE(index + 0),
|
|
589
594
|
flags: this.master.readUInt8(index + 8),
|
|
590
595
|
parents: this.master.readUInt32LE(index + 9),
|
|
@@ -1172,7 +1177,7 @@ class SnomedReferenceSetIndex {
|
|
|
1172
1177
|
}
|
|
1173
1178
|
}
|
|
1174
1179
|
|
|
1175
|
-
return
|
|
1180
|
+
return -1; // Not found
|
|
1176
1181
|
}
|
|
1177
1182
|
|
|
1178
1183
|
count() {
|
package/tx/tx.fhir.org.yml
CHANGED