fhirsmith 0.13.3 → 0.14.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 +47 -1
- package/README.md +48 -4
- package/config-template.json +70 -1
- package/library/cgroup-memory.js +113 -0
- package/library/folder-setup.js +8 -1
- package/nginx.md +391 -0
- package/package.json +3 -2
- package/publisher/git-remote.js +141 -0
- package/publisher/github-release.js +195 -0
- package/publisher/publisher.js +146 -14
- package/server.js +51 -14
- package/stats.js +19 -13
- package/testing/readme.md +112 -0
- package/testing/render.js +482 -0
- package/testing/search.js +378 -0
- package/testing/store.js +266 -0
- package/testing/testing-template.html +126 -0
- package/testing/testing.js +486 -0
- package/translations/Messages.properties +32 -0
- package/tx/closure/closure-store.js +396 -0
- package/tx/cs/cs-api.js +6 -1
- package/tx/cs/cs-rxnorm.js +35 -8
- package/tx/cs/cs-snomed.js +142 -27
- package/tx/html/codesystem-operations.liquid +177 -0
- package/tx/html/ecl-panel.liquid +93 -0
- package/tx/html/home-metrics.liquid +39 -76
- package/tx/html/tx-template.html +1 -0
- package/tx/library/conceptmap.js +22 -6
- package/tx/library/designations.js +12 -3
- package/tx/library/extensions.js +2 -2
- package/tx/library/operation-outcome.js +3 -1
- package/tx/library/renderer.js +42 -5
- package/tx/operation-context.js +111 -15
- package/tx/provider.js +85 -0
- package/tx/sct/expressions.js +128 -44
- package/tx/sct/text-index.js +419 -0
- package/tx/tx-html.js +426 -44
- package/tx/tx.js +117 -9
- package/tx/workers/cache-control.js +2 -2
- package/tx/workers/closure.js +483 -13
- package/tx/workers/expand.js +20 -0
- package/tx/workers/metadata.js +29 -4
- package/tx/workers/read.js +18 -3
- package/tx/workers/subsumes.js +18 -2
- package/tx/workers/translate.js +167 -20
- package/tx/workers/validate.js +61 -182
- package/tx/workers/worker.js +9 -0
- package/tx/xversion/xv-conceptmap.js +17 -8
- package/utilities/submit-test-report.js +150 -0
package/tx/html/tx-template.html
CHANGED
|
@@ -89,6 +89,7 @@
|
|
|
89
89
|
<a href="[%endpoint-path%]/metadata" style="color: gold">Capability Statement</a> |
|
|
90
90
|
<a href="[%endpoint-path%]/metadata?mode=terminology" style="color: gold">Terminology Capabilities</a> |
|
|
91
91
|
<a href="[%endpoint-path%]/op.html" style="color: gold">Operations</a> |
|
|
92
|
+
<a href="[%endpoint-path%]/ecl" style="color: gold">ECL</a> |
|
|
92
93
|
<a href="[%endpoint-path%]/problems.html" style="color: gold">Problems</a> |
|
|
93
94
|
[%library-link%]
|
|
94
95
|
|
package/tx/library/conceptmap.js
CHANGED
|
@@ -196,6 +196,14 @@ class ConceptMap extends CanonicalResource {
|
|
|
196
196
|
* their elements matched. group.unmapped only applies to a group that was in scope
|
|
197
197
|
* for the translation, so the caller needs the same source/target test that
|
|
198
198
|
* listTranslations applies - without the per-element filter.
|
|
199
|
+
*
|
|
200
|
+
* "In scope" here is only about this group's source and target code systems. It assumes
|
|
201
|
+
* the MAP itself is in scope, which is a separate and prior question: ConceptMap.sourceScope
|
|
202
|
+
* limits the map to codes that are members of that value set, and a map that does not cover
|
|
203
|
+
* the code has no groups in scope at all, whatever their systems say. That test needs a value
|
|
204
|
+
* set lookup and a $validate-code, so it lives in the worker (TranslateWorker.filterMapsByScope)
|
|
205
|
+
* and is applied to the map before this is ever called. Do not read a group coming back from
|
|
206
|
+
* here as evidence that the map applies.
|
|
199
207
|
* @returns {Object[]} the matching group objects
|
|
200
208
|
*/
|
|
201
209
|
listGroupsInScope(coding, targetScope, targetSystem) {
|
|
@@ -238,19 +246,27 @@ class ConceptMap extends CanonicalResource {
|
|
|
238
246
|
return result;
|
|
239
247
|
}
|
|
240
248
|
/**
|
|
241
|
-
* Gets the source scope (R5) or source
|
|
242
|
-
*
|
|
249
|
+
* Gets the source scope (R5+) or source (R3/R4).
|
|
250
|
+
*
|
|
251
|
+
* R5 renamed ConceptMap.source[x] to ConceptMap.sourceScope[x], so an R4 resource spells this
|
|
252
|
+
* sourceUri / sourceCanonical and an R5 one sourceScopeUri / sourceScopeCanonical. Both have to
|
|
253
|
+
* be read: this server serves R4 as well as R5, and the R4 client sends the R4 spelling. Reading
|
|
254
|
+
* only the R5 names made this silently undefined on an R4 server, which made every map look
|
|
255
|
+
* unscoped - so the scope never limited anything, and $translate answered for codes outside it.
|
|
256
|
+
* @returns {string|undefined} Source scope
|
|
243
257
|
*/
|
|
244
258
|
get sourceScope() {
|
|
245
|
-
return this.jsonObj.sourceScopeUri
|
|
259
|
+
return this.jsonObj.sourceScopeUri || this.jsonObj.sourceScopeCanonical
|
|
260
|
+
|| this.jsonObj.sourceUri || this.jsonObj.sourceCanonical;
|
|
246
261
|
}
|
|
247
262
|
|
|
248
263
|
/**
|
|
249
|
-
* Gets the target scope (R5) or target
|
|
250
|
-
* @returns {string|undefined} Target scope
|
|
264
|
+
* Gets the target scope (R5+) or target (R3/R4). See sourceScope for why both are read.
|
|
265
|
+
* @returns {string|undefined} Target scope
|
|
251
266
|
*/
|
|
252
267
|
get targetScope() {
|
|
253
|
-
return this.jsonObj.targetScopeUri
|
|
268
|
+
return this.jsonObj.targetScopeUri || this.jsonObj.targetScopeCanonical
|
|
269
|
+
|| this.jsonObj.targetUri || this.jsonObj.targetCanonical;
|
|
254
270
|
}
|
|
255
271
|
|
|
256
272
|
/**
|
|
@@ -26,6 +26,10 @@ class SearchFilterText {
|
|
|
26
26
|
|
|
27
27
|
this.filter = filter ? filter.toLowerCase() : null;
|
|
28
28
|
this.stems = [];
|
|
29
|
+
// The words the stems came from, index-aligned with stems. A provider whose text
|
|
30
|
+
// index cannot hold a particular token (RxNorm's stem table has nothing that starts
|
|
31
|
+
// with a digit, for one) needs the word itself to search the text with instead.
|
|
32
|
+
this.words = [];
|
|
29
33
|
this.stemmer = natural.PorterStemmer;
|
|
30
34
|
if (filter) {
|
|
31
35
|
this._process();
|
|
@@ -135,6 +139,7 @@ class SearchFilterText {
|
|
|
135
139
|
|
|
136
140
|
_process() {
|
|
137
141
|
let i = 0;
|
|
142
|
+
const found = [];
|
|
138
143
|
|
|
139
144
|
while (i < this.filter.length) {
|
|
140
145
|
if (this._isAlphaNumeric(this.filter[i])) {
|
|
@@ -142,14 +147,18 @@ class SearchFilterText {
|
|
|
142
147
|
while (i < this.filter.length && this._isAlphaNumeric(this.filter[i])) {
|
|
143
148
|
i++;
|
|
144
149
|
}
|
|
145
|
-
const word = this.filter.substring(j, i);
|
|
146
|
-
|
|
150
|
+
const word = this.filter.substring(j, i).toLowerCase();
|
|
151
|
+
found.push({ word: word, stem: this.stemmer.stem(word) });
|
|
147
152
|
} else {
|
|
148
153
|
i++;
|
|
149
154
|
}
|
|
150
155
|
}
|
|
151
156
|
|
|
152
|
-
|
|
157
|
+
// Sorted by stem, because _find() binary searches stems - words ride along so that
|
|
158
|
+
// words[n] is always the word stems[n] came from.
|
|
159
|
+
found.sort((a, b) => (a.stem < b.stem ? -1 : (a.stem > b.stem ? 1 : 0)));
|
|
160
|
+
this.stems = found.map((f) => f.stem);
|
|
161
|
+
this.words = found.map((f) => f.word);
|
|
153
162
|
}
|
|
154
163
|
|
|
155
164
|
_isAlphaNumeric(char) {
|
package/tx/library/extensions.js
CHANGED
|
@@ -24,8 +24,8 @@ const Extensions = {
|
|
|
24
24
|
if (resource.jsonObj) {
|
|
25
25
|
resource = resource.jsonObj
|
|
26
26
|
}
|
|
27
|
-
if (resource.implicitRules) {
|
|
28
|
-
throw new Issue("error", "business-rule", null, null, 'Cannot process resource "'+name+'" due to the presence of implicit rules @'+place);
|
|
27
|
+
if (resource.implicitRules && 'https://hl7.org/fhir/implicit-rules/omitted-mandatory-interversion-code' != resource.implicitRules) {
|
|
28
|
+
throw new Issue("error", "business-rule", null, null, 'Cannot process resource "'+name+'" due to the presence of implicit rules \''+resource.implicitRules+'\' @'+place);
|
|
29
29
|
}
|
|
30
30
|
},
|
|
31
31
|
|
|
@@ -198,7 +198,9 @@ class OperationOutcome {
|
|
|
198
198
|
listMissedErrors(list) {
|
|
199
199
|
for (let iss of this.jsonObj.issue || []) {
|
|
200
200
|
if (iss.severity === 'error' && iss.details && iss.details.text && !list.find(msg => msg === iss.details.text )) {
|
|
201
|
-
return
|
|
201
|
+
// every error that nothing else reported, not just the first one - an early return here
|
|
202
|
+
// meant the summary message named only one of them
|
|
203
|
+
list.push(iss.details.text);
|
|
202
204
|
}
|
|
203
205
|
}
|
|
204
206
|
|
package/tx/library/renderer.js
CHANGED
|
@@ -1445,6 +1445,7 @@ class Renderer {
|
|
|
1445
1445
|
|
|
1446
1446
|
// Analyze columns needed
|
|
1447
1447
|
const columnInfo = this.analyzeExpansionColumns(vs.expansion);
|
|
1448
|
+
columnInfo.versions = this.expansionVersions(vs);
|
|
1448
1449
|
|
|
1449
1450
|
// Build the expansion table
|
|
1450
1451
|
const expTbl = x.table("codes");
|
|
@@ -1595,6 +1596,40 @@ class Renderer {
|
|
|
1595
1596
|
/**
|
|
1596
1597
|
* Add a row for an expansion contains entry
|
|
1597
1598
|
*/
|
|
1599
|
+
/**
|
|
1600
|
+
* The version each code system was expanded at, by system uri.
|
|
1601
|
+
*
|
|
1602
|
+
* An expansion.contains only carries a version when the expansion needed to distinguish
|
|
1603
|
+
* one, so a link built from the row alone would silently point at whichever version the
|
|
1604
|
+
* server happens to prefer - not what the reader is looking at. The expansion says which
|
|
1605
|
+
* version it used in its used-codesystem parameters, and a value set that came with its
|
|
1606
|
+
* definition says so in its compose; either will do.
|
|
1607
|
+
*
|
|
1608
|
+
* @param {Object} vs the expanded ValueSet
|
|
1609
|
+
* @returns {Map<string, string>} system uri to version
|
|
1610
|
+
*/
|
|
1611
|
+
expansionVersions(vs) {
|
|
1612
|
+
const versions = new Map();
|
|
1613
|
+
for (const inc of vs.compose?.include || []) {
|
|
1614
|
+
if (inc.system && inc.version && !versions.has(inc.system)) {
|
|
1615
|
+
versions.set(inc.system, inc.version);
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
for (const p of vs.expansion?.parameter || []) {
|
|
1619
|
+
if (p.name !== 'used-codesystem') {
|
|
1620
|
+
continue;
|
|
1621
|
+
}
|
|
1622
|
+
const vurl = getValuePrimitive(p);
|
|
1623
|
+
if (vurl && vurl.includes('|')) {
|
|
1624
|
+
const system = vurl.substring(0, vurl.indexOf('|'));
|
|
1625
|
+
if (!versions.has(system)) {
|
|
1626
|
+
versions.set(system, vurl.substring(vurl.indexOf('|') + 1));
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
return versions;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1598
1633
|
async addExpansionRow(tbl, contains, level, columnInfo) {
|
|
1599
1634
|
const tr = tbl.tr();
|
|
1600
1635
|
|
|
@@ -1617,12 +1652,14 @@ class Renderer {
|
|
|
1617
1652
|
codeTd.tx('\u00A0'.repeat(level * 2)); // Non-breaking spaces for indentation
|
|
1618
1653
|
}
|
|
1619
1654
|
|
|
1620
|
-
//
|
|
1655
|
+
// The code links to $lookup on it - what a reader wants from a row in an expansion is
|
|
1656
|
+
// to find out what that code is - at the version the expansion used.
|
|
1621
1657
|
if (contains.code) {
|
|
1622
|
-
const
|
|
1623
|
-
|
|
1658
|
+
const version = contains.version || columnInfo.versions?.get(contains.system) || null;
|
|
1659
|
+
const link = this.linkResolver?.lookupLink
|
|
1660
|
+
? this.linkResolver.lookupLink(contains.system, version, contains.code) : null;
|
|
1624
1661
|
if (link) {
|
|
1625
|
-
codeTd.ah(link.
|
|
1662
|
+
codeTd.ah(link, 'Look this code up').code().tx(contains.code);
|
|
1626
1663
|
} else {
|
|
1627
1664
|
codeTd.code().tx(contains.code);
|
|
1628
1665
|
}
|
|
@@ -2732,4 +2769,4 @@ class Renderer {
|
|
|
2732
2769
|
|
|
2733
2770
|
}
|
|
2734
2771
|
|
|
2735
|
-
module.exports = { Renderer };
|
|
2772
|
+
module.exports = { Renderer, VALID_FILTER_OPS };
|
package/tx/operation-context.js
CHANGED
|
@@ -4,6 +4,7 @@ const crypto = require("crypto");
|
|
|
4
4
|
const {Languages} = require("../library/languages");
|
|
5
5
|
const {Issue} = require("./library/operation-outcome");
|
|
6
6
|
const Logger = require("../library/logger");
|
|
7
|
+
const { readCgroupMemoryLimit } = require('../library/cgroup-memory');
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Check if running under a debugger
|
|
@@ -110,6 +111,63 @@ class ResourceCache {
|
|
|
110
111
|
// so a client polling $cache-control?mode=check can work out how often it needs
|
|
111
112
|
// to poll instead of guessing. Null = not advertised.
|
|
112
113
|
this.idleTimeoutMs = null;
|
|
114
|
+
// This server instance's code (modules.tx.instanceCode), prefixed onto every
|
|
115
|
+
// cache-id it issues as "<code>.<uuid>". A proxy in front of several instances
|
|
116
|
+
// routes on the prefix to get a cached request back to the one instance that
|
|
117
|
+
// holds the cache (see nginx.md). Null = unprefixed ids (a single server).
|
|
118
|
+
this.instanceCode = null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Set the instance code that is prefixed onto the cache-ids this cache issues.
|
|
123
|
+
* 1-16 letters and digits: the code is followed by a '.', a proxy has to be able
|
|
124
|
+
* to match it with a simple pattern, and the whole id must stay a valid FHIR id. Empty/null/undefined means no prefix.
|
|
125
|
+
*
|
|
126
|
+
* @param {string|null|undefined} code
|
|
127
|
+
* @throws {Error} if the code is not letters and digits
|
|
128
|
+
*/
|
|
129
|
+
setInstanceCode(code) {
|
|
130
|
+
this.instanceCode = ResourceCache.checkInstanceCode(code);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Validate an instance code from config.
|
|
135
|
+
* @param {string|null|undefined} code
|
|
136
|
+
* @returns {string|null} the code, or null for none (empty/null/undefined)
|
|
137
|
+
* @throws {Error} if the code is not letters and digits
|
|
138
|
+
*/
|
|
139
|
+
static checkInstanceCode(code) {
|
|
140
|
+
if (code === undefined || code === null || code === '') {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
// At most 16 characters: the cache-id goes back to the client as a FHIR id
|
|
144
|
+
// (valueId), which is capped at 64 - and "<code>." plus a 36-character UUID
|
|
145
|
+
// has to fit.
|
|
146
|
+
if (typeof code !== 'string' || !/^[A-Za-z0-9]{1,16}$/.test(code)) {
|
|
147
|
+
throw new Error(`tx instanceCode '${code}' is not valid: it must be 1-16 letters and digits`);
|
|
148
|
+
}
|
|
149
|
+
return code;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Mint a new cache-id: a UUID, prefixed with "<instanceCode>." when this server
|
|
154
|
+
* has an instance code. Minting only - the caller creates the entry with set().
|
|
155
|
+
* @returns {string}
|
|
156
|
+
*/
|
|
157
|
+
newCacheId() {
|
|
158
|
+
const uuid = crypto.randomUUID();
|
|
159
|
+
return this.instanceCode ? `${this.instanceCode}.${uuid}` : uuid;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The instance code in a cache-id, or null if it has none. Ids this server mints
|
|
164
|
+
* are either a bare UUID (no '.') or "<code>.<uuid>".
|
|
165
|
+
* @param {string} cacheId
|
|
166
|
+
* @returns {string|null}
|
|
167
|
+
*/
|
|
168
|
+
static issuerOf(cacheId) {
|
|
169
|
+
const dot = typeof cacheId === 'string' ? cacheId.indexOf('.') : -1;
|
|
170
|
+
return dot > 0 ? cacheId.substring(0, dot) : null;
|
|
113
171
|
}
|
|
114
172
|
|
|
115
173
|
/**
|
|
@@ -164,7 +222,9 @@ class ResourceCache {
|
|
|
164
222
|
|
|
165
223
|
/**
|
|
166
224
|
* The message to report for a cache-id that isn't here: which of the three
|
|
167
|
-
* fates it met, with the numbers that make it checkable
|
|
225
|
+
* fates it met, with the numbers that make it checkable - or, when there's no
|
|
226
|
+
* record of it and its prefix names another instance, that it was routed here
|
|
227
|
+
* by mistake. Both throw sites
|
|
168
228
|
* (worker.setupAdditionalResources and batchValidate.frontLoadBatch) use this
|
|
169
229
|
* so they can never drift apart.
|
|
170
230
|
*
|
|
@@ -175,6 +235,15 @@ class ResourceCache {
|
|
|
175
235
|
describeMissing(cacheId) {
|
|
176
236
|
const t = this.tombstone(cacheId);
|
|
177
237
|
if (!t) {
|
|
238
|
+
// No record of it here. If its prefix names a different instance, say so:
|
|
239
|
+
// that's a routing problem (proxy misconfigured, or the owning instance is
|
|
240
|
+
// down and the request fell through to a backup), not a lifecycle one.
|
|
241
|
+
const issuer = ResourceCache.issuerOf(cacheId);
|
|
242
|
+
if (issuer && issuer !== this.instanceCode) {
|
|
243
|
+
return this.instanceCode
|
|
244
|
+
? { messageId: 'CACHE_ID_OTHER_INSTANCE', params: [cacheId, issuer, this.instanceCode] }
|
|
245
|
+
: { messageId: 'CACHE_ID_OTHER_INSTANCE_UNNAMED', params: [cacheId, issuer] };
|
|
246
|
+
}
|
|
178
247
|
return { messageId: 'CACHE_ID_UNKNOWN', params: [cacheId] };
|
|
179
248
|
}
|
|
180
249
|
const ago = formatDuration(Date.now() - t.at);
|
|
@@ -752,18 +821,12 @@ class ExpansionCache {
|
|
|
752
821
|
* Returns the byte limit, or 0 if unavailable (disables the check).
|
|
753
822
|
*/
|
|
754
823
|
function readMemoryLimit() {
|
|
755
|
-
|
|
756
|
-
const raw = require('fs').readFileSync('/sys/fs/cgroup/memory.max', 'utf8').trim();
|
|
757
|
-
if (raw === 'max') return 0; // no cgroup limit
|
|
758
|
-
return parseInt(raw);
|
|
759
|
-
} catch {
|
|
760
|
-
return 0; // not on Linux / no cgroup
|
|
761
|
-
}
|
|
824
|
+
return readCgroupMemoryLimit().limit;
|
|
762
825
|
}
|
|
763
826
|
|
|
764
827
|
const MEMORY_LIMIT = readMemoryLimit();
|
|
765
828
|
const MEMORY_FRACTION = 0.98;
|
|
766
|
-
const MEMORY_THRESHOLD = MEMORY_LIMIT > 0 ? MEMORY_LIMIT * MEMORY_FRACTION : 0; //
|
|
829
|
+
const MEMORY_THRESHOLD = MEMORY_LIMIT > 0 ? MEMORY_LIMIT * MEMORY_FRACTION : 0; // MEMORY_FRACTION of cgroup limit
|
|
767
830
|
const CHECK_FREQUENCY = 100;
|
|
768
831
|
// How long an operation may compute without yielding the event loop (ms).
|
|
769
832
|
// Node runs all JS on one thread: while a long operation executes, no other
|
|
@@ -891,8 +954,8 @@ class OperationContext {
|
|
|
891
954
|
if (elapsed > this.timeLimit) {
|
|
892
955
|
const timeInSeconds = Math.round(this.timeLimit / 1000);
|
|
893
956
|
this.log(`Operation took too long @ ${place} (${this.constructor.name})`);
|
|
894
|
-
const error = new Issue("error", "too-costly", null,
|
|
895
|
-
`Operation exceeded time limit of ${timeInSeconds} seconds at ${place}
|
|
957
|
+
const error = new Issue("error", "too-costly", null, null,
|
|
958
|
+
`Operation exceeded time limit of ${timeInSeconds} seconds at ${place}`, "too-costly", 422);
|
|
896
959
|
error.diagnostics = this.diagnostics();
|
|
897
960
|
throw error;
|
|
898
961
|
}
|
|
@@ -904,8 +967,8 @@ class OperationContext {
|
|
|
904
967
|
const usedGB = (rss / 1024 / 1024 / 1024).toFixed(1);
|
|
905
968
|
const limitGB = (MEMORY_LIMIT / 1024 / 1024 / 1024).toFixed(1);
|
|
906
969
|
this.log(`Memory Limit: ${usedGB} GB of ${limitGB} GB limit @ ${place}`);
|
|
907
|
-
const error = new Issue("error", "too-costly", null,
|
|
908
|
-
`Operation aborted: server memory usage (${usedGB} GB) exceeds safe threshold (${MEMORY_FRACTION * 100}% of ${limitGB} GB limit) at ${place}
|
|
970
|
+
const error = new Issue("error", "too-costly", null, null,
|
|
971
|
+
`Operation aborted: server memory usage (${usedGB} GB) exceeds safe threshold (${MEMORY_FRACTION * 100}% of ${limitGB} GB limit) at ${place}`, "too-costly", 422);
|
|
909
972
|
error.diagnostics = this.diagnostics();
|
|
910
973
|
throw error;
|
|
911
974
|
}
|
|
@@ -963,8 +1026,8 @@ class OperationContext {
|
|
|
963
1026
|
this._clock.lastYield = resumed;
|
|
964
1027
|
if (this._clock.clientGone) {
|
|
965
1028
|
this.log(`Operation abandoned @ ${place}: client disconnected`);
|
|
966
|
-
const error = new Issue("error", "too-costly", null,
|
|
967
|
-
`Operation abandoned at ${place}: the client disconnected before the response was ready
|
|
1029
|
+
const error = new Issue("error", "too-costly", null, null,
|
|
1030
|
+
`Operation abandoned at ${place}: the client disconnected before the response was ready`, "too-costly", 422);
|
|
968
1031
|
error.abandoned = true;
|
|
969
1032
|
error.diagnostics = this.diagnostics();
|
|
970
1033
|
throw error;
|
|
@@ -972,6 +1035,39 @@ class OperationContext {
|
|
|
972
1035
|
}
|
|
973
1036
|
}
|
|
974
1037
|
|
|
1038
|
+
/**
|
|
1039
|
+
* Await something this operation does not itself compute - typically a
|
|
1040
|
+
* shared structure another request is building - without charging the wait
|
|
1041
|
+
* to the compute deadline. The current compute slice is closed before
|
|
1042
|
+
* waiting and a new one opened after; aborts if the client disconnected
|
|
1043
|
+
* meanwhile.
|
|
1044
|
+
*
|
|
1045
|
+
* @param {Promise} promise
|
|
1046
|
+
* @param {string} place - Location identifier for debugging
|
|
1047
|
+
* @returns {Promise<*>} what the promise resolves to
|
|
1048
|
+
*/
|
|
1049
|
+
async waitFor(promise, place = 'unknown') {
|
|
1050
|
+
const now = performance.now();
|
|
1051
|
+
this._clock.compute += now - this._clock.sliceStart;
|
|
1052
|
+
let result;
|
|
1053
|
+
try {
|
|
1054
|
+
result = await promise;
|
|
1055
|
+
} finally {
|
|
1056
|
+
const resumed = performance.now();
|
|
1057
|
+
this._clock.sliceStart = resumed;
|
|
1058
|
+
this._clock.lastYield = resumed;
|
|
1059
|
+
}
|
|
1060
|
+
if (this._clock.clientGone) {
|
|
1061
|
+
this.log(`Operation abandoned @ ${place}: client disconnected`);
|
|
1062
|
+
const error = new Issue("error", "too-costly", null, null,
|
|
1063
|
+
`Operation abandoned at ${place}: the client disconnected before the response was ready`, "too-costly", 422);
|
|
1064
|
+
error.abandoned = true;
|
|
1065
|
+
error.diagnostics = this.diagnostics();
|
|
1066
|
+
throw error;
|
|
1067
|
+
}
|
|
1068
|
+
return result;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
975
1071
|
unSeeAll() {
|
|
976
1072
|
this.contexts = [];
|
|
977
1073
|
}
|
package/tx/provider.js
CHANGED
|
@@ -449,6 +449,91 @@ class Provider {
|
|
|
449
449
|
return null;
|
|
450
450
|
}
|
|
451
451
|
|
|
452
|
+
/**
|
|
453
|
+
* A checker for codes in one code system, so that a page can ask about many codes
|
|
454
|
+
* without paying to build the code system each time.
|
|
455
|
+
*
|
|
456
|
+
* This is for deciding whether to offer a link. A property value that came back from
|
|
457
|
+
* $lookup as a code is, by the definition of CodeSystem.property.type, a concept in the
|
|
458
|
+
* same code system - but a provider can declare a property that way and then put
|
|
459
|
+
* something else in it (ICD-11's classKind is chapter|block|window|category, none of
|
|
460
|
+
* which is an ICD-11 concept), and a link to a code that is not there is worse than no
|
|
461
|
+
* link. So the codes are checked, once each, against the code system itself.
|
|
462
|
+
*
|
|
463
|
+
* @param {OperationContext} opContext
|
|
464
|
+
* @param {string} system
|
|
465
|
+
* @param {string|null} version
|
|
466
|
+
* @returns {Promise<function(string): Promise<string|null>>|null} a function returning
|
|
467
|
+
* the code's display (possibly empty) if it exists and null if it does not, or null
|
|
468
|
+
* when this server does not have that code system at all
|
|
469
|
+
*/
|
|
470
|
+
async codeChecker(opContext, system, version) {
|
|
471
|
+
validateParameter(opContext, "opContext", OperationContext);
|
|
472
|
+
if (!system) {
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
if (system.includes("|")) {
|
|
476
|
+
version = version || system.substring(system.indexOf("|") + 1);
|
|
477
|
+
system = system.substring(0, system.indexOf("|"));
|
|
478
|
+
}
|
|
479
|
+
const vurl = system + (version ? "|" + version : "");
|
|
480
|
+
const vurlMM = VersionUtilities.isSemVer(version) ? system + "|" + VersionUtilities.getMajMin(version) : null;
|
|
481
|
+
|
|
482
|
+
let factory = this.codeSystemFactories.get(vurl);
|
|
483
|
+
if (factory == null && vurlMM) {
|
|
484
|
+
factory = this.codeSystemFactories.get(vurlMM);
|
|
485
|
+
}
|
|
486
|
+
if (factory != null) {
|
|
487
|
+
const csp = await factory.build(opContext, []);
|
|
488
|
+
if (csp) {
|
|
489
|
+
opContext.registerProvider(csp);
|
|
490
|
+
return async (code) => {
|
|
491
|
+
const located = await csp.locate(code);
|
|
492
|
+
return located && located.context ? (await csp.display(located.context)) || '' : null;
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
let cs = this.codeSystems.get(vurl);
|
|
498
|
+
if (cs == null && vurlMM) {
|
|
499
|
+
cs = this.codeSystems.get(vurlMM);
|
|
500
|
+
}
|
|
501
|
+
if (cs != null) {
|
|
502
|
+
return async (code) => {
|
|
503
|
+
const c = cs.codeMap.get(code);
|
|
504
|
+
return c ? (c.display || '') : null;
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* A link to $lookup for a code on this server.
|
|
512
|
+
*
|
|
513
|
+
* resolveCode() answers "where does this code live" - a code system page, or the
|
|
514
|
+
* publisher's own browser - which is the right answer in prose. In an expansion the
|
|
515
|
+
* question is different: the reader has a list of codes in front of them and wants to
|
|
516
|
+
* know what one of them actually is, which is what $lookup answers, here, in the
|
|
517
|
+
* version the expansion used.
|
|
518
|
+
*
|
|
519
|
+
* @param {string} system
|
|
520
|
+
* @param {string|null} version - may also be carried on system as system|version
|
|
521
|
+
* @param {string} code
|
|
522
|
+
* @returns {string|null} the URL, or null if there is not enough to build one
|
|
523
|
+
*/
|
|
524
|
+
lookupLink(system, version, code) {
|
|
525
|
+
if (!system || !code) {
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
if (system.includes("|")) {
|
|
529
|
+
version = version || system.substring(system.indexOf("|") + 1);
|
|
530
|
+
system = system.substring(0, system.indexOf("|"));
|
|
531
|
+
}
|
|
532
|
+
return this.path + "/CodeSystem/$lookup?system=" + encodeURIComponent(system)
|
|
533
|
+
+ (version ? "&version=" + encodeURIComponent(version) : "")
|
|
534
|
+
+ "&code=" + encodeURIComponent(code);
|
|
535
|
+
}
|
|
536
|
+
|
|
452
537
|
async hasCsVersion(system, version) {
|
|
453
538
|
for (let cs of this.codeSystems.values()) {
|
|
454
539
|
if (cs.url == system && cs.version == version) {
|