fhirsmith 0.9.7 → 0.10.1
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/package.json +1 -1
- package/packages/package-crawler.js +123 -6
- package/packages/packages.js +104 -0
- package/publisher/publisher.js +191 -5
- package/registry/crawler.js +85 -6
- package/registry/registry.js +18 -1
- package/server.js +5 -0
- package/stats.js +26 -18
- package/translations/Messages.properties +1 -0
- package/tx/html/home-metrics.liquid +10 -10
- package/tx/library/codesystem.js +31 -0
- package/tx/library/conceptmap.js +24 -0
- package/tx/library/renderer.js +8 -8
- package/tx/library/valueset.js +46 -0
- package/tx/operation-context.js +109 -4
- package/tx/provider.js +3 -3
- package/tx/tx-html.js +27 -0
- package/tx/tx.js +80 -17
- package/tx/vs/vs-database.js +41 -8
- package/tx/vs/vs-package.js +3 -0
- package/tx/vs/vs-vsac.js +4 -0
- package/tx/workers/cache-control.js +186 -0
- package/tx/workers/{related.js → compare.js} +27 -27
- package/tx/workers/metadata.js +11 -6
- package/tx/workers/worker.js +133 -46
- package/tx/data/OperationDefinition-ValueSet-related.json +0 -133
package/tx/operation-context.js
CHANGED
|
@@ -3,6 +3,7 @@ const inspector = require("inspector");
|
|
|
3
3
|
const crypto = require("crypto");
|
|
4
4
|
const {Languages} = require("../library/languages");
|
|
5
5
|
const {Issue} = require("./library/operation-outcome");
|
|
6
|
+
const Logger = require("../library/logger");
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Check if running under a debugger
|
|
@@ -59,6 +60,37 @@ class ResourceCache {
|
|
|
59
60
|
this.stats = stats;
|
|
60
61
|
this.cache = new Map();
|
|
61
62
|
this.locks = new Map(); // For thread-safety with async operations
|
|
63
|
+
this.log = Logger.getInstance().child({module: 'tx-cache'});
|
|
64
|
+
// Running total of concepts across every resource in every entry, maintained
|
|
65
|
+
// incrementally on each mutation so cache sizing/limits are O(1) to consult.
|
|
66
|
+
// Each entry also carries its own `concepts` subtotal so removal/replacement
|
|
67
|
+
// can adjust the total without rescanning.
|
|
68
|
+
this.totalConcepts = 0;
|
|
69
|
+
// High-water marks (never decremented) - the most caches and the most concepts
|
|
70
|
+
// this cache has held at once, for capacity reporting.
|
|
71
|
+
this.maxConcepts = 0;
|
|
72
|
+
this.maxSizeValue = 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Update the high-water marks after a mutation that may have grown the cache.
|
|
77
|
+
*/
|
|
78
|
+
_trackMax() {
|
|
79
|
+
if (this.totalConcepts > this.maxConcepts) {
|
|
80
|
+
this.maxConcepts = this.totalConcepts;
|
|
81
|
+
}
|
|
82
|
+
if (this.cache.size > this.maxSizeValue) {
|
|
83
|
+
this.maxSizeValue = this.cache.size;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Concept count of a single cached resource, or 0 if it doesn't expose one.
|
|
89
|
+
* @param {Object} resource
|
|
90
|
+
* @returns {number}
|
|
91
|
+
*/
|
|
92
|
+
_conceptsOf(resource) {
|
|
93
|
+
return resource && typeof resource.conceptCount === 'function' ? resource.conceptCount() : 0;
|
|
62
94
|
}
|
|
63
95
|
|
|
64
96
|
/**
|
|
@@ -70,8 +102,10 @@ class ResourceCache {
|
|
|
70
102
|
const entry = this.cache.get(cacheId);
|
|
71
103
|
if (entry) {
|
|
72
104
|
entry.lastUsed = Date.now();
|
|
105
|
+
this.log.info(`cache-id '${cacheId}': hit, returning ${entry.resources.length} resource(s): ${entry.resources.map(r => this._resourceKey(r)).join(', ')}`);
|
|
73
106
|
return [...entry.resources]; // Return a copy
|
|
74
107
|
}
|
|
108
|
+
this.log.info(`cache-id '${cacheId}': miss (no entry)`);
|
|
75
109
|
return [];
|
|
76
110
|
}
|
|
77
111
|
|
|
@@ -92,22 +126,32 @@ class ResourceCache {
|
|
|
92
126
|
add(cacheId, resources) {
|
|
93
127
|
if (!resources || resources.length === 0) return;
|
|
94
128
|
|
|
95
|
-
const entry = this.cache.get(cacheId) || { resources: [], lastUsed: Date.now() };
|
|
129
|
+
const entry = this.cache.get(cacheId) || { resources: [], lastUsed: Date.now(), concepts: 0 };
|
|
96
130
|
|
|
97
|
-
// Merge resources, avoiding duplicates by url+version
|
|
131
|
+
// Merge resources, avoiding duplicates by url+version. Keep the entry's concept
|
|
132
|
+
// subtotal and the cache-wide total in step with each insertion/replacement.
|
|
98
133
|
for (const resource of resources) {
|
|
99
134
|
const key = this._resourceKey(resource);
|
|
135
|
+
const newConcepts = this._conceptsOf(resource);
|
|
100
136
|
const existingIndex = entry.resources.findIndex(r => this._resourceKey(r) === key);
|
|
101
137
|
if (existingIndex >= 0) {
|
|
102
|
-
// Replace existing
|
|
138
|
+
// Replace existing: adjust by the difference
|
|
139
|
+
const delta = newConcepts - this._conceptsOf(entry.resources[existingIndex]);
|
|
103
140
|
entry.resources[existingIndex] = resource;
|
|
141
|
+
entry.concepts += delta;
|
|
142
|
+
this.totalConcepts += delta;
|
|
143
|
+
this.log.info(`cache-id '${cacheId}': replaced ${key}`);
|
|
104
144
|
} else {
|
|
105
145
|
entry.resources.push(resource);
|
|
146
|
+
entry.concepts += newConcepts;
|
|
147
|
+
this.totalConcepts += newConcepts;
|
|
148
|
+
this.log.info(`cache-id '${cacheId}': added ${key}`);
|
|
106
149
|
}
|
|
107
150
|
}
|
|
108
151
|
|
|
109
152
|
entry.lastUsed = Date.now();
|
|
110
153
|
this.cache.set(cacheId, entry);
|
|
154
|
+
this._trackMax();
|
|
111
155
|
}
|
|
112
156
|
|
|
113
157
|
/**
|
|
@@ -116,10 +160,23 @@ class ResourceCache {
|
|
|
116
160
|
* @param {Array} resources - Resources to set
|
|
117
161
|
*/
|
|
118
162
|
set(cacheId, resources) {
|
|
163
|
+
this.log.info(`cache-id '${cacheId}': set (replace all) with ${resources.length} resource(s): ${resources.map(r => this._resourceKey(r)).join(', ')}`);
|
|
164
|
+
// Drop the old entry's contribution, then count the replacement.
|
|
165
|
+
const existing = this.cache.get(cacheId);
|
|
166
|
+
if (existing) {
|
|
167
|
+
this.totalConcepts -= existing.concepts || 0;
|
|
168
|
+
}
|
|
169
|
+
let concepts = 0;
|
|
170
|
+
for (const resource of resources) {
|
|
171
|
+
concepts += this._conceptsOf(resource);
|
|
172
|
+
}
|
|
173
|
+
this.totalConcepts += concepts;
|
|
119
174
|
this.cache.set(cacheId, {
|
|
120
175
|
resources: [...resources],
|
|
121
|
-
lastUsed: Date.now()
|
|
176
|
+
lastUsed: Date.now(),
|
|
177
|
+
concepts
|
|
122
178
|
});
|
|
179
|
+
this._trackMax();
|
|
123
180
|
}
|
|
124
181
|
|
|
125
182
|
/**
|
|
@@ -127,6 +184,10 @@ class ResourceCache {
|
|
|
127
184
|
* @param {string} cacheId - The cache identifier
|
|
128
185
|
*/
|
|
129
186
|
clear(cacheId) {
|
|
187
|
+
const entry = this.cache.get(cacheId);
|
|
188
|
+
if (entry) {
|
|
189
|
+
this.totalConcepts -= entry.concepts || 0;
|
|
190
|
+
}
|
|
130
191
|
this.cache.delete(cacheId);
|
|
131
192
|
}
|
|
132
193
|
|
|
@@ -135,6 +196,7 @@ class ResourceCache {
|
|
|
135
196
|
*/
|
|
136
197
|
clearAll() {
|
|
137
198
|
this.cache.clear();
|
|
199
|
+
this.totalConcepts = 0;
|
|
138
200
|
}
|
|
139
201
|
|
|
140
202
|
/**
|
|
@@ -150,6 +212,7 @@ class ResourceCache {
|
|
|
150
212
|
for (const [cacheId, entry] of this.cache.entries()) {
|
|
151
213
|
if (now - entry.lastUsed > maxAge) {
|
|
152
214
|
i++;
|
|
215
|
+
this.totalConcepts -= entry.concepts || 0;
|
|
153
216
|
this.cache.delete(cacheId);
|
|
154
217
|
}
|
|
155
218
|
}
|
|
@@ -166,6 +229,41 @@ class ResourceCache {
|
|
|
166
229
|
return this.cache.size;
|
|
167
230
|
}
|
|
168
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Total number of concepts held across every entry in the cache. Maintained
|
|
234
|
+
* incrementally, so this is O(1).
|
|
235
|
+
* @returns {number}
|
|
236
|
+
*/
|
|
237
|
+
conceptCount() {
|
|
238
|
+
return this.totalConcepts;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Highest number of concepts this cache has held at once (high-water mark).
|
|
243
|
+
* @returns {number}
|
|
244
|
+
*/
|
|
245
|
+
maxConceptCount() {
|
|
246
|
+
return this.maxConcepts;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Highest number of caches (cache-ids) this cache has held at once.
|
|
251
|
+
* @returns {number}
|
|
252
|
+
*/
|
|
253
|
+
maxSize() {
|
|
254
|
+
return this.maxSizeValue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Number of concepts held under a single cache-id (0 if unknown).
|
|
259
|
+
* @param {string} cacheId
|
|
260
|
+
* @returns {number}
|
|
261
|
+
*/
|
|
262
|
+
conceptCountFor(cacheId) {
|
|
263
|
+
const entry = this.cache.get(cacheId);
|
|
264
|
+
return entry ? (entry.concepts || 0) : 0;
|
|
265
|
+
}
|
|
266
|
+
|
|
169
267
|
/**
|
|
170
268
|
* Generate a key for a resource based on url and version
|
|
171
269
|
* @param {Object} resource - The resource
|
|
@@ -437,6 +535,12 @@ class OperationContext {
|
|
|
437
535
|
this.logEntries = [];
|
|
438
536
|
this.resourceCache = resourceCache;
|
|
439
537
|
this.expansionCache = expansionCache;
|
|
538
|
+
// Server-issued cache-id carried on the request (X-Cache-Id header). Set once
|
|
539
|
+
// per request from the header so every worker can consult it uniformly via
|
|
540
|
+
// setupAdditionalResources, regardless of how that worker assembles its
|
|
541
|
+
// Parameters (buildParameters, raw req.body, query/form). An explicit
|
|
542
|
+
// cache-id *parameter* still takes precedence over this.
|
|
543
|
+
this.cacheId = null;
|
|
440
544
|
this.debugging = isDebugging();
|
|
441
545
|
// Providers opened during this operation that need their underlying
|
|
442
546
|
// resources (sqlite connections, etc.) released when the operation ends.
|
|
@@ -470,6 +574,7 @@ class OperationContext {
|
|
|
470
574
|
newContext.timeTracker = this.timeTracker.link();
|
|
471
575
|
newContext.logEntries = [...this.logEntries];
|
|
472
576
|
newContext.debugging = this.debugging;
|
|
577
|
+
newContext.cacheId = this.cacheId;
|
|
473
578
|
newContext.usageTracker = this.usageTracker;
|
|
474
579
|
// Share the same provider-cleanup list so providers opened by the copy
|
|
475
580
|
// are released when the parent operation ends.
|
package/tx/provider.js
CHANGED
|
@@ -231,8 +231,8 @@ class Provider {
|
|
|
231
231
|
return vs;
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
|
-
let
|
|
235
|
-
return
|
|
234
|
+
let vs1 = await this.findKnownValueSet(url, version);
|
|
235
|
+
return vs1;
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
async getConceptMapById(opContext, id) {
|
|
@@ -381,7 +381,7 @@ class Provider {
|
|
|
381
381
|
if (vs) {
|
|
382
382
|
return {
|
|
383
383
|
link: this.path+"/ValueSet/"+vs.id,
|
|
384
|
-
description: (vs.title ? vs.title : vs.name)+(version ? " v"+version : "")
|
|
384
|
+
description: (vs.title ? vs.title : vs.name)+(version ? " v"+version : vs.version ? " v"+vs.version : "")
|
|
385
385
|
};
|
|
386
386
|
}
|
|
387
387
|
let cm = await this.findConceptMap(opContext, system, version);
|
package/tx/tx-html.js
CHANGED
|
@@ -471,6 +471,33 @@ class TxHtmlRenderer {
|
|
|
471
471
|
if (param.valueCode !== undefined) {
|
|
472
472
|
return `<code>${escape(param.valueCode)}</code>`;
|
|
473
473
|
}
|
|
474
|
+
if (param.valueId !== undefined) {
|
|
475
|
+
return escape(String(param.valueId));
|
|
476
|
+
}
|
|
477
|
+
if (param.valueOid !== undefined) {
|
|
478
|
+
return escape(String(param.valueOid));
|
|
479
|
+
}
|
|
480
|
+
if (param.valueUuid !== undefined) {
|
|
481
|
+
return escape(String(param.valueUuid));
|
|
482
|
+
}
|
|
483
|
+
if (param.valueMarkdown !== undefined) {
|
|
484
|
+
// Render markdown to HTML the same way the rest of the server does, using
|
|
485
|
+
// commonmark in safe mode (raw HTML in the markdown is escaped, so this is
|
|
486
|
+
// XSS-safe).
|
|
487
|
+
const commonmark = require('commonmark');
|
|
488
|
+
const reader = new commonmark.Parser();
|
|
489
|
+
const writer = new commonmark.HtmlRenderer({ safe: true });
|
|
490
|
+
return writer.render(reader.parse(String(param.valueMarkdown)));
|
|
491
|
+
}
|
|
492
|
+
if (param.valueInteger64 !== undefined) {
|
|
493
|
+
return escape(String(param.valueInteger64));
|
|
494
|
+
}
|
|
495
|
+
if (param.valuePositiveInt !== undefined) {
|
|
496
|
+
return escape(String(param.valuePositiveInt));
|
|
497
|
+
}
|
|
498
|
+
if (param.valueUnsignedInt !== undefined) {
|
|
499
|
+
return escape(String(param.valueUnsignedInt));
|
|
500
|
+
}
|
|
474
501
|
if (param.valueDate !== undefined) {
|
|
475
502
|
return escape(param.valueDate);
|
|
476
503
|
}
|
package/tx/tx.js
CHANGED
|
@@ -27,6 +27,7 @@ const LookupWorker = require('./workers/lookup');
|
|
|
27
27
|
const SubsumesWorker = require('./workers/subsumes');
|
|
28
28
|
const { MetadataHandler } = require('./workers/metadata');
|
|
29
29
|
const { BatchValidateWorker } = require('./workers/batch-validate');
|
|
30
|
+
const { CacheControlWorker } = require('./workers/cache-control');
|
|
30
31
|
const {CapabilityStatementXML} = require("./xml/capabilitystatement-xml");
|
|
31
32
|
const {TerminologyCapabilitiesXML} = require("./xml/terminologycapabilities-xml");
|
|
32
33
|
const {ParametersXML} = require("./xml/parameters-xml");
|
|
@@ -36,7 +37,7 @@ const {ConceptMapXML} = require("./xml/conceptmap-xml");
|
|
|
36
37
|
const {TxHtmlRenderer} = require("./tx-html");
|
|
37
38
|
const {Renderer} = require("./library/renderer");
|
|
38
39
|
const {OperationsWorker} = require("./workers/operations");
|
|
39
|
-
const {
|
|
40
|
+
const {CompareWorker} = require("./workers/compare");
|
|
40
41
|
const {codeSystemFromR5} = require("./xversion/xv-codesystem");
|
|
41
42
|
const {operationOutcomeFromR5} = require("./xversion/xv-operationoutcome");
|
|
42
43
|
const {parametersFromR5} = require("./xversion/xv-parameters");
|
|
@@ -296,6 +297,12 @@ class TXModule {
|
|
|
296
297
|
endpointInfo.resourceCache, endpointInfo.expansionCache
|
|
297
298
|
);
|
|
298
299
|
opContext.usageTracker = this.usageTracker;
|
|
300
|
+
// Normalise the cache-id from the X-Cache-Id header onto the operation
|
|
301
|
+
// context once, here, so every worker honours a front-loaded cache no
|
|
302
|
+
// matter how it later assembles its Parameters (some use buildParameters,
|
|
303
|
+
// others read req.body / query directly). An explicit cache-id parameter
|
|
304
|
+
// on the request still wins (see setupAdditionalResources).
|
|
305
|
+
opContext.cacheId = req.get('X-Cache-Id') || null;
|
|
299
306
|
|
|
300
307
|
// Attach everything to request
|
|
301
308
|
req.txProvider = endpointInfo.provider;
|
|
@@ -595,23 +602,23 @@ class TXModule {
|
|
|
595
602
|
}
|
|
596
603
|
});
|
|
597
604
|
|
|
598
|
-
// ValueSet/$
|
|
599
|
-
router.get('/ValueSet/\\$
|
|
605
|
+
// ValueSet/$compare(GET and POST)
|
|
606
|
+
router.get('/ValueSet/\\$compare', async (req, res) => {
|
|
600
607
|
const start = Date.now();
|
|
601
608
|
try {
|
|
602
|
-
let worker = new
|
|
609
|
+
let worker = new CompareWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
603
610
|
await worker.handle(req, res);
|
|
604
611
|
} finally {
|
|
605
|
-
this.countRequest('$
|
|
612
|
+
this.countRequest('$compare', Date.now() - start);
|
|
606
613
|
}
|
|
607
614
|
});
|
|
608
|
-
router.post('/ValueSet/\\$
|
|
615
|
+
router.post('/ValueSet/\\$compare', async (req, res) => {
|
|
609
616
|
const start = Date.now();
|
|
610
617
|
try {
|
|
611
|
-
let worker = new
|
|
618
|
+
let worker = new CompareWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
612
619
|
await worker.handle(req, res);
|
|
613
620
|
} finally {
|
|
614
|
-
this.countRequest('$
|
|
621
|
+
this.countRequest('$compare', Date.now() - start);
|
|
615
622
|
}
|
|
616
623
|
});
|
|
617
624
|
|
|
@@ -655,6 +662,26 @@ class TXModule {
|
|
|
655
662
|
}
|
|
656
663
|
});
|
|
657
664
|
|
|
665
|
+
// $cache-control (GET and POST) - base-level operation: mode=start|end
|
|
666
|
+
router.get('/\\$cache-control', async (req, res) => {
|
|
667
|
+
const start = Date.now();
|
|
668
|
+
try {
|
|
669
|
+
let worker = new CacheControlWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
670
|
+
await worker.handle(req, res, this.log);
|
|
671
|
+
} finally {
|
|
672
|
+
this.countRequest('$cache-control', Date.now() - start);
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
router.post('/\\$cache-control', async (req, res) => {
|
|
676
|
+
const start = Date.now();
|
|
677
|
+
try {
|
|
678
|
+
let worker = new CacheControlWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
679
|
+
await worker.handle(req, res, this.log);
|
|
680
|
+
} finally {
|
|
681
|
+
this.countRequest('$cache-control', Date.now() - start);
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
|
|
658
685
|
// ConceptMap/$translate (GET and POST)
|
|
659
686
|
router.get('/ConceptMap/\\$translate', async (req, res) => {
|
|
660
687
|
const start = Date.now();
|
|
@@ -779,23 +806,23 @@ class TXModule {
|
|
|
779
806
|
});
|
|
780
807
|
|
|
781
808
|
|
|
782
|
-
// ValueSet/[id]/$
|
|
783
|
-
router.get('/ValueSet/:id/\\$
|
|
809
|
+
// ValueSet/[id]/$compare
|
|
810
|
+
router.get('/ValueSet/:id/\\$compare', async (req, res) => {
|
|
784
811
|
const start = Date.now();
|
|
785
812
|
try {
|
|
786
|
-
let worker = new
|
|
813
|
+
let worker = new CompareWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
787
814
|
await worker.handleInstance(req, res, this.log);
|
|
788
815
|
} finally {
|
|
789
|
-
this.countRequest('$
|
|
816
|
+
this.countRequest('$compare', Date.now() - start);
|
|
790
817
|
}
|
|
791
818
|
});
|
|
792
|
-
router.post('/ValueSet/:id/\\$
|
|
819
|
+
router.post('/ValueSet/:id/\\$compare', async (req, res) => {
|
|
793
820
|
const start = Date.now();
|
|
794
821
|
try {
|
|
795
|
-
let worker = new
|
|
822
|
+
let worker = new CompareWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
796
823
|
await worker.handleInstance(req, res, this.log);
|
|
797
824
|
} finally {
|
|
798
|
-
this.countRequest('$
|
|
825
|
+
this.countRequest('$compare', Date.now() - start);
|
|
799
826
|
}
|
|
800
827
|
});
|
|
801
828
|
|
|
@@ -1177,10 +1204,46 @@ class TXModule {
|
|
|
1177
1204
|
}
|
|
1178
1205
|
}
|
|
1179
1206
|
|
|
1180
|
-
|
|
1207
|
+
// Number of entries in the expansion cache, summed across endpoints.
|
|
1208
|
+
expansionItemCount() {
|
|
1209
|
+
let count = 0;
|
|
1210
|
+
for (let ep of this.endpoints) {
|
|
1211
|
+
count = count + ep.expansionCache.size();
|
|
1212
|
+
}
|
|
1213
|
+
return count;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// Number of concepts held in the client (resource) cache, summed across endpoints.
|
|
1217
|
+
clientConceptCount() {
|
|
1218
|
+
let count = 0;
|
|
1219
|
+
for (let ep of this.endpoints) {
|
|
1220
|
+
count = count + ep.resourceCache.conceptCount();
|
|
1221
|
+
}
|
|
1222
|
+
return count;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// Number of caches (cache-ids) currently held in the client cache, across endpoints.
|
|
1226
|
+
clientCacheCount() {
|
|
1227
|
+
let count = 0;
|
|
1228
|
+
for (let ep of this.endpoints) {
|
|
1229
|
+
count = count + ep.resourceCache.size();
|
|
1230
|
+
}
|
|
1231
|
+
return count;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// High-water marks for the client cache, summed across endpoints.
|
|
1235
|
+
maxClientCacheCount() {
|
|
1236
|
+
let count = 0;
|
|
1237
|
+
for (let ep of this.endpoints) {
|
|
1238
|
+
count = count + ep.resourceCache.maxSize();
|
|
1239
|
+
}
|
|
1240
|
+
return count;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
maxClientConceptCount() {
|
|
1181
1244
|
let count = 0;
|
|
1182
1245
|
for (let ep of this.endpoints) {
|
|
1183
|
-
count = count + ep.resourceCache.
|
|
1246
|
+
count = count + ep.resourceCache.maxConceptCount();
|
|
1184
1247
|
}
|
|
1185
1248
|
return count;
|
|
1186
1249
|
}
|
package/tx/vs/vs-database.js
CHANGED
|
@@ -7,6 +7,30 @@ const ValueSet = require("../library/valueset");
|
|
|
7
7
|
// Columns that can be returned directly without parsing JSON
|
|
8
8
|
const INDEXED_COLUMNS = ['id', 'url', 'version', 'date', 'description', 'name', 'publisher', 'status', 'title'];
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Is `candidate` a newer version than `current`? Used to keep the version-less
|
|
12
|
+
* (bare-url) and url|major.minor map keys pointing at the latest version when several
|
|
13
|
+
* versions of the same value set are present. Handles both semver (IG/package value
|
|
14
|
+
* sets) and VSAC-style YYYYMMDD date versions (not semver, but fixed-width so they
|
|
15
|
+
* order correctly numerically).
|
|
16
|
+
* @param {string|null|undefined} candidate
|
|
17
|
+
* @param {string|null|undefined} current
|
|
18
|
+
* @returns {boolean}
|
|
19
|
+
*/
|
|
20
|
+
function isNewerVersion(candidate, current) {
|
|
21
|
+
if (current == null) return true;
|
|
22
|
+
if (candidate == null) return false;
|
|
23
|
+
if (candidate === current) return false;
|
|
24
|
+
if (VersionUtilities.isSemVer(candidate) && VersionUtilities.isSemVer(current)) {
|
|
25
|
+
return VersionUtilities.isThisOrLater(current, candidate); // candidate >= current (semver-aware)
|
|
26
|
+
}
|
|
27
|
+
if (/^\d+$/.test(candidate) && /^\d+$/.test(current)) {
|
|
28
|
+
// e.g. VSAC YYYYMMDD - compare numerically (handles differing widths too)
|
|
29
|
+
return Number(candidate) > Number(current);
|
|
30
|
+
}
|
|
31
|
+
return candidate > current; // best-effort lexical fallback
|
|
32
|
+
}
|
|
33
|
+
|
|
10
34
|
/**
|
|
11
35
|
* Shared database layer for ValueSet providers
|
|
12
36
|
* Handles SQLite operations for indexing and searching ValueSets
|
|
@@ -737,21 +761,22 @@ class ValueSetDatabase {
|
|
|
737
761
|
}
|
|
738
762
|
|
|
739
763
|
addToMap(valueSetMap, id, url, version, valueSet) {
|
|
740
|
-
|
|
741
|
-
|
|
764
|
+
// The bare-url key must resolve to the *latest* version, but rows arrive in no
|
|
765
|
+
// particular order (and several versions of the same url coexist - e.g. VSAC, where
|
|
766
|
+
// each date-version is its own row). So only overwrite it when this version is newer.
|
|
767
|
+
this._setIfNewer(valueSetMap, url, version, valueSet);
|
|
768
|
+
valueSetMap.set(id, valueSet); // id is unique per version
|
|
742
769
|
|
|
743
770
|
if (version) {
|
|
744
|
-
// Store by url|version
|
|
745
|
-
|
|
746
|
-
valueSetMap.set(versionKey, valueSet);
|
|
771
|
+
// Store by url|version (exact - unique per version, always set)
|
|
772
|
+
valueSetMap.set(`${url}|${version}`, valueSet);
|
|
747
773
|
|
|
748
|
-
// If version is semver, also store by url|major.minor
|
|
774
|
+
// If version is semver, also store by url|major.minor, keeping the latest patch
|
|
749
775
|
try {
|
|
750
776
|
if (VersionUtilities.isSemVer(version)) {
|
|
751
777
|
const majorMinor = VersionUtilities.getMajMin(version);
|
|
752
778
|
if (majorMinor) {
|
|
753
|
-
|
|
754
|
-
valueSetMap.set(majorMinorKey, valueSet);
|
|
779
|
+
this._setIfNewer(valueSetMap, `${url}|${majorMinor}`, version, valueSet);
|
|
755
780
|
}
|
|
756
781
|
}
|
|
757
782
|
} catch (error) {
|
|
@@ -760,6 +785,14 @@ class ValueSetDatabase {
|
|
|
760
785
|
}
|
|
761
786
|
}
|
|
762
787
|
|
|
788
|
+
// Set map[key] = valueSet only if `version` is newer than the version already there.
|
|
789
|
+
_setIfNewer(valueSetMap, key, version, valueSet) {
|
|
790
|
+
const existing = valueSetMap.get(key);
|
|
791
|
+
if (!existing || isNewerVersion(version, existing.version)) {
|
|
792
|
+
valueSetMap.set(key, valueSet);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
763
796
|
/**
|
|
764
797
|
* Search for ValueSets based on criteria
|
|
765
798
|
* @param {Array<{name: string, value: string}>} searchParams - Search criteria
|
package/tx/vs/vs-package.js
CHANGED