fhirsmith 0.3.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 (277) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/FHIRsmith.png +0 -0
  3. package/README.md +277 -0
  4. package/config-template.json +144 -0
  5. package/library/folder-setup.js +58 -0
  6. package/library/html-server.js +166 -0
  7. package/library/html.js +835 -0
  8. package/library/i18nsupport.js +259 -0
  9. package/library/languages.js +779 -0
  10. package/library/logger-telnet.js +205 -0
  11. package/library/logger.js +279 -0
  12. package/library/package-manager.js +876 -0
  13. package/library/utilities.js +196 -0
  14. package/library/version-utilities.js +1056 -0
  15. package/npmprojector/config-example.json +13 -0
  16. package/npmprojector/indexer.js +394 -0
  17. package/npmprojector/npmprojector.js +395 -0
  18. package/npmprojector/readme.md +174 -0
  19. package/npmprojector/watcher.js +335 -0
  20. package/package.json +119 -0
  21. package/packages/package-crawler.js +846 -0
  22. package/packages/packages-template.html +126 -0
  23. package/packages/packages.js +2838 -0
  24. package/passwords.ini +2 -0
  25. package/publisher/publisher-template.html +208 -0
  26. package/publisher/publisher.js +2167 -0
  27. package/publisher/task-draft.js +458 -0
  28. package/registry/api.js +735 -0
  29. package/registry/crawler.js +637 -0
  30. package/registry/model.js +513 -0
  31. package/registry/readme.md +243 -0
  32. package/registry/registry-data.json +121015 -0
  33. package/registry/registry-template.html +126 -0
  34. package/registry/registry.js +1395 -0
  35. package/registry/test-runner.js +237 -0
  36. package/root-template.html +124 -0
  37. package/server.js +524 -0
  38. package/shl/private-key.pem +5 -0
  39. package/shl/public-key.pem +18 -0
  40. package/shl/shl.js +1125 -0
  41. package/shl/vhl.js +69 -0
  42. package/static/FHIRsmith128.png +0 -0
  43. package/static/FHIRsmith16.png +0 -0
  44. package/static/FHIRsmith32.png +0 -0
  45. package/static/FHIRsmith64.png +0 -0
  46. package/static/assets/css/bootstrap-fhir.css +5302 -0
  47. package/static/assets/css/bootstrap-glyphicons.css +2 -0
  48. package/static/assets/css/bootstrap.css +4097 -0
  49. package/static/assets/css/jquery-ui.css +523 -0
  50. package/static/assets/css/jquery-ui.structure.css +863 -0
  51. package/static/assets/css/jquery-ui.structure.min.css +5 -0
  52. package/static/assets/css/jquery-ui.theme.css +439 -0
  53. package/static/assets/css/jquery-ui.theme.min.css +5 -0
  54. package/static/assets/css/jquery.ui.all.css +7 -0
  55. package/static/assets/css/modules.css +18 -0
  56. package/static/assets/css/project.css +367 -0
  57. package/static/assets/css/pygments-manni.css +66 -0
  58. package/static/assets/css/tags.css +74 -0
  59. package/static/assets/css/xml.css +2 -0
  60. package/static/assets/fonts/glyphiconshalflings-regular.eot +0 -0
  61. package/static/assets/fonts/glyphiconshalflings-regular.otf +0 -0
  62. package/static/assets/fonts/glyphiconshalflings-regular.svg +175 -0
  63. package/static/assets/fonts/glyphiconshalflings-regular.ttf +0 -0
  64. package/static/assets/fonts/glyphiconshalflings-regular.woff +0 -0
  65. package/static/assets/ico/apple-touch-icon-114-precomposed.png +0 -0
  66. package/static/assets/ico/apple-touch-icon-144-precomposed.png +0 -0
  67. package/static/assets/ico/apple-touch-icon-57-precomposed.png +0 -0
  68. package/static/assets/ico/apple-touch-icon-72-precomposed.png +0 -0
  69. package/static/assets/ico/favicon.ico +0 -0
  70. package/static/assets/ico/favicon.png +0 -0
  71. package/static/assets/images/fhir-logo-www.png +0 -0
  72. package/static/assets/images/fhir-logo.png +0 -0
  73. package/static/assets/images/hl7-logo.png +0 -0
  74. package/static/assets/images/logo_ansinew.jpg +0 -0
  75. package/static/assets/images/search.png +0 -0
  76. package/static/assets/images/stripe.png +0 -0
  77. package/static/assets/images/target.png +0 -0
  78. package/static/assets/images/tx-registry-root.gif +0 -0
  79. package/static/assets/images/tx-registry.png +0 -0
  80. package/static/assets/images/tx-server.png +0 -0
  81. package/static/assets/images/tx-version.png +0 -0
  82. package/static/assets/js/bootstrap.min.js +6 -0
  83. package/static/assets/js/fhir-gw.js +259 -0
  84. package/static/assets/js/fhir.js +2 -0
  85. package/static/assets/js/html5shiv.js +8 -0
  86. package/static/assets/js/jcookie.js +96 -0
  87. package/static/assets/js/jquery-ui.min.js +6 -0
  88. package/static/assets/js/jquery.js +10716 -0
  89. package/static/assets/js/jquery.min.js +2 -0
  90. package/static/assets/js/jquery.ui.core.js +314 -0
  91. package/static/assets/js/jquery.ui.draggable.js +825 -0
  92. package/static/assets/js/jquery.ui.mouse.js +162 -0
  93. package/static/assets/js/jquery.ui.resizable.js +842 -0
  94. package/static/assets/js/jquery.ui.widget.js +268 -0
  95. package/static/assets/js/json2.js +487 -0
  96. package/static/assets/js/jtip.js +97 -0
  97. package/static/assets/js/respond.min.js +6 -0
  98. package/static/assets/js/statuspage.js +70 -0
  99. package/static/assets/js/xml.js +2 -0
  100. package/static/dist/js/bootstrap.js +1964 -0
  101. package/static/favicon.png +0 -0
  102. package/static/fhir.css +626 -0
  103. package/static/icon-fhir-16.png +0 -0
  104. package/static/images/ui-bg_diagonals-thick_18_b81900_40x40.png +0 -0
  105. package/static/images/ui-bg_diagonals-thick_20_666666_40x40.png +0 -0
  106. package/static/images/ui-bg_flat_10_000000_40x100.png +0 -0
  107. package/static/images/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
  108. package/static/images/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
  109. package/static/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  110. package/static/images/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
  111. package/static/images/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
  112. package/static/images/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
  113. package/static/images/ui-icons_222222_256x240.png +0 -0
  114. package/static/images/ui-icons_228ef1_256x240.png +0 -0
  115. package/static/images/ui-icons_ef8c08_256x240.png +0 -0
  116. package/static/images/ui-icons_ffd27a_256x240.png +0 -0
  117. package/static/images/ui-icons_ffffff_256x240.png +0 -0
  118. package/static/js/jquery.effects.blind.js +49 -0
  119. package/static/js/jquery.effects.bounce.js +78 -0
  120. package/static/js/jquery.effects.clip.js +54 -0
  121. package/static/js/jquery.effects.core.js +763 -0
  122. package/static/js/jquery.effects.drop.js +50 -0
  123. package/static/js/jquery.effects.explode.js +79 -0
  124. package/static/js/jquery.effects.fade.js +32 -0
  125. package/static/js/jquery.effects.fold.js +56 -0
  126. package/static/js/jquery.effects.highlight.js +50 -0
  127. package/static/js/jquery.effects.pulsate.js +51 -0
  128. package/static/js/jquery.effects.scale.js +178 -0
  129. package/static/js/jquery.effects.shake.js +57 -0
  130. package/static/js/jquery.effects.slide.js +50 -0
  131. package/static/js/jquery.effects.transfer.js +45 -0
  132. package/static/js/jquery.ui.accordion.js +611 -0
  133. package/static/js/jquery.ui.autocomplete.js +612 -0
  134. package/static/js/jquery.ui.button.js +416 -0
  135. package/static/js/jquery.ui.datepicker.js +1823 -0
  136. package/static/js/jquery.ui.dialog.js +878 -0
  137. package/static/js/jquery.ui.droppable.js +296 -0
  138. package/static/js/jquery.ui.position.js +252 -0
  139. package/static/js/jquery.ui.progressbar.js +109 -0
  140. package/static/js/jquery.ui.selectable.js +266 -0
  141. package/static/js/jquery.ui.slider.js +666 -0
  142. package/static/js/jquery.ui.sortable.js +1077 -0
  143. package/static/js/jquery.ui.tabs.js +758 -0
  144. package/stats.js +80 -0
  145. package/test-cache/vsac/vsac-valuesets.db +0 -0
  146. package/token/nginx_passport_setup.md +383 -0
  147. package/token/security_guide.md +294 -0
  148. package/token/token-template.html +330 -0
  149. package/token/token.js +1300 -0
  150. package/translations/Messages.properties +1510 -0
  151. package/translations/Messages_ar.properties +1399 -0
  152. package/translations/Messages_de.properties +836 -0
  153. package/translations/Messages_es.properties +737 -0
  154. package/translations/Messages_fr.properties +1 -0
  155. package/translations/Messages_ja.properties +893 -0
  156. package/translations/Messages_nl.properties +1357 -0
  157. package/translations/Messages_pt.properties +1302 -0
  158. package/translations/Messages_ru.properties +1 -0
  159. package/translations/Messages_uz.properties +1 -0
  160. package/translations/Messages_zh.properties +1 -0
  161. package/translations/rendering-phrases.properties +1128 -0
  162. package/translations/rendering-phrases_ar.properties +1091 -0
  163. package/translations/rendering-phrases_de.properties +6 -0
  164. package/translations/rendering-phrases_es.properties +6 -0
  165. package/translations/rendering-phrases_fr.properties +624 -0
  166. package/translations/rendering-phrases_ja.properties +21 -0
  167. package/translations/rendering-phrases_nl.properties +970 -0
  168. package/translations/rendering-phrases_pt.properties +1020 -0
  169. package/translations/rendering-phrases_ru.properties +1094 -0
  170. package/translations/rendering-phrases_uz.properties +1 -0
  171. package/translations/rendering-phrases_zh.properties +1 -0
  172. package/tx/README.md +418 -0
  173. package/tx/cm/cm-api.js +110 -0
  174. package/tx/cm/cm-database.js +735 -0
  175. package/tx/cm/cm-package.js +325 -0
  176. package/tx/cs/cs-api.js +789 -0
  177. package/tx/cs/cs-areacode.js +615 -0
  178. package/tx/cs/cs-country.js +1110 -0
  179. package/tx/cs/cs-cpt.js +785 -0
  180. package/tx/cs/cs-cs.js +1579 -0
  181. package/tx/cs/cs-currency.js +539 -0
  182. package/tx/cs/cs-db.js +1321 -0
  183. package/tx/cs/cs-hgvs.js +329 -0
  184. package/tx/cs/cs-lang.js +465 -0
  185. package/tx/cs/cs-loinc.js +1485 -0
  186. package/tx/cs/cs-mimetypes.js +238 -0
  187. package/tx/cs/cs-ndc.js +704 -0
  188. package/tx/cs/cs-omop.js +1025 -0
  189. package/tx/cs/cs-provider-api.js +43 -0
  190. package/tx/cs/cs-provider-list.js +37 -0
  191. package/tx/cs/cs-rxnorm.js +808 -0
  192. package/tx/cs/cs-snomed.js +1102 -0
  193. package/tx/cs/cs-ucum.js +514 -0
  194. package/tx/cs/cs-unii.js +271 -0
  195. package/tx/cs/cs-uri.js +218 -0
  196. package/tx/cs/cs-usstates.js +305 -0
  197. package/tx/dev.fhir.org.yml +14 -0
  198. package/tx/fixtures/test-cases-setup.json +18 -0
  199. package/tx/fixtures/test-cases.yml +16 -0
  200. package/tx/html/codesystem-operations.liquid +25 -0
  201. package/tx/html/home-metrics.liquid +247 -0
  202. package/tx/html/operations-form.liquid +148 -0
  203. package/tx/html/search-form.liquid +62 -0
  204. package/tx/html/tx-template.html +133 -0
  205. package/tx/html/valueset-operations.liquid +54 -0
  206. package/tx/importers/atc-to-fhir.js +316 -0
  207. package/tx/importers/import-loinc.module.js +1536 -0
  208. package/tx/importers/import-ndc.module.js +1088 -0
  209. package/tx/importers/import-rxnorm.module.js +898 -0
  210. package/tx/importers/import-sct.module.js +2457 -0
  211. package/tx/importers/import-unii.module.js +601 -0
  212. package/tx/importers/readme.md +453 -0
  213. package/tx/importers/subset-loinc.module.js +1081 -0
  214. package/tx/importers/subset-rxnorm.module.js +938 -0
  215. package/tx/importers/tx-import-base.js +351 -0
  216. package/tx/importers/tx-import-settings.js +310 -0
  217. package/tx/importers/tx-import.js +357 -0
  218. package/tx/library/canonical-resource.js +88 -0
  219. package/tx/library/capabilitystatement.js +292 -0
  220. package/tx/library/codesystem.js +774 -0
  221. package/tx/library/conceptmap.js +568 -0
  222. package/tx/library/designations.js +932 -0
  223. package/tx/library/errors.js +77 -0
  224. package/tx/library/extensions.js +117 -0
  225. package/tx/library/namingsystem.js +322 -0
  226. package/tx/library/operation-outcome.js +127 -0
  227. package/tx/library/parameters.js +105 -0
  228. package/tx/library/renderer.js +1559 -0
  229. package/tx/library/terminologycapabilities.js +418 -0
  230. package/tx/library/ucum-parsers.js +1029 -0
  231. package/tx/library/ucum-service.js +370 -0
  232. package/tx/library/ucum-types.js +1099 -0
  233. package/tx/library/valueset.js +543 -0
  234. package/tx/library.js +676 -0
  235. package/tx/ocl/cm-ocl.js +106 -0
  236. package/tx/ocl/cs-ocl.js +39 -0
  237. package/tx/ocl/vs-ocl.js +105 -0
  238. package/tx/operation-context.js +568 -0
  239. package/tx/params.js +613 -0
  240. package/tx/provider.js +403 -0
  241. package/tx/sct/ecl.js +1560 -0
  242. package/tx/sct/expressions.js +2077 -0
  243. package/tx/sct/structures.js +1396 -0
  244. package/tx/tx-html.js +1063 -0
  245. package/tx/tx.fhir.org.yml +39 -0
  246. package/tx/tx.js +927 -0
  247. package/tx/vs/vs-api.js +112 -0
  248. package/tx/vs/vs-database.js +786 -0
  249. package/tx/vs/vs-package.js +358 -0
  250. package/tx/vs/vs-vsac.js +366 -0
  251. package/tx/workers/batch-validate.js +129 -0
  252. package/tx/workers/batch.js +361 -0
  253. package/tx/workers/closure.js +32 -0
  254. package/tx/workers/expand.js +1845 -0
  255. package/tx/workers/lookup.js +407 -0
  256. package/tx/workers/metadata.js +467 -0
  257. package/tx/workers/operations.js +34 -0
  258. package/tx/workers/read.js +164 -0
  259. package/tx/workers/search.js +384 -0
  260. package/tx/workers/subsumes.js +334 -0
  261. package/tx/workers/translate.js +492 -0
  262. package/tx/workers/validate.js +2504 -0
  263. package/tx/workers/worker.js +904 -0
  264. package/tx/xml/capabilitystatement-xml.js +63 -0
  265. package/tx/xml/codesystem-xml.js +62 -0
  266. package/tx/xml/conceptmap-xml.js +65 -0
  267. package/tx/xml/namingsystem-xml.js +65 -0
  268. package/tx/xml/operationoutcome-xml.js +127 -0
  269. package/tx/xml/parameters-xml.js +312 -0
  270. package/tx/xml/terminologycapabilities-xml.js +64 -0
  271. package/tx/xml/valueset-xml.js +64 -0
  272. package/tx/xml/xml-base.js +603 -0
  273. package/vcl/vcl-parser.js +1098 -0
  274. package/vcl/vcl.js +253 -0
  275. package/windows-install.js +19 -0
  276. package/xig/xig-template.html +124 -0
  277. package/xig/xig.js +3049 -0
@@ -0,0 +1,568 @@
1
+ const assert = require("assert");
2
+ const inspector = require("inspector");
3
+ const crypto = require("crypto");
4
+ const {Languages} = require("../library/languages");
5
+ const {Issue} = require("./library/operation-outcome");
6
+
7
+ /**
8
+ * Check if running under a debugger
9
+ * @returns {boolean}
10
+ */
11
+ function isDebugging() {
12
+ // Check if inspector is connected
13
+ if (inspector.url() !== undefined) {
14
+ return true;
15
+ }
16
+ // Also check for debug flags in case inspector not yet attached
17
+ return process.execArgv.some(arg =>
18
+ arg.includes('--inspect') || arg.includes('--debug')
19
+ );
20
+ }
21
+
22
+
23
+ class TimeTracker {
24
+ constructor() {
25
+ this.startTime = performance.now();
26
+ this.steps = [];
27
+ }
28
+
29
+ step(note) {
30
+ const elapsed = Math.round(performance.now() - this.startTime);
31
+ this.steps.push(`${elapsed}ms ${note}`);
32
+ }
33
+
34
+ log() {
35
+ return this.steps.join('\n');
36
+ }
37
+
38
+ link() {
39
+ const newTracker = new TimeTracker();
40
+ newTracker.startTime = this.startTime;
41
+ newTracker.steps = [...this.steps];
42
+ return newTracker;
43
+ }
44
+ }
45
+
46
+
47
+ /**
48
+ * Thread-safe resource cache for tx-resource parameters
49
+ * Stores resources by cache-id for reuse across requests
50
+ */
51
+ class ResourceCache {
52
+ constructor() {
53
+ this.cache = new Map();
54
+ this.locks = new Map(); // For thread-safety with async operations
55
+ }
56
+
57
+ /**
58
+ * Get resources for a cache-id
59
+ * @param {string} cacheId - The cache identifier
60
+ * @returns {Array} Array of resources, or empty array if not found
61
+ */
62
+ get(cacheId) {
63
+ const entry = this.cache.get(cacheId);
64
+ if (entry) {
65
+ entry.lastUsed = Date.now();
66
+ return [...entry.resources]; // Return a copy
67
+ }
68
+ return [];
69
+ }
70
+
71
+ /**
72
+ * Check if a cache-id exists
73
+ * @param {string} cacheId - The cache identifier
74
+ * @returns {boolean}
75
+ */
76
+ has(cacheId) {
77
+ return this.cache.has(cacheId);
78
+ }
79
+
80
+ /**
81
+ * Add resources to a cache-id (merges with existing)
82
+ * @param {string} cacheId - The cache identifier
83
+ * @param {Array} resources - Resources to add
84
+ */
85
+ add(cacheId, resources) {
86
+ if (!resources || resources.length === 0) return;
87
+
88
+ const entry = this.cache.get(cacheId) || { resources: [], lastUsed: Date.now() };
89
+
90
+ // Merge resources, avoiding duplicates by url+version
91
+ for (const resource of resources) {
92
+ const key = this._resourceKey(resource);
93
+ const existingIndex = entry.resources.findIndex(r => this._resourceKey(r) === key);
94
+ if (existingIndex >= 0) {
95
+ // Replace existing
96
+ entry.resources[existingIndex] = resource;
97
+ } else {
98
+ entry.resources.push(resource);
99
+ }
100
+ }
101
+
102
+ entry.lastUsed = Date.now();
103
+ this.cache.set(cacheId, entry);
104
+ }
105
+
106
+ /**
107
+ * Set resources for a cache-id (replaces existing)
108
+ * @param {string} cacheId - The cache identifier
109
+ * @param {Array} resources - Resources to set
110
+ */
111
+ set(cacheId, resources) {
112
+ this.cache.set(cacheId, {
113
+ resources: [...resources],
114
+ lastUsed: Date.now()
115
+ });
116
+ }
117
+
118
+ /**
119
+ * Clear a specific cache-id
120
+ * @param {string} cacheId - The cache identifier
121
+ */
122
+ clear(cacheId) {
123
+ this.cache.delete(cacheId);
124
+ }
125
+
126
+ /**
127
+ * Clear all cached entries
128
+ */
129
+ clearAll() {
130
+ this.cache.clear();
131
+ }
132
+
133
+ /**
134
+ * Remove entries older than maxAge milliseconds
135
+ * @param {number} maxAge - Maximum age in milliseconds
136
+ */
137
+ prune(maxAge = 3600000) { // Default 1 hour
138
+ const now = Date.now();
139
+ for (const [cacheId, entry] of this.cache.entries()) {
140
+ if (now - entry.lastUsed > maxAge) {
141
+ this.cache.delete(cacheId);
142
+ }
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Get the number of cached entries
148
+ * @returns {number}
149
+ */
150
+ size() {
151
+ return this.cache.size;
152
+ }
153
+
154
+ /**
155
+ * Generate a key for a resource based on url and version
156
+ * @param {Object} resource - The resource
157
+ * @returns {string}
158
+ */
159
+ _resourceKey(resource) {
160
+ const url = resource.url || resource.id || '';
161
+ const version = resource.version || '';
162
+ const type = resource.resourceType || '';
163
+ return `${type}|${url}|${version}`;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Cache for expanded ValueSets
169
+ * Stores expansions keyed by hash of (valueSet, params, additionalResources)
170
+ * Only caches expansions that took longer than the minimum cache time
171
+ */
172
+ class ExpansionCache {
173
+ /**
174
+ * Minimum time (ms) an expansion must take before we cache it
175
+ */
176
+ static MIN_CACHE_TIME_MS = 250;
177
+
178
+ /**
179
+ * Default maximum number of cached entries
180
+ */
181
+ static DEFAULT_MAX_SIZE = 1000;
182
+
183
+ /**
184
+ * @param {number} maxSize - Maximum number of entries to keep (default 1000)
185
+ * @param {number} memoryThresholdMB - Heap usage in MB that triggers dropping oldest half (0 = disabled)
186
+ */
187
+ constructor(maxSize = ExpansionCache.DEFAULT_MAX_SIZE, memoryThresholdMB = 0) {
188
+ this.cache = new Map();
189
+ this.maxSize = maxSize;
190
+ this.memoryThresholdBytes = memoryThresholdMB * 1024 * 1024;
191
+ }
192
+
193
+ /**
194
+ * Compute a hash key for an expansion request.
195
+ * This must hash the actual content of resources, not just their identity,
196
+ * because clients can submit variations on the same ValueSet/CodeSystem.
197
+ *
198
+ * @param {Object|ValueSet} valueSet - The ValueSet to expand (wrapper or JSON)
199
+ * @param {Object} params - Parameters resource (tx-resource and valueSet params excluded)
200
+ * @param {Array} additionalResources - Additional resources in scope (CodeSystem/ValueSet wrappers)
201
+ * @returns {string} Hash key
202
+ */
203
+ computeKey(valueSet, params, additionalResources) {
204
+ const keyParts = [];
205
+
206
+ // ValueSet content - always hash the full JSON content
207
+ // The ValueSet might be a wrapper class or raw JSON
208
+ const vsJson = valueSet.jsonObj || valueSet;
209
+ keyParts.push(`vs:${JSON.stringify(vsJson)}`);
210
+
211
+ // Parameters - filter out tx-resource and valueSet params, sort for consistency
212
+ if (params) {
213
+ keyParts.push(`params:`+params.hashSource());
214
+ }
215
+
216
+ // Additional resources - hash the full content of each resource
217
+ // Resources are now CodeSystem/ValueSet wrappers, not raw JSON
218
+ if (additionalResources && additionalResources.length > 0) {
219
+ const resourceHashes = additionalResources
220
+ .map(r => {
221
+ // Get the JSON object from wrapper or use directly
222
+ const json = r.jsonObj || r;
223
+ // Create a content hash for this resource
224
+ return crypto.createHash('sha256')
225
+ .update(JSON.stringify(json))
226
+ .digest('hex')
227
+ .substring(0, 16); // Use first 16 chars for brevity
228
+ })
229
+ .sort();
230
+ keyParts.push(`additional:${resourceHashes.join(',')}`);
231
+ }
232
+
233
+ // Create SHA256 hash of the combined key
234
+ const keyString = keyParts.join('||');
235
+ return crypto.createHash('sha256').update(keyString).digest('hex');
236
+ }
237
+
238
+
239
+ /**
240
+ * Get a cached expansion
241
+ * @param {string} key - Hash key from computeKey()
242
+ * @returns {Object|null} Cached expanded ValueSet or null
243
+ */
244
+ get(key) {
245
+ const entry = this.cache.get(key);
246
+ if (entry) {
247
+ entry.lastUsed = Date.now();
248
+ entry.hitCount++;
249
+ return entry.expansion;
250
+ }
251
+ return null;
252
+ }
253
+
254
+ /**
255
+ * Check if a cached expansion exists
256
+ * @param {string} key - Hash key
257
+ * @returns {boolean}
258
+ */
259
+ has(key) {
260
+ return this.cache.has(key);
261
+ }
262
+
263
+ /**
264
+ * Store an expansion in the cache (only if duration exceeds minimum)
265
+ * @param {string} key - Hash key from computeKey()
266
+ * @param {Object} expansion - The expanded ValueSet
267
+ * @param {number} durationMs - How long the expansion took
268
+ * @returns {boolean} True if cached, false if duration too short
269
+ */
270
+ set(key, expansion, durationMs) {
271
+ // Only cache if expansion took significant time
272
+ if (durationMs < ExpansionCache.MIN_CACHE_TIME_MS) {
273
+ return false;
274
+ }
275
+
276
+ // Enforce max size before adding - evict oldest (by lastUsed) if needed
277
+ if (this.cache.size >= this.maxSize) {
278
+ this.evictOldest(1);
279
+ }
280
+
281
+ this.cache.set(key, {
282
+ expansion: expansion,
283
+ createdAt: Date.now(),
284
+ lastUsed: Date.now(),
285
+ durationMs: durationMs,
286
+ hitCount: 0
287
+ });
288
+ return true;
289
+ }
290
+
291
+ /**
292
+ * Evict the N oldest entries by lastUsed time
293
+ * @param {number} count - Number of entries to evict
294
+ * @returns {number} Number of entries actually evicted
295
+ */
296
+ evictOldest(count) {
297
+ if (this.cache.size === 0 || count <= 0) return 0;
298
+
299
+ // Get entries sorted by lastUsed (oldest first)
300
+ const entries = Array.from(this.cache.entries())
301
+ .sort((a, b) => a[1].lastUsed - b[1].lastUsed);
302
+
303
+ const toEvict = Math.min(count, entries.length);
304
+ for (let i = 0; i < toEvict; i++) {
305
+ this.cache.delete(entries[i][0]);
306
+ }
307
+ return toEvict;
308
+ }
309
+
310
+ /**
311
+ * Drop the oldest half of entries (by lastUsed)
312
+ * Called when memory pressure is detected
313
+ * @returns {number} Number of entries evicted
314
+ */
315
+ evictOldestHalf() {
316
+ const halfSize = Math.floor(this.cache.size / 2);
317
+ return this.evictOldest(halfSize);
318
+ }
319
+
320
+ /**
321
+ * Check memory usage and evict oldest half if over threshold
322
+ * @returns {boolean} True if eviction was triggered
323
+ */
324
+ checkMemoryPressure() {
325
+ if (this.memoryThresholdBytes <= 0) return false;
326
+
327
+ const heapUsed = process.memoryUsage().heapUsed;
328
+ if (heapUsed > this.memoryThresholdBytes) {
329
+ this.evictOldestHalf();
330
+ return true;
331
+ }
332
+ return false;
333
+ }
334
+
335
+ /**
336
+ * Force-store an expansion regardless of duration (for testing)
337
+ * @param {string} key - Hash key
338
+ * @param {Object} expansion - The expanded ValueSet
339
+ */
340
+ forceSet(key, expansion) {
341
+ this.cache.set(key, {
342
+ expansion: expansion,
343
+ createdAt: Date.now(),
344
+ lastUsed: Date.now(),
345
+ durationMs: 0,
346
+ hitCount: 0
347
+ });
348
+ }
349
+
350
+ /**
351
+ * Clear a specific entry
352
+ * @param {string} key - Hash key
353
+ */
354
+ clear(key) {
355
+ this.cache.delete(key);
356
+ }
357
+
358
+ /**
359
+ * Clear all cached entries
360
+ */
361
+ clearAll() {
362
+ this.cache.clear();
363
+ }
364
+
365
+ /**
366
+ * Get cache statistics
367
+ * @returns {Object} Stats object
368
+ */
369
+ stats() {
370
+ let totalHits = 0;
371
+ let totalDuration = 0;
372
+ for (const entry of this.cache.values()) {
373
+ totalHits += entry.hitCount;
374
+ totalDuration += entry.durationMs;
375
+ }
376
+ return {
377
+ size: this.cache.size,
378
+ maxSize: this.maxSize,
379
+ memoryThresholdMB: this.memoryThresholdBytes > 0 ? this.memoryThresholdBytes / (1024 * 1024) : 0,
380
+ totalHits,
381
+ totalDurationSaved: totalHits > 0 ? totalDuration * totalHits : 0
382
+ };
383
+ }
384
+
385
+ size() {
386
+ return this.cache.size;
387
+ }
388
+ }
389
+
390
+
391
+ class OperationContext {
392
+ constructor(langs, i18n = null, id = null, timeLimit = 30, resourceCache = null, expansionCache = null) {
393
+ this.i18n = i18n;
394
+ this.langs = this._ensureLanguages(langs);
395
+ this.id = id || this._generateId();
396
+ this.startTime = performance.now();
397
+ this.contexts = [];
398
+ this.timeLimit = timeLimit * 1000; // Convert to milliseconds
399
+ this.timeTracker = new TimeTracker();
400
+ this.logEntries = [];
401
+ this.resourceCache = resourceCache;
402
+ this.expansionCache = expansionCache;
403
+ this.debugging = isDebugging();
404
+
405
+ this.timeTracker.step('tx-op');
406
+ }
407
+
408
+ _ensureLanguages(param) {
409
+ assert(typeof param === 'string' || param instanceof Languages, 'Parameter must be string or Languages object');
410
+ return typeof param === 'string' ? Languages.fromAcceptLanguage(param, this.i18n.languageDefinitions, false) : param;
411
+ }
412
+
413
+ _generateId() {
414
+ return 'op_' + Math.random().toString(36).substr(2, 9) + '_' + Date.now();
415
+ }
416
+
417
+ /**
418
+ * Create a copy of this operation context
419
+ * @returns {OperationContext}
420
+ */
421
+ copy() {
422
+ const newContext = new OperationContext(
423
+ this.langs, this.i18n, this.id, this.timeLimit / 1000,
424
+ this.resourceCache, this.expansionCache
425
+ );
426
+ newContext.contexts = [...this.contexts];
427
+ newContext.startTime = this.startTime;
428
+ newContext.timeTracker = this.timeTracker.link();
429
+ newContext.logEntries = [...this.logEntries];
430
+ newContext.debugging = this.debugging;
431
+ return newContext;
432
+ }
433
+
434
+ /**
435
+ * Check if operation has exceeded time limit
436
+ * Skipped when running under debugger
437
+ * @param {string} place - Location identifier for debugging
438
+ * @returns {boolean} true if operation should be terminated
439
+ */
440
+ deadCheck(place = 'unknown') {
441
+ // Skip time limit checks when debugging
442
+ if (this.debugging) {
443
+ return false;
444
+ }
445
+
446
+ const elapsed = performance.now() - this.startTime;
447
+
448
+ if (elapsed > this.timeLimit) {
449
+ const timeInSeconds = Math.round(this.timeLimit / 1000);
450
+ this.log(`Operation took too long @ ${place} (${this.constructor.name})`);
451
+
452
+ const error = new Issue("error", "too-costly", null, `Operation exceeded time limit of ${timeInSeconds} seconds at ${place}`);
453
+ error.diagnostics = this.diagnostics();
454
+ throw error;
455
+ }
456
+
457
+ return false;
458
+ }
459
+
460
+ /**
461
+ * Track a context URL and detect circular references
462
+ * @param {string} vurl - Value set URL to track
463
+ */
464
+ seeContext(vurl) {
465
+ if (this.contexts.includes(vurl)) {
466
+ 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);
468
+ }
469
+ this.contexts.push(vurl);
470
+ }
471
+
472
+ /**
473
+ * Clear all tracked contexts
474
+ */
475
+ clearContexts() {
476
+ this.contexts = [];
477
+ }
478
+
479
+ /**
480
+ * Add a log entry with timestamp
481
+ * @param {string} note - Log message
482
+ */
483
+ log(note) {
484
+ const elapsed = Math.round(performance.now() - this.startTime);
485
+ const logEntry = `${elapsed}ms ${note}`;
486
+ this.logEntries.push(logEntry);
487
+ this.timeTracker.step(note);
488
+ }
489
+
490
+ /**
491
+ * Add a note specific to a value set
492
+ * @param {Object} vs - Value set object (should have vurl property)
493
+ * @param {string} note - Note to add
494
+ */
495
+ addNote(vs, note) {
496
+ const vurl = vs && vs.vurl ? vs.vurl : 'unknown-valueset';
497
+ const elapsed = Math.round(performance.now() - this.startTime);
498
+ const logEntry = `${elapsed}ms ${vurl}: ${note}`;
499
+ this.logEntries.push(logEntry);
500
+ this.timeTracker.step(`${vurl}: ${note}`);
501
+ }
502
+
503
+ /**
504
+ * Get diagnostic information including timing and logs
505
+ * @returns {string}
506
+ */
507
+ diagnostics() {
508
+ return this.timeTracker.log();
509
+ }
510
+
511
+ /**
512
+ * Execute and time an async operation, logging if it exceeds threshold
513
+ * @param {string} name - Operation name for logging
514
+ * @param {Function} fn - Async function to execute
515
+ * @param {number} warnThreshold - Log warning if operation exceeds this ms (default 50)
516
+ * @returns {*} Result of the function
517
+ */
518
+ async timed(name, fn, warnThreshold = 50) {
519
+ const start = performance.now();
520
+ try {
521
+ return await fn();
522
+ } finally {
523
+ const duration = performance.now() - start;
524
+ if (duration > warnThreshold) {
525
+ this.log(`SLOW: ${name} took ${Math.round(duration)}ms`);
526
+ }
527
+ }
528
+ }
529
+
530
+ /**
531
+ * Get elapsed time since operation started
532
+ * @returns {number} Elapsed time in milliseconds
533
+ */
534
+ elapsed() {
535
+ return performance.now() - this.startTime;
536
+ }
537
+
538
+ /**
539
+ * Get the request ID
540
+ * @returns {string}
541
+ */
542
+ get reqId() {
543
+ return this.id;
544
+ }
545
+
546
+ /**
547
+ * @type {Languages} languages specified in request
548
+ */
549
+ langs;
550
+ }
551
+
552
+ /**
553
+ * Version rule modes for expansion parameters
554
+ */
555
+ const ExpansionParamsVersionRuleMode = {
556
+ DEFAULT: 0,
557
+ CHECK: 1,
558
+ OVERRIDE: 2
559
+ };
560
+
561
+ module.exports = {
562
+ OperationContext,
563
+ ExpansionParamsVersionRuleMode,
564
+ TimeTracker,
565
+ ResourceCache,
566
+ ExpansionCache,
567
+ isDebugging
568
+ };