fhirsmith 0.18.1 → 1.0.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/ACCESSIBILITY.md +119 -0
- package/CHANGELOG.md +190 -9
- package/CONFIGURATION.md +305 -0
- package/LICENSE +29 -0
- package/README.md +29 -122
- package/cluster.js +1220 -0
- package/cluster.md +707 -0
- package/config-template.json +77 -8
- package/extension-tracker/extension-tracker-template.html +5 -8
- package/extension-tracker/extension-tracker.js +0 -2
- package/folder/folder.js +7 -6
- package/ips/analysis.js +560 -0
- package/ips/ips-template.html +5 -8
- package/ips/ips.js +331 -33
- package/ips/jurisdictions.js +65 -0
- package/ips/nationality.js +120 -0
- package/ips/pipeline.js +131 -17
- package/ips/presenter.js +1 -1
- package/ips/profile-editor.js +283 -4
- package/ips/readme.md +170 -20
- package/ips/render.js +769 -109
- package/ips/scripts.js +46 -0
- package/ips/store.js +123 -11
- package/ips/structure.js +247 -0
- package/ips/terminology.js +1 -1
- package/library/body-limit.js +22 -0
- package/library/cache-generation.js +15 -0
- package/library/cgroup-memory.js +133 -1
- package/library/cluster-counts.js +121 -0
- package/library/cluster-events.js +153 -0
- package/library/cluster-metrics.js +193 -0
- package/library/cluster-status.js +468 -0
- package/library/config-reader.js +96 -0
- package/library/daily-report.js +100 -2
- package/library/html-server.js +27 -1
- package/library/i18nsupport.js +23 -1
- package/library/keep-alive.js +18 -0
- package/library/languages.js +17 -3
- package/library/logger.js +31 -0
- package/library/memory-guard.js +428 -0
- package/library/native-memory.js +52 -0
- package/library/npm-audit.js +51 -25
- package/library/openapi-doc.js +7 -5
- package/library/package-manager.js +8 -0
- package/library/role.js +197 -0
- package/library/server-credentials.js +169 -0
- package/library/sqlite-utilities.js +59 -0
- package/library/ssrf-guard.js +100 -0
- package/library/static-response-stats.js +16 -0
- package/library/version-utilities.js +50 -0
- package/library/worker-count.js +192 -0
- package/nginx.md +30 -11
- package/npmprojector/indexer.js +18 -10
- package/package.json +11 -8
- package/packages/feed-check-page.js +117 -0
- package/packages/feed-check.js +279 -0
- package/packages/npm-check.js +57 -3
- package/packages/npm-signing.js +219 -0
- package/packages/openapi.yaml +207 -1
- package/packages/package-crawler.js +322 -41
- package/packages/packages-template.html +6 -8
- package/packages/packages.js +604 -57
- package/packages/readme.md +113 -1
- package/publisher/publisher-template.html +2 -5
- package/publisher/publisher.js +121 -110
- package/publisher/readme.md +75 -0
- package/registry/api.js +179 -1
- package/registry/crawler.js +86 -108
- package/registry/documentation/api.md +68 -2
- package/registry/documentation/configuration.md +34 -0
- package/registry/documentation/internals.md +4 -1
- package/registry/documentation/readme.md +3 -2
- package/registry/model.js +8 -0
- package/registry/openapi.yaml +84 -0
- package/registry/registry-template.html +5 -8
- package/registry/registry.js +58 -6
- package/root-bare-template.html +7 -4
- package/root-template.html +5 -8
- package/server.js +398 -198
- package/shl/shl.js +38 -37
- package/static/a11y/a11y-check.js +832 -0
- package/static/a11y/axe.min.js +12 -0
- package/static/assets/css/bootstrap-fhir.css +15 -15
- package/static/assets/images/flags/au.svg +18 -0
- package/static/assets/images/flags/unknown.svg +11 -0
- package/static/assets/images/flags/us.svg +63 -0
- package/static/assets/images/icon_audit.png +0 -0
- package/static/assets/images/icon_clinical.png +0 -0
- package/static/assets/images/icon_document.png +0 -0
- package/static/assets/images/icon_external.png +0 -0
- package/static/assets/images/icon_medication.png +0 -0
- package/static/assets/images/icon_organization.png +0 -0
- package/static/assets/images/icon_other.png +0 -0
- package/static/assets/images/icon_person.png +0 -0
- package/static/assets/images/icon_section.png +0 -0
- package/stats-db.js +45 -0
- package/stats.js +197 -10
- package/testing/testing-template.html +5 -8
- package/token/nginx_passport_setup.md +1 -1
- package/token/security_guide.md +1 -1
- package/token/token-template.html +2 -5
- package/token/token.js +40 -36
- package/translations/Messages.properties +19 -0
- package/tx/README.md +158 -4
- package/tx/closure/closure-store.js +126 -3
- package/tx/cm/cm-database.js +69 -57
- package/tx/cm/cm-package.js +50 -16
- package/tx/cs/cs-api.js +284 -19
- package/tx/cs/cs-api.md +41 -4
- package/tx/cs/cs-areacode.js +1 -1
- package/tx/cs/cs-country.js +17 -5
- package/tx/cs/cs-cpt.js +33 -16
- package/tx/cs/cs-cs.js +199 -24
- package/tx/cs/cs-currency.js +1 -1
- package/tx/cs/cs-icd11.js +21 -13
- package/tx/cs/cs-icf.js +22 -3
- package/tx/cs/cs-lang.js +2 -2
- package/tx/cs/cs-loinc.js +193 -67
- package/tx/cs/cs-mimetypes.js +12 -2
- package/tx/cs/cs-ndc.js +40 -25
- package/tx/cs/cs-omop.js +54 -29
- package/tx/cs/cs-rxnorm.js +300 -196
- package/tx/cs/cs-snomed.js +861 -95
- package/tx/cs/cs-ucum.js +14 -4
- package/tx/cs/cs-unii.js +18 -14
- package/tx/cs/cs-virtual.js +210 -0
- package/tx/ecosystem-resolver.js +413 -0
- package/tx/html/cluster-dash-metrics.liquid +26 -0
- package/tx/html/cluster-home-metrics.liquid +31 -0
- package/tx/html/cluster-metrics-script.liquid +251 -0
- package/tx/html/codesystem-operations.liquid +3 -3
- package/tx/html/ecl-panel.liquid +1 -1
- package/tx/html/external-metrics.liquid +17 -0
- package/tx/html/operations-form.liquid +17 -17
- package/tx/html/search-form.liquid +11 -11
- package/tx/html/status-line.liquid +58 -0
- package/tx/html/status-panel.liquid +140 -0
- package/tx/html/status-tasks.liquid +25 -0
- package/tx/html/tx-template.html +2 -5
- package/tx/importers/atc-to-fhir.js +17 -16
- package/tx/importers/export-icd11-codesystem.js +3 -2
- package/tx/importers/import-icd11.js +4 -3
- package/tx/importers/import-loinc.module.js +118 -56
- package/tx/importers/import-ndc.module.js +56 -44
- package/tx/importers/import-rxnorm.module.js +118 -79
- package/tx/importers/import-sct.module.js +125 -61
- package/tx/importers/import-unii.module.js +37 -36
- package/tx/importers/subset-loinc.module.js +49 -48
- package/tx/importers/subset-rxnorm.module.js +29 -28
- package/tx/importers/terminal.js +18 -0
- package/tx/importers/tx-import-base.js +13 -12
- package/tx/importers/tx-import-settings.js +5 -4
- package/tx/importers/tx-import.js +53 -52
- package/tx/library/canonical-resource.js +49 -0
- package/tx/library/codesystem.js +94 -5
- package/tx/library/designations.js +45 -2
- package/tx/library/extensions.js +13 -7
- package/tx/library/implicit-valuesets.js +66 -0
- package/tx/library/renderer.js +77 -21
- package/tx/library/sqlite-utilities.js +2 -0
- package/tx/library/tho-precedence.js +40 -0
- package/tx/library/ucum-parsers.js +11 -1
- package/tx/library/wait-for-cache.js +43 -0
- package/tx/library.js +321 -22
- package/tx/openapi-operations.js +6 -4
- package/tx/operation-context.js +460 -99
- package/tx/params.js +25 -0
- package/tx/problems.js +3 -5
- package/tx/provider.js +314 -67
- package/tx/resource-cache-sqlite.js +1050 -0
- package/tx/sct/documentation.md +45 -21
- package/tx/sct/ecl.js +81 -17
- package/tx/sct/editions.js +39 -0
- package/tx/sct/expressions.js +10 -1
- package/tx/sct/load-config.js +107 -0
- package/tx/sct/mmap.js +109 -0
- package/tx/sct/structures.js +175 -15
- package/tx/sct/text-index.js +5 -1
- package/tx/syndication/syndication-feed.js +2 -2
- package/tx/tx-html.js +44 -8
- package/tx/tx.fhir.org.yml +8 -8
- package/tx/tx.js +282 -80
- package/tx/usage-tracker.js +39 -11
- package/tx/vs/vs-api.js +19 -0
- package/tx/vs/vs-database.js +386 -62
- package/tx/vs/vs-package.js +44 -17
- package/tx/vs/vs-vsac.js +649 -101
- package/tx/workers/batch-validate.js +37 -10
- package/tx/workers/batch.js +6 -1
- package/tx/workers/cache-control.js +15 -4
- package/tx/workers/closure.js +156 -13
- package/tx/workers/compare.js +8 -5
- package/tx/workers/expand.js +1173 -289
- package/tx/workers/metadata.js +35 -0
- package/tx/workers/search.js +53 -1
- package/tx/workers/supplement-filters.js +145 -0
- package/tx/workers/translate.js +169 -4
- package/tx/workers/validate.js +223 -119
- package/tx/workers/worker.js +123 -31
- package/tx/xversion/xv-valueset.js +7 -4
- package/utilities/submit-test-report.js +3 -1
- package/utilities/tx-stress-test.js +482 -0
- package/vcl/readme.md +37 -0
- package/vcl/vcl-parser.js +15 -0
- package/xig/readme.md +52 -0
- package/xig/xig-template.html +5 -8
- package/xig/xig.js +238 -422
- package/changelog-template.md +0 -22
- package/registry/test-runner.js +0 -237
- package/release.sh +0 -80
- package/test-scripts/repro-re2-wasm-leak.js +0 -34
- package/tsconfig.eslint.json +0 -9
- package/tx/html/dash-metrics.liquid +0 -147
- package/tx/html/home-metrics.liquid +0 -225
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
// system/codeSystem entry validates against a code system), so both batch routes
|
|
12
12
|
// share one implementation and even a mixed batch is handled correctly.
|
|
13
13
|
//
|
|
14
|
+
// The exception is `url`, which names a value set to ValueSet/$validate-code but a
|
|
15
|
+
// code system to CodeSystem/$validate-code. So on the CodeSystem route `url` is a
|
|
16
|
+
// shared global (the code system), and every entry is a CodeSystem validation;
|
|
17
|
+
// only the ValueSet route dispatches by shape.
|
|
18
|
+
//
|
|
14
19
|
|
|
15
20
|
const { TerminologyWorker } = require('./worker');
|
|
16
21
|
const { OperationOutcome, Issue, buildOperationOutcome, outcomeFromError } = require('../library/operation-outcome');
|
|
@@ -33,14 +38,15 @@ class BatchValidateWorker extends TerminologyWorker {
|
|
|
33
38
|
super(opContext, log, provider, languages, i18n);
|
|
34
39
|
// "Global" parameters may be supplied once at the top level and are applied to
|
|
35
40
|
// every entry that doesn't override them. tx-resource is always shared. The
|
|
36
|
-
// primary differs by operation: a ValueSet batch shares url/valueSet
|
|
37
|
-
// CodeSystem batch shares
|
|
41
|
+
// primary differs by operation: a ValueSet batch shares url/valueSet (the value
|
|
42
|
+
// set), a CodeSystem batch shares url/version/codeSystem (the code system), as
|
|
43
|
+
// CodeSystem/$validate-code names them - and system, which it also accepts.
|
|
38
44
|
this.valueSetGlobalNames = new Set([
|
|
39
45
|
"tx-resource", "url", "valueSet", "lenient-display-validation",
|
|
40
46
|
"__Accept-Language", "__Content-Language"
|
|
41
47
|
]);
|
|
42
48
|
this.codeSystemGlobalNames = new Set([
|
|
43
|
-
"tx-resource", "system", "codeSystem", "lenient-display-validation",
|
|
49
|
+
"tx-resource", "url", "version", "system", "codeSystem", "lenient-display-validation",
|
|
44
50
|
"__Accept-Language", "__Content-Language"
|
|
45
51
|
]);
|
|
46
52
|
// Back-compat: `globalNames` was the (ValueSet) set before CodeSystem batches.
|
|
@@ -57,12 +63,12 @@ class BatchValidateWorker extends TerminologyWorker {
|
|
|
57
63
|
|
|
58
64
|
/** ValueSet/$batch-validate-code: shared globals are url/valueSet. */
|
|
59
65
|
async handleValueSet(req, res) {
|
|
60
|
-
return this.processBatch(req, res, this.valueSetGlobalNames);
|
|
66
|
+
return this.processBatch(req, res, this.valueSetGlobalNames, false);
|
|
61
67
|
}
|
|
62
68
|
|
|
63
|
-
/** CodeSystem/$batch-validate-code: shared globals are system/codeSystem. */
|
|
69
|
+
/** CodeSystem/$batch-validate-code: shared globals are url/version/system/codeSystem. */
|
|
64
70
|
async handleCodeSystem(req, res) {
|
|
65
|
-
return this.processBatch(req, res, this.codeSystemGlobalNames);
|
|
71
|
+
return this.processBatch(req, res, this.codeSystemGlobalNames, true);
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
/**
|
|
@@ -74,8 +80,11 @@ class BatchValidateWorker extends TerminologyWorker {
|
|
|
74
80
|
* @param {express.Request} req
|
|
75
81
|
* @param {express.Response} res
|
|
76
82
|
* @param {Set<string>} globalNames - which top-level params are shared globals
|
|
83
|
+
* @param {boolean} codeSystemRoute - true for CodeSystem/$batch-validate-code: every
|
|
84
|
+
* entry is a CodeSystem validation (`url` there is the code system). On the
|
|
85
|
+
* ValueSet route, an entry with no url/valueSet is a CodeSystem validation
|
|
77
86
|
*/
|
|
78
|
-
async processBatch(req, res, globalNames) {
|
|
87
|
+
async processBatch(req, res, globalNames, codeSystemRoute = false) {
|
|
79
88
|
try {
|
|
80
89
|
let params = req.body;
|
|
81
90
|
this.addHttpParams(req, params);
|
|
@@ -120,16 +129,32 @@ class BatchValidateWorker extends TerminologyWorker {
|
|
|
120
129
|
: p.resource.parameter;
|
|
121
130
|
op.jsonObj.parameter.push(...entryParams);
|
|
122
131
|
|
|
123
|
-
|
|
132
|
+
// Each entry gets its own provider list, closed as soon as the entry is
|
|
133
|
+
// answered. Sharing the batch's list would keep every entry's providers
|
|
134
|
+
// (each with an open sqlite connection, and a reference to the entry's
|
|
135
|
+
// context and its log) alive until the whole batch ends.
|
|
136
|
+
const entryContext = this.opContext.copy({ ownProviders: true });
|
|
137
|
+
let worker = new ValidateWorker(entryContext, this.log, this.provider, this.languages, this.i18n);
|
|
124
138
|
try {
|
|
139
|
+
// Validating an entry never yields the event loop by itself, and IG builds send batches
|
|
140
|
+
// of up to 1,000 entries (~1 s of compute): give other requests a turn between entries
|
|
141
|
+
// (checkAndYield yields only after YIELD_INTERVAL ms of compute, so small batches don't),
|
|
142
|
+
// and see a disconnect. Inside the try: the time limit tripping here is this entry's
|
|
143
|
+
// too-costly, and the entries already answered keep their answers; a memory abort or
|
|
144
|
+
// a disconnect is rethrown below and stops the whole batch.
|
|
145
|
+
await this.opContext.checkAndYield('batch-validate');
|
|
125
146
|
let p;
|
|
126
|
-
if (this.hasValueSet(op.jsonObj.parameter)) {
|
|
147
|
+
if (!codeSystemRoute && this.hasValueSet(op.jsonObj.parameter)) {
|
|
127
148
|
p = await worker.handleValueSetInner(op.jsonObj);
|
|
128
149
|
} else {
|
|
129
150
|
p = await worker.handleCodeSystemInner(op.jsonObj);
|
|
130
151
|
}
|
|
131
152
|
output.push({name: "validation", resource : p});
|
|
132
153
|
} catch (error) {
|
|
154
|
+
// the client has gone, or memory is short: the whole batch stops, not this entry
|
|
155
|
+
if (error.abandoned || error.memoryAbort) {
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
133
158
|
this.log.error(error);
|
|
134
159
|
debugLog(error);
|
|
135
160
|
if (error instanceof Issue) {
|
|
@@ -139,6 +164,8 @@ class BatchValidateWorker extends TerminologyWorker {
|
|
|
139
164
|
} else {
|
|
140
165
|
output.push({name: "validation", resource : outcomeFromError(error) } );
|
|
141
166
|
}
|
|
167
|
+
} finally {
|
|
168
|
+
await entryContext.closeProviders();
|
|
142
169
|
}
|
|
143
170
|
}
|
|
144
171
|
}
|
|
@@ -209,7 +236,7 @@ class BatchValidateWorker extends TerminologyWorker {
|
|
|
209
236
|
const { txResources, primaryResources } = this.collectSuppliedResources({ parameter: allParams });
|
|
210
237
|
const pool = txResources.concat(primaryResources);
|
|
211
238
|
if (pool.length > 0) {
|
|
212
|
-
|
|
239
|
+
this.addToClientCache(cacheId, pool);
|
|
213
240
|
}
|
|
214
241
|
return true;
|
|
215
242
|
}
|
package/tx/workers/batch.js
CHANGED
|
@@ -79,7 +79,8 @@ class BatchWorker extends TerminologyWorker {
|
|
|
79
79
|
const responseEntries = [];
|
|
80
80
|
|
|
81
81
|
for (let i = 0; i < entries.length; i++) {
|
|
82
|
-
|
|
82
|
+
// a turn for the event loop between entries, so a disconnect is seen
|
|
83
|
+
await this.checkAndYield(`batch-entry-${i}`);
|
|
83
84
|
const entry = entries[i];
|
|
84
85
|
const responseEntry = await this.processEntry(entry, i);
|
|
85
86
|
responseEntries.push(responseEntry);
|
|
@@ -159,6 +160,10 @@ class BatchWorker extends TerminologyWorker {
|
|
|
159
160
|
};
|
|
160
161
|
|
|
161
162
|
} catch (error) {
|
|
163
|
+
// the client has gone, or memory is short: the whole batch stops, not this entry
|
|
164
|
+
if (error.abandoned || error.memoryAbort) {
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
162
167
|
this.log.error(error);
|
|
163
168
|
debugLog(error);
|
|
164
169
|
const statusCode = error.statusCode || 500;
|
|
@@ -97,7 +97,8 @@ class CacheControlWorker extends TerminologyWorker {
|
|
|
97
97
|
|
|
98
98
|
/**
|
|
99
99
|
* mode=start: mint a server-issued cache-id, create the (per-endpoint) cache,
|
|
100
|
-
* front-load any supplied resources into it, and return the id in the body
|
|
100
|
+
* front-load any supplied resources into it, and return the id in the body
|
|
101
|
+
* (and, for proxy request logs, in the `${CACHE_ID_HEADER}` response header).
|
|
101
102
|
*
|
|
102
103
|
* The id is returned in the body rather than a header to keep it readable by
|
|
103
104
|
* browser clients without CORS expose-header configuration; subsequent calls
|
|
@@ -134,9 +135,19 @@ class CacheControlWorker extends TerminologyWorker {
|
|
|
134
135
|
// Flip this to default-true once all clients send an explicit value.
|
|
135
136
|
const sealed = this.readSealed(params.jsonObj);
|
|
136
137
|
|
|
137
|
-
// "<instanceCode
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
// "<instanceCode>.<...>" when this server has an instance code, else a bare UUID
|
|
139
|
+
let cacheId;
|
|
140
|
+
try {
|
|
141
|
+
cacheId = cache.start(resources, sealed);
|
|
142
|
+
} catch (e) {
|
|
143
|
+
throw this.translateCacheIssue(e);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Also echoed as a response header, so that a request log at the proxy (nginx
|
|
147
|
+
// $sent_http_x_cache_id) can tie each issued id to the start request that got
|
|
148
|
+
// it - which is what replaying a captured request log needs, because a replay
|
|
149
|
+
// target issues different ids. The body stays the authoritative carrier (see above).
|
|
150
|
+
res.setHeader(CACHE_ID_HEADER, cacheId);
|
|
140
151
|
|
|
141
152
|
return res.status(200).json({
|
|
142
153
|
resourceType: 'Parameters',
|
package/tx/workers/closure.js
CHANGED
|
@@ -35,6 +35,13 @@
|
|
|
35
35
|
// name can read, add to, or reset that table. On a public server, require UUID names
|
|
36
36
|
// (requireUuidNames) so that a name is effectively a secret held by the client that made it.
|
|
37
37
|
//
|
|
38
|
+
// Shared (server.cluster.sharedClosure): other processes answer $closure on the same tables.
|
|
39
|
+
// The per-table lock still orders this process's calls, but a write may find the table has
|
|
40
|
+
// moved on since it was read: then nothing is written, and the call is worked out again
|
|
41
|
+
// from the table as it is now (see add, and the header of closure-store.js). After
|
|
42
|
+
// writeRetries such goes, the call gets the same 429 as a busy queue. The code lookups and
|
|
43
|
+
// subsumption tests done before are remembered for the retries, so a retry costs reads.
|
|
44
|
+
//
|
|
38
45
|
// Cost: everything a client can make this do is bounded (see LIMITS) - the tables, the
|
|
39
46
|
// concepts and entries in a table, what one call can add, what a table's configuration
|
|
40
47
|
// can store, and how many calls can queue for one table and for how long. The work itself
|
|
@@ -68,6 +75,11 @@ const LIMITS = {
|
|
|
68
75
|
|
|
69
76
|
const RESYNC_PAGE = 2000; // rows read from the store at a time
|
|
70
77
|
|
|
78
|
+
// shared: how many times an add is worked out again when another process changed the table
|
|
79
|
+
// first, and the most it waits (with jitter) before a retry
|
|
80
|
+
const WRITE_RETRIES = 10;
|
|
81
|
+
const RETRY_WAIT_MS = 50;
|
|
82
|
+
|
|
71
83
|
class ClosureWorker extends TerminologyWorker {
|
|
72
84
|
/**
|
|
73
85
|
* @param {OperationContext} opContext - Operation context
|
|
@@ -219,7 +231,19 @@ class ClosureWorker extends TerminologyWorker {
|
|
|
219
231
|
if (!table && this.store.tableCount() >= max) {
|
|
220
232
|
throw this.closureIssue('too-costly', 'CLOSURE_TOO_MANY_TABLES', [String(max)], 'too-costly', 422);
|
|
221
233
|
}
|
|
222
|
-
|
|
234
|
+
if (this.store.shared) {
|
|
235
|
+
// the checks above again, in the same transaction as the write
|
|
236
|
+
const made = this.store.createTableChecked(name, config, { reset, maxTables: max });
|
|
237
|
+
if (made.refused === 'exists') {
|
|
238
|
+
throw this.closureIssue('duplicate', 'CLOSURE_TABLE_EXISTS', [name], 'business-rule', 409);
|
|
239
|
+
}
|
|
240
|
+
if (made.refused === 'limit') {
|
|
241
|
+
throw this.closureIssue('too-costly', 'CLOSURE_TOO_MANY_TABLES', [String(max)], 'too-costly', 422);
|
|
242
|
+
}
|
|
243
|
+
table = made.table;
|
|
244
|
+
} else {
|
|
245
|
+
table = this.store.createTable(name, config);
|
|
246
|
+
}
|
|
223
247
|
this.log.info(`closure table '${name}' ${reset ? 'reset' : 'created'}`);
|
|
224
248
|
if (concepts.length === 0) {
|
|
225
249
|
return await this.buildConceptMap(name, table.version, [], fhirVersion);
|
|
@@ -297,7 +321,9 @@ class ClosureWorker extends TerminologyWorker {
|
|
|
297
321
|
let after = 0;
|
|
298
322
|
for (;;) {
|
|
299
323
|
await this.checkAndYield('closure-resync');
|
|
300
|
-
const page = this.store.
|
|
324
|
+
const page = this.store.shared
|
|
325
|
+
? this.store.entriesPage(table.id, v, after, RESYNC_PAGE, table.version)
|
|
326
|
+
: this.store.entriesPage(table.id, v, after, RESYNC_PAGE);
|
|
301
327
|
for (const r of page) {
|
|
302
328
|
entries.push({
|
|
303
329
|
source: { system: r.source_system, code: r.source_code, display: r.source_display },
|
|
@@ -310,6 +336,17 @@ class ClosureWorker extends TerminologyWorker {
|
|
|
310
336
|
}
|
|
311
337
|
after = page[page.length - 1].rowid;
|
|
312
338
|
}
|
|
339
|
+
if (this.store.shared) {
|
|
340
|
+
// Reset/dispose deletes the old pages. Never return a truncated snapshot, or
|
|
341
|
+
// entries from a replacement, as though they belonged to the version we read.
|
|
342
|
+
const current = this.store.getTable(name);
|
|
343
|
+
if (!current) {
|
|
344
|
+
throw this.closureIssue('not-found', 'CLOSURE_TABLE_UNKNOWN', [name], 'not-found', 404);
|
|
345
|
+
}
|
|
346
|
+
if (current.id !== table.id) {
|
|
347
|
+
throw this.closureIssue('conflict', 'CLOSURE_TABLE_CHANGED', [name], 'business-rule', 409);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
313
350
|
return await this.buildConceptMap(name, table.version, entries, fhirVersion);
|
|
314
351
|
}
|
|
315
352
|
|
|
@@ -320,6 +357,66 @@ class ClosureWorker extends TerminologyWorker {
|
|
|
320
357
|
* system or code, or going over a limit, fails the whole call.
|
|
321
358
|
*/
|
|
322
359
|
async add(table, name, conceptParams, fhirVersion) {
|
|
360
|
+
if (this.store.shared) {
|
|
361
|
+
return await this.addShared(table, name, conceptParams, fhirVersion);
|
|
362
|
+
}
|
|
363
|
+
const { fresh, entries, out } = await this.workOutAdd(table, name, conceptParams, null);
|
|
364
|
+
const version = this.store.recordAdd(table, fresh, entries);
|
|
365
|
+
return await this.addResult(name, version, fresh, entries, out, fhirVersion);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Shared: add, with the write checked against the table as it was read
|
|
370
|
+
* (recordAddChecked). If another process changed the table first, nothing was written:
|
|
371
|
+
* read the table again and work the add out again, up to writeRetries times, waiting a
|
|
372
|
+
* little (with jitter) in between; then 429, as for a busy queue. A table that is gone
|
|
373
|
+
* by then is unknown (404), as it would be to a call that came after the one that
|
|
374
|
+
* dropped it.
|
|
375
|
+
*/
|
|
376
|
+
async addShared(table, name, conceptParams, fhirVersion) {
|
|
377
|
+
const configured = this.closureConfig.writeRetries;
|
|
378
|
+
const retries = Number.isInteger(configured) && configured >= 0 ? configured : WRITE_RETRIES;
|
|
379
|
+
let memo = null;
|
|
380
|
+
for (let attempt = 0; ; attempt++) {
|
|
381
|
+
if (!memo || memo.tableId !== table.id) {
|
|
382
|
+
// what's remembered holds for one table's configuration: a reset starts again
|
|
383
|
+
memo = { tableId: table.id, providers: new Map(), codes: new Map(), subsumes: new Map() };
|
|
384
|
+
}
|
|
385
|
+
const { fresh, entries, out } = await this.workOutAdd(table, name, conceptParams, memo);
|
|
386
|
+
let version = null;
|
|
387
|
+
try {
|
|
388
|
+
version = this.store.recordAddChecked(table, fresh, entries);
|
|
389
|
+
} catch (error) {
|
|
390
|
+
// the busy timeout ran out: another process held the write lock throughout
|
|
391
|
+
if (!String(error.code || '').startsWith('SQLITE_BUSY')) {
|
|
392
|
+
throw error;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (version !== null) {
|
|
396
|
+
return await this.addResult(name, version, fresh, entries, out, fhirVersion);
|
|
397
|
+
}
|
|
398
|
+
if (attempt >= retries) {
|
|
399
|
+
this.log.info(`closure table '${name}': changed by another process on each of ${attempt + 1} tries; answering busy`);
|
|
400
|
+
throw this.closureIssue('too-costly', 'CLOSURE_TABLE_BUSY', [name], 'too-costly', 429);
|
|
401
|
+
}
|
|
402
|
+
this.log.info(`closure table '${name}': changed by another process since it was read; working the add out again (retry ${attempt + 1} of ${retries})`);
|
|
403
|
+
const wait = Math.random() * Math.min(RETRY_WAIT_MS, 5 * 2 ** attempt);
|
|
404
|
+
await this.opContext.waitFor(new Promise(resolve => setTimeout(resolve, wait)), 'closure-retry');
|
|
405
|
+
table = this.store.getTable(name);
|
|
406
|
+
if (!table) {
|
|
407
|
+
throw this.closureIssue('not-found', 'CLOSURE_TABLE_UNKNOWN', [name], 'not-found', 404);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Work out what adding the concepts to the table, as read, would write: the new
|
|
414
|
+
* concepts and the entries they make. Writes nothing.
|
|
415
|
+
* @param {Object} memo - shared only (else null): code lookups and subsumption tests
|
|
416
|
+
* already done for this table, kept across retries
|
|
417
|
+
* @returns {{fresh: Array, entries: Array, out: Array}}
|
|
418
|
+
*/
|
|
419
|
+
async workOutAdd(table, name, conceptParams, memo) {
|
|
323
420
|
// replay the table's configuration: its code systems, and its version rules
|
|
324
421
|
const stored = { resourceType: 'Parameters', parameter: this.store.getParams(table.id) };
|
|
325
422
|
this.setupAdditionalResources(stored);
|
|
@@ -331,7 +428,7 @@ class ClosureWorker extends TerminologyWorker {
|
|
|
331
428
|
for (const c of existing) {
|
|
332
429
|
known.add(c.system + '|' + c.code);
|
|
333
430
|
}
|
|
334
|
-
const providers = new Map();
|
|
431
|
+
const providers = memo ? memo.providers : new Map();
|
|
335
432
|
const fresh = [];
|
|
336
433
|
|
|
337
434
|
for (const p of conceptParams) {
|
|
@@ -346,20 +443,31 @@ class ClosureWorker extends TerminologyWorker {
|
|
|
346
443
|
this.seeSourceProvider(cs, coding.system);
|
|
347
444
|
providers.set(coding.system, cs);
|
|
348
445
|
}
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
446
|
+
const memoKey = memo ? coding.system + '|' + (coding.version || '') + '|' + coding.code : null;
|
|
447
|
+
let found = memo ? memo.codes.get(memoKey) : null;
|
|
448
|
+
if (!found) {
|
|
449
|
+
const loc = await cs.locate(coding.code);
|
|
450
|
+
if (!loc || !loc.context) {
|
|
451
|
+
throw this.unknownCodeIssue(cs, coding.code, loc ? loc.message : null, txp);
|
|
452
|
+
}
|
|
453
|
+
// the code system's own spelling of the code, so a case-insensitive code system
|
|
454
|
+
// doesn't end up with the same concept in the table twice
|
|
455
|
+
found = { context: loc.context, code: (await cs.code(loc.context)) || coding.code };
|
|
456
|
+
if (memo) {
|
|
457
|
+
memo.codes.set(memoKey, found);
|
|
458
|
+
}
|
|
352
459
|
}
|
|
353
|
-
|
|
354
|
-
// doesn't end up with the same concept in the table twice
|
|
355
|
-
const code = (await cs.code(loc.context)) || coding.code;
|
|
460
|
+
const code = found.code;
|
|
356
461
|
const key = coding.system + '|' + code;
|
|
357
462
|
if (known.has(key)) {
|
|
358
463
|
continue;
|
|
359
464
|
}
|
|
360
465
|
known.add(key);
|
|
466
|
+
if (found.display === undefined) {
|
|
467
|
+
found.display = await cs.display(found.context);
|
|
468
|
+
}
|
|
361
469
|
// ref: how the store refers to it until it has an id (see ClosureStore.recordAdd)
|
|
362
|
-
fresh.push({ key, ref: key, system: coding.system, code, display:
|
|
470
|
+
fresh.push({ key, ref: key, system: coding.system, code, display: found.display, cs });
|
|
363
471
|
}
|
|
364
472
|
|
|
365
473
|
const maxConcepts = this.limit('maxConceptsPerTable');
|
|
@@ -374,14 +482,36 @@ class ClosureWorker extends TerminologyWorker {
|
|
|
374
482
|
this.limit('maxEntriesPerTable') - (fresh.length > 0 ? this.store.entryCount(table.id) : 0));
|
|
375
483
|
const entries = [];
|
|
376
484
|
const out = [];
|
|
377
|
-
const before = existing.map(c => ({ ref: c.id, system: c.system, code: c.code, display: c.display }));
|
|
485
|
+
const before = existing.map(c => ({ key: c.system + '|' + c.code, ref: c.id, system: c.system, code: c.code, display: c.display }));
|
|
486
|
+
// Where the provider can list a code's ancestors (CodeSystemProvider.ancestorsOf), a pair
|
|
487
|
+
// is related from the two ancestor sets, which costs one provider call per concept rather
|
|
488
|
+
// than a subsumesTest per pair. Each set is asked for once per call, when first needed,
|
|
489
|
+
// and only for concepts of a system that has new concepts; a concept the provider has no
|
|
490
|
+
// set for (null, the default) is compared by subsumesTest, as before. The sets are not
|
|
491
|
+
// kept between calls, so they always come from the code system as it is now.
|
|
492
|
+
const ancestorSets = new Map(); // key -> Set of codes, or null
|
|
493
|
+
const ancestorsOf = async (cs, c) => {
|
|
494
|
+
if (!ancestorSets.has(c.key)) {
|
|
495
|
+
await this.checkAndYield('closure-ancestors');
|
|
496
|
+
ancestorSets.set(c.key, await cs.ancestorsOf(c.code));
|
|
497
|
+
}
|
|
498
|
+
return ancestorSets.get(c.key);
|
|
499
|
+
};
|
|
378
500
|
for (const n of fresh) {
|
|
501
|
+
const an = await ancestorsOf(n.cs, n);
|
|
379
502
|
for (const o of before) {
|
|
380
503
|
if (o.system !== n.system) {
|
|
381
504
|
continue;
|
|
382
505
|
}
|
|
383
506
|
await this.checkAndYield('closure-subsumes');
|
|
384
|
-
const
|
|
507
|
+
const ao = an ? await ancestorsOf(n.cs, o) : null;
|
|
508
|
+
let outcome;
|
|
509
|
+
if (ao) {
|
|
510
|
+
// the same outcome subsumesTest gives (see ancestorsOf): n and o are different codes
|
|
511
|
+
outcome = ao.has(n.code) ? 'subsumes' : an.has(o.code) ? 'subsumed-by' : 'not-subsumed';
|
|
512
|
+
} else {
|
|
513
|
+
outcome = memo ? await this.memoSubsumes(memo, n, o) : await n.cs.subsumesTest(n.code, o.code);
|
|
514
|
+
}
|
|
385
515
|
let source, target, relationship;
|
|
386
516
|
if (outcome === 'subsumes') { // the new concept is the broader one
|
|
387
517
|
source = o; target = n; relationship = NARROWER;
|
|
@@ -401,7 +531,20 @@ class ClosureWorker extends TerminologyWorker {
|
|
|
401
531
|
before.push(n);
|
|
402
532
|
}
|
|
403
533
|
|
|
404
|
-
|
|
534
|
+
return { fresh, entries, out };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** n.cs.subsumesTest(n.code, o.code), remembered in the memo (shared retries). */
|
|
538
|
+
async memoSubsumes(memo, n, o) {
|
|
539
|
+
const key = n.system + '|' + n.code + '|' + o.code;
|
|
540
|
+
if (!memo.subsumes.has(key)) {
|
|
541
|
+
memo.subsumes.set(key, await n.cs.subsumesTest(n.code, o.code));
|
|
542
|
+
}
|
|
543
|
+
return memo.subsumes.get(key);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** The response to an add, once it is written. */
|
|
547
|
+
async addResult(name, version, fresh, entries, out, fhirVersion) {
|
|
405
548
|
if (fresh.length > 0) {
|
|
406
549
|
this.log.info(`closure table '${name}': added ${fresh.length} concept(s), ${entries.length} entr${entries.length === 1 ? 'y' : 'ies'}, now version ${version}`);
|
|
407
550
|
}
|
package/tx/workers/compare.js
CHANGED
|
@@ -172,18 +172,21 @@ class CompareWorker extends TerminologyWorker {
|
|
|
172
172
|
|
|
173
173
|
async doCompare(txp, thisVS, otherVS) {
|
|
174
174
|
|
|
175
|
-
// ok, we have to compare the composes. we don't care about anything else
|
|
176
|
-
|
|
177
|
-
|
|
175
|
+
// ok, we have to compare the composes. we don't care about anything else.
|
|
176
|
+
// Copies: tidyIncludes merges, removes and sorts includes in place, and these value
|
|
177
|
+
// sets are shared - cache-id resources, and the server's own - so must not change
|
|
178
|
+
// (ExpansionCache.computeKey remembers a cached resource's hash, for one).
|
|
179
|
+
const thisC = thisVS.jsonObj.compose ? structuredClone(thisVS.jsonObj.compose) : thisVS.jsonObj.compose;
|
|
180
|
+
const otherC = otherVS.jsonObj.compose ? structuredClone(otherVS.jsonObj.compose) : otherVS.jsonObj.compose;
|
|
178
181
|
if (!thisC) {
|
|
179
182
|
return this.makeOutcome("indeterminate", `The ValueSet ${thisVS.vurl} has no compose`);
|
|
180
183
|
}
|
|
181
|
-
Extensions.checkNoModifiers(thisC, 'CompareWorker.doCompare', 'compose', thisVS.vurl)
|
|
184
|
+
Extensions.checkNoModifiers(thisC, 'CompareWorker.doCompare', 'ValueSet.compose', thisVS.vurl)
|
|
182
185
|
this.checkNoLockedDate(thisVS.vurl, thisC);
|
|
183
186
|
if (!otherC) {
|
|
184
187
|
return this.makeOutcome("indeterminate", `The ValueSet ${otherVS.vurl} has no compose`);
|
|
185
188
|
}
|
|
186
|
-
Extensions.checkNoModifiers(otherC, 'CompareWorker.doCompare', 'compose', otherVS.vurl)
|
|
189
|
+
Extensions.checkNoModifiers(otherC, 'CompareWorker.doCompare', 'ValueSet.compose', otherVS.vurl)
|
|
187
190
|
this.checkNoLockedDate(otherVS.vurl, otherC);
|
|
188
191
|
|
|
189
192
|
let systems = new Map(); // tracks whether the comparison is version dependent or not
|