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.
- package/CHANGELOG.md +42 -0
- package/FHIRsmith.png +0 -0
- package/README.md +277 -0
- package/config-template.json +144 -0
- package/library/folder-setup.js +58 -0
- package/library/html-server.js +166 -0
- package/library/html.js +835 -0
- package/library/i18nsupport.js +259 -0
- package/library/languages.js +779 -0
- package/library/logger-telnet.js +205 -0
- package/library/logger.js +279 -0
- package/library/package-manager.js +876 -0
- package/library/utilities.js +196 -0
- package/library/version-utilities.js +1056 -0
- package/npmprojector/config-example.json +13 -0
- package/npmprojector/indexer.js +394 -0
- package/npmprojector/npmprojector.js +395 -0
- package/npmprojector/readme.md +174 -0
- package/npmprojector/watcher.js +335 -0
- package/package.json +119 -0
- package/packages/package-crawler.js +846 -0
- package/packages/packages-template.html +126 -0
- package/packages/packages.js +2838 -0
- package/passwords.ini +2 -0
- package/publisher/publisher-template.html +208 -0
- package/publisher/publisher.js +2167 -0
- package/publisher/task-draft.js +458 -0
- package/registry/api.js +735 -0
- package/registry/crawler.js +637 -0
- package/registry/model.js +513 -0
- package/registry/readme.md +243 -0
- package/registry/registry-data.json +121015 -0
- package/registry/registry-template.html +126 -0
- package/registry/registry.js +1395 -0
- package/registry/test-runner.js +237 -0
- package/root-template.html +124 -0
- package/server.js +524 -0
- package/shl/private-key.pem +5 -0
- package/shl/public-key.pem +18 -0
- package/shl/shl.js +1125 -0
- package/shl/vhl.js +69 -0
- package/static/FHIRsmith128.png +0 -0
- package/static/FHIRsmith16.png +0 -0
- package/static/FHIRsmith32.png +0 -0
- package/static/FHIRsmith64.png +0 -0
- package/static/assets/css/bootstrap-fhir.css +5302 -0
- package/static/assets/css/bootstrap-glyphicons.css +2 -0
- package/static/assets/css/bootstrap.css +4097 -0
- package/static/assets/css/jquery-ui.css +523 -0
- package/static/assets/css/jquery-ui.structure.css +863 -0
- package/static/assets/css/jquery-ui.structure.min.css +5 -0
- package/static/assets/css/jquery-ui.theme.css +439 -0
- package/static/assets/css/jquery-ui.theme.min.css +5 -0
- package/static/assets/css/jquery.ui.all.css +7 -0
- package/static/assets/css/modules.css +18 -0
- package/static/assets/css/project.css +367 -0
- package/static/assets/css/pygments-manni.css +66 -0
- package/static/assets/css/tags.css +74 -0
- package/static/assets/css/xml.css +2 -0
- package/static/assets/fonts/glyphiconshalflings-regular.eot +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.otf +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.svg +175 -0
- package/static/assets/fonts/glyphiconshalflings-regular.ttf +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.woff +0 -0
- package/static/assets/ico/apple-touch-icon-114-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-144-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-57-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-72-precomposed.png +0 -0
- package/static/assets/ico/favicon.ico +0 -0
- package/static/assets/ico/favicon.png +0 -0
- package/static/assets/images/fhir-logo-www.png +0 -0
- package/static/assets/images/fhir-logo.png +0 -0
- package/static/assets/images/hl7-logo.png +0 -0
- package/static/assets/images/logo_ansinew.jpg +0 -0
- package/static/assets/images/search.png +0 -0
- package/static/assets/images/stripe.png +0 -0
- package/static/assets/images/target.png +0 -0
- package/static/assets/images/tx-registry-root.gif +0 -0
- package/static/assets/images/tx-registry.png +0 -0
- package/static/assets/images/tx-server.png +0 -0
- package/static/assets/images/tx-version.png +0 -0
- package/static/assets/js/bootstrap.min.js +6 -0
- package/static/assets/js/fhir-gw.js +259 -0
- package/static/assets/js/fhir.js +2 -0
- package/static/assets/js/html5shiv.js +8 -0
- package/static/assets/js/jcookie.js +96 -0
- package/static/assets/js/jquery-ui.min.js +6 -0
- package/static/assets/js/jquery.js +10716 -0
- package/static/assets/js/jquery.min.js +2 -0
- package/static/assets/js/jquery.ui.core.js +314 -0
- package/static/assets/js/jquery.ui.draggable.js +825 -0
- package/static/assets/js/jquery.ui.mouse.js +162 -0
- package/static/assets/js/jquery.ui.resizable.js +842 -0
- package/static/assets/js/jquery.ui.widget.js +268 -0
- package/static/assets/js/json2.js +487 -0
- package/static/assets/js/jtip.js +97 -0
- package/static/assets/js/respond.min.js +6 -0
- package/static/assets/js/statuspage.js +70 -0
- package/static/assets/js/xml.js +2 -0
- package/static/dist/js/bootstrap.js +1964 -0
- package/static/favicon.png +0 -0
- package/static/fhir.css +626 -0
- package/static/icon-fhir-16.png +0 -0
- package/static/images/ui-bg_diagonals-thick_18_b81900_40x40.png +0 -0
- package/static/images/ui-bg_diagonals-thick_20_666666_40x40.png +0 -0
- package/static/images/ui-bg_flat_10_000000_40x100.png +0 -0
- package/static/images/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
- package/static/images/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
- package/static/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- package/static/images/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
- package/static/images/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
- package/static/images/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
- package/static/images/ui-icons_222222_256x240.png +0 -0
- package/static/images/ui-icons_228ef1_256x240.png +0 -0
- package/static/images/ui-icons_ef8c08_256x240.png +0 -0
- package/static/images/ui-icons_ffd27a_256x240.png +0 -0
- package/static/images/ui-icons_ffffff_256x240.png +0 -0
- package/static/js/jquery.effects.blind.js +49 -0
- package/static/js/jquery.effects.bounce.js +78 -0
- package/static/js/jquery.effects.clip.js +54 -0
- package/static/js/jquery.effects.core.js +763 -0
- package/static/js/jquery.effects.drop.js +50 -0
- package/static/js/jquery.effects.explode.js +79 -0
- package/static/js/jquery.effects.fade.js +32 -0
- package/static/js/jquery.effects.fold.js +56 -0
- package/static/js/jquery.effects.highlight.js +50 -0
- package/static/js/jquery.effects.pulsate.js +51 -0
- package/static/js/jquery.effects.scale.js +178 -0
- package/static/js/jquery.effects.shake.js +57 -0
- package/static/js/jquery.effects.slide.js +50 -0
- package/static/js/jquery.effects.transfer.js +45 -0
- package/static/js/jquery.ui.accordion.js +611 -0
- package/static/js/jquery.ui.autocomplete.js +612 -0
- package/static/js/jquery.ui.button.js +416 -0
- package/static/js/jquery.ui.datepicker.js +1823 -0
- package/static/js/jquery.ui.dialog.js +878 -0
- package/static/js/jquery.ui.droppable.js +296 -0
- package/static/js/jquery.ui.position.js +252 -0
- package/static/js/jquery.ui.progressbar.js +109 -0
- package/static/js/jquery.ui.selectable.js +266 -0
- package/static/js/jquery.ui.slider.js +666 -0
- package/static/js/jquery.ui.sortable.js +1077 -0
- package/static/js/jquery.ui.tabs.js +758 -0
- package/stats.js +80 -0
- package/test-cache/vsac/vsac-valuesets.db +0 -0
- package/token/nginx_passport_setup.md +383 -0
- package/token/security_guide.md +294 -0
- package/token/token-template.html +330 -0
- package/token/token.js +1300 -0
- package/translations/Messages.properties +1510 -0
- package/translations/Messages_ar.properties +1399 -0
- package/translations/Messages_de.properties +836 -0
- package/translations/Messages_es.properties +737 -0
- package/translations/Messages_fr.properties +1 -0
- package/translations/Messages_ja.properties +893 -0
- package/translations/Messages_nl.properties +1357 -0
- package/translations/Messages_pt.properties +1302 -0
- package/translations/Messages_ru.properties +1 -0
- package/translations/Messages_uz.properties +1 -0
- package/translations/Messages_zh.properties +1 -0
- package/translations/rendering-phrases.properties +1128 -0
- package/translations/rendering-phrases_ar.properties +1091 -0
- package/translations/rendering-phrases_de.properties +6 -0
- package/translations/rendering-phrases_es.properties +6 -0
- package/translations/rendering-phrases_fr.properties +624 -0
- package/translations/rendering-phrases_ja.properties +21 -0
- package/translations/rendering-phrases_nl.properties +970 -0
- package/translations/rendering-phrases_pt.properties +1020 -0
- package/translations/rendering-phrases_ru.properties +1094 -0
- package/translations/rendering-phrases_uz.properties +1 -0
- package/translations/rendering-phrases_zh.properties +1 -0
- package/tx/README.md +418 -0
- package/tx/cm/cm-api.js +110 -0
- package/tx/cm/cm-database.js +735 -0
- package/tx/cm/cm-package.js +325 -0
- package/tx/cs/cs-api.js +789 -0
- package/tx/cs/cs-areacode.js +615 -0
- package/tx/cs/cs-country.js +1110 -0
- package/tx/cs/cs-cpt.js +785 -0
- package/tx/cs/cs-cs.js +1579 -0
- package/tx/cs/cs-currency.js +539 -0
- package/tx/cs/cs-db.js +1321 -0
- package/tx/cs/cs-hgvs.js +329 -0
- package/tx/cs/cs-lang.js +465 -0
- package/tx/cs/cs-loinc.js +1485 -0
- package/tx/cs/cs-mimetypes.js +238 -0
- package/tx/cs/cs-ndc.js +704 -0
- package/tx/cs/cs-omop.js +1025 -0
- package/tx/cs/cs-provider-api.js +43 -0
- package/tx/cs/cs-provider-list.js +37 -0
- package/tx/cs/cs-rxnorm.js +808 -0
- package/tx/cs/cs-snomed.js +1102 -0
- package/tx/cs/cs-ucum.js +514 -0
- package/tx/cs/cs-unii.js +271 -0
- package/tx/cs/cs-uri.js +218 -0
- package/tx/cs/cs-usstates.js +305 -0
- package/tx/dev.fhir.org.yml +14 -0
- package/tx/fixtures/test-cases-setup.json +18 -0
- package/tx/fixtures/test-cases.yml +16 -0
- package/tx/html/codesystem-operations.liquid +25 -0
- package/tx/html/home-metrics.liquid +247 -0
- package/tx/html/operations-form.liquid +148 -0
- package/tx/html/search-form.liquid +62 -0
- package/tx/html/tx-template.html +133 -0
- package/tx/html/valueset-operations.liquid +54 -0
- package/tx/importers/atc-to-fhir.js +316 -0
- package/tx/importers/import-loinc.module.js +1536 -0
- package/tx/importers/import-ndc.module.js +1088 -0
- package/tx/importers/import-rxnorm.module.js +898 -0
- package/tx/importers/import-sct.module.js +2457 -0
- package/tx/importers/import-unii.module.js +601 -0
- package/tx/importers/readme.md +453 -0
- package/tx/importers/subset-loinc.module.js +1081 -0
- package/tx/importers/subset-rxnorm.module.js +938 -0
- package/tx/importers/tx-import-base.js +351 -0
- package/tx/importers/tx-import-settings.js +310 -0
- package/tx/importers/tx-import.js +357 -0
- package/tx/library/canonical-resource.js +88 -0
- package/tx/library/capabilitystatement.js +292 -0
- package/tx/library/codesystem.js +774 -0
- package/tx/library/conceptmap.js +568 -0
- package/tx/library/designations.js +932 -0
- package/tx/library/errors.js +77 -0
- package/tx/library/extensions.js +117 -0
- package/tx/library/namingsystem.js +322 -0
- package/tx/library/operation-outcome.js +127 -0
- package/tx/library/parameters.js +105 -0
- package/tx/library/renderer.js +1559 -0
- package/tx/library/terminologycapabilities.js +418 -0
- package/tx/library/ucum-parsers.js +1029 -0
- package/tx/library/ucum-service.js +370 -0
- package/tx/library/ucum-types.js +1099 -0
- package/tx/library/valueset.js +543 -0
- package/tx/library.js +676 -0
- package/tx/ocl/cm-ocl.js +106 -0
- package/tx/ocl/cs-ocl.js +39 -0
- package/tx/ocl/vs-ocl.js +105 -0
- package/tx/operation-context.js +568 -0
- package/tx/params.js +613 -0
- package/tx/provider.js +403 -0
- package/tx/sct/ecl.js +1560 -0
- package/tx/sct/expressions.js +2077 -0
- package/tx/sct/structures.js +1396 -0
- package/tx/tx-html.js +1063 -0
- package/tx/tx.fhir.org.yml +39 -0
- package/tx/tx.js +927 -0
- package/tx/vs/vs-api.js +112 -0
- package/tx/vs/vs-database.js +786 -0
- package/tx/vs/vs-package.js +358 -0
- package/tx/vs/vs-vsac.js +366 -0
- package/tx/workers/batch-validate.js +129 -0
- package/tx/workers/batch.js +361 -0
- package/tx/workers/closure.js +32 -0
- package/tx/workers/expand.js +1845 -0
- package/tx/workers/lookup.js +407 -0
- package/tx/workers/metadata.js +467 -0
- package/tx/workers/operations.js +34 -0
- package/tx/workers/read.js +164 -0
- package/tx/workers/search.js +384 -0
- package/tx/workers/subsumes.js +334 -0
- package/tx/workers/translate.js +492 -0
- package/tx/workers/validate.js +2504 -0
- package/tx/workers/worker.js +904 -0
- package/tx/xml/capabilitystatement-xml.js +63 -0
- package/tx/xml/codesystem-xml.js +62 -0
- package/tx/xml/conceptmap-xml.js +65 -0
- package/tx/xml/namingsystem-xml.js +65 -0
- package/tx/xml/operationoutcome-xml.js +127 -0
- package/tx/xml/parameters-xml.js +312 -0
- package/tx/xml/terminologycapabilities-xml.js +64 -0
- package/tx/xml/valueset-xml.js +64 -0
- package/tx/xml/xml-base.js +603 -0
- package/vcl/vcl-parser.js +1098 -0
- package/vcl/vcl.js +253 -0
- package/windows-install.js +19 -0
- package/xig/xig-template.html +124 -0
- 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
|
+
};
|