fhirsmith 0.9.5 → 0.9.7

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 (43) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/config-template.json +2 -1
  3. package/library/folder-content-loader.js +91 -0
  4. package/library/regex-utilities.js +49 -12
  5. package/npmprojector/npmprojector.js +2 -6
  6. package/package.json +2 -2
  7. package/publisher/publisher.js +105 -12
  8. package/registry/registry.js +6 -6
  9. package/server.js +6 -2
  10. package/test-scripts/repro-re2-wasm-leak.js +8 -7
  11. package/translations/Messages.properties +1 -1
  12. package/tx/cs/cs-cs.js +8 -0
  13. package/tx/cs/cs-loinc.js +13 -12
  14. package/tx/cs/cs-omop.js +24 -23
  15. package/tx/cs/cs-provider-list.js +2 -1
  16. package/tx/cs/cs-snomed.js +142 -59
  17. package/tx/cs/cs-unii.js +11 -11
  18. package/tx/data/snomed-testing.cache +0 -0
  19. package/tx/library/canonical-resource.js +4 -2
  20. package/tx/library/designations.js +27 -20
  21. package/tx/library/renderer.js +303 -22
  22. package/tx/library/ucum-types.js +4 -1
  23. package/tx/library.js +65 -21
  24. package/tx/operation-context.js +52 -23
  25. package/tx/params.js +36 -8
  26. package/tx/problems.js +0 -4
  27. package/tx/provider.js +7 -3
  28. package/tx/tx-html.js +7 -0
  29. package/tx/tx.js +24 -13
  30. package/tx/vs/vs-vsac.js +157 -9
  31. package/tx/workers/expand.js +100 -96
  32. package/tx/workers/lookup.js +6 -0
  33. package/tx/workers/read.js +1 -1
  34. package/tx/workers/translate.js +21 -29
  35. package/tx/workers/validate.js +18 -10
  36. package/tx/workers/worker.js +5 -1
  37. package/tx/xversion/xv-bundle.js +1 -2
  38. package/tx/xversion/xv-codesystem.js +5 -2
  39. package/tx/xversion/xv-parameters.js +4 -4
  40. package/tx/xversion/xv-resource.js +2 -2
  41. package/tx/xversion/xv-terminologyCapabilities.js +11 -6
  42. package/tx/xversion/xv-valueset.js +7 -7
  43. package/publisher/task-draft.js +0 -458
package/CHANGELOG.md CHANGED
@@ -6,11 +6,49 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
 
9
+ ## [v0.9.7] - 2026-06-12
10
+
11
+ ### Added
12
+
13
+ - Support for tx modele to load resources directly
14
+ - Missing overwork protection processing ECL
15
+
16
+ ### Fixed
17
+
18
+ - Fix bug validating secondary displays across languages
19
+ - Fix CORS issue (double headers)
20
+ - Fix czech code in snomed import
21
+ - Many minor validation fixes
22
+ - ECL processing bugs
23
+ - Publishing: better logging, and fix timeout & restart error
24
+
25
+ ### Tx Conformance Statement
26
+
27
+ FHIRsmith passed all 2503 HL7 terminology service tests (modes tx.fhir.org+omop+general+snomed, tests v1.9.1, runner v6.9.10)
28
+
29
+ ## [v0.9.6] - 2026-05-21
30
+
31
+ ### Added
32
+
33
+ - increase timeout when publishing IGs (for US Core)
34
+
35
+ ### Fixed
36
+
37
+ - Replace re2-wasm with re2js in library/regex-utilities.js to eliminate the underlying WASM-heap leak
38
+ - Fix async problem loading OMOP
39
+ - Fix memory leaks
40
+ - Don't leak database connections
41
+
42
+ ### Tx Conformance Statement
43
+
44
+ FHIRsmith passed all 1649 HL7 terminology service tests (modes tx.fhir.org+omop+general+snomed, tests v1.9.1, runner v6.9.7)
45
+
9
46
  ## [v0.9.5] - 2026-05-16
10
47
 
11
48
  ### Fixed
12
49
 
13
50
  - Workaround for memory leak in re2-wasm library that reduces it's severity
51
+ - Replace re2-wasm with re2js in library/regex-utilities.js to eliminate the underlying WASM-heap leak
14
52
 
15
53
  ### Tx Conformance Statement
16
54
 
@@ -63,7 +63,8 @@
63
63
  "enabled": true,
64
64
  "database": "/absolute/path/to/publisher.db",
65
65
  "sessionSecret": "change-this-to-a-secure-random-string",
66
- "workspaceRoot": "/absolute/path/to/workspaces"
66
+ "workspaceRoot": "/absolute/path/to/workspaces",
67
+ "igPublisherTimeoutMinutes": 60
67
68
  },
68
69
  "npmprojector": {
69
70
  "enabled": true,
@@ -0,0 +1,91 @@
1
+ const fs = require('fs').promises;
2
+ const path = require('path');
3
+ const { PackageContentLoader } = require('./package-manager');
4
+
5
+ /**
6
+ * A content loader that scans a folder for JSON FHIR terminology resources
7
+ * (CodeSystem, ValueSet, ConceptMap). It synthesizes the package layout that
8
+ * PackageContentLoader expects so the existing PackageValueSetProvider /
9
+ * PackageConceptMapProvider machinery can be reused without modification.
10
+ *
11
+ * The user's source folder is treated as read-only. Any SQLite caches built
12
+ * by the Package*Provider classes are written to the cacheFolder passed in,
13
+ * not to the source folder.
14
+ */
15
+ class FolderContentLoader extends PackageContentLoader {
16
+ /**
17
+ * @param {string} sourceFolder - Folder to scan for JSON resources
18
+ * @param {string} cacheFolder - Folder where Package*Provider DBs may live
19
+ */
20
+ constructor(sourceFolder, cacheFolder) {
21
+ super(cacheFolder);
22
+ this.sourceFolder = sourceFolder;
23
+ // Resources are read directly from the source folder rather than from
24
+ // <packageFolder>/package as PackageContentLoader assumes.
25
+ this.packageSubfolder = sourceFolder;
26
+ }
27
+
28
+ /**
29
+ * Replaces the standard initialize: there is no package.json or .index.json
30
+ * to read, so we synthesize an index from the JSON files in the folder.
31
+ */
32
+ async initialize() {
33
+ if (this.loaded) {
34
+ return;
35
+ }
36
+
37
+ try {
38
+ const stat = await fs.stat(this.sourceFolder);
39
+ if (!stat.isDirectory()) {
40
+ throw new Error(`Folder source path is not a directory: ${this.sourceFolder}`);
41
+ }
42
+ } catch (err) {
43
+ if (err.code === 'ENOENT') {
44
+ throw new Error(`Folder source path does not exist: ${this.sourceFolder}`);
45
+ }
46
+ throw err;
47
+ }
48
+
49
+ // Synthesize package metadata so id()/version()/pid() return something usable.
50
+ this.package = {
51
+ name: 'folder.' + path.basename(this.sourceFolder),
52
+ version: '0.0.0',
53
+ fhirVersions: ['5.0.0']
54
+ };
55
+
56
+ const files = [];
57
+ const entries = await fs.readdir(this.sourceFolder, { withFileTypes: true });
58
+ for (const entry of entries) {
59
+ if (!entry.isFile()) continue;
60
+ if (!entry.name.toLowerCase().endsWith('.json')) continue;
61
+
62
+ const fullPath = path.join(this.sourceFolder, entry.name);
63
+ let json;
64
+ try {
65
+ const content = await fs.readFile(fullPath, 'utf8');
66
+ json = JSON.parse(content);
67
+ } catch {
68
+ // Skip unreadable / non-JSON files rather than failing the whole load.
69
+ continue;
70
+ }
71
+
72
+ if (!json || typeof json !== 'object') continue;
73
+ const rt = json.resourceType;
74
+ if (rt !== 'CodeSystem' && rt !== 'ValueSet' && rt !== 'ConceptMap') continue;
75
+
76
+ files.push({
77
+ filename: entry.name,
78
+ resourceType: rt,
79
+ id: json.id,
80
+ url: json.url,
81
+ version: json.version
82
+ });
83
+ }
84
+
85
+ this.index = { files, 'index-version': 2 };
86
+ this.buildIndexes();
87
+ this.loaded = true;
88
+ }
89
+ }
90
+
91
+ module.exports = { FolderContentLoader };
@@ -1,27 +1,64 @@
1
- const { RE2 } = require('re2-wasm');
1
+ const { RE2JS } = require('re2js');
2
+
3
+ // Translate a JS-style flags string ("i", "im", "is", etc.) into the bit-flag
4
+ // integer that RE2JS.compile() expects. Only the flags actually used by callers
5
+ // (and their natural counterparts) are mapped; anything else is ignored.
6
+ function toRE2JSFlags(flags) {
7
+ if (!flags) return 0;
8
+ let bits = 0;
9
+ if (flags.includes('i')) bits |= RE2JS.CASE_INSENSITIVE;
10
+ if (flags.includes('m')) bits |= RE2JS.MULTILINE;
11
+ if (flags.includes('s')) bits |= RE2JS.DOTALL;
12
+ // The 'u' flag was a re2-wasm workaround; re2js is pure JS and handles
13
+ // unicode natively, so it's a no-op here.
14
+ return bits;
15
+ }
16
+
17
+ // Thin wrapper that exposes the only method the rest of the codebase uses
18
+ // against compiled regexes: a JS-RegExp-style `.test(input)` that returns
19
+ // true if the pattern is found anywhere in `input`.
20
+ class CompiledRegex {
21
+ constructor(pattern, flags) {
22
+ this._pattern = RE2JS.compile(pattern, toRE2JSFlags(flags));
23
+ }
24
+
25
+ test(input) {
26
+ if (input == null) return false;
27
+ return this._pattern.matcher(String(input)).find();
28
+ }
29
+ }
2
30
 
3
31
  class RegExUtilities {
4
32
 
33
+ // re2js doesn't have re2-wasm's WASM-heap leak, so the cache here is
34
+ // purely a performance optimisation. We cap it with a simple LRU so a
35
+ // workload with many distinct regex patterns can't grow it without bound.
36
+ static MAX_CACHE_SIZE = 1000;
37
+
5
38
  constructor() {
6
39
  this._cache = new Map();
7
40
  }
8
41
 
9
42
  compile(pattern, flags) {
10
- // re2-wasm has a fixed 16 MB WASM heap with no real free(): every
11
- // new RE2(...) permanently consumes a few KB. Cache by (pattern, flags)
12
- // so the same regex compiles at most once.
13
- // TODO: replace re2-wasm with native re2 to eliminate the underlying leak.
14
- const re2Flags = flags && flags.includes('u') ? flags : (flags || '') + 'u';
15
- const key = pattern + '|' + re2Flags;
16
- let compiled = this._cache.get(key);
17
- if (!compiled) {
18
- compiled = new RE2(pattern, re2Flags);
19
- this._cache.set(key, compiled);
43
+ const key = pattern + '|' + (flags || '');
44
+ const cached = this._cache.get(key);
45
+ if (cached) {
46
+ // Move to most-recently-used position by re-inserting.
47
+ this._cache.delete(key);
48
+ this._cache.set(key, cached);
49
+ return cached;
50
+ }
51
+ const compiled = new CompiledRegex(pattern, flags);
52
+ if (this._cache.size >= RegExUtilities.MAX_CACHE_SIZE) {
53
+ // Evict the oldest entry. Map iteration order is insertion order, so
54
+ // the first key is the least-recently-used.
55
+ const oldest = this._cache.keys().next().value;
56
+ this._cache.delete(oldest);
20
57
  }
58
+ this._cache.set(key, compiled);
21
59
  return compiled;
22
60
  }
23
61
 
24
62
  }
25
63
 
26
64
  module.exports = new RegExUtilities();
27
-
@@ -127,12 +127,8 @@ class NpmProjectorModule {
127
127
  * Set up Express routes
128
128
  */
129
129
  setupRoutes() {
130
- // CORS for browser access
131
- this.router.use((req, res, next) => {
132
- res.header('Access-Control-Allow-Origin', '*');
133
- res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
134
- next();
135
- });
130
+ // CORS is handled once at the app level (server.js, from config.server.cors).
131
+ // Do not set Access-Control-* headers here.
136
132
 
137
133
  // Root - module info
138
134
  this.router.get('/', (req, res) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fhirsmith",
3
- "version": "0.9.5",
3
+ "version": "0.9.7",
4
4
  "txVersion": "1.9.1",
5
5
  "description": "A Node.js server that provides a collection of tools to serve the FHIR ecosystem",
6
6
  "main": "server.js",
@@ -61,7 +61,7 @@
61
61
  "passport-github2": "^0.1.12",
62
62
  "passport-google-oauth20": "^2.0.0",
63
63
  "properties-file": "^3.6.4",
64
- "re2-wasm": "^1.0.2",
64
+ "re2js": "^2.8.0",
65
65
  "rimraf": "^5.0.10",
66
66
  "sqlite3": "^5.1.7",
67
67
  "tar": "^7.5.7",
@@ -17,6 +17,7 @@ class PublisherModule {
17
17
  this.logger = null;
18
18
  this.taskProcessor = null;
19
19
  this.isProcessing = false;
20
+ this.activeTaskIds = new Set();
20
21
  this.shutdownRequested = false;
21
22
  this.stats = stats;
22
23
  }
@@ -308,13 +309,19 @@ class PublisherModule {
308
309
  if (this.shutdownRequested) return;
309
310
 
310
311
  if (this.isProcessing) {
312
+ // A publication can legitimately run for longer than an hour (go-publish on a
313
+ // large IG), so never reset isProcessing while a task is in flight — doing so
314
+ // lets the poller start a second, concurrent run of the same task, which then
315
+ // fails on 'ig-registry already exists' and stomps the real run's status.
316
+ // Recovery from a genuinely hung IG Publisher is handled by the
317
+ // igPublisherTimeoutMinutes kill in runIgPublisher/runPublisherGoPublish.
311
318
  const stuckMs = this.isProcessingStarted ? Date.now() - this.isProcessingStarted : 0;
312
- if (stuckMs > 60 * 60 * 1000) {
313
- this.logger.warn('Task processor appears stuck (' + Math.round(stuckMs / 60000) + ' min) — resetting');
314
- this.isProcessing = false;
315
- } else {
316
- return;
319
+ const warnAfterMs = ((this.config.igPublisherTimeoutMinutes || 60) + 30) * 60 * 1000;
320
+ if (stuckMs > warnAfterMs) {
321
+ this.logger.warn('Task processor still busy after ' + Math.round(stuckMs / 60000) + ' min (tasks: ' +
322
+ Array.from(this.activeTaskIds).join(', ') + ') — possible hang, not starting new work');
317
323
  }
324
+ return;
318
325
  }
319
326
 
320
327
  await this.processNextTask();
@@ -325,10 +332,14 @@ class PublisherModule {
325
332
  this.isProcessing = true;
326
333
  this.isProcessingStarted = Date.now();
327
334
 
335
+ let claimedTaskId = null;
328
336
  try {
329
337
  // Look for queued tasks first (draft builds)
330
338
  let task = await this.getNextQueuedTask();
331
339
  if (task) {
340
+ if (this.activeTaskIds.has(task.id)) return; // already being processed
341
+ claimedTaskId = task.id;
342
+ this.activeTaskIds.add(task.id);
332
343
  this.stats.task('Publisher', 'Building ' + task.npm_package_id + '#' + task.version);
333
344
  await this.processDraftBuild(task);
334
345
  this.stats.taskDone('Publisher', 'Built ' + task.npm_package_id + '#' + task.version);
@@ -338,6 +349,9 @@ class PublisherModule {
338
349
  // Then look for approved tasks (publishing)
339
350
  task = await this.getNextApprovedTask();
340
351
  if (task) {
352
+ if (this.activeTaskIds.has(task.id)) return; // already being processed
353
+ claimedTaskId = task.id;
354
+ this.activeTaskIds.add(task.id);
341
355
  this.stats.task('Publisher', 'Publishing ' + task.npm_package_id + '#' + task.version);
342
356
  await this.processPublication(task);
343
357
  this.stats.taskDone('Publisher', 'Published ' + task.npm_package_id + '#' + task.version);
@@ -349,6 +363,9 @@ class PublisherModule {
349
363
  this.logger.error('Error in task processor:', error);
350
364
  this.stats.taskError('Publisher', 'Error: ' + error.message);
351
365
  } finally {
366
+ if (claimedTaskId !== null) {
367
+ this.activeTaskIds.delete(claimedTaskId);
368
+ }
352
369
  this.isProcessing = false;
353
370
  }
354
371
  }
@@ -390,6 +407,9 @@ class PublisherModule {
390
407
  fields.push('waiting_approval_at = CURRENT_TIMESTAMP');
391
408
  } else if (status === 'complete') {
392
409
  fields.push('completed_at = CURRENT_TIMESTAMP');
410
+ // A successful completion invalidates any earlier failure (e.g. from a
411
+ // superseded duplicate run) — don't let a stale message outlive success.
412
+ fields.push('failure_reason = NULL');
393
413
  } else if (status === 'failed') {
394
414
  fields.push('failed_at = CURRENT_TIMESTAMP');
395
415
  }
@@ -414,6 +434,28 @@ class PublisherModule {
414
434
  });
415
435
  }
416
436
 
437
+ // Update arbitrary task fields WITHOUT touching status. Use this from long-running
438
+ // work (e.g. saving the announcement mid-publication) — writing back the in-memory
439
+ // task.status can resurrect a stale status and cause the poller to re-run the task.
440
+ async updateTaskFields(taskId, fields) {
441
+ const keys = Object.keys(fields);
442
+ if (keys.length === 0) return;
443
+ const assignments = keys.map(key => key + ' = ?');
444
+ const values = keys.map(key => fields[key]);
445
+ values.push(taskId);
446
+
447
+ return new Promise((resolve, reject) => {
448
+ this.db.run(
449
+ 'UPDATE tasks SET ' + assignments.join(', ') + ' WHERE id = ?',
450
+ values,
451
+ (err) => {
452
+ if (err) reject(err);
453
+ else resolve();
454
+ }
455
+ );
456
+ });
457
+ }
458
+
417
459
  async logTaskMessage(taskId, level, message) {
418
460
  return new Promise((resolve) => {
419
461
  this.db.run(
@@ -622,14 +664,38 @@ class PublisherModule {
622
664
  // Create log file stream
623
665
  const logStream = fs.createWriteStream(logFile);
624
666
 
667
+ const buildStart = Date.now();
668
+ let lastDataAt = Date.now();
669
+
625
670
  java.stdout.on('data', (data) => {
671
+ lastDataAt = Date.now();
626
672
  logStream.write(data);
627
673
  });
628
674
 
629
675
  java.stderr.on('data', (data) => {
676
+ lastDataAt = Date.now();
630
677
  logStream.write(data);
631
678
  });
632
679
 
680
+ // Heartbeat: emit a status line every 60s regardless of stdout activity,
681
+ // so silent phases of the Publisher (e.g. "Validating Resources") still
682
+ // surface a signal-of-life in the task log.
683
+ const heartbeat = setInterval(async () => {
684
+ const elapsedMs = Date.now() - buildStart;
685
+ const sinceDataMs = Date.now() - lastDataAt;
686
+ let logKb = 0;
687
+ try { logKb = Math.round(fs.statSync(logFile).size / 1024); } catch (_) {}
688
+ const elapsedMin = Math.floor(elapsedMs / 60000);
689
+ const elapsedSec = Math.floor(elapsedMs / 1000) % 60;
690
+ const idleSec = Math.floor(sinceDataMs / 1000);
691
+ await this.logTaskMessage(
692
+ taskId,
693
+ 'info',
694
+ 'IG Publisher heartbeat: elapsed ' + elapsedMin + 'm' + elapsedSec + 's, ' +
695
+ 'log ' + logKb + ' KB, last output ' + idleSec + 's ago'
696
+ );
697
+ }, 60 * 1000);
698
+
633
699
  java.on('close', async (code) => {
634
700
  logStream.end();
635
701
 
@@ -649,16 +715,18 @@ class PublisherModule {
649
715
  reject(error);
650
716
  });
651
717
 
652
- // Timeout after 30 minutes
718
+ // Timeout configurable via publisher.igPublisherTimeoutMinutes (default: 60 minutes)
719
+ const timeoutMinutes = this.config.igPublisherTimeoutMinutes || 60;
653
720
  const timeout = setTimeout(async () => {
654
721
  java.kill();
655
722
  logStream.end();
656
- await this.logTaskMessage(taskId, 'error', 'IG Publisher timed out after 30 minutes');
723
+ await this.logTaskMessage(taskId, 'error', 'IG Publisher timed out after ' + timeoutMinutes + ' minutes');
657
724
  reject(new Error('IG Publisher timed out'));
658
- }, 30 * 60 * 1000);
725
+ }, timeoutMinutes * 60 * 1000);
659
726
 
660
727
  java.on('close', () => {
661
728
  clearTimeout(timeout);
729
+ clearInterval(heartbeat);
662
730
  });
663
731
  });
664
732
  }
@@ -788,7 +856,7 @@ class PublisherModule {
788
856
  if (fs.existsSync(announcementPath)) {
789
857
  try {
790
858
  const announcement = fs.readFileSync(announcementPath, 'utf8');
791
- await this.updateTaskStatus(task.id, task.status, { announcement: announcement });
859
+ await this.updateTaskFields(task.id, { announcement: announcement });
792
860
  await this.logTaskMessage(task.id, 'info', 'Announcement text saved (' + announcement.length + ' chars)');
793
861
  } catch (err) {
794
862
  await this.logTaskMessage(task.id, 'warn', 'Failed to read announcement file: ' + err.message);
@@ -829,14 +897,37 @@ class PublisherModule {
829
897
 
830
898
  const logStream = fs.createWriteStream(logFile);
831
899
 
900
+ const buildStart = Date.now();
901
+ let lastDataAt = Date.now();
902
+
832
903
  java.stdout.on('data', (data) => {
904
+ lastDataAt = Date.now();
833
905
  logStream.write(data);
834
906
  });
835
907
 
836
908
  java.stderr.on('data', (data) => {
909
+ lastDataAt = Date.now();
837
910
  logStream.write(data);
838
911
  });
839
912
 
913
+ // Heartbeat: emit a status line every 60s regardless of stdout activity,
914
+ // so silent phases of the Publisher still surface a signal-of-life.
915
+ const heartbeat = setInterval(async () => {
916
+ const elapsedMs = Date.now() - buildStart;
917
+ const sinceDataMs = Date.now() - lastDataAt;
918
+ let logKb = 0;
919
+ try { logKb = Math.round(fs.statSync(logFile).size / 1024); } catch (_) {}
920
+ const elapsedMin = Math.floor(elapsedMs / 60000);
921
+ const elapsedSec = Math.floor(elapsedMs / 1000) % 60;
922
+ const idleSec = Math.floor(sinceDataMs / 1000);
923
+ await this.logTaskMessage(
924
+ taskId,
925
+ 'info',
926
+ 'IG Publisher go-publish heartbeat: elapsed ' + elapsedMin + 'm' + elapsedSec + 's, ' +
927
+ 'log ' + logKb + ' KB, last output ' + idleSec + 's ago'
928
+ );
929
+ }, 60 * 1000);
930
+
840
931
  java.on('close', async (code) => {
841
932
  logStream.end();
842
933
  if (code === 0) {
@@ -855,16 +946,18 @@ class PublisherModule {
855
946
  reject(error);
856
947
  });
857
948
 
858
- // Timeout after 60 minutes for publication (longer than draft build)
949
+ // Timeout configurable via publisher.igPublisherTimeoutMinutes (default: 60 minutes)
950
+ const timeoutMinutes = this.config.igPublisherTimeoutMinutes || 60;
859
951
  const timeout = setTimeout(async () => {
860
952
  java.kill();
861
953
  logStream.end();
862
- await this.logTaskMessage(taskId, 'error', 'IG Publisher go-publish timed out after 60 minutes');
954
+ await this.logTaskMessage(taskId, 'error', 'IG Publisher go-publish timed out after ' + timeoutMinutes + ' minutes');
863
955
  reject(new Error('IG Publisher go-publish timed out'));
864
- }, 60 * 60 * 1000);
956
+ }, timeoutMinutes * 60 * 1000);
865
957
 
866
958
  java.on('close', () => {
867
959
  clearTimeout(timeout);
960
+ clearInterval(heartbeat);
868
961
  });
869
962
  });
870
963
  }
@@ -758,7 +758,7 @@ class RegistryModule {
758
758
  case '11000267109': edition = 'KR'; break;
759
759
  case '900000001000122104': edition = 'ES-ES'; break;
760
760
  case '2011000195101': edition = 'CH'; break;
761
- case '11000279109': edition = 'CX'; break;
761
+ case '11000279109': edition = 'CZ'; break;
762
762
  case '999000021000000109': edition = 'UK+Clinical'; break;
763
763
  case '5631000179106': edition = 'UY'; break;
764
764
  case '21000325107': edition = 'CL'; break;
@@ -1233,7 +1233,7 @@ class RegistryModule {
1233
1233
  // FHIR Version field
1234
1234
  html += '<p>';
1235
1235
  html += '<label for="fhirVersion" class="form-label fw-bold">FHIR Version <span class="text-danger">*</span></label>';
1236
- html += `<input type="text" class="form-control" id="fhirVersion" name="fhirVersion" size="8"
1236
+ html += `<input type="text" class="form-control" id="fhirVersion" name="fhirVersion" size="8"
1237
1237
  value="${escape(fhirVersion)}" required>`;
1238
1238
  html += '</p>';
1239
1239
  html += '<p class="text-muted small">Examples: R4, 4.0.1, 5.0.0, etc.</p>';
@@ -1241,7 +1241,7 @@ class RegistryModule {
1241
1241
  html += '<div class="alert alert-info">Either Code System URL or Value Set URL must be provided:</div>';
1242
1242
  html += '<p>';
1243
1243
  html += '<label for="url" class="form-label fw-bold">Code System URL</label>';
1244
- html += `<input type="url" class="form-control" id="url" name="url"
1244
+ html += `<input type="url" class="form-control" id="url" name="url"
1245
1245
  value="${escape(url)}">`;
1246
1246
  html += '</p>';
1247
1247
  html += '<p class="text-muted small">Example: http://loinc.org</p>';
@@ -1249,7 +1249,7 @@ class RegistryModule {
1249
1249
  // ValueSet URL field - now vertical
1250
1250
  html += '<p>';
1251
1251
  html += '<label for="valueSet" class="form-label fw-bold">Value Set URL</label>';
1252
- html += `<input type="url" class="form-control" id="valueSet" name="valueSet"
1252
+ html += `<input type="url" class="form-control" id="valueSet" name="valueSet"
1253
1253
  value="${escape(valueSet)}">`;
1254
1254
  html += '</p>';
1255
1255
  html += '<p class="text-muted small">Example: http://hl7.org/fhir/ValueSet/observation-codes</p>';
@@ -1257,7 +1257,7 @@ class RegistryModule {
1257
1257
 
1258
1258
  // Authoritative Only checkbox
1259
1259
  html += '<p>';
1260
- html += `<input type="checkbox" class="form-check-input" id="authoritativeOnly"
1260
+ html += `<input type="checkbox" class="form-check-input" id="authoritativeOnly"
1261
1261
  name="authoritativeOnly" value="true" ${authoritativeOnly ? 'checked' : ''}>`;
1262
1262
  html += '<label class="form-check-label" for="authoritativeOnly">&nbsp;Show only authoritative servers</label>';
1263
1263
  html += '</p>';
@@ -1276,7 +1276,7 @@ class RegistryModule {
1276
1276
  document.querySelector('form').addEventListener('submit', function(e) {
1277
1277
  const url = document.getElementById('url').value.trim();
1278
1278
  const valueSet = document.getElementById('valueSet').value.trim();
1279
-
1279
+
1280
1280
  if (!url && !valueSet) {
1281
1281
  e.preventDefault();
1282
1282
  alert('You must provide either a Code System URL or a Value Set URL');
package/server.js CHANGED
@@ -7,7 +7,7 @@
7
7
  //
8
8
 
9
9
  const express = require('express');
10
- // const cors = require('cors');
10
+ const cors = require('cors');
11
11
  const path = require('path');
12
12
  const fs = require('fs');
13
13
  const os = require('os');
@@ -82,7 +82,11 @@ app.use((req, res, next) => {
82
82
  next();
83
83
  });
84
84
 
85
- // app.use(cors(config.server.cors));
85
+ // Single, app-level CORS policy for all modules, driven by config.server.cors.
86
+ // This also handles OPTIONS preflight, so modules must NOT set their own
87
+ // Access-Control-* headers (doing so stacks a second CORS layer and produces
88
+ // duplicate, conflicting headers that browsers reject).
89
+ app.use(cors(config.server.cors));
86
90
 
87
91
  // Module instances
88
92
  const modules = {};
@@ -1,14 +1,15 @@
1
- // Manual reproducer for the re2-wasm WASM heap leak.
2
- // See library/regex-utilities.js for the workaround.
1
+ // Historical reproducer for the re2-wasm WASM heap leak.
2
+ // regex-utilities.js now sits on top of re2js (a pure-JS RE2 port), so the
3
+ // underlying leak is gone. Kept as a stress test for the compile cache:
3
4
  //
4
5
  // node test-scripts/repro-re2-wasm-leak.js # same-pattern stress
5
6
  // node test-scripts/repro-re2-wasm-leak.js --unique # unique-pattern stress
6
7
  //
7
- // Without the cache, same-pattern OOMs at ~2965 iterations.
8
- // With the cache, same-pattern runs indefinitely.
9
- // Unique-pattern still OOMs (each pattern is a real compile and the underlying
10
- // re2-wasm heap leak still applies). A proper fix is to replace re2-wasm with
11
- // the native `re2` package.
8
+ // Background (re2-wasm era): without the cache, same-pattern OOMed at ~2965
9
+ // iterations; with the cache, same-pattern ran indefinitely but unique-pattern
10
+ // still OOMed because every distinct pattern was a real compile and re2-wasm
11
+ // could not free its WASM heap. Under re2js both modes now run indefinitely
12
+ // (unique-pattern is bounded only by ordinary V8 heap growth from the cache).
12
13
 
13
14
  const re = require('../library/regex-utilities');
14
15
 
@@ -544,7 +544,7 @@ NO_VALID_DISPLAY_FOUND_LANG_NONE_other = ''{5}'' is the default display; the cod
544
544
  NO_VALID_DISPLAY_FOUND_LANG_SOME_one = ''{5}'' is the default display; no valid Display Names found for {1}#{2} in the language {4}
545
545
  NO_VALID_DISPLAY_FOUND_LANG_SOME_other = ''{5}'' is the default display; no valid Display Names found for {1}#{2} in the language {4}
546
546
  NO_VALID_DISPLAY_FOUND_NONE_FOR_LANG_ERR = Wrong Display Name ''{0}'' for {1}#{2}. There are no valid display names found for language(s) ''{3}''. Default display is ''{4}''
547
- NO_VALID_DISPLAY_FOUND_NONE_FOR_LANG_OK = There are no valid display names found for the code {1}#{2} for language(s) ''{3}''. The display is ''{4}'' which is the default language display
547
+ NO_VALID_DISPLAY_FOUND_NONE_FOR_LANG_OK = There are no valid display names found for the code {1}#{2} for language(s) ''{3}''. The display is ''{0}'' which is a valid display for the default language
548
548
  NO_VALID_DISPLAY_FOUND_one = No valid Display Names found for {1}#{2} in the language {4}
549
549
  NO_VALID_DISPLAY_FOUND_other = No valid Display Names found for {1}#{2} in the languages {4}
550
550
  No_validator_configured = No validator configured