fhirsmith 0.18.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/ACCESSIBILITY.md +119 -0
  2. package/CHANGELOG.md +190 -9
  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 +331 -33
  15. package/ips/jurisdictions.js +65 -0
  16. package/ips/nationality.js +120 -0
  17. package/ips/pipeline.js +131 -17
  18. package/ips/presenter.js +1 -1
  19. package/ips/profile-editor.js +283 -4
  20. package/ips/readme.md +170 -20
  21. package/ips/render.js +769 -109
  22. package/ips/scripts.js +46 -0
  23. package/ips/store.js +123 -11
  24. package/ips/structure.js +247 -0
  25. package/ips/terminology.js +1 -1
  26. package/library/body-limit.js +22 -0
  27. package/library/cache-generation.js +15 -0
  28. package/library/cgroup-memory.js +133 -1
  29. package/library/cluster-counts.js +121 -0
  30. package/library/cluster-events.js +153 -0
  31. package/library/cluster-metrics.js +193 -0
  32. package/library/cluster-status.js +468 -0
  33. package/library/config-reader.js +96 -0
  34. package/library/daily-report.js +100 -2
  35. package/library/html-server.js +27 -1
  36. package/library/i18nsupport.js +23 -1
  37. package/library/keep-alive.js +18 -0
  38. package/library/languages.js +17 -3
  39. package/library/logger.js +31 -0
  40. package/library/memory-guard.js +428 -0
  41. package/library/native-memory.js +52 -0
  42. package/library/npm-audit.js +51 -25
  43. package/library/openapi-doc.js +7 -5
  44. package/library/package-manager.js +8 -0
  45. package/library/role.js +197 -0
  46. package/library/server-credentials.js +169 -0
  47. package/library/sqlite-utilities.js +59 -0
  48. package/library/ssrf-guard.js +100 -0
  49. package/library/static-response-stats.js +16 -0
  50. package/library/version-utilities.js +50 -0
  51. package/library/worker-count.js +192 -0
  52. package/nginx.md +30 -11
  53. package/npmprojector/indexer.js +18 -10
  54. package/package.json +11 -8
  55. package/packages/feed-check-page.js +117 -0
  56. package/packages/feed-check.js +279 -0
  57. package/packages/npm-check.js +57 -3
  58. package/packages/npm-signing.js +219 -0
  59. package/packages/openapi.yaml +207 -1
  60. package/packages/package-crawler.js +322 -41
  61. package/packages/packages-template.html +6 -8
  62. package/packages/packages.js +604 -57
  63. package/packages/readme.md +113 -1
  64. package/publisher/publisher-template.html +2 -5
  65. package/publisher/publisher.js +121 -110
  66. package/publisher/readme.md +75 -0
  67. package/registry/api.js +179 -1
  68. package/registry/crawler.js +86 -108
  69. package/registry/documentation/api.md +68 -2
  70. package/registry/documentation/configuration.md +34 -0
  71. package/registry/documentation/internals.md +4 -1
  72. package/registry/documentation/readme.md +3 -2
  73. package/registry/model.js +8 -0
  74. package/registry/openapi.yaml +84 -0
  75. package/registry/registry-template.html +5 -8
  76. package/registry/registry.js +58 -6
  77. package/root-bare-template.html +7 -4
  78. package/root-template.html +5 -8
  79. package/server.js +398 -198
  80. package/shl/shl.js +38 -37
  81. package/static/a11y/a11y-check.js +832 -0
  82. package/static/a11y/axe.min.js +12 -0
  83. package/static/assets/css/bootstrap-fhir.css +15 -15
  84. package/static/assets/images/flags/au.svg +18 -0
  85. package/static/assets/images/flags/unknown.svg +11 -0
  86. package/static/assets/images/flags/us.svg +63 -0
  87. package/static/assets/images/icon_audit.png +0 -0
  88. package/static/assets/images/icon_clinical.png +0 -0
  89. package/static/assets/images/icon_document.png +0 -0
  90. package/static/assets/images/icon_external.png +0 -0
  91. package/static/assets/images/icon_medication.png +0 -0
  92. package/static/assets/images/icon_organization.png +0 -0
  93. package/static/assets/images/icon_other.png +0 -0
  94. package/static/assets/images/icon_person.png +0 -0
  95. package/static/assets/images/icon_section.png +0 -0
  96. package/stats-db.js +45 -0
  97. package/stats.js +197 -10
  98. package/testing/testing-template.html +5 -8
  99. package/token/nginx_passport_setup.md +1 -1
  100. package/token/security_guide.md +1 -1
  101. package/token/token-template.html +2 -5
  102. package/token/token.js +40 -36
  103. package/translations/Messages.properties +19 -0
  104. package/tx/README.md +158 -4
  105. package/tx/closure/closure-store.js +126 -3
  106. package/tx/cm/cm-database.js +69 -57
  107. package/tx/cm/cm-package.js +50 -16
  108. package/tx/cs/cs-api.js +284 -19
  109. package/tx/cs/cs-api.md +41 -4
  110. package/tx/cs/cs-areacode.js +1 -1
  111. package/tx/cs/cs-country.js +17 -5
  112. package/tx/cs/cs-cpt.js +33 -16
  113. package/tx/cs/cs-cs.js +199 -24
  114. package/tx/cs/cs-currency.js +1 -1
  115. package/tx/cs/cs-icd11.js +21 -13
  116. package/tx/cs/cs-icf.js +22 -3
  117. package/tx/cs/cs-lang.js +2 -2
  118. package/tx/cs/cs-loinc.js +193 -67
  119. package/tx/cs/cs-mimetypes.js +12 -2
  120. package/tx/cs/cs-ndc.js +40 -25
  121. package/tx/cs/cs-omop.js +54 -29
  122. package/tx/cs/cs-rxnorm.js +300 -196
  123. package/tx/cs/cs-snomed.js +861 -95
  124. package/tx/cs/cs-ucum.js +14 -4
  125. package/tx/cs/cs-unii.js +18 -14
  126. package/tx/cs/cs-virtual.js +210 -0
  127. package/tx/ecosystem-resolver.js +413 -0
  128. package/tx/html/cluster-dash-metrics.liquid +26 -0
  129. package/tx/html/cluster-home-metrics.liquid +31 -0
  130. package/tx/html/cluster-metrics-script.liquid +251 -0
  131. package/tx/html/codesystem-operations.liquid +3 -3
  132. package/tx/html/ecl-panel.liquid +1 -1
  133. package/tx/html/external-metrics.liquid +17 -0
  134. package/tx/html/operations-form.liquid +17 -17
  135. package/tx/html/search-form.liquid +11 -11
  136. package/tx/html/status-line.liquid +58 -0
  137. package/tx/html/status-panel.liquid +140 -0
  138. package/tx/html/status-tasks.liquid +25 -0
  139. package/tx/html/tx-template.html +2 -5
  140. package/tx/importers/atc-to-fhir.js +17 -16
  141. package/tx/importers/export-icd11-codesystem.js +3 -2
  142. package/tx/importers/import-icd11.js +4 -3
  143. package/tx/importers/import-loinc.module.js +118 -56
  144. package/tx/importers/import-ndc.module.js +56 -44
  145. package/tx/importers/import-rxnorm.module.js +118 -79
  146. package/tx/importers/import-sct.module.js +125 -61
  147. package/tx/importers/import-unii.module.js +37 -36
  148. package/tx/importers/subset-loinc.module.js +49 -48
  149. package/tx/importers/subset-rxnorm.module.js +29 -28
  150. package/tx/importers/terminal.js +18 -0
  151. package/tx/importers/tx-import-base.js +13 -12
  152. package/tx/importers/tx-import-settings.js +5 -4
  153. package/tx/importers/tx-import.js +53 -52
  154. package/tx/library/canonical-resource.js +49 -0
  155. package/tx/library/codesystem.js +94 -5
  156. package/tx/library/designations.js +45 -2
  157. package/tx/library/extensions.js +13 -7
  158. package/tx/library/implicit-valuesets.js +66 -0
  159. package/tx/library/renderer.js +77 -21
  160. package/tx/library/sqlite-utilities.js +2 -0
  161. package/tx/library/tho-precedence.js +40 -0
  162. package/tx/library/ucum-parsers.js +11 -1
  163. package/tx/library/wait-for-cache.js +43 -0
  164. package/tx/library.js +321 -22
  165. package/tx/openapi-operations.js +6 -4
  166. package/tx/operation-context.js +460 -99
  167. package/tx/params.js +25 -0
  168. package/tx/problems.js +3 -5
  169. package/tx/provider.js +314 -67
  170. package/tx/resource-cache-sqlite.js +1050 -0
  171. package/tx/sct/documentation.md +45 -21
  172. package/tx/sct/ecl.js +81 -17
  173. package/tx/sct/editions.js +39 -0
  174. package/tx/sct/expressions.js +10 -1
  175. package/tx/sct/load-config.js +107 -0
  176. package/tx/sct/mmap.js +109 -0
  177. package/tx/sct/structures.js +175 -15
  178. package/tx/sct/text-index.js +5 -1
  179. package/tx/syndication/syndication-feed.js +2 -2
  180. package/tx/tx-html.js +44 -8
  181. package/tx/tx.fhir.org.yml +8 -8
  182. package/tx/tx.js +282 -80
  183. package/tx/usage-tracker.js +39 -11
  184. package/tx/vs/vs-api.js +19 -0
  185. package/tx/vs/vs-database.js +386 -62
  186. package/tx/vs/vs-package.js +44 -17
  187. package/tx/vs/vs-vsac.js +649 -101
  188. package/tx/workers/batch-validate.js +37 -10
  189. package/tx/workers/batch.js +6 -1
  190. package/tx/workers/cache-control.js +15 -4
  191. package/tx/workers/closure.js +156 -13
  192. package/tx/workers/compare.js +8 -5
  193. package/tx/workers/expand.js +1173 -289
  194. package/tx/workers/metadata.js +35 -0
  195. package/tx/workers/search.js +53 -1
  196. package/tx/workers/supplement-filters.js +145 -0
  197. package/tx/workers/translate.js +169 -4
  198. package/tx/workers/validate.js +223 -119
  199. package/tx/workers/worker.js +123 -31
  200. package/tx/xversion/xv-valueset.js +7 -4
  201. package/utilities/submit-test-report.js +3 -1
  202. package/utilities/tx-stress-test.js +482 -0
  203. package/vcl/readme.md +37 -0
  204. package/vcl/vcl-parser.js +15 -0
  205. package/xig/readme.md +52 -0
  206. package/xig/xig-template.html +5 -8
  207. package/xig/xig.js +238 -422
  208. package/changelog-template.md +0 -22
  209. package/registry/test-runner.js +0 -237
  210. package/release.sh +0 -80
  211. package/test-scripts/repro-re2-wasm-leak.js +0 -34
  212. package/tsconfig.eslint.json +0 -9
  213. package/tx/html/dash-metrics.liquid +0 -147
  214. package/tx/html/home-metrics.liquid +0 -225
@@ -12,6 +12,9 @@
12
12
  // The report is:
13
13
  // - a paragraph on the server's own stability since the last report: how much of the
14
14
  // time it was up, restarts, and any upgrade (from the run history ServerStats keeps)
15
+ // - under the launcher (cluster.js), a paragraph on the cluster: processes that exited or
16
+ // were recycled and how long they were unavailable, and what the launcher refused or
17
+ // abandoned itself (library/cluster-events.js)
15
18
  // - a paragraph from each running module that has something to say. A module takes part
16
19
  // by having `dailyReport({since, now})` return markdown (a string, or a promise of one);
17
20
  // an empty return means no paragraph. By default modules have nothing to say
@@ -94,6 +97,11 @@ function describeSpan(ms) {
94
97
  return plural(Math.round(hours / 24), 'day');
95
98
  }
96
99
 
100
+ /** "once", "twice", "3 times" */
101
+ function times(n) {
102
+ return n === 1 ? 'once' : n === 2 ? 'twice' : `${n} times`;
103
+ }
104
+
97
105
  function plural(n, word) {
98
106
  return `${n} ${word}${n === 1 ? '' : 's'}`;
99
107
  }
@@ -112,9 +120,10 @@ class DailyReport {
112
120
 
113
121
  /**
114
122
  * @param config - the "dailyReport" block
115
- * @param deps - {log, stats, modules, mailer, npmAudit, version, serverId, stateFile}
123
+ * @param deps - {log, stats, modules, mailer, npmAudit, version, serverId, stateFile, cluster}
116
124
  * modules is the server's {name: instance} map; npmAudit the server's NpmAudit (optional);
117
- * version the server's version; serverId the name the report goes out under (config.hostName)
125
+ * version the server's version; serverId the name the report goes out under (config.hostName);
126
+ * cluster true for the master under the launcher (the cluster paragraph is always there)
118
127
  */
119
128
  constructor(config, deps) {
120
129
  this.config = config || {};
@@ -125,6 +134,7 @@ class DailyReport {
125
134
  this.npmAudit = deps.npmAudit || null;
126
135
  this.version = deps.version;
127
136
  this.serverId = deps.serverId || os.hostname();
137
+ this.cluster = Boolean(deps.cluster);
128
138
  this.stateFile = deps.stateFile || folders.filePath(STATE_FILE);
129
139
  this.timer = null;
130
140
  this.running = false;
@@ -255,6 +265,11 @@ class DailyReport {
255
265
  parts.push(`# ${this.serverId} daily report for ${localDateTime(now)} covering the last ${describeSpan(now - since)}`);
256
266
  parts.push('## Stability');
257
267
  parts.push(this.stabilityParagraph(since, now));
268
+ const cluster = this.clusterParagraph(since, now);
269
+ if (cluster) {
270
+ parts.push('## Cluster');
271
+ parts.push(cluster);
272
+ }
258
273
  for (const [name, module] of Object.entries(this.modules)) {
259
274
  const text = await this.moduleParagraph(name, module, since, now);
260
275
  if (text) {
@@ -333,6 +348,88 @@ class DailyReport {
333
348
  return sentences.join(' ');
334
349
  }
335
350
 
351
+ /**
352
+ * Under the launcher: what it saw (library/cluster-events.js) - processes that exited or
353
+ * were recycled, how long each was unavailable, and what it answered itself. Null for a
354
+ * single server (no role, and no events recorded).
355
+ */
356
+ clusterParagraph(since, now) {
357
+ const events = this.stats && typeof this.stats.clusterEventsSince === 'function' ? this.stats.clusterEventsSince(since, now) : null;
358
+ if (!this.cluster && !(events && events.length > 0)) {
359
+ return null;
360
+ }
361
+ if (!events) {
362
+ return 'No record of the launcher\'s events is available (the statistics database is not in use).';
363
+ }
364
+ const sum = (rows) => rows.reduce((n, r) => n + r.count, 0);
365
+ const of = (kind, process) => events.filter(e => e.kind === kind && (process === undefined || e.process === process));
366
+ const sentences = [];
367
+ const started = of('launcher-started');
368
+ if (started.length > 0) {
369
+ sentences.push(`The launcher started ${times(started.length)} (${started.map(e => localDateTime(e.time)).join(', ')}).`);
370
+ }
371
+
372
+ const names = [...new Set(events.filter(e => e.process).map(e => e.process))]
373
+ .sort((a, b) => (a === 'master' ? -1 : b === 'master' ? 1 : a.localeCompare(b)));
374
+ const lines = [];
375
+ for (const name of names) {
376
+ const said = [];
377
+ const exited = of('exited', name);
378
+ if (exited.length > 0) {
379
+ const how = new Map();
380
+ for (const e of exited) {
381
+ const d = e.detail == null ? 'unknown' : /^-?\d+$/.test(e.detail) ? `exit code ${e.detail}` : e.detail;
382
+ how.set(d, (how.get(d) || 0) + 1);
383
+ }
384
+ said.push(`exited ${times(exited.length)} (${[...how].map(([d, n]) => n > 1 ? `${n} × ${d}` : d).join(', ')})`);
385
+ }
386
+ const recycled = of('recycled', name);
387
+ if (recycled.length > 0) {
388
+ said.push(`was recycled ${times(recycled.length)} (the last because ${recycled[recycled.length - 1].detail || 'of its memory'})`);
389
+ }
390
+ const down = of('ready', name).reduce((n, e) => n + e.ms, 0);
391
+ if (down > 0) {
392
+ said.push(`was unavailable for ${formatSpan(down)} in all`);
393
+ }
394
+ const timedOut = sum(of('upstream-timeout', name));
395
+ if (timedOut > 0) {
396
+ said.push(`did not begin ${plural(timedOut, 'response')} in time (504)`);
397
+ }
398
+ const failed = sum(of('upstream-failed', name));
399
+ if (failed > 0) {
400
+ said.push(`failed to answer ${plural(failed, 'request')} (502)`);
401
+ }
402
+ const lost = sum(of('lost-to-restart', name));
403
+ if (lost > 0) {
404
+ said.push(`was restarting when ${plural(lost, 'request')} came for a cache-id it had issued`);
405
+ }
406
+ if (said.length > 0) {
407
+ lines.push(`- ${name}: ${said.join('; ')}`);
408
+ }
409
+ }
410
+ if (of('exited').length === 0 && of('recycled').length === 0) {
411
+ sentences.push('No process exited or was recycled.');
412
+ }
413
+
414
+ const refused = [
415
+ [sum(of('refused-busy')), 'because no process was free in time'],
416
+ [sum(of('refused-not-ready')), 'because no process was ready'],
417
+ [sum(of('refused-stopping')), 'while it was stopping']
418
+ ].filter(([n]) => n > 0);
419
+ if (refused.length > 0) {
420
+ const total = refused.reduce((n, [c]) => n + c, 0);
421
+ sentences.push(`The launcher refused ${plural(total, 'request')} itself (503): ${refused.map(([n, why]) => `${n} ${why}`).join(', ')}.`);
422
+ } else {
423
+ sentences.push('The launcher refused no requests itself.');
424
+ }
425
+ const cut = sum(of('write-timeout'));
426
+ if (cut > 0) {
427
+ sentences.push(`It closed ${plural(cut, 'connection')} whose client took none of a response in time (writeTimeoutSeconds).`);
428
+ }
429
+ const text = sentences.join(' ');
430
+ return lines.length > 0 ? `${text}\n\n${lines.join('\n')}` : text;
431
+ }
432
+
336
433
  /**
337
434
  * A module's paragraph, if it has one. A module that throws gets a line
338
435
  * saying so rather than taking the report down.
@@ -430,3 +527,4 @@ module.exports.describeSpan = describeSpan;
430
527
  // for modules writing their paragraph: "3 packages", "1 package"
431
528
  module.exports.plural = plural;
432
529
  module.exports.localDateTime = localDateTime;
530
+ module.exports.formatBytes = formatBytes;
@@ -12,11 +12,24 @@ const packageJson = require('../package.json');
12
12
 
13
13
  let sponsorMessage = '';
14
14
 
15
+ // The accessibility check panel (config accessibility.checkPanel): a button at the bottom of
16
+ // every page that runs axe-core (WCAG 2.0 A/AA) and FHIRsmith's own structure checks on the page
17
+ // as the browser has it. The scripts are in static/a11y/; a11y-check.js only loads axe-core when
18
+ // the button is pressed.
19
+ const A11Y_PANEL = `
20
+ <div id="fhir-a11y-panel" data-axe="/a11y/axe.min.js" lang="en" style="clear: both; margin: 20px 10px; padding: 8px; border: 1px solid #888888; background-color: #f4f4f4; color: #000000; font-size: 13px; font-family: sans-serif">
21
+ <p style="margin: 0"><b>Accessibility check</b> (checks this page as it is now against WCAG 2.0 A/AA, plus heading structure, reported by HHS 508 checklist item) <button type="button" class="fhir-a11y-run">Check this page</button> <span class="fhir-a11y-status" role="status"></span></p>
22
+ <div class="fhir-a11y-results"></div>
23
+ </div>
24
+ <script type="text/javascript" src="/a11y/a11y-check.js"></script>
25
+ `;
26
+
15
27
  class HtmlServer {
16
28
  log;
17
29
 
18
30
  constructor() {
19
31
  this.templates = new Map(); // templateName -> template content
32
+ this.accessibilityPanel = false;
20
33
  }
21
34
 
22
35
  useLog(logv) {
@@ -27,6 +40,17 @@ class HtmlServer {
27
40
  sponsorMessage = msg;
28
41
  }
29
42
 
43
+ setAccessibilityPanel(enabled) {
44
+ this.accessibilityPanel = !!enabled;
45
+ }
46
+
47
+ // put the accessibility check panel at the end of the page body (or the end of the page, if
48
+ // there's no </body>)
49
+ addAccessibilityPanel(html) {
50
+ const i = html.toLowerCase().lastIndexOf('</body>');
51
+ return i < 0 ? html + A11Y_PANEL : html.slice(0, i) + A11Y_PANEL + html.slice(i);
52
+ }
53
+
30
54
  // Template Management
31
55
  loadTemplate(templateName, templatePath) {
32
56
  try {
@@ -92,6 +116,8 @@ class HtmlServer {
92
116
  ['library-link', () => renderOptions.libraryLink || ''],
93
117
  // raw HTML too: the OpenAPI description, where a template's module has one in some places only
94
118
  ['api-link', () => renderOptions.apiLink || ''],
119
+ // raw HTML: the packages module's feed check, in the nav bar only when it's enabled
120
+ ['feed-check-link', () => renderOptions.feedCheckLink || ''],
95
121
  // raw HTML: a link to the current project (or other context) in the nav bar, where the
96
122
  // module has one
97
123
  ['context-link', () => renderOptions.contextLink || ''],
@@ -108,7 +134,7 @@ class HtmlServer {
108
134
  // a placeholder that isn't known is left as it is, as before
109
135
  const html = template.replace(/\[%([^%\]\s]{1,64})%\]/g, (m, name) => (values.has(name) ? values.get(name)() : m));
110
136
 
111
- return html;
137
+ return this.accessibilityPanel ? this.addAccessibilityPanel(html) : html;
112
138
  }
113
139
 
114
140
  // Express Response Helper
@@ -254,6 +254,28 @@ class I18nSupport {
254
254
  }
255
255
  }
256
256
 
257
+ let english = null;
258
+
259
+ /**
260
+ * A message from translations/Messages.properties, in English, with its parameters, as
261
+ * I18nSupport.translate gives it: for code with no request languages to hand (the shared
262
+ * cache store's lazy reads, the launcher). The message id goes with it, so a client (or a
263
+ * caller that has the languages: msgParams) can render it in another language.
264
+ * @param {string} messageId
265
+ * @param {Array} [parameters]
266
+ */
267
+ function englishMessage(messageId, parameters = []) {
268
+ if (!english) {
269
+ english = getProperties(fs.readFileSync(path.join(__dirname, '..', 'translations', 'Messages.properties'), 'utf8'));
270
+ }
271
+ const message = english[messageId];
272
+ if (!message) {
273
+ return messageId;
274
+ }
275
+ return I18nSupport.prototype._substituteParameters(message.trim(), parameters).replaceAll("''", "'");
276
+ }
277
+
257
278
  module.exports = {
258
- I18nSupport
279
+ I18nSupport,
280
+ englishMessage
259
281
  };
@@ -0,0 +1,18 @@
1
+ // Keep-alive timeouts for the HTTP server behind a reverse proxy.
2
+ //
3
+ // nginx keeps idle upstream connections open for 60 s (keepalive_timeout in an upstream block),
4
+ // while Node closes an idle one after 5 s. When nginx reuses a connection at the moment Node
5
+ // closes it, the request fails with a reset, and nginx does not retry a POST: the client gets a
6
+ // 502. Node must therefore keep idle connections open longer than the proxy does, and
7
+ // headersTimeout must be longer still, or Node applies it to the idle connection first.
8
+
9
+ const KEEP_ALIVE_TIMEOUT_MS = 65000;
10
+ const HEADERS_TIMEOUT_MS = 66000;
11
+
12
+ function applyKeepAliveTimeouts(server) {
13
+ server.keepAliveTimeout = KEEP_ALIVE_TIMEOUT_MS;
14
+ server.headersTimeout = HEADERS_TIMEOUT_MS;
15
+ return server;
16
+ }
17
+
18
+ module.exports = { applyKeepAliveTimeouts, KEEP_ALIVE_TIMEOUT_MS, HEADERS_TIMEOUT_MS };
@@ -583,6 +583,12 @@ class Languages {
583
583
  * Global language definitions loaded from IETF registry
584
584
  */
585
585
  class LanguageDefinitions {
586
+ // Private use tags of any length are valid, so the cache of parsed tags is capped
587
+ // with a simple LRU (as RegExUtilities does), or unique tags would grow it forever.
588
+ // Default for languageTagCacheSize: an 8 KB tag takes about 46 KB parsed, so 1,000
589
+ // such tags are at most about 46 MB; real requests use far fewer distinct tags.
590
+ static MAX_PARSED = 1000;
591
+
586
592
  constructor() {
587
593
  this.languages = new Map();
588
594
  this.extLanguages = new Map();
@@ -596,6 +602,7 @@ class LanguageDefinitions {
596
602
  // case-insensitive, and the registry writes these in mixed case.
597
603
  this.wholeTags = new Map();
598
604
  this.parsed = new Map(); // Cache for parsed languages
605
+ this.maxParsed = LanguageDefinitions.MAX_PARSED; // Infinity = no limit
599
606
  }
600
607
 
601
608
  /**
@@ -949,13 +956,20 @@ class LanguageDefinitions {
949
956
  // The cache key carries the options: the same tag can be valid under one set of
950
957
  // rules and not another, so a lenient parse must not be served to a strict caller.
951
958
  const key = (options.caseInsensitive ? 'i' : '') + (options.checkCombinations ? 'c' : '') + ':' + code;
952
- if (this.parsed.has(key)) {
953
- return this.parsed.get(key);
959
+ const cached = this.parsed.get(key);
960
+ if (cached) {
961
+ // Move to most-recently-used position by re-inserting.
962
+ this.parsed.delete(key);
963
+ this.parsed.set(key, cached);
964
+ return cached;
954
965
  }
955
966
 
956
967
  try {
957
968
  const lang = new Language(code, this, options);
958
- // Cache the result
969
+ if (this.parsed.size >= this.maxParsed) {
970
+ // Evict the oldest entry: Map iteration order is insertion order.
971
+ this.parsed.delete(this.parsed.keys().next().value);
972
+ }
959
973
  this.parsed.set(key, lang);
960
974
  return lang;
961
975
  } catch (e) {
package/library/logger.js CHANGED
@@ -299,6 +299,37 @@ class Logger {
299
299
  };
300
300
  }
301
301
 
302
+ // Apply config.logging to a logger that is already running (the server starts logging with the
303
+ // defaults before config.json is read). Same options as the constructor; anything not given keeps
304
+ // its default
305
+ configure(options = {}) {
306
+ this._flushSync();
307
+ this.level = options.level || DEFAULTS.level;
308
+ this.maxFiles = options.maxFiles ?? DEFAULTS.maxFiles;
309
+ this.maxSize = Logger._parseSize(options.maxSize) || DEFAULTS.maxSize;
310
+ this.showConsole = options.console ?? DEFAULTS.console;
311
+ this.consoleErrors = options.consoleErrors ?? DEFAULTS.consoleErrors;
312
+ this._flushSize = options.flushSize ?? DEFAULTS.flushSize;
313
+
314
+ clearInterval(this._flushTimer);
315
+ this._flushTimer = setInterval(() => this._flush(), options.flushInterval ?? DEFAULTS.flushInterval);
316
+ if (this._flushTimer.unref) this._flushTimer.unref();
317
+
318
+ if (options.logDir && path.resolve(options.logDir) !== path.resolve(this.logDir)) {
319
+ if (this._fd !== null) {
320
+ try { fs.closeSync(this._fd); } catch (_) { /* intentional */ }
321
+ }
322
+ this._fd = null;
323
+ this._currentDate = null;
324
+ this._currentFileSize = 0;
325
+ this.logDir = options.logDir;
326
+ if (!fs.existsSync(this.logDir)) {
327
+ fs.mkdirSync(this.logDir, { recursive: true });
328
+ }
329
+ }
330
+ this.info('Logger configured @ ' + this.logDir + ', level ' + this.level, {});
331
+ }
332
+
302
333
  setLevel(level) {
303
334
  this.level = level;
304
335
  this.info(`Log level changed to ${level}`);