fhirsmith 0.4.2 → 0.5.0

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.
Files changed (92) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -1
  3. package/library/cron-utilities.js +136 -0
  4. package/library/html-server.js +13 -29
  5. package/library/html.js +3 -8
  6. package/library/languages.js +160 -37
  7. package/library/package-manager.js +48 -1
  8. package/library/utilities.js +100 -19
  9. package/package.json +2 -2
  10. package/packages/package-crawler.js +6 -1
  11. package/packages/packages.js +38 -54
  12. package/publisher/publisher.js +19 -27
  13. package/registry/api.js +11 -10
  14. package/registry/crawler.js +31 -29
  15. package/registry/model.js +5 -26
  16. package/registry/registry.js +32 -41
  17. package/server.js +53 -5
  18. package/shl/shl.js +0 -18
  19. package/static/assets/js/statuspage.js +1 -9
  20. package/stats.js +39 -1
  21. package/token/token.js +14 -9
  22. package/translations/Messages.properties +2 -1
  23. package/tx/README.md +17 -6
  24. package/tx/cs/cs-api.js +19 -1
  25. package/tx/cs/cs-base.js +77 -0
  26. package/tx/cs/cs-country.js +46 -0
  27. package/tx/cs/cs-cpt.js +9 -5
  28. package/tx/cs/cs-cs.js +27 -13
  29. package/tx/cs/cs-lang.js +60 -22
  30. package/tx/cs/cs-loinc.js +69 -98
  31. package/tx/cs/cs-mimetypes.js +4 -0
  32. package/tx/cs/cs-ndc.js +6 -0
  33. package/tx/cs/cs-omop.js +16 -15
  34. package/tx/cs/cs-rxnorm.js +23 -1
  35. package/tx/cs/cs-snomed.js +283 -40
  36. package/tx/cs/cs-ucum.js +90 -70
  37. package/tx/importers/import-sct.module.js +371 -35
  38. package/tx/importers/readme.md +117 -7
  39. package/tx/library/bundle.js +5 -0
  40. package/tx/library/capabilitystatement.js +3 -142
  41. package/tx/library/codesystem.js +19 -173
  42. package/tx/library/conceptmap.js +4 -218
  43. package/tx/library/designations.js +14 -1
  44. package/tx/library/extensions.js +7 -0
  45. package/tx/library/namingsystem.js +3 -89
  46. package/tx/library/operation-outcome.js +8 -3
  47. package/tx/library/parameters.js +3 -2
  48. package/tx/library/renderer.js +10 -6
  49. package/tx/library/terminologycapabilities.js +3 -243
  50. package/tx/library/valueset.js +3 -235
  51. package/tx/library.js +100 -13
  52. package/tx/operation-context.js +23 -4
  53. package/tx/params.js +35 -38
  54. package/tx/provider.js +6 -5
  55. package/tx/sct/expressions.js +12 -3
  56. package/tx/tx-html.js +80 -89
  57. package/tx/tx.fhir.org.yml +6 -5
  58. package/tx/tx.js +163 -13
  59. package/tx/vs/vs-database.js +56 -39
  60. package/tx/vs/vs-package.js +21 -2
  61. package/tx/vs/vs-vsac.js +175 -39
  62. package/tx/workers/batch-validate.js +2 -0
  63. package/tx/workers/batch.js +2 -0
  64. package/tx/workers/expand.js +132 -112
  65. package/tx/workers/lookup.js +33 -14
  66. package/tx/workers/metadata.js +2 -2
  67. package/tx/workers/read.js +3 -2
  68. package/tx/workers/related.js +574 -0
  69. package/tx/workers/search.js +46 -9
  70. package/tx/workers/subsumes.js +13 -3
  71. package/tx/workers/translate.js +7 -3
  72. package/tx/workers/validate.js +258 -285
  73. package/tx/workers/worker.js +43 -39
  74. package/tx/xml/bundle-xml.js +237 -0
  75. package/tx/xml/xml-base.js +215 -64
  76. package/tx/xversion/xv-bundle.js +71 -0
  77. package/tx/xversion/xv-capabiliityStatement.js +137 -0
  78. package/tx/xversion/xv-codesystem.js +169 -0
  79. package/tx/xversion/xv-conceptmap.js +224 -0
  80. package/tx/xversion/xv-namingsystem.js +88 -0
  81. package/tx/xversion/xv-operationoutcome.js +27 -0
  82. package/tx/xversion/xv-parameters.js +87 -0
  83. package/tx/xversion/xv-resource.js +45 -0
  84. package/tx/xversion/xv-terminologyCapabilities.js +214 -0
  85. package/tx/xversion/xv-valueset.js +234 -0
  86. package/utilities/dev-proxy-server.js +126 -0
  87. package/utilities/explode-results.js +58 -0
  88. package/utilities/split-by-system.js +198 -0
  89. package/utilities/vsac-cs-fetcher.js +0 -0
  90. package/{windows-install.js → utilities/windows-install.js} +2 -0
  91. package/vcl/vcl.js +0 -18
  92. package/xig/xig.js +108 -99
@@ -1,6 +1,5 @@
1
1
  const {CanonicalResource} = require("./canonical-resource");
2
- const {getValueName} = require("../../library/utilities");
3
- const {VersionUtilities} = require("../../library/version-utilities");
2
+ const {valueSetToR5, valueSetFromR5} = require("../xversion/xv-valueset");
4
3
 
5
4
  /**
6
5
  * Represents a FHIR ValueSet resource with version conversion support
@@ -23,7 +22,7 @@ class ValueSet extends CanonicalResource {
23
22
  constructor(jsonObj, fhirVersion = 'R5') {
24
23
  super(jsonObj, fhirVersion);
25
24
  // Convert to R5 format internally (modifies input for performance)
26
- this.jsonObj = this._convertToR5(jsonObj, fhirVersion);
25
+ this.jsonObj = valueSetToR5(jsonObj, fhirVersion);
27
26
  this.validate();
28
27
  this.buildMaps();
29
28
  }
@@ -51,241 +50,10 @@ class ValueSet extends CanonicalResource {
51
50
  * @returns {string} JSON string
52
51
  */
53
52
  toJSONString(version = 'R5') {
54
- const outputObj = this.convertFromR5(this.jsonObj, version);
53
+ const outputObj = valueSetFromR5(this.jsonObj, version);
55
54
  return JSON.stringify(outputObj);
56
55
  }
57
56
 
58
- /**
59
- * Converts input ValueSet to R5 format (modifies input object for performance)
60
- * @param {Object} jsonObj - The input ValueSet object
61
- * @param {string} version - Source FHIR version
62
- * @returns {Object} The same object, potentially modified to R5 format
63
- * @private
64
- */
65
- _convertToR5(jsonObj, version) {
66
- if (version === 'R5') {
67
- return jsonObj; // Already R5, no conversion needed
68
- }
69
-
70
- if (version === 'R3') {
71
- // R3 to R5: Remove extensible field (we ignore it completely)
72
- if (jsonObj.extensible !== undefined) {
73
- delete jsonObj.extensible;
74
- }
75
- return jsonObj;
76
- }
77
-
78
- if (version === 'R4') {
79
- // R4 to R5: No structural conversion needed
80
- // R5 is backward compatible for the structural elements we care about
81
- return jsonObj;
82
- }
83
-
84
- throw new Error(`Unsupported FHIR version: ${version}`);
85
- }
86
-
87
- /**
88
- * Converts R5 ValueSet to target version format (clones object first)
89
- * @param {Object} r5Obj - The R5 format ValueSet object
90
- * @param {string} targetVersion - Target FHIR version
91
- * @returns {Object} New object in target version format
92
- * @private
93
- */
94
- convertFromR5(r5Obj, targetVersion) {
95
- if (VersionUtilities.isR5Ver(targetVersion)) {
96
- return r5Obj; // No conversion needed
97
- }
98
-
99
- // Clone the object to avoid modifying the original
100
- const cloned = JSON.parse(JSON.stringify(r5Obj));
101
-
102
- if (VersionUtilities.isR4Ver(targetVersion)) {
103
- return this._convertR5ToR4(cloned);
104
- } else if (VersionUtilities.isR3Ver(targetVersion)) {
105
- return this._convertR5ToR3(cloned);
106
- }
107
-
108
- throw new Error(`Unsupported target FHIR version: ${targetVersion}`);
109
- }
110
-
111
- /**
112
- * Converts R5 ValueSet to R4 format
113
- * @param {Object} r5Obj - Cloned R5 ValueSet object
114
- * @returns {Object} R4 format ValueSet
115
- * @private
116
- */
117
- _convertR5ToR4(r5Obj) {
118
- // Remove R5-specific elements that don't exist in R4
119
- if (r5Obj.versionAlgorithmString) {
120
- delete r5Obj.versionAlgorithmString;
121
- }
122
- if (r5Obj.versionAlgorithmCoding) {
123
- delete r5Obj.versionAlgorithmCoding;
124
- }
125
-
126
- // Filter out R5-only filter operators in compose
127
- if (r5Obj.compose && r5Obj.compose.include) {
128
- r5Obj.compose.include = r5Obj.compose.include.map(include => {
129
- if (include.filter && Array.isArray(include.filter)) {
130
- include.filter = include.filter.map(filter => {
131
- if (filter.op && this._isR5OnlyFilterOperator(filter.op)) {
132
- // Remove R5-only operators
133
- return null;
134
- }
135
- return filter;
136
- }).filter(filter => filter !== null);
137
- }
138
- return include;
139
- });
140
- }
141
-
142
- if (r5Obj.compose && r5Obj.compose.exclude) {
143
- r5Obj.compose.exclude = r5Obj.compose.exclude.map(exclude => {
144
- if (exclude.filter && Array.isArray(exclude.filter)) {
145
- exclude.filter = exclude.filter.map(filter => {
146
- if (filter.op && this._isR5OnlyFilterOperator(filter.op)) {
147
- // Remove R5-only operators
148
- return null;
149
- }
150
- return filter;
151
- }).filter(filter => filter !== null);
152
- }
153
- return exclude;
154
- });
155
- }
156
-
157
- if (r5Obj.expansion) {
158
- let exp = r5Obj.expansion;
159
-
160
- // Convert ValueSet.expansion.property to extensions
161
- if (exp.property && exp.property.length > 0) {
162
- exp.extension = exp.extension || [];
163
- for (let prop of exp.property) {
164
- exp.extension.push({
165
- url: "http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.expansion.property",
166
- extension: [
167
- { url: "code", valueCode: prop.code },
168
- { url: "uri", valueUri: prop.uri }
169
- ]
170
- });
171
- }
172
- delete exp.property;
173
- this.convertContainsPropertyR5ToR4(exp.contains);
174
-
175
- }
176
- }
177
-
178
- return r5Obj;
179
- }
180
-
181
- // Recursive function to convert contains.property
182
- convertContainsPropertyR5ToR4(containsList) {
183
- if (!containsList) return;
184
-
185
- for (let item of containsList) {
186
- if (item.property && item.property.length > 0) {
187
- item.extension = item.extension || [];
188
- for (let prop of item.property) {
189
- let ext = {
190
- url: "http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.expansion.contains.property",
191
- extension: [
192
- { url: "code", valueCode: prop.code }
193
- ]
194
- };
195
- let pn = getValueName(prop);
196
- let subExt = { url: "value" };
197
- subExt[pn] = prop[pn];
198
- ext.extension.push(subExt);
199
- item.extension.push(ext);
200
- }
201
- delete item.property;
202
- }
203
-
204
- // Recurse into nested contains
205
- if (item.contains) {
206
- this.convertContainsPropertyR5ToR4(item.contains);
207
- }
208
- }
209
- }
210
-
211
- /**
212
- * Converts R5 ValueSet to R3 format
213
- * @param {Object} r5Obj - Cloned R5 ValueSet object
214
- * @returns {Object} R3 format ValueSet
215
- * @private
216
- */
217
- _convertR5ToR3(r5Obj) {
218
- // First apply R4 conversions
219
- const r4Obj = this._convertR5ToR4(r5Obj);
220
-
221
- // R3 has more limited filter operator support
222
- if (r4Obj.compose && r4Obj.compose.include) {
223
- r4Obj.compose.include = r4Obj.compose.include.map(include => {
224
- if (include.filter && Array.isArray(include.filter)) {
225
- include.filter = include.filter.map(filter => {
226
- if (filter.op && !this._isR3CompatibleFilterOperator(filter.op)) {
227
- // Remove non-R3-compatible operators
228
- return null;
229
- }
230
- return filter;
231
- }).filter(filter => filter !== null);
232
- }
233
- return include;
234
- });
235
- }
236
-
237
- if (r4Obj.compose && r4Obj.compose.exclude) {
238
- r4Obj.compose.exclude = r4Obj.compose.exclude.map(exclude => {
239
- if (exclude.filter && Array.isArray(exclude.filter)) {
240
- exclude.filter = exclude.filter.map(filter => {
241
- if (filter.op && !this._isR3CompatibleFilterOperator(filter.op)) {
242
- // Remove non-R3-compatible operators
243
- return null;
244
- }
245
- return filter;
246
- }).filter(filter => filter !== null);
247
- }
248
- return exclude;
249
- });
250
- }
251
-
252
- return r4Obj;
253
- }
254
-
255
- /**
256
- * Checks if a filter operator is R5-only
257
- * @param {string} operator - Filter operator code
258
- * @returns {boolean} True if operator is R5-only
259
- * @private
260
- */
261
- _isR5OnlyFilterOperator(operator) {
262
- const r5OnlyOperators = [
263
- 'generalizes', // Added in R5
264
- // Add other R5-only operators as they're identified
265
- ];
266
- return r5OnlyOperators.includes(operator);
267
- }
268
-
269
- /**
270
- * Checks if a filter operator is compatible with R3
271
- * @param {string} operator - Filter operator code
272
- * @returns {boolean} True if operator is R3-compatible
273
- * @private
274
- */
275
- _isR3CompatibleFilterOperator(operator) {
276
- const r3CompatibleOperators = [
277
- '=', // Equal
278
- 'is-a', // Is-A relationship
279
- 'descendent-of', // Descendant of (note: R3 spelling)
280
- 'is-not-a', // Is-Not-A relationship
281
- 'regex', // Regular expression
282
- 'in', // In set
283
- 'not-in', // Not in set
284
- 'exists', // Property exists
285
- ];
286
- return r3CompatibleOperators.includes(operator);
287
- }
288
-
289
57
  /**
290
58
  * Gets the FHIR version this ValueSet was loaded from
291
59
  * @returns {string} FHIR version ('R3', 'R4', or 'R5')
package/tx/library.js CHANGED
@@ -30,6 +30,7 @@ const {ListCodeSystemProvider} = require("./cs/cs-provider-list");
30
30
  const { Provider } = require("./provider");
31
31
  const {I18nSupport} = require("../library/i18nsupport");
32
32
  const folders = require('../library/folder-setup');
33
+ const {VSACValueSetProvider} = require("./vs/vs-vsac");
33
34
 
34
35
  /**
35
36
  * This class holds all the loaded content ready for processing
@@ -68,6 +69,7 @@ class Library {
68
69
  startMemory = process.memoryUsage();
69
70
  lastTime = null;
70
71
  totalDownloaded = 0;
72
+ vsacCfg = undefined;
71
73
 
72
74
  registerProvider(source, factory, isDefault = false) {
73
75
  this.#logSystem(factory.system(), factory.version(), source);
@@ -82,9 +84,12 @@ class Library {
82
84
  }
83
85
  }
84
86
 
85
- constructor(configFile, log) {
87
+ constructor(configFile, vsacCfg, log, stats) {
86
88
  this.configFile = configFile;
89
+ this.vsacCfg = vsacCfg;
87
90
  this.log = log;
91
+ this.stats = stats;
92
+
88
93
  // Only synchronous initialization here
89
94
  this.codeSystemFactories = new Map();
90
95
  this.codeSystemProviders = [];
@@ -140,7 +145,7 @@ class Library {
140
145
 
141
146
  async load() {
142
147
  this.startTime = Date.now();
143
- this.languageDefinitions = await LanguageDefinitions.fromFile(path.join(__dirname, '../tx/data/lang.dat'));
148
+ this.languageDefinitions = await LanguageDefinitions.fromFiles(path.join(__dirname, '../tx/data'));
144
149
  this.i18n = new I18nSupport(path.join(__dirname, '../translations'), this.languageDefinitions);
145
150
  await this.i18n.load();
146
151
 
@@ -243,9 +248,21 @@ class Library {
243
248
  break;
244
249
 
245
250
  case 'npm':
246
- await this.loadNpm(packageManager, details, isDefault, mode);
251
+ await this.loadNpm(packageManager, details, isDefault, mode, false);
252
+ break;
253
+
254
+ case 'npm/cs':
255
+ await this.loadNpm(packageManager, details, isDefault, mode, true);
256
+ break;
257
+
258
+ case 'url':
259
+ await this.loadUrl(packageManager, details, isDefault, mode, false);
247
260
  break;
248
261
 
262
+ case 'url/cs':
263
+ await this.loadUrl(packageManager, details, isDefault, mode, true);
264
+ break;
265
+
249
266
  default:
250
267
  throw new Error(`Unknown source type: ${type}`);
251
268
  }
@@ -301,6 +318,21 @@ class Library {
301
318
  this.registerProvider('internal', hgvs);
302
319
  break;
303
320
  }
321
+ case "vsac" : {
322
+ if (!this.vsacCfg || !this.vsacCfg.apiKey) {
323
+ throw new Error("Unable to load VSAC provider unless vsacCfg is provided in the configuration");
324
+ }
325
+ let vsac = new VSACValueSetProvider(this.vsacCfg, this.stats);
326
+ vsac.initialize();
327
+ this.valueSetProviders.push(vsac);
328
+ //const mem = process.memoryUsage();
329
+ let time = Math.floor(Date.now() - this.lastTime).toString().padStart(5)+" ";
330
+ let system = "vsac".padEnd(50);
331
+ let version = "n/a".padEnd(62);
332
+ this.log.info(`${time}${system}${version}${vsac.baseUrl}`);
333
+ this.lastTime = Date.now();
334
+ break;
335
+ }
304
336
  default:
305
337
  throw new Error("Unknown Internal Provider "+details);
306
338
  }
@@ -392,7 +424,7 @@ class Library {
392
424
  this.registerProvider(omopFN, omop, isDefault);
393
425
  }
394
426
 
395
- async loadNpm(packageManager, details, isDefault, mode) {
427
+ async loadNpm(packageManager, details, isDefault, mode, csOnly) {
396
428
  // Parse packageId and version from details (e.g., "hl7.terminology.r4#6.0.2")
397
429
  let packageId = details;
398
430
  let version = null;
@@ -422,14 +454,52 @@ class Library {
422
454
  csc++;
423
455
  }
424
456
  this.codeSystemProviders.push(cp);
425
- const vs = new PackageValueSetProvider(contentLoader);
426
- await vs.initialize();
427
- this.valueSetProviders.push(vs);
428
- const cm = new PackageConceptMapProvider(contentLoader);
429
- await cm.initialize();
430
- this.conceptMapProviders.push(cm);
431
-
432
- this.#logPackage(contentLoader.id(), contentLoader.version(), csc, vs.valueSetMap.size);
457
+ let vs = null;
458
+ if (!csOnly) {
459
+ vs = new PackageValueSetProvider(contentLoader);
460
+ await vs.initialize();
461
+ this.valueSetProviders.push(vs);
462
+ const cm = new PackageConceptMapProvider(contentLoader);
463
+ await cm.initialize();
464
+ this.conceptMapProviders.push(cm);
465
+ }
466
+
467
+ this.#logPackage(contentLoader.id(), contentLoader.version(), csc, vs ? vs.valueSetMap.size : 0);
468
+ }
469
+
470
+ async loadUrl(packageManager, url, isDefault, mode, csOnly) {
471
+ const packagePath = await packageManager.fetchUrl(url);
472
+ if (mode === "fetch" || mode === "cs") {
473
+ return;
474
+ }
475
+ const fullPackagePath = path.join(this.cacheFolder, packagePath);
476
+ const contentLoader = new PackageContentLoader(fullPackagePath);
477
+ await contentLoader.initialize();
478
+
479
+ this.contentSources.push(contentLoader.id()+"#"+contentLoader.version());
480
+
481
+ let cp = new ListCodeSystemProvider();
482
+ const resources = await contentLoader.getResourcesByType("CodeSystem");
483
+ let csc = 0;
484
+ for (const resource of resources) {
485
+ const cs = new CodeSystem(await contentLoader.loadFile(resource, contentLoader.fhirVersion()));
486
+ cs.sourcePackage = contentLoader.pid();
487
+ cp.codeSystems.set(cs.url, cs);
488
+ cp.codeSystems.set(cs.vurl, cs);
489
+ csc++;
490
+ }
491
+ this.codeSystemProviders.push(cp);
492
+ let vs = null;
493
+ if (!csOnly) {
494
+ vs = new PackageValueSetProvider(contentLoader);
495
+ await vs.initialize();
496
+ this.valueSetProviders.push(vs);
497
+ const cm = new PackageConceptMapProvider(contentLoader);
498
+ await cm.initialize();
499
+ this.conceptMapProviders.push(cm);
500
+ }
501
+
502
+ this.#logPackage(contentLoader.id(), contentLoader.version(), csc, vs ? vs.valueSetMap.size : 0);
433
503
  }
434
504
 
435
505
  /**
@@ -442,6 +512,17 @@ class Library {
442
512
  // Ensure folder exists
443
513
  await this.ensureFolderExists(this.cacheFolder);
444
514
 
515
+ if (fileName.includes("|")) {
516
+ // in this case, we split it into two. if the first file exists, we go with that. Otherwise
517
+ // fallback to the second.
518
+ let firstName = fileName.substring(0, fileName.indexOf("|"));
519
+ fileName = fileName.substring(fileName.indexOf("|")+1);
520
+
521
+ const firstPath = path.join(this.cacheFolder, firstName);
522
+ if (await this.fileExists(firstPath)) {
523
+ return firstPath;
524
+ }
525
+ }
445
526
  const filePath = path.join(this.cacheFolder, fileName);
446
527
 
447
528
  // Check if file already exists
@@ -581,7 +662,7 @@ class Library {
581
662
 
582
663
  // Load FHIR packages - these will be added to valueSetProviders first
583
664
  for (const packageId of fhirPackages) {
584
- await provider.loadNpm(this.packageManager, this.cacheFolder, packageId, false, "npm");
665
+ await provider.loadNpm(this.packageManager, this.cacheFolder, packageId, false, "npm", false);
585
666
  }
586
667
 
587
668
 
@@ -608,6 +689,12 @@ class Library {
608
689
  provider.valueSetProviders.push(...this.valueSetProviders);
609
690
  provider.conceptMapProviders.push(...this.conceptMapProviders);
610
691
 
692
+ // bind UCUM common value set
693
+ let ucum = provider.codeSystemFactories.get("http://unitsofmeasure.org");
694
+ let vs = await provider.findValueSet(null, "http://hl7.org/fhir/ValueSet/ucum-common", null);
695
+ if (ucum && vs) {
696
+ ucum.processCommonUnits(vs);
697
+ }
611
698
  return provider;
612
699
  }
613
700
 
@@ -49,7 +49,8 @@ class TimeTracker {
49
49
  * Stores resources by cache-id for reuse across requests
50
50
  */
51
51
  class ResourceCache {
52
- constructor() {
52
+ constructor(stats) {
53
+ this.stats = stats;
53
54
  this.cache = new Map();
54
55
  this.locks = new Map(); // For thread-safety with async operations
55
56
  }
@@ -135,12 +136,20 @@ class ResourceCache {
135
136
  * @param {number} maxAge - Maximum age in milliseconds
136
137
  */
137
138
  prune(maxAge = 3600000) { // Default 1 hour
139
+ if (this.stats) {
140
+ this.stats.task("Client Cache", `Pruning (${this.cache.size} entries)`);
141
+ }
142
+ let i = 0;
138
143
  const now = Date.now();
139
144
  for (const [cacheId, entry] of this.cache.entries()) {
140
145
  if (now - entry.lastUsed > maxAge) {
146
+ i++;
141
147
  this.cache.delete(cacheId);
142
148
  }
143
149
  }
150
+ if (this.stats) {
151
+ this.stats.task("Client Cache", `Pruned ${i} of ${this.cache.size} entries`);
152
+ }
144
153
  }
145
154
 
146
155
  /**
@@ -184,7 +193,8 @@ class ExpansionCache {
184
193
  * @param {number} maxSize - Maximum number of entries to keep (default 1000)
185
194
  * @param {number} memoryThresholdMB - Heap usage in MB that triggers dropping oldest half (0 = disabled)
186
195
  */
187
- constructor(maxSize = ExpansionCache.DEFAULT_MAX_SIZE, memoryThresholdMB = 0) {
196
+ constructor(stats, maxSize = ExpansionCache.DEFAULT_MAX_SIZE, memoryThresholdMB = 0) {
197
+ this.stats = stats;
188
198
  this.cache = new Map();
189
199
  this.maxSize = maxSize;
190
200
  this.memoryThresholdBytes = memoryThresholdMB * 1024 * 1024;
@@ -322,13 +332,22 @@ class ExpansionCache {
322
332
  * @returns {boolean} True if eviction was triggered
323
333
  */
324
334
  checkMemoryPressure() {
335
+ if (this.stats) {
336
+ this.stats.task('Expansion Cache', 'Checking Memory Pressure');
337
+ }
325
338
  if (this.memoryThresholdBytes <= 0) return false;
326
339
 
327
340
  const heapUsed = process.memoryUsage().heapUsed;
328
341
  if (heapUsed > this.memoryThresholdBytes) {
329
- this.evictOldestHalf();
342
+ const i = this.evictOldestHalf();
343
+ if (this.stats) {
344
+ this.stats.task('Expansion Cache', `Checked Memory Pressure: evicted half (${i} entries)`);
345
+ }
330
346
  return true;
331
347
  }
348
+ if (this.stats) {
349
+ this.stats.task('Expansion Cache', `Checked Memory Pressure - OK (${this.cache.size} entries)`);
350
+ }
332
351
  return false;
333
352
  }
334
353
 
@@ -464,7 +483,7 @@ class OperationContext {
464
483
  seeContext(vurl) {
465
484
  if (this.contexts.includes(vurl)) {
466
485
  const contextList = '[' + this.contexts.join(', ') + ']';
467
- throw new Issue("error", "processing", null, 'VALUESET_CIRCULAR_REFERENCE', this.i18n.formatMessage(this.langs, 'VALUESET_CIRCULAR_REFERENCE', [vurl, contextList]), null).handleAsOO(400);
486
+ throw new Issue("error", "processing", null, 'VALUESET_CIRCULAR_REFERENCE', this.i18n.formatMessage(this.langs, 'VALUESET_CIRCULAR_REFERENCE', [vurl, contextList]), "vs-invalid").handleAsOO(400);
468
487
  }
469
488
  this.contexts.push(vurl);
470
489
  }