fhirsmith 0.18.2 → 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.
Files changed (203) hide show
  1. package/ACCESSIBILITY.md +119 -0
  2. package/CHANGELOG.md +149 -0
  3. package/CONFIGURATION.md +305 -0
  4. package/LICENSE +29 -0
  5. package/README.md +29 -122
  6. package/cluster.js +1220 -0
  7. package/cluster.md +707 -0
  8. package/config-template.json +77 -8
  9. package/extension-tracker/extension-tracker-template.html +5 -8
  10. package/extension-tracker/extension-tracker.js +0 -2
  11. package/folder/folder.js +7 -6
  12. package/ips/analysis.js +560 -0
  13. package/ips/ips-template.html +5 -8
  14. package/ips/ips.js +132 -59
  15. package/ips/nationality.js +120 -0
  16. package/ips/pipeline.js +74 -21
  17. package/ips/presenter.js +1 -1
  18. package/ips/readme.md +132 -25
  19. package/ips/render.js +466 -77
  20. package/ips/store.js +79 -22
  21. package/ips/structure.js +247 -0
  22. package/ips/terminology.js +1 -1
  23. package/library/body-limit.js +22 -0
  24. package/library/cache-generation.js +15 -0
  25. package/library/cgroup-memory.js +133 -1
  26. package/library/cluster-counts.js +121 -0
  27. package/library/cluster-events.js +153 -0
  28. package/library/cluster-metrics.js +193 -0
  29. package/library/cluster-status.js +468 -0
  30. package/library/config-reader.js +96 -0
  31. package/library/daily-report.js +100 -2
  32. package/library/html-server.js +27 -1
  33. package/library/i18nsupport.js +23 -1
  34. package/library/keep-alive.js +18 -0
  35. package/library/languages.js +17 -3
  36. package/library/logger.js +31 -0
  37. package/library/memory-guard.js +428 -0
  38. package/library/native-memory.js +52 -0
  39. package/library/npm-audit.js +51 -25
  40. package/library/openapi-doc.js +7 -5
  41. package/library/package-manager.js +8 -0
  42. package/library/role.js +197 -0
  43. package/library/server-credentials.js +169 -0
  44. package/library/sqlite-utilities.js +59 -0
  45. package/library/ssrf-guard.js +100 -0
  46. package/library/static-response-stats.js +16 -0
  47. package/library/version-utilities.js +50 -0
  48. package/library/worker-count.js +192 -0
  49. package/nginx.md +30 -11
  50. package/npmprojector/indexer.js +18 -10
  51. package/package.json +11 -8
  52. package/packages/feed-check-page.js +117 -0
  53. package/packages/feed-check.js +279 -0
  54. package/packages/npm-check.js +57 -3
  55. package/packages/npm-signing.js +219 -0
  56. package/packages/openapi.yaml +207 -1
  57. package/packages/package-crawler.js +322 -41
  58. package/packages/packages-template.html +6 -8
  59. package/packages/packages.js +604 -57
  60. package/packages/readme.md +113 -1
  61. package/publisher/publisher-template.html +2 -5
  62. package/publisher/publisher.js +121 -110
  63. package/publisher/readme.md +75 -0
  64. package/registry/crawler.js +84 -108
  65. package/registry/documentation/configuration.md +34 -0
  66. package/registry/registry-template.html +5 -8
  67. package/registry/registry.js +5 -4
  68. package/root-bare-template.html +7 -4
  69. package/root-template.html +5 -8
  70. package/server.js +398 -198
  71. package/shl/shl.js +38 -37
  72. package/static/a11y/a11y-check.js +832 -0
  73. package/static/a11y/axe.min.js +12 -0
  74. package/static/assets/css/bootstrap-fhir.css +15 -15
  75. package/static/assets/images/flags/au.svg +18 -0
  76. package/static/assets/images/flags/unknown.svg +11 -0
  77. package/static/assets/images/flags/us.svg +63 -0
  78. package/static/assets/images/icon_audit.png +0 -0
  79. package/static/assets/images/icon_clinical.png +0 -0
  80. package/static/assets/images/icon_document.png +0 -0
  81. package/static/assets/images/icon_external.png +0 -0
  82. package/static/assets/images/icon_medication.png +0 -0
  83. package/static/assets/images/icon_organization.png +0 -0
  84. package/static/assets/images/icon_other.png +0 -0
  85. package/static/assets/images/icon_person.png +0 -0
  86. package/static/assets/images/icon_section.png +0 -0
  87. package/stats-db.js +45 -0
  88. package/stats.js +197 -10
  89. package/testing/testing-template.html +5 -8
  90. package/token/nginx_passport_setup.md +1 -1
  91. package/token/security_guide.md +1 -1
  92. package/token/token-template.html +2 -5
  93. package/token/token.js +40 -36
  94. package/translations/Messages.properties +19 -0
  95. package/tx/README.md +154 -4
  96. package/tx/closure/closure-store.js +126 -3
  97. package/tx/cm/cm-database.js +69 -57
  98. package/tx/cm/cm-package.js +29 -1
  99. package/tx/cs/cs-api.js +284 -19
  100. package/tx/cs/cs-api.md +41 -4
  101. package/tx/cs/cs-areacode.js +1 -1
  102. package/tx/cs/cs-country.js +17 -5
  103. package/tx/cs/cs-cpt.js +33 -16
  104. package/tx/cs/cs-cs.js +148 -14
  105. package/tx/cs/cs-currency.js +1 -1
  106. package/tx/cs/cs-icd11.js +21 -13
  107. package/tx/cs/cs-icf.js +22 -3
  108. package/tx/cs/cs-lang.js +2 -2
  109. package/tx/cs/cs-loinc.js +185 -66
  110. package/tx/cs/cs-mimetypes.js +12 -2
  111. package/tx/cs/cs-ndc.js +40 -25
  112. package/tx/cs/cs-omop.js +54 -29
  113. package/tx/cs/cs-rxnorm.js +300 -196
  114. package/tx/cs/cs-snomed.js +608 -64
  115. package/tx/cs/cs-ucum.js +14 -4
  116. package/tx/cs/cs-unii.js +18 -14
  117. package/tx/cs/cs-virtual.js +210 -0
  118. package/tx/ecosystem-resolver.js +413 -0
  119. package/tx/html/cluster-dash-metrics.liquid +26 -0
  120. package/tx/html/cluster-home-metrics.liquid +31 -0
  121. package/tx/html/cluster-metrics-script.liquid +251 -0
  122. package/tx/html/codesystem-operations.liquid +3 -3
  123. package/tx/html/ecl-panel.liquid +1 -1
  124. package/tx/html/external-metrics.liquid +17 -0
  125. package/tx/html/operations-form.liquid +17 -17
  126. package/tx/html/search-form.liquid +11 -11
  127. package/tx/html/status-line.liquid +58 -0
  128. package/tx/html/status-panel.liquid +140 -0
  129. package/tx/html/status-tasks.liquid +25 -0
  130. package/tx/html/tx-template.html +2 -5
  131. package/tx/importers/atc-to-fhir.js +17 -16
  132. package/tx/importers/export-icd11-codesystem.js +3 -2
  133. package/tx/importers/import-icd11.js +4 -3
  134. package/tx/importers/import-loinc.module.js +118 -56
  135. package/tx/importers/import-ndc.module.js +56 -44
  136. package/tx/importers/import-rxnorm.module.js +118 -79
  137. package/tx/importers/import-sct.module.js +57 -58
  138. package/tx/importers/import-unii.module.js +37 -36
  139. package/tx/importers/subset-loinc.module.js +49 -48
  140. package/tx/importers/subset-rxnorm.module.js +29 -28
  141. package/tx/importers/terminal.js +18 -0
  142. package/tx/importers/tx-import-base.js +13 -12
  143. package/tx/importers/tx-import-settings.js +5 -4
  144. package/tx/importers/tx-import.js +53 -52
  145. package/tx/library/canonical-resource.js +49 -0
  146. package/tx/library/codesystem.js +94 -5
  147. package/tx/library/designations.js +45 -2
  148. package/tx/library/extensions.js +13 -7
  149. package/tx/library/implicit-valuesets.js +66 -0
  150. package/tx/library/renderer.js +77 -21
  151. package/tx/library/sqlite-utilities.js +2 -0
  152. package/tx/library/ucum-parsers.js +11 -1
  153. package/tx/library/wait-for-cache.js +43 -0
  154. package/tx/library.js +321 -22
  155. package/tx/openapi-operations.js +6 -4
  156. package/tx/operation-context.js +460 -99
  157. package/tx/params.js +25 -0
  158. package/tx/problems.js +3 -5
  159. package/tx/provider.js +278 -42
  160. package/tx/resource-cache-sqlite.js +1050 -0
  161. package/tx/sct/ecl.js +47 -6
  162. package/tx/sct/editions.js +39 -0
  163. package/tx/sct/expressions.js +10 -1
  164. package/tx/sct/load-config.js +107 -0
  165. package/tx/sct/mmap.js +109 -0
  166. package/tx/sct/structures.js +166 -13
  167. package/tx/sct/text-index.js +5 -1
  168. package/tx/syndication/syndication-feed.js +2 -2
  169. package/tx/tx-html.js +44 -8
  170. package/tx/tx.fhir.org.yml +8 -8
  171. package/tx/tx.js +282 -80
  172. package/tx/usage-tracker.js +39 -11
  173. package/tx/vs/vs-api.js +19 -0
  174. package/tx/vs/vs-database.js +386 -62
  175. package/tx/vs/vs-package.js +27 -4
  176. package/tx/vs/vs-vsac.js +648 -100
  177. package/tx/workers/batch-validate.js +37 -10
  178. package/tx/workers/batch.js +6 -1
  179. package/tx/workers/cache-control.js +15 -4
  180. package/tx/workers/closure.js +156 -13
  181. package/tx/workers/compare.js +8 -5
  182. package/tx/workers/expand.js +1173 -289
  183. package/tx/workers/metadata.js +35 -0
  184. package/tx/workers/search.js +53 -1
  185. package/tx/workers/supplement-filters.js +145 -0
  186. package/tx/workers/translate.js +169 -4
  187. package/tx/workers/validate.js +223 -119
  188. package/tx/workers/worker.js +123 -31
  189. package/tx/xversion/xv-valueset.js +7 -4
  190. package/utilities/submit-test-report.js +3 -1
  191. package/utilities/tx-stress-test.js +482 -0
  192. package/vcl/readme.md +37 -0
  193. package/vcl/vcl-parser.js +15 -0
  194. package/xig/readme.md +52 -0
  195. package/xig/xig-template.html +5 -8
  196. package/xig/xig.js +238 -422
  197. package/changelog-template.md +0 -22
  198. package/registry/test-runner.js +0 -237
  199. package/release.sh +0 -80
  200. package/test-scripts/repro-re2-wasm-leak.js +0 -34
  201. package/tsconfig.eslint.json +0 -9
  202. package/tx/html/dash-metrics.liquid +0 -147
  203. package/tx/html/home-metrics.liquid +0 -225
package/tx/sct/ecl.js CHANGED
@@ -479,6 +479,22 @@ class ECLLexer {
479
479
  }
480
480
  }
481
481
 
482
+ /**
483
+ * Bounds on the shape of an ECL expression, so that a hostile expression is a
484
+ * clean parse error rather than a RangeError (stack overflow) here, in the
485
+ * evaluator, or in any of the other recursive AST walkers.
486
+ *
487
+ * MAX_ECL_DEPTH limits nesting - bracketed expression constraints, bracketed
488
+ * refinements and attribute sets. Real ECL nests a handful of levels.
489
+ *
490
+ * MAX_ECL_TERMS limits the number of sub expression constraints (focus
491
+ * concepts, refinement values, ...). It is needed as well as the depth limit
492
+ * because a compound chain ("A OR B OR C ...") is parsed iteratively but builds
493
+ * a left-deep tree, one level per operator, which every walker then recurses.
494
+ */
495
+ const MAX_ECL_DEPTH = 100;
496
+ const MAX_ECL_TERMS = 500;
497
+
482
498
  /**
483
499
  * ECL Parser - Parses tokens into AST
484
500
  */
@@ -487,6 +503,20 @@ class ECLParser {
487
503
  this.tokens = tokens;
488
504
  this.position = 0;
489
505
  this.current = this.tokens[0] || { type: ECLTokenType.EOF };
506
+ this.depth = 0;
507
+ this.terms = 0;
508
+ }
509
+
510
+ /** Run a parse step one nesting level deeper, enforcing MAX_ECL_DEPTH. */
511
+ #nested(fn) {
512
+ if (++this.depth > MAX_ECL_DEPTH) {
513
+ this.error(`The expression nests more than ${MAX_ECL_DEPTH} levels deep`);
514
+ }
515
+ try {
516
+ return fn();
517
+ } finally {
518
+ this.depth--;
519
+ }
490
520
  }
491
521
 
492
522
  error(message) {
@@ -526,7 +556,7 @@ class ECLParser {
526
556
  }
527
557
 
528
558
  parseExpressionConstraint() {
529
- return this.parseCompoundExpressionConstraint();
559
+ return this.#nested(() => this.parseCompoundExpressionConstraint());
530
560
  }
531
561
 
532
562
  parseCompoundExpressionConstraint() {
@@ -607,6 +637,9 @@ class ECLParser {
607
637
  }
608
638
 
609
639
  parseSubExpressionConstraint() {
640
+ if (++this.terms > MAX_ECL_TERMS) {
641
+ this.error(`The expression has more than ${MAX_ECL_TERMS} terms`);
642
+ }
610
643
  // Handle constraint operators
611
644
  let operator = null;
612
645
  if (this.match(
@@ -701,7 +734,8 @@ class ECLParser {
701
734
  }
702
735
 
703
736
  /**
704
- * Parse one "{{ ... }}" block. Returns { kind: 'M'|'D'|'C'|'+', filters } where
737
+ * Parse one "{{ ... }}" block (the D of a description filter is optional). Returns
738
+ * { kind: 'M'|'D'|'C'|'+', filters } where
705
739
  * filters is only populated for member filters; the other kinds keep their
706
740
  * raw token text so the evaluator can report them as unsupported.
707
741
  */
@@ -714,8 +748,13 @@ class ECLParser {
714
748
  } else if (this.match(ECLTokenType.SCTID) && /^[MDCmdc]$/.test(this.current.value)) {
715
749
  kind = this.current.value.toUpperCase();
716
750
  this.advance();
751
+ } else if (this.match(ECLTokenType.SCTID) && /^[A-Za-z_]/.test(this.current.value)) {
752
+ // ECL 2.2: descriptionFilterConstraint = "{{" ws [ "d" / "D" ] ws descriptionFilter ...
753
+ // - the D is optional, so a filter that starts with a field name (term, language,
754
+ // type, ...) is a description filter
755
+ kind = 'D';
717
756
  } else {
718
- this.error('Expected M, D, C or + after {{');
757
+ this.error('Expected M, D, C, + or a description filter after {{');
719
758
  }
720
759
 
721
760
  if (kind === 'C' || kind === 'D') {
@@ -1256,7 +1295,7 @@ class ECLParser {
1256
1295
  * returned as-is; otherwise an ATTRIBUTE_SET with operator 'AND' or 'OR'.
1257
1296
  */
1258
1297
  parseRefinement() {
1259
- return this.parseRefinementSet(() => this.parseSubRefinement(), 'refinement');
1298
+ return this.#nested(() => this.parseRefinementSet(() => this.parseSubRefinement(), 'refinement'));
1260
1299
  }
1261
1300
 
1262
1301
  parseRefinementSet(parseItem, what) {
@@ -1332,7 +1371,7 @@ class ECLParser {
1332
1371
  * (the contents of an attribute group - no nested groups)
1333
1372
  */
1334
1373
  parseAttributeSet() {
1335
- return this.parseRefinementSet(() => {
1374
+ return this.#nested(() => this.parseRefinementSet(() => {
1336
1375
  if (this.match(ECLTokenType.LPAREN) && !this.#parenIsAttributeName()) {
1337
1376
  this.advance();
1338
1377
  const inner = this.parseAttributeSet();
@@ -1340,7 +1379,7 @@ class ECLParser {
1340
1379
  return inner;
1341
1380
  }
1342
1381
  return this.parseAttribute();
1343
- }, 'attribute set');
1382
+ }, 'attribute set'));
1344
1383
  }
1345
1384
 
1346
1385
  parseAttributeGroup(cardinality = null) {
@@ -2355,6 +2394,8 @@ class ECLValidator {
2355
2394
  }
2356
2395
 
2357
2396
  module.exports = {
2397
+ MAX_ECL_DEPTH,
2398
+ MAX_ECL_TERMS,
2358
2399
  ECLValidator,
2359
2400
  ECLLexer,
2360
2401
  ECLParser,
@@ -63,8 +63,47 @@ function editionCode(edition) {
63
63
  return e ? e.code : null;
64
64
  }
65
65
 
66
+ /**
67
+ * The editions snomed.info can resolve an edition-specific concept link for: the same list as
68
+ * SnomedUtilities.getEditionFromVersion in org.hl7.fhir.core. A code from any other edition
69
+ * links to http://snomed.info/id/<code>. Also the editions offered when an edition is chosen
70
+ * (the IPS module's national profiles), in this order.
71
+ */
72
+ const SNOMED_LINKABLE_EDITIONS = new Set([
73
+ '900000000000207008', // International
74
+ '449081005', // International Spanish
75
+ '11000221109', // Argentinian
76
+ '32506021000036107', // Australian (with drug extension)
77
+ '11000234105', // Austrian
78
+ '11000172109', // Belgian
79
+ '20621000087109', // Canadian English
80
+ '20611000087101', // Canadian Canadian French
81
+ '21000325107', // Chilean
82
+ '11000279109', // Czech
83
+ '554471000005108', // Danish
84
+ '11000181102', // Estonian
85
+ '11000229106', // Finnish
86
+ '11000274103', // German
87
+ '1121000189102', // Indian
88
+ '11000220105', // Irish
89
+ '11000146104', // Netherlands
90
+ '21000210109', // New Zealand
91
+ '51000202101', // Norwegian
92
+ '11000267109', // Republic of Korea (South Korea)
93
+ '900000001000122104', // Spanish National
94
+ '45991000052106', // Swedish
95
+ '2011000195101', // Swiss
96
+ '83821000000107', // UK
97
+ '999000021000000109', // UK Clinical
98
+ '5631000179106', // Uruguay
99
+ '731000124108', // US
100
+ '5991000124107', // US (with ICD-10-CM maps)
101
+ '827022005' // IPS Terminology
102
+ ]);
103
+
66
104
  module.exports = {
67
105
  EDITIONS,
106
+ SNOMED_LINKABLE_EDITIONS,
68
107
  editionName,
69
108
  editionCode
70
109
  };
@@ -13,6 +13,10 @@ const MAX_TERM_LENGTH = 1024;
13
13
  // Attribute values are themselves expanded, so a chain of definitions can descend a
14
14
  // long way; in real data it is a handful of levels.
15
15
  const MAX_NORMALISATION_DEPTH = 50;
16
+ // How deep bracketed attribute values may nest in an expression the parser accepts. The parser
17
+ // (and everything that walks the parsed expression) recurses once per level, so without a bound
18
+ // a hostile expression is a RangeError (stack overflow) rather than a parse error.
19
+ const MAX_EXPRESSION_DEPTH = 100;
16
20
 
17
21
  const NO_REFERENCE = 0xFFFFFFFF;
18
22
 
@@ -483,6 +487,7 @@ class SnomedExpressionParser {
483
487
  parse(source) {
484
488
  this.source = source;
485
489
  this.cursor = 0;
490
+ this.depth = 0;
486
491
 
487
492
  const result = new SnomedExpression();
488
493
  result.start = this.cursor;
@@ -681,9 +686,12 @@ class SnomedExpressionParser {
681
686
  this.ws();
682
687
 
683
688
  if (this.gchar('(')) {
689
+ this.depth++;
690
+ this.rule(this.depth <= MAX_EXPRESSION_DEPTH, `The expression nests more than ${MAX_EXPRESSION_DEPTH} levels deep`);
684
691
  const result = this.expression();
685
692
  this.ws();
686
693
  this.fixed(')');
694
+ this.depth--;
687
695
  return result;
688
696
  } else {
689
697
  const result = new SnomedExpression();
@@ -2951,6 +2959,7 @@ module.exports = {
2951
2959
 
2952
2960
  // Enums and constants
2953
2961
  SnomedServicesRenderOption,
2954
- SnomedRefinementGroupMatchState
2962
+ SnomedRefinementGroupMatchState,
2963
+ MAX_EXPRESSION_DEPTH
2955
2964
  };
2956
2965
 
@@ -0,0 +1,107 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * How each SNOMED CT cache file is loaded, from the tx module's "snomed" block in config.json:
5
+ *
6
+ * "snomed": {
7
+ * "load": "read" | "mmap", // the default for every edition (absent: "read")
8
+ * "prewarm": [ ... ], // map these, then read them through once in the background at startup
9
+ * "read": [ ... ] // read these into the process even when load is "mmap"
10
+ * }
11
+ *
12
+ * An entry names a cache file (e.g. "sct_us_20250901b.cache"), an edition version URI
13
+ * ("http://snomed.info/sct/731000124108/version/20250901"), or an edition URI
14
+ * ("http://snomed.info/sct/731000124108", every loaded version of the edition). "read" wins
15
+ * over "prewarm", which wins over "load".
16
+ *
17
+ * read - the file is read into this process (private memory, as before)
18
+ * mmap - the file is mapped read-only; pages are read from disk when first touched, and
19
+ * shared with every other process that maps the same file
20
+ * prewarm - mapped, and read through once in the background at startup so its pages are
21
+ * already in the shared page cache when the first requests arrive
22
+ */
23
+
24
+ const path = require('path');
25
+
26
+ const MODES = ['read', 'mmap'];
27
+
28
+ class SnomedLoadConfig {
29
+ /**
30
+ * @param {object} [block] - the "snomed" block of the tx module config
31
+ * @param {function(string)} [warn]
32
+ */
33
+ constructor(block, warn = () => {}) {
34
+ this.warn = warn;
35
+ block = block || {};
36
+ this.load = 'read';
37
+ if (block.load !== undefined) {
38
+ if (MODES.includes(block.load)) {
39
+ this.load = block.load;
40
+ } else {
41
+ warn(`snomed.load must be "read" or "mmap", not ${JSON.stringify(block.load)}; using "read"`);
42
+ }
43
+ }
44
+ this.lists = {};
45
+ for (const name of ['prewarm', 'read']) {
46
+ const v = block[name];
47
+ if (v === undefined) {
48
+ this.lists[name] = [];
49
+ } else if (Array.isArray(v) && v.every(e => typeof e === 'string' && e.trim())) {
50
+ this.lists[name] = v.map(e => e.trim());
51
+ } else {
52
+ warn(`snomed.${name} must be a list of cache file names or edition URIs; ignored`);
53
+ this.lists[name] = [];
54
+ }
55
+ }
56
+ for (const e of this.lists.prewarm) {
57
+ if (this.lists.read.includes(e)) {
58
+ warn(`snomed: ${e} is in both "prewarm" and "read"; it will be read`);
59
+ }
60
+ }
61
+ this.used = new Set();
62
+ }
63
+
64
+ /** True if any edition can end up mapped. */
65
+ mapsAnything() {
66
+ return this.load === 'mmap' || this.lists.prewarm.length > 0;
67
+ }
68
+
69
+ static #matches(entry, fileName, versionUri) {
70
+ if (entry === fileName) return true;
71
+ if (!versionUri) return false;
72
+ return entry === versionUri || versionUri.startsWith(entry.replace(/\/+$/, '') + '/version/');
73
+ }
74
+
75
+ #find(list, fileName, versionUri) {
76
+ const hit = this.lists[list].filter(e => SnomedLoadConfig.#matches(e, fileName, versionUri));
77
+ hit.forEach(e => this.used.add(e));
78
+ return hit.length > 0;
79
+ }
80
+
81
+ /**
82
+ * @param {string} filePath - the cache file
83
+ * @param {string|null} versionUri - its edition version URI, e.g. http://snomed.info/sct/900000000000207008/version/20250201
84
+ * @returns {'read'|'mmap'|'prewarm'}
85
+ */
86
+ modeFor(filePath, versionUri) {
87
+ const fileName = path.basename(filePath);
88
+ const read = this.#find('read', fileName, versionUri);
89
+ const prewarm = this.#find('prewarm', fileName, versionUri);
90
+ if (read) return 'read';
91
+ if (prewarm) return 'prewarm';
92
+ return this.load;
93
+ }
94
+
95
+ /** Warn about entries that matched no loaded edition. Call once every edition is loaded. */
96
+ checkUnused() {
97
+ for (const list of ['prewarm', 'read']) {
98
+ for (const e of this.lists[list]) {
99
+ if (!this.used.has(e)) {
100
+ this.warn(`snomed.${list}: "${e}" matches no SNOMED CT edition in the library`);
101
+ }
102
+ }
103
+ }
104
+ }
105
+ }
106
+
107
+ module.exports = { SnomedLoadConfig };
package/tx/sct/mmap.js ADDED
@@ -0,0 +1,109 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Read-only memory mapping of SNOMED CT cache files ("snomed": {"load": "mmap"} in config.json).
5
+ *
6
+ * Node has no mmap of its own, so this calls the C library's mmap() through koffi (an FFI
7
+ * module that ships prebuilt binaries for Linux glibc and musl, macOS and the BSDs, so
8
+ * nothing is compiled at install time). The mapping is never unmapped: a loaded edition
9
+ * stays loaded for the life of the process.
10
+ *
11
+ * mapFile() returns null where mapping is not available (Windows, koffi missing, mmap
12
+ * failing), and the caller then reads the file into memory as before. It says why through the
13
+ * warn function it is given (the server log), or console.warn where there is none.
14
+ */
15
+
16
+ const fs = require('fs');
17
+
18
+ const PROT_READ = 1;
19
+ const MAP_PRIVATE = 2; // same value on Linux, macOS and the BSDs
20
+ const MAP_FAILED = 0xFFFFFFFFFFFFFFFFn;
21
+
22
+ let libcMmap; // undefined: not tried yet; null: not available
23
+ let unavailable = null; // why libcMmap is null
24
+
25
+ function getMmap() {
26
+ if (libcMmap === undefined) {
27
+ libcMmap = null;
28
+ if (process.platform === 'win32' || !process.arch.endsWith('64')) {
29
+ unavailable = `not supported on ${process.platform}/${process.arch}`;
30
+ } else {
31
+ try {
32
+ const koffi = require('koffi');
33
+ const mmap = koffi.load(null).func('void *mmap(void *addr, size_t len, int prot, int flags, int fd, int64_t off)');
34
+ libcMmap = { koffi, mmap };
35
+ } catch (e) {
36
+ unavailable = e.message;
37
+ }
38
+ }
39
+ }
40
+ return libcMmap;
41
+ }
42
+
43
+ /** True if mapFile() can map files on this platform (it may still fail for a given file). */
44
+ function mappingAvailable() {
45
+ return getMmap() !== null;
46
+ }
47
+
48
+ /**
49
+ * @param {string} filePath
50
+ * @param {function(string)} [warn] - where to say why the file was not mapped
51
+ * @returns {Buffer|null} a read-only Buffer over the whole file, or null
52
+ */
53
+ function mapFile(filePath, warn = (msg) => console.warn(msg)) {
54
+ const lib = getMmap();
55
+ if (!lib) {
56
+ warn(`SNOMED CT: memory mapping is not available here (${unavailable}); reading ${filePath} into memory`);
57
+ return null;
58
+ }
59
+ const fd = fs.openSync(filePath, 'r');
60
+ try {
61
+ const size = fs.fstatSync(fd).size;
62
+ if (size === 0) return null;
63
+ const ptr = lib.mmap(null, size, PROT_READ, MAP_PRIVATE, fd, 0);
64
+ if (ptr === null || lib.koffi.address(ptr) === MAP_FAILED) {
65
+ warn(`SNOMED CT: could not map ${filePath} (errno ${lib.koffi.errno()}); reading it into memory`);
66
+ return null;
67
+ }
68
+ return Buffer.from(lib.koffi.view(ptr, size));
69
+ } finally {
70
+ fs.closeSync(fd); // the mapping keeps its own reference to the file
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Read a file through once, in the background, so its pages are in the page cache before
76
+ * requests need them (madvise(MADV_WILLNEED) is only a hint, and on Linux it read almost
77
+ * nothing of a mapped file). The reads go through one small buffer, so this costs no process
78
+ * memory; the pages land in the page cache that every mapping of the file shares.
79
+ *
80
+ * @param {string} filePath
81
+ * @returns {Promise<number>} milliseconds taken
82
+ */
83
+ async function prewarmFile(filePath) {
84
+ const start = Date.now();
85
+ const buf = Buffer.allocUnsafe(4 * 1024 * 1024);
86
+ const fh = await fs.promises.open(filePath, 'r');
87
+ try {
88
+ let pos = 0;
89
+ for (;;) {
90
+ const { bytesRead } = await fh.read(buf, 0, buf.length, pos);
91
+ if (bytesRead === 0) break;
92
+ pos += bytesRead;
93
+ }
94
+ } finally {
95
+ await fh.close();
96
+ }
97
+ return Date.now() - start;
98
+ }
99
+
100
+ /**
101
+ * For tests: replace the mmap binding ({koffi, mmap}, or null with a reason for "not
102
+ * available"), or with no arguments go back to the real one.
103
+ */
104
+ function _setMmapForTesting(lib, reason = 'disabled by a test') {
105
+ libcMmap = arguments.length === 0 ? undefined : lib;
106
+ unavailable = lib ? null : reason;
107
+ }
108
+
109
+ module.exports = { mapFile, prewarmFile, mappingAvailable, _setMmapForTesting };