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
@@ -1,9 +1,30 @@
1
1
  const fs = require('fs');
2
+ const { mapFile, prewarmFile } = require('./mmap');
3
+
4
+ // The ArrayBuffers of cache files loaded by SnomedFileReader. A structure uses its section of
5
+ // one in place (nothing writes to a loaded structure, and the file stays loaded for the life
6
+ // of the process); any other buffer is copied, so the structure owns its bytes.
7
+ const fileBuffers = new WeakSet();
8
+
9
+ function adopt(buffer) {
10
+ if (!buffer) return Buffer.alloc(0);
11
+ return fileBuffers.has(buffer.buffer) ? buffer : Buffer.from(buffer);
12
+ }
13
+
14
+ // A structure made from a buffer is a loaded one, and is read-only: it may be sharing the
15
+ // cache file's memory (see adopt). Only a structure the importer creates empty and builds can
16
+ // be written to. Every method that changes a structure calls this first.
17
+ function checkWritable(structure) {
18
+ if (structure.readOnly) {
19
+ throw new Error(`${structure.constructor.name} is read-only: it was loaded from a cache file, and can only be changed while importing`);
20
+ }
21
+ }
2
22
 
3
23
 
4
24
  class SnomedStrings {
5
25
  constructor(buffer = null) {
6
- this.master = buffer ? Buffer.from(buffer) : Buffer.alloc(0);
26
+ this.master = adopt(buffer);
27
+ this.readOnly = !!buffer;
7
28
  this.decoder = new TextDecoder('utf-8');
8
29
  this.builder = null;
9
30
  this.currentOffset = 0; // ADD: Track offset directly
@@ -28,11 +49,13 @@ class SnomedStrings {
28
49
  }
29
50
 
30
51
  startBuild() {
52
+ checkWritable(this);
31
53
  this.builder = [];
32
54
  this.currentOffset = 0; // RESET: offset when starting build
33
55
  }
34
56
 
35
57
  reopen() {
58
+ checkWritable(this);
36
59
  this.builder = [this.master];
37
60
  this.currentOffset = this.master.length; // SET: offset to current master length
38
61
  }
@@ -75,6 +98,7 @@ class SnomedStrings {
75
98
  }
76
99
 
77
100
  clear() {
101
+ checkWritable(this);
78
102
  this.master = Buffer.alloc(0);
79
103
  this.currentOffset = 0; // RESET: offset when clearing
80
104
  }
@@ -86,7 +110,8 @@ class SnomedStrings {
86
110
 
87
111
  class SnomedWords {
88
112
  constructor(buffer = null) {
89
- this.master = buffer ? Buffer.from(buffer) : Buffer.alloc(0);
113
+ this.master = adopt(buffer);
114
+ this.readOnly = !!buffer;
90
115
  this.builder = null;
91
116
  }
92
117
 
@@ -114,6 +139,7 @@ class SnomedWords {
114
139
  }
115
140
 
116
141
  startBuild() {
142
+ checkWritable(this);
117
143
  this.builder = [];
118
144
  }
119
145
 
@@ -140,6 +166,7 @@ class SnomedWords {
140
166
  }
141
167
 
142
168
  clear() {
169
+ checkWritable(this);
143
170
  this.master = Buffer.alloc(0);
144
171
  }
145
172
 
@@ -150,7 +177,8 @@ class SnomedWords {
150
177
 
151
178
  class SnomedStems {
152
179
  constructor(buffer = null) {
153
- this.master = buffer ? Buffer.from(buffer) : Buffer.alloc(0);
180
+ this.master = adopt(buffer);
181
+ this.readOnly = !!buffer;
154
182
  this.builder = null;
155
183
  }
156
184
 
@@ -178,6 +206,7 @@ class SnomedStems {
178
206
  }
179
207
 
180
208
  startBuild() {
209
+ checkWritable(this);
181
210
  this.builder = [];
182
211
  }
183
212
 
@@ -204,6 +233,7 @@ class SnomedStems {
204
233
  }
205
234
 
206
235
  clear() {
236
+ checkWritable(this);
207
237
  this.master = Buffer.alloc(0);
208
238
  }
209
239
 
@@ -214,7 +244,8 @@ class SnomedStems {
214
244
 
215
245
  class SnomedReferences {
216
246
  constructor(buffer = null) {
217
- this.master = buffer ? Buffer.from(buffer) : Buffer.alloc(0);
247
+ this.master = adopt(buffer);
248
+ this.readOnly = !!buffer;
218
249
  this.builder = null;
219
250
  this.currentOffset = 0; // ADD: Track offset directly
220
251
  this.MAGIC_NO_CHILDREN = 0xFFFFFFFF; // Assuming this constant - adjust as needed
@@ -263,6 +294,7 @@ class SnomedReferences {
263
294
  }
264
295
 
265
296
  startBuild() {
297
+ checkWritable(this);
266
298
  this.builder = [];
267
299
  this.currentOffset = 0; // RESET: offset when starting build
268
300
  }
@@ -316,6 +348,7 @@ class SnomedReferences {
316
348
  }
317
349
 
318
350
  clear() {
351
+ checkWritable(this);
319
352
  this.master = Buffer.alloc(0);
320
353
  this.currentOffset = 0; // RESET: offset when clearing
321
354
  }
@@ -342,7 +375,8 @@ class SnomedDescriptions {
342
375
  static FLAG_PendingMove = 11;
343
376
 
344
377
  constructor(buffer = null) {
345
- this.master = buffer ? Buffer.from(buffer) : Buffer.alloc(0);
378
+ this.master = adopt(buffer);
379
+ this.readOnly = !!buffer;
346
380
  this.builder = null;
347
381
  this.currentOffset = 0; // ADD: Track offset directly
348
382
  }
@@ -382,6 +416,7 @@ class SnomedDescriptions {
382
416
  }
383
417
 
384
418
  startBuild() {
419
+ checkWritable(this);
385
420
  this.builder = [];
386
421
  this.currentOffset = 0; // RESET: offset when starting build
387
422
  }
@@ -417,7 +452,44 @@ class SnomedDescriptions {
417
452
  return currentOffset;
418
453
  }
419
454
 
455
+ /**
456
+ * The concepts that have an active description in the language (the RF2 language code
457
+ * byte) whose term passes test, for descriptions [start, end) by position, leaving out
458
+ * descriptions of type skipKind (a concept index, or -1 for none). Reads the records in
459
+ * place rather than decoding each one, since a scan covers the whole edition. Adds the
460
+ * concept indexes to out (a Set); returns how many descriptions there are.
461
+ */
462
+ matchActiveTerms(start, end, lang, strings, test, out, skipKind = -1) {
463
+ const size = SnomedDescriptions.DESC_SIZE;
464
+ const count = Math.floor(this.master.length / size);
465
+ const last = Math.min(end, count);
466
+ for (let i = start; i < last; i++) {
467
+ const offset = i * size;
468
+ if (this.master.readUInt8(offset + 4) === 0 || this.master.readUInt8(offset + 31) !== lang
469
+ || this.master.readUInt32LE(offset + 21) === skipKind) {
470
+ continue;
471
+ }
472
+ if (test(strings.getEntry(this.master.readUInt32LE(offset)))) {
473
+ out.add(this.master.readUInt32LE(offset + 13));
474
+ }
475
+ }
476
+ return count;
477
+ }
478
+
479
+ /** The language code bytes that active descriptions use */
480
+ activeLanguages() {
481
+ const size = SnomedDescriptions.DESC_SIZE;
482
+ const langs = new Set();
483
+ for (let offset = 0; offset + size <= this.master.length; offset += size) {
484
+ if (this.master.readUInt8(offset + 4) !== 0) {
485
+ langs.add(this.master.readUInt8(offset + 31));
486
+ }
487
+ }
488
+ return langs;
489
+ }
490
+
420
491
  setRefsets(index, refsets, valueses) {
492
+ checkWritable(this);
421
493
  if (index >= this.master.length) {
422
494
  throw new Error('Wrong length index getting snomed Desc Details');
423
495
  }
@@ -441,6 +513,7 @@ class SnomedDescriptions {
441
513
  }
442
514
 
443
515
  clear() {
516
+ checkWritable(this);
444
517
  this.master = Buffer.alloc(0);
445
518
  this.currentOffset = 0; // RESET: offset when clearing
446
519
  }
@@ -452,7 +525,8 @@ class SnomedDescriptions {
452
525
 
453
526
  class SnomedDescriptionIndex {
454
527
  constructor(buffer = null) {
455
- this.master = buffer ? Buffer.from(buffer) : Buffer.alloc(0);
528
+ this.master = adopt(buffer);
529
+ this.readOnly = !!buffer;
456
530
  this.builder = null;
457
531
  }
458
532
 
@@ -488,6 +562,7 @@ class SnomedDescriptionIndex {
488
562
  }
489
563
 
490
564
  startBuild() {
565
+ checkWritable(this);
491
566
  this.builder = [];
492
567
  }
493
568
 
@@ -517,6 +592,7 @@ class SnomedDescriptionIndex {
517
592
  }
518
593
 
519
594
  clear() {
595
+ checkWritable(this);
520
596
  this.master = Buffer.alloc(0);
521
597
  }
522
598
 
@@ -537,7 +613,8 @@ class SnomedConceptList {
537
613
  static MAGIC_NO_CHILDREN = 0xFFFFFFFF;
538
614
 
539
615
  constructor(buffer = null) {
540
- this.master = buffer ? Buffer.from(buffer) : Buffer.alloc(0);
616
+ this.master = adopt(buffer);
617
+ this.readOnly = !!buffer;
541
618
  this.builder = null;
542
619
  this.currentOffset = 0; // ADD: Track offset directly
543
620
  }
@@ -669,6 +746,7 @@ class SnomedConceptList {
669
746
  }
670
747
 
671
748
  setNormalFormDuringBuild(index, value) {
749
+ checkWritable(this);
672
750
  // Special version that can be called during building phase
673
751
  // This bypasses the post-build access check for normal forms specifically
674
752
  if (index >= this.master.length) {
@@ -709,67 +787,80 @@ class SnomedConceptList {
709
787
 
710
788
  // All the setter methods - these require doneBuild() to have been called
711
789
  setParents(index, active, inactive) {
790
+ checkWritable(this);
712
791
  this.#checkPostBuildAccess(index);
713
792
  this.master.writeUInt32LE(active, index + 9);
714
793
  this.master.writeUInt32LE(inactive, index + 52);
715
794
  }
716
795
 
717
796
  setDescriptions(index, value) {
797
+ checkWritable(this);
718
798
  this.#checkPostBuildAccess(index);
719
799
  this.master.writeUInt32LE(value, index + 13);
720
800
  }
721
801
 
722
802
  setModuleId(index, moduleIndex) {
803
+ checkWritable(this);
723
804
  this.#checkPostBuildAccess(index);
724
805
  this.master.writeUInt32LE(moduleIndex, index + 36);
725
806
  }
726
807
 
727
808
  setInbounds(index, value) {
809
+ checkWritable(this);
728
810
  this.#checkPostBuildAccess(index);
729
811
  this.master.writeUInt32LE(value, index + 17);
730
812
  }
731
813
 
732
814
  setOutbounds(index, value) {
815
+ checkWritable(this);
733
816
  this.#checkPostBuildAccess(index);
734
817
  this.master.writeUInt32LE(value, index + 21);
735
818
  }
736
819
 
737
820
  setAllDesc(index, value) {
821
+ checkWritable(this);
738
822
  this.#checkPostBuildAccess(index);
739
823
  this.master.writeUInt32LE(value, index + 25);
740
824
  }
741
825
 
742
826
  setDepth(index, value) {
827
+ checkWritable(this);
743
828
  this.#checkPostBuildAccess(index);
744
829
  this.master.writeUInt8(value, index + 29);
745
830
  }
746
831
 
747
832
  setStems(index, value) {
833
+ checkWritable(this);
748
834
  this.#checkPostBuildAccess(index);
749
835
  this.master.writeUInt32LE(value, index + 30);
750
836
  }
751
837
 
752
838
  setDate(index, effectiveTime) {
839
+ checkWritable(this);
753
840
  this.#checkPostBuildAccess(index);
754
841
  this.master.writeUInt16LE(effectiveTime, index + 34);
755
842
  }
756
843
 
757
844
  setStatus(index, value) {
845
+ checkWritable(this);
758
846
  this.#checkPostBuildAccess(index);
759
847
  this.master.writeUInt32LE(value, index + 40);
760
848
  }
761
849
 
762
850
  setRefsets(index, value) {
851
+ checkWritable(this);
763
852
  this.#checkPostBuildAccess(index);
764
853
  this.master.writeUInt32LE(value, index + 44);
765
854
  }
766
855
 
767
856
  setNormalForm(index, value) {
857
+ checkWritable(this);
768
858
  this.#checkPostBuildAccess(index);
769
859
  this.master.writeUInt32LE(value, index + 48);
770
860
  }
771
861
 
772
862
  setFlag(index, flags) {
863
+ checkWritable(this);
773
864
  this.#checkPostBuildAccess(index);
774
865
  this.master.writeUInt8(flags, index + 8);
775
866
  }
@@ -780,6 +871,7 @@ class SnomedConceptList {
780
871
 
781
872
  // Build methods
782
873
  startBuild() {
874
+ checkWritable(this);
783
875
  this.builder = [];
784
876
  this.currentOffset = 0; // RESET: offset when starting build
785
877
  }
@@ -831,6 +923,7 @@ class SnomedConceptList {
831
923
  }
832
924
 
833
925
  clear() {
926
+ checkWritable(this);
834
927
  this.master = Buffer.alloc(0);
835
928
  this.currentOffset = 0; // RESET: offset when clearing
836
929
  }
@@ -846,7 +939,8 @@ class SnomedRelationshipList {
846
939
  static REFSET_SIZE_LANG = 32;
847
940
 
848
941
  constructor(buffer = null) {
849
- this.master = buffer ? Buffer.from(buffer) : Buffer.alloc(0);
942
+ this.master = adopt(buffer);
943
+ this.readOnly = !!buffer;
850
944
  this.builder = null;
851
945
  this.currentOffset = 0; // ADD: Track offset directly
852
946
  }
@@ -871,12 +965,21 @@ class SnomedRelationshipList {
871
965
  };
872
966
  }
873
967
 
968
+ // Whether the relationship row at index is active, without decoding the whole row
969
+ isActive(index) {
970
+ if (index >= this.master.length) {
971
+ throw new Error('Wrong length index getting snomed relationship Details');
972
+ }
973
+ return this.master.readUInt8(index + 26) !== 0;
974
+ }
975
+
874
976
  count() {
875
977
  return Math.floor(this.master.length / SnomedRelationshipList.RELATIONSHIP_SIZE);
876
978
  }
877
979
 
878
980
  // Build methods
879
981
  startBuild() {
982
+ checkWritable(this);
880
983
  this.builder = [];
881
984
  this.currentOffset = 0; // RESET: offset when starting build
882
985
  }
@@ -924,6 +1027,7 @@ class SnomedRelationshipList {
924
1027
  }
925
1028
 
926
1029
  clear() {
1030
+ checkWritable(this);
927
1031
  this.master = Buffer.alloc(0);
928
1032
  this.currentOffset = 0; // RESET: offset when clearing
929
1033
  }
@@ -937,7 +1041,8 @@ class SnomedReferenceSetMembers {
937
1041
  static MAGIC_NO_CHILDREN = 0xFFFFFFFF;
938
1042
 
939
1043
  constructor(buffer = null) {
940
- this.master = buffer ? Buffer.from(buffer) : Buffer.alloc(0);
1044
+ this.master = adopt(buffer);
1045
+ this.readOnly = !!buffer;
941
1046
  this.builder = null;
942
1047
  this.currentOffset = 0; // ADD: Track offset directly
943
1048
  }
@@ -1007,6 +1112,7 @@ class SnomedReferenceSetMembers {
1007
1112
  }
1008
1113
 
1009
1114
  startBuild() {
1115
+ checkWritable(this);
1010
1116
  this.builder = [];
1011
1117
  this.currentOffset = 0; // RESET: offset when starting build
1012
1118
  }
@@ -1101,6 +1207,7 @@ class SnomedReferenceSetMembers {
1101
1207
  }
1102
1208
 
1103
1209
  clear() {
1210
+ checkWritable(this);
1104
1211
  this.master = Buffer.alloc(0);
1105
1212
  this.currentOffset = 0; // RESET: offset when clearing
1106
1213
  }
@@ -1115,7 +1222,8 @@ class SnomedReferenceSetIndex {
1115
1222
  static REFSET_SIZE_NOLANG = 28;
1116
1223
 
1117
1224
  constructor(buffer = null, hasLangs = true) {
1118
- this.master = buffer ? Buffer.from(buffer) : Buffer.alloc(0);
1225
+ this.master = adopt(buffer);
1226
+ this.readOnly = !!buffer;
1119
1227
  this.builder = null;
1120
1228
  this.hasLangs = hasLangs;
1121
1229
  this.recordSize = hasLangs ? SnomedReferenceSetIndex.REFSET_SIZE_LANG : SnomedReferenceSetIndex.REFSET_SIZE_NOLANG;
@@ -1186,6 +1294,7 @@ class SnomedReferenceSetIndex {
1186
1294
 
1187
1295
  // Build methods
1188
1296
  startBuild() {
1297
+ checkWritable(this);
1189
1298
  this.builder = [];
1190
1299
  }
1191
1300
 
@@ -1222,6 +1331,7 @@ class SnomedReferenceSetIndex {
1222
1331
  }
1223
1332
 
1224
1333
  clear() {
1334
+ checkWritable(this);
1225
1335
  this.master = Buffer.alloc(0);
1226
1336
  }
1227
1337
 
@@ -1232,15 +1342,58 @@ class SnomedReferenceSetIndex {
1232
1342
 
1233
1343
 
1234
1344
  class SnomedFileReader {
1235
- constructor(filePath) {
1345
+ /**
1346
+ * @param {string} filePath
1347
+ * @param {string} [loadMode] 'read' (default): read the file into memory. 'mmap': map it
1348
+ * read-only, so its pages are file-backed and shared with the page cache. 'prewarm': mapped,
1349
+ * then read through once in the background at startup, so its pages are already in the
1350
+ * page cache when requests arrive; startup doesn't wait for it. Where mapping is not
1351
+ * available (see mmap.js) the file is read, and `mapped` stays false.
1352
+ * @param {object} [log] - a logger with warn(); says why a file that was to be mapped was read
1353
+ */
1354
+ constructor(filePath, loadMode = 'read', log = null) {
1236
1355
  this.filePath = filePath;
1356
+ this.loadMode = loadMode;
1357
+ this.log = log;
1237
1358
  this.buffer = null;
1359
+ this.mapped = false; // true once load() has the buffer as a memory mapping of the file
1238
1360
  this.offset = 0;
1239
1361
  }
1240
1362
 
1363
+ /**
1364
+ * The edition version URI in a cache file's header, without loading the file.
1365
+ * @param {string} filePath
1366
+ * @returns {string|null}
1367
+ */
1368
+ static versionUri(filePath) {
1369
+ const head = Buffer.alloc(1024);
1370
+ const fd = fs.openSync(filePath, 'r');
1371
+ let n;
1372
+ try {
1373
+ n = fs.readSync(fd, head, 0, head.length, 0);
1374
+ } finally {
1375
+ fs.closeSync(fd);
1376
+ }
1377
+ const reader = new SnomedFileReader(filePath);
1378
+ reader.buffer = head.subarray(0, n);
1379
+ try {
1380
+ reader.readString(); // cache version
1381
+ return reader.readString();
1382
+ } catch {
1383
+ return null;
1384
+ }
1385
+ }
1386
+
1241
1387
  async load() {
1242
- // Read entire file into buffer
1243
- this.buffer = await fs.promises.readFile(this.filePath);
1388
+ const warn = this.log ? (msg) => this.log.warn(msg) : undefined;
1389
+ const mapped = this.loadMode !== 'read' ? mapFile(this.filePath, warn) : null;
1390
+ this.mapped = mapped !== null;
1391
+ this.buffer = mapped || await fs.promises.readFile(this.filePath);
1392
+ fileBuffers.add(this.buffer.buffer);
1393
+ if (mapped && this.loadMode === 'prewarm') {
1394
+ // not awaited: the server starts while the file is read into the page cache
1395
+ this.prewarmed = prewarmFile(this.filePath).catch(() => null);
1396
+ }
1244
1397
  this.offset = 0;
1245
1398
  }
1246
1399
 
@@ -89,9 +89,10 @@ class SnomedTextIndex {
89
89
  * Build the index for a loaded edition. Yields to the event loop every
90
90
  * BUILD_SLICE ms of work.
91
91
  * @param {object} sct - SnomedServices
92
+ * @param {number|null} lang - only descriptions in this language (byte), or null for all
92
93
  * @returns {Promise<SnomedTextIndex>}
93
94
  */
94
- static async build(sct) {
95
+ static async build(sct, lang = null) {
95
96
  const concepts = sct.concepts;
96
97
  const conceptSize = concepts.constructor.CONCEPT_SIZE;
97
98
  const conceptCount = concepts.count();
@@ -117,6 +118,9 @@ class SnomedTextIndex {
117
118
  if (descMaster.readUInt8(descIndex + 4) === 0) {
118
119
  continue; // inactive description
119
120
  }
121
+ if (lang !== null && descMaster.readUInt8(descIndex + 31) !== lang) {
122
+ continue; // another language
123
+ }
120
124
  const term = sct.strings.getEntry(descMaster.readUInt32LE(descIndex));
121
125
  for (const word of tokenize(term)) {
122
126
  if (word.length < MIN_WORD_LENGTH) {
@@ -202,11 +202,11 @@ function renderHtml(params) {
202
202
  h.push('<tr><th>Title</th><th>Type</th><th>Version</th><th>Added</th><th>Download</th></tr>');
203
203
  for (const e of [...sel.entries].reverse()) {
204
204
  h.push('<tr>');
205
- h.push(`<td>${esc(e.title)}${e.summary ? `<br/><span style="color: grey">${esc(e.summary)}</span>` : ''}${e.rights ? `<br/><i>${esc(e.rights)}</i>` : ''}</td>`);
205
+ h.push(`<td>${esc(e.title)}${e.summary ? `<br/><span style="background-color: #eeeeee">${esc(e.summary)}</span>` : ''}${e.rights ? `<br/><i>${esc(e.rights)}</i>` : ''}</td>`);
206
206
  h.push(`<td>${esc(e.category.term)}${isFhirCategory(e) ? ` (R${esc((e.fhirVersion || fhirVersion).split('.')[0])})` : ''}</td>`);
207
207
  h.push(`<td><code>${esc(e.version)}</code></td>`);
208
208
  h.push(`<td>${esc(e.inducted.substring(0, 10))}</td>`);
209
- h.push(`<td><a href="${esc(linkHref(e, params.base))}">${esc(e.link.length.toLocaleString())} bytes</a><br/><span style="font-size: 0.8em; color: grey">sha256: ${esc(e.link.sha256)}</span></td>`);
209
+ h.push(`<td><a href="${esc(linkHref(e, params.base))}">${esc(e.link.length.toLocaleString())} bytes</a><br/><span style="font-size: 0.8em; background-color: #eeeeee">sha256: ${esc(e.link.sha256)}</span></td>`);
210
210
  h.push('</tr>');
211
211
  }
212
212
  h.push('</table>');
package/tx/tx-html.js CHANGED
@@ -21,7 +21,7 @@ const {ParametersXML} = require("./xml/parameters-xml");
21
21
  const {OperationOutcomeXML} = require("./xml/operationoutcome-xml");
22
22
  const {debugLog} = require("./operation-context");
23
23
  const {InvalidError} = require("./library/errors");
24
- const {VALID_FILTER_OPS} = require("./library/renderer");
24
+ const {VALID_FILTER_OPS, MAX_RENDERED_INCLUDE_CODES} = require("./library/renderer");
25
25
  const txOpenApi = require("./openapi");
26
26
 
27
27
  const txHtmlLog = Logger.getInstance().child({ module: 'tx-html' });
@@ -61,6 +61,12 @@ const ELEMENT_OPTIONS = ['id', 'url', 'version', 'name', 'title', 'status', 'dat
61
61
  // something, not about terminology, so it does not belong in the FHIR tools namespace.
62
62
  const SNOMED_URI = 'http://snomed.info/sct';
63
63
 
64
+ // A value set page lists the first MAX_RENDERED_INCLUDE_CODES codes of each include;
65
+ // ?_showAll=true lists them all. Underscored, as the other parameters that control how a
66
+ // result is presented are (_format, _summary, _elements), which also keeps it apart from
67
+ // the $expand parameters: the page's query string is passed to the expansion it shows.
68
+ const SHOW_ALL_PARAM = '_showAll';
69
+
64
70
  const TRANSIENT_VALUESET = 'http://healthintersections.com.au/fhirsmith/StructureDefinition/valueset-transient';
65
71
 
66
72
  const SEVERITY_ALERT_CLASS = {
@@ -223,8 +229,8 @@ const YAML_STYLE = '<style>' +
223
229
  'font-family: Menlo, Consolas, monospace; font-size: 90%; line-height: 1.45; }' +
224
230
  '.yaml-source .y-k { color: #0b5394; font-weight: bold; }' +
225
231
  '.yaml-source .y-v { color: #444; }' +
226
- '.yaml-source .y-p, .yaml-source .y-d { color: #999; }' +
227
- '.yaml-source .y-c { color: #777; font-style: italic; }' +
232
+ '.yaml-source .y-p, .yaml-source .y-d { color: #444; }' +
233
+ '.yaml-source .y-c { color: #444; font-style: italic; }' +
228
234
  '</style>';
229
235
 
230
236
  /**
@@ -428,7 +434,7 @@ class TxHtmlRenderer {
428
434
  html += '</h3>';
429
435
 
430
436
  // List Packages
431
- html += '<h6>FHIR Packages</h6>';
437
+ html += '<h4>FHIR Packages</h4>';
432
438
  if (provider.packageSources && provider.packageSources.length > 0) {
433
439
  const sorted = [...provider.packageSources].sort();
434
440
  html += '<ul>';
@@ -441,7 +447,7 @@ class TxHtmlRenderer {
441
447
  }
442
448
 
443
449
  // List Packages
444
- html += '<h6>External Sources</h6>';
450
+ html += '<h4>External Sources</h4>';
445
451
  if (provider.externalSources && provider.externalSources.length > 0) {
446
452
  const sorted = [...provider.externalSources].sort();
447
453
  html += '<ul>';
@@ -462,7 +468,7 @@ class TxHtmlRenderer {
462
468
  html += '<p><em>No External Sources Configured</em></p>';
463
469
  }
464
470
 
465
- html += '<h6 class="mt-4">Special CodeSystems</h6>';
471
+ html += '<h5 class="mt-4">Special CodeSystems</h5>';
466
472
  html += '<table class="grid">';
467
473
  html += '<thead><tr><th>Name</th><th>URI</th><th>Version</th><th>Use Count</th></tr></thead>';
468
474
  html += '<tbody>';
@@ -592,6 +598,7 @@ class TxHtmlRenderer {
592
598
  case 'CodeSystem':
593
599
  return await this.renderCodeSystem(json, inBundle, _fmt, op, req.sourcePackage);
594
600
  case 'ValueSet': {
601
+ this.limitIncludeCodes(json, req, inBundle);
595
602
  let exp = undefined;
596
603
  if (!inBundle && !op && (!_fmt || _fmt == 'html')) {
597
604
  try {
@@ -626,6 +633,35 @@ class TxHtmlRenderer {
626
633
  }
627
634
  }
628
635
 
636
+ /**
637
+ * How many codes of each include the value set's page lists, and where its "Show all"
638
+ * link goes: this page with ?_showAll=true, or, for a value set shown in a bundle, the
639
+ * value set's own page. A page that came from a POST has no URL to ask again with, so
640
+ * it says how many codes there are but has no link.
641
+ *
642
+ * @param {Object} json the value set
643
+ * @param {express.Request|null} req
644
+ * @param {boolean} inBundle
645
+ */
646
+ limitIncludeCodes(json, req, inBundle) {
647
+ if (!inBundle && req?.query?.[SHOW_ALL_PARAM] === 'true') {
648
+ this.renderer.includeCodeLimit = null;
649
+ this.renderer.showAllCodesHref = null;
650
+ return;
651
+ }
652
+ this.renderer.includeCodeLimit = MAX_RENDERED_INCLUDE_CODES;
653
+ if (inBundle) {
654
+ this.renderer.showAllCodesHref = json.id
655
+ ? `${this.path}/ValueSet/${encodeURIComponent(json.id)}?_format=html&${SHOW_ALL_PARAM}=true` : null;
656
+ } else if (req?.method === 'GET' && req.originalUrl) {
657
+ const url = new URL(req.originalUrl, 'http://localhost');
658
+ url.searchParams.set(SHOW_ALL_PARAM, 'true');
659
+ this.renderer.showAllCodesHref = url.pathname + url.search;
660
+ } else {
661
+ this.renderer.showAllCodesHref = null;
662
+ }
663
+ }
664
+
629
665
  /**
630
666
  * Render Parameters resource
631
667
  */
@@ -1686,7 +1722,7 @@ class TxHtmlRenderer {
1686
1722
 
1687
1723
  if (entry.resource) {
1688
1724
  const resource = entry.resource;
1689
- html += `<h4>${escape(resource.resourceType)}/${escape(resource.id || 'unknown')}</h4>`;
1725
+ html += `<h3>${escape(resource.resourceType)}/${escape(resource.id || 'unknown')}</h3>`;
1690
1726
 
1691
1727
  if (entry.fullUrl) {
1692
1728
  html += `<p><small><a href="${escape(entry.fullUrl)}">${escape(entry.fullUrl)}</a></small></p>`;
@@ -1746,7 +1782,7 @@ class TxHtmlRenderer {
1746
1782
 
1747
1783
  // Links
1748
1784
  if (json.link && json.link.length > 0) {
1749
- html += '<h4>Links</h4>';
1785
+ html += '<h3>Links</h3>';
1750
1786
  html += '<ul>';
1751
1787
  for (const link of json.link) {
1752
1788
  html += `<li><strong>${escape(link.relation)}:</strong> <a href="${escape(link.url)}">${escape(link.url)}</a></li>`;
@@ -20,19 +20,19 @@ sources:
20
20
  - internal:hgvs
21
21
  - ucum:tx/data/ucum-essence.xml
22
22
  - loinc:loinc-2.77-a.db
23
- - loinc!:Loinc_2.83.db
24
- - rxnorm:rxnorm_03022026.db
23
+ - loinc!:Loinc_2.83c.db
24
+ - rxnorm:rxnorm_20261005.cache
25
25
  - ndc:ndc-20211101.db
26
26
  - unii:unii_20240622.db
27
- - snomed:sct_intl_20240201.cache
28
- - snomed!:sct_intl_20250201.cache
29
- - snomed:sct_ips_20241216.cache
30
- - snomed:sct_nl_20240930.cache
27
+ - snomed:sct_intl_20250201.cache
28
+ - snomed!:Snomed_Intl_20260701-a.cache
29
+ - snomed:snomed-nl-20260930-a.cache
31
30
  - snomed:sct_uk_20230412.cache
32
31
  - snomed:sct_us_20230301.cache
33
32
  - snomed:sct_us_20240301.cache
34
- - snomed:sct_us_20250901.cache
35
- - snomed:sct_test_20250909e.cache
33
+ - snomed:sct_us_20250901-c.cache
34
+ - snomed:Snomed_loinc_20261021-a.cache
35
+ - snomed:sct_test_20250909i.cache
36
36
  - cpt:CodeSystem-cpt.db|cpt-2023-fragment-0.1.db
37
37
  - omop:omop_v20250227.db
38
38
  - npm:hl7.terminology=tho