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.
- package/ACCESSIBILITY.md +119 -0
- package/CHANGELOG.md +149 -0
- package/CONFIGURATION.md +305 -0
- package/LICENSE +29 -0
- package/README.md +29 -122
- package/cluster.js +1220 -0
- package/cluster.md +707 -0
- package/config-template.json +77 -8
- package/extension-tracker/extension-tracker-template.html +5 -8
- package/extension-tracker/extension-tracker.js +0 -2
- package/folder/folder.js +7 -6
- package/ips/analysis.js +560 -0
- package/ips/ips-template.html +5 -8
- package/ips/ips.js +132 -59
- package/ips/nationality.js +120 -0
- package/ips/pipeline.js +74 -21
- package/ips/presenter.js +1 -1
- package/ips/readme.md +132 -25
- package/ips/render.js +466 -77
- package/ips/store.js +79 -22
- package/ips/structure.js +247 -0
- package/ips/terminology.js +1 -1
- package/library/body-limit.js +22 -0
- package/library/cache-generation.js +15 -0
- package/library/cgroup-memory.js +133 -1
- package/library/cluster-counts.js +121 -0
- package/library/cluster-events.js +153 -0
- package/library/cluster-metrics.js +193 -0
- package/library/cluster-status.js +468 -0
- package/library/config-reader.js +96 -0
- package/library/daily-report.js +100 -2
- package/library/html-server.js +27 -1
- package/library/i18nsupport.js +23 -1
- package/library/keep-alive.js +18 -0
- package/library/languages.js +17 -3
- package/library/logger.js +31 -0
- package/library/memory-guard.js +428 -0
- package/library/native-memory.js +52 -0
- package/library/npm-audit.js +51 -25
- package/library/openapi-doc.js +7 -5
- package/library/package-manager.js +8 -0
- package/library/role.js +197 -0
- package/library/server-credentials.js +169 -0
- package/library/sqlite-utilities.js +59 -0
- package/library/ssrf-guard.js +100 -0
- package/library/static-response-stats.js +16 -0
- package/library/version-utilities.js +50 -0
- package/library/worker-count.js +192 -0
- package/nginx.md +30 -11
- package/npmprojector/indexer.js +18 -10
- package/package.json +11 -8
- package/packages/feed-check-page.js +117 -0
- package/packages/feed-check.js +279 -0
- package/packages/npm-check.js +57 -3
- package/packages/npm-signing.js +219 -0
- package/packages/openapi.yaml +207 -1
- package/packages/package-crawler.js +322 -41
- package/packages/packages-template.html +6 -8
- package/packages/packages.js +604 -57
- package/packages/readme.md +113 -1
- package/publisher/publisher-template.html +2 -5
- package/publisher/publisher.js +121 -110
- package/publisher/readme.md +75 -0
- package/registry/crawler.js +84 -108
- package/registry/documentation/configuration.md +34 -0
- package/registry/registry-template.html +5 -8
- package/registry/registry.js +5 -4
- package/root-bare-template.html +7 -4
- package/root-template.html +5 -8
- package/server.js +398 -198
- package/shl/shl.js +38 -37
- package/static/a11y/a11y-check.js +832 -0
- package/static/a11y/axe.min.js +12 -0
- package/static/assets/css/bootstrap-fhir.css +15 -15
- package/static/assets/images/flags/au.svg +18 -0
- package/static/assets/images/flags/unknown.svg +11 -0
- package/static/assets/images/flags/us.svg +63 -0
- package/static/assets/images/icon_audit.png +0 -0
- package/static/assets/images/icon_clinical.png +0 -0
- package/static/assets/images/icon_document.png +0 -0
- package/static/assets/images/icon_external.png +0 -0
- package/static/assets/images/icon_medication.png +0 -0
- package/static/assets/images/icon_organization.png +0 -0
- package/static/assets/images/icon_other.png +0 -0
- package/static/assets/images/icon_person.png +0 -0
- package/static/assets/images/icon_section.png +0 -0
- package/stats-db.js +45 -0
- package/stats.js +197 -10
- package/testing/testing-template.html +5 -8
- package/token/nginx_passport_setup.md +1 -1
- package/token/security_guide.md +1 -1
- package/token/token-template.html +2 -5
- package/token/token.js +40 -36
- package/translations/Messages.properties +19 -0
- package/tx/README.md +154 -4
- package/tx/closure/closure-store.js +126 -3
- package/tx/cm/cm-database.js +69 -57
- package/tx/cm/cm-package.js +29 -1
- package/tx/cs/cs-api.js +284 -19
- package/tx/cs/cs-api.md +41 -4
- package/tx/cs/cs-areacode.js +1 -1
- package/tx/cs/cs-country.js +17 -5
- package/tx/cs/cs-cpt.js +33 -16
- package/tx/cs/cs-cs.js +148 -14
- package/tx/cs/cs-currency.js +1 -1
- package/tx/cs/cs-icd11.js +21 -13
- package/tx/cs/cs-icf.js +22 -3
- package/tx/cs/cs-lang.js +2 -2
- package/tx/cs/cs-loinc.js +185 -66
- package/tx/cs/cs-mimetypes.js +12 -2
- package/tx/cs/cs-ndc.js +40 -25
- package/tx/cs/cs-omop.js +54 -29
- package/tx/cs/cs-rxnorm.js +300 -196
- package/tx/cs/cs-snomed.js +608 -64
- package/tx/cs/cs-ucum.js +14 -4
- package/tx/cs/cs-unii.js +18 -14
- package/tx/cs/cs-virtual.js +210 -0
- package/tx/ecosystem-resolver.js +413 -0
- package/tx/html/cluster-dash-metrics.liquid +26 -0
- package/tx/html/cluster-home-metrics.liquid +31 -0
- package/tx/html/cluster-metrics-script.liquid +251 -0
- package/tx/html/codesystem-operations.liquid +3 -3
- package/tx/html/ecl-panel.liquid +1 -1
- package/tx/html/external-metrics.liquid +17 -0
- package/tx/html/operations-form.liquid +17 -17
- package/tx/html/search-form.liquid +11 -11
- package/tx/html/status-line.liquid +58 -0
- package/tx/html/status-panel.liquid +140 -0
- package/tx/html/status-tasks.liquid +25 -0
- package/tx/html/tx-template.html +2 -5
- package/tx/importers/atc-to-fhir.js +17 -16
- package/tx/importers/export-icd11-codesystem.js +3 -2
- package/tx/importers/import-icd11.js +4 -3
- package/tx/importers/import-loinc.module.js +118 -56
- package/tx/importers/import-ndc.module.js +56 -44
- package/tx/importers/import-rxnorm.module.js +118 -79
- package/tx/importers/import-sct.module.js +57 -58
- package/tx/importers/import-unii.module.js +37 -36
- package/tx/importers/subset-loinc.module.js +49 -48
- package/tx/importers/subset-rxnorm.module.js +29 -28
- package/tx/importers/terminal.js +18 -0
- package/tx/importers/tx-import-base.js +13 -12
- package/tx/importers/tx-import-settings.js +5 -4
- package/tx/importers/tx-import.js +53 -52
- package/tx/library/canonical-resource.js +49 -0
- package/tx/library/codesystem.js +94 -5
- package/tx/library/designations.js +45 -2
- package/tx/library/extensions.js +13 -7
- package/tx/library/implicit-valuesets.js +66 -0
- package/tx/library/renderer.js +77 -21
- package/tx/library/sqlite-utilities.js +2 -0
- package/tx/library/ucum-parsers.js +11 -1
- package/tx/library/wait-for-cache.js +43 -0
- package/tx/library.js +321 -22
- package/tx/openapi-operations.js +6 -4
- package/tx/operation-context.js +460 -99
- package/tx/params.js +25 -0
- package/tx/problems.js +3 -5
- package/tx/provider.js +278 -42
- package/tx/resource-cache-sqlite.js +1050 -0
- package/tx/sct/ecl.js +47 -6
- package/tx/sct/editions.js +39 -0
- package/tx/sct/expressions.js +10 -1
- package/tx/sct/load-config.js +107 -0
- package/tx/sct/mmap.js +109 -0
- package/tx/sct/structures.js +166 -13
- package/tx/sct/text-index.js +5 -1
- package/tx/syndication/syndication-feed.js +2 -2
- package/tx/tx-html.js +44 -8
- package/tx/tx.fhir.org.yml +8 -8
- package/tx/tx.js +282 -80
- package/tx/usage-tracker.js +39 -11
- package/tx/vs/vs-api.js +19 -0
- package/tx/vs/vs-database.js +386 -62
- package/tx/vs/vs-package.js +27 -4
- package/tx/vs/vs-vsac.js +648 -100
- package/tx/workers/batch-validate.js +37 -10
- package/tx/workers/batch.js +6 -1
- package/tx/workers/cache-control.js +15 -4
- package/tx/workers/closure.js +156 -13
- package/tx/workers/compare.js +8 -5
- package/tx/workers/expand.js +1173 -289
- package/tx/workers/metadata.js +35 -0
- package/tx/workers/search.js +53 -1
- package/tx/workers/supplement-filters.js +145 -0
- package/tx/workers/translate.js +169 -4
- package/tx/workers/validate.js +223 -119
- package/tx/workers/worker.js +123 -31
- package/tx/xversion/xv-valueset.js +7 -4
- package/utilities/submit-test-report.js +3 -1
- package/utilities/tx-stress-test.js +482 -0
- package/vcl/readme.md +37 -0
- package/vcl/vcl-parser.js +15 -0
- package/xig/readme.md +52 -0
- package/xig/xig-template.html +5 -8
- package/xig/xig.js +238 -422
- package/changelog-template.md +0 -22
- package/registry/test-runner.js +0 -237
- package/release.sh +0 -80
- package/test-scripts/repro-re2-wasm-leak.js +0 -34
- package/tsconfig.eslint.json +0 -9
- package/tx/html/dash-metrics.liquid +0 -147
- package/tx/html/home-metrics.liquid +0 -225
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process memory guard - the one place that decides how close to its memory limit the
|
|
3
|
+
* server is, so the request gate (503) and the in-operation check (deadCheck, 422) agree.
|
|
4
|
+
*
|
|
5
|
+
* The limit is the cgroup limit the kernel will enforce (MemoryMax on the systemd unit, or
|
|
6
|
+
* the container limit); with none, the machine's total RAM. Two thresholds, as fractions of
|
|
7
|
+
* the limit, both measured against process RSS (with a role, the cgroup's private memory: see
|
|
8
|
+
* below):
|
|
9
|
+
*
|
|
10
|
+
* - rejectFraction: new requests get 503 + Retry-After before any work (or body parsing)
|
|
11
|
+
* is done. Shedding load early is what gives in-flight work and the GC room to recover.
|
|
12
|
+
* - abortFraction: operations already running are aborted at their next deadCheck (422
|
|
13
|
+
* too-costly). Higher than rejectFraction, so the gate acts first.
|
|
14
|
+
*
|
|
15
|
+
* Both are configurable in config.server.memory: { "rejectFraction": 0.85, "abortFraction": 0.92 }
|
|
16
|
+
*
|
|
17
|
+
* While refusing, the guard looks now and then for memory it can get back: a forced last-resort
|
|
18
|
+
* GC (the JS heap, and the freed pages V8 keeps for reuse), then malloc_trim (memory the C
|
|
19
|
+
* allocator holds free - closed SQLite connections, above all; library/native-memory.js), if
|
|
20
|
+
* "trimAllocator" is true: by default only with a role (library/role.js), so a server with no
|
|
21
|
+
* role only forces the GC, as before.
|
|
22
|
+
*
|
|
23
|
+
* A master and its tx-workers (--role, cluster.md) share one cgroup, and the kernel's limit is
|
|
24
|
+
* on all of them together, so with a role (config.server.memory.group) the measure is the
|
|
25
|
+
* group's: the cgroup's private memory (memory.stat anon), read by each process itself. Every
|
|
26
|
+
* process refuses new requests above rejectFraction; above abortFraction every process aborts
|
|
27
|
+
* the operations it has running. Not only the largest: a process's size doesn't say whether it
|
|
28
|
+
* is busy, and a large idle one (the master, say) would leave a worker's runaway $expand to the
|
|
29
|
+
* kernel's OOM killer. This needs a cgroup with a limit of its own (MemoryMax on the unit or
|
|
30
|
+
* scope, a container limit); without one each process measures its own RSS, as one does.
|
|
31
|
+
*
|
|
32
|
+
* @module library/memory-guard
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const fs = require('fs');
|
|
36
|
+
const os = require('os');
|
|
37
|
+
const v8 = require('v8');
|
|
38
|
+
const vm = require('vm');
|
|
39
|
+
const path = require('path');
|
|
40
|
+
const { readCgroupMemoryLimit, ownCgroupDir, readCgroupAnon, readCgroupProcessRss } = require('./cgroup-memory');
|
|
41
|
+
const { trimNativeMemory, canTrimNativeMemory } = require('./native-memory');
|
|
42
|
+
|
|
43
|
+
const DEFAULT_REJECT_FRACTION = 0.85;
|
|
44
|
+
// malloc_trim (trimAllocator): off unless asked for, so a plain server is as it was. With a
|
|
45
|
+
// role, library/role.js turns it on (ROLE_TRIM_ALLOCATOR) unless the config says false
|
|
46
|
+
const DEFAULT_TRIM_ALLOCATOR = false;
|
|
47
|
+
const DEFAULT_ABORT_FRACTION = 0.92;
|
|
48
|
+
// RSS is cheap to read, but the gate runs on every request; a sample this old is fine
|
|
49
|
+
const SAMPLE_MS = 250;
|
|
50
|
+
// While refusing, look for memory to get back (a forced GC, then malloc_trim) at most this
|
|
51
|
+
// often. Refusing requests removes the allocation activity that would otherwise trigger a GC,
|
|
52
|
+
// and nothing returns the C allocator's free memory by itself, so without this RSS might never
|
|
53
|
+
// fall back below the threshold and the server would stay shut.
|
|
54
|
+
const GC_INTERVAL_MS = 5000;
|
|
55
|
+
// A look that gets back less than a tenth of the excess doubles the wait before the next, up
|
|
56
|
+
// to this: when most of RSS is in use (the SNOMED and LOINC cache buffers), they free little,
|
|
57
|
+
// and every full GC stops the process. One that gets back more keeps the wait: closing
|
|
58
|
+
// thousands of SQLite connections takes seconds, and the next look gets back the rest
|
|
59
|
+
const GC_MAX_INTERVAL_MS = 60000;
|
|
60
|
+
// Between those looks, malloc_trim alone, before a request is refused, at most this often and
|
|
61
|
+
// with no back-off: memory used in a burst is freed when the burst ends, and the C allocator
|
|
62
|
+
// keeps it resident until asked, so a quiet server could sit just over the threshold and
|
|
63
|
+
// refuse everything until the next look, up to GC_MAX_INTERVAL_MS away. With nothing to give
|
|
64
|
+
// back, a trim takes well under a millisecond (about 20 us measured, on a 3.3 GB heap)
|
|
65
|
+
const TRIM_INTERVAL_MS = 1000;
|
|
66
|
+
// The forced GC's form. A plain gc() collects the JS heap but keeps the pages it frees for
|
|
67
|
+
// reuse, resident and outside heapTotal, so they still count against the limit: after bursts of
|
|
68
|
+
// value set searches, a server held 2.7 GB of anon memory with a 1.0 GB heap, and plain GCs gave
|
|
69
|
+
// back nothing. A last-resort GC gives those pages back (that server: 1.4 GB, 1.1 GB with
|
|
70
|
+
// malloc_trim). "flavor" is in V8 12 (Node 22) on; Node 18 took the object for a lesser GC than
|
|
71
|
+
// a plain gc(), so before V8 12, plain gc()
|
|
72
|
+
const LAST_RESORT_GC = Number(process.versions.v8.split('.')[0]) >= 12;
|
|
73
|
+
const LAST_RESORT_GC_ARG = { type: 'major', execution: 'sync', flavor: 'last-resort' };
|
|
74
|
+
// the group measure reads a file: the in-operation check (about ten a request) reuses one
|
|
75
|
+
// at most this old
|
|
76
|
+
const GROUP_SAMPLE_MS = 100;
|
|
77
|
+
// RssFile (/proc/self/status) is read at most this often. The in-operation check runs many
|
|
78
|
+
// times a request, and a read of /proc/self/status costs twice what the RSS itself does;
|
|
79
|
+
// between reads, a fresh RSS less the last RssFile is a close enough estimate
|
|
80
|
+
const FILE_SAMPLE_MS = 200;
|
|
81
|
+
// the in-operation check reuses a measure at most this old. It runs about ten times a request,
|
|
82
|
+
// and RSS moves little in 10 ms
|
|
83
|
+
const ABORT_SAMPLE_MS = 10;
|
|
84
|
+
|
|
85
|
+
const state = {
|
|
86
|
+
limit: 0,
|
|
87
|
+
source: null,
|
|
88
|
+
rejectFraction: DEFAULT_REJECT_FRACTION,
|
|
89
|
+
abortFraction: DEFAULT_ABORT_FRACTION,
|
|
90
|
+
rejectAt: 0,
|
|
91
|
+
abortAt: 0,
|
|
92
|
+
trimAllocator: DEFAULT_TRIM_ALLOCATOR,
|
|
93
|
+
lastResortGc: LAST_RESORT_GC, // the forced GC's form (see LAST_RESORT_GC)
|
|
94
|
+
sampleTime: 0,
|
|
95
|
+
sampleRss: 0,
|
|
96
|
+
// a role (master, tx-worker): the cgroup measured, from ownCgroupDir; else null
|
|
97
|
+
group: null,
|
|
98
|
+
groupWanted: false,
|
|
99
|
+
// set by measureExcludingFileBacked()
|
|
100
|
+
excludeFileBacked: false,
|
|
101
|
+
fileTime: 0,
|
|
102
|
+
fileRss: 0 // the last RssFile read (see fileBackedRss)
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
function fraction(v, dflt) {
|
|
106
|
+
return (typeof v === 'number' && v > 0 && v <= 1) ? v : dflt;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The limit and the two thresholds for config.server.memory, and with opts.group the cgroup to
|
|
111
|
+
* measure (one with the limit on it, else null). configure() uses it, and so does the launcher
|
|
112
|
+
* (cluster.js), so both agree on where the threshold is.
|
|
113
|
+
* @param {object} [opts] - { rejectFraction, abortFraction, group, limit and cgroup (tests) }
|
|
114
|
+
*/
|
|
115
|
+
function thresholds(opts = {}) {
|
|
116
|
+
let limit = opts.limit;
|
|
117
|
+
let source = limit ? 'configured' : null;
|
|
118
|
+
let cg = null;
|
|
119
|
+
if (!limit) {
|
|
120
|
+
cg = readCgroupMemoryLimit();
|
|
121
|
+
limit = cg.limit;
|
|
122
|
+
source = cg.source;
|
|
123
|
+
}
|
|
124
|
+
let group = null;
|
|
125
|
+
if (opts.group) {
|
|
126
|
+
// only a cgroup that is the group's own, with the limit on it
|
|
127
|
+
const own = opts.cgroup !== undefined ? opts.cgroup : ownCgroupDir();
|
|
128
|
+
const limitFile = opts.cgroup !== undefined ? null : (cg || readCgroupMemoryLimit()).source;
|
|
129
|
+
if (own && (opts.cgroup !== undefined || (limitFile && path.dirname(limitFile) === own.dir)) && readCgroupAnon(own) > 0) {
|
|
130
|
+
group = own;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (!limit) {
|
|
134
|
+
limit = os.totalmem();
|
|
135
|
+
source = 'os.totalmem()';
|
|
136
|
+
}
|
|
137
|
+
const rejectFraction = fraction(opts.rejectFraction, DEFAULT_REJECT_FRACTION);
|
|
138
|
+
const abortFraction = fraction(opts.abortFraction, DEFAULT_ABORT_FRACTION);
|
|
139
|
+
return { limit, source, group, rejectFraction, abortFraction, rejectAt: limit * rejectFraction, abortAt: limit * abortFraction };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* (Re)initialise. Called once at startup from server.js with config.server.memory; the module
|
|
144
|
+
* also self-initialises with defaults so code that loads without server.js (tests, tools) works.
|
|
145
|
+
* @param {object} [opts] - as thresholds(), and trimAllocator
|
|
146
|
+
*/
|
|
147
|
+
function configure(opts = {}) {
|
|
148
|
+
Object.assign(state, thresholds(opts));
|
|
149
|
+
state.groupWanted = !!opts.group;
|
|
150
|
+
state.trimAllocator = (opts.trimAllocator ?? DEFAULT_TRIM_ALLOCATOR) === true;
|
|
151
|
+
state.sampleTime = 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Measure RSS less its file-backed pages (Linux only). With SNOMED CT cache files memory-mapped
|
|
156
|
+
* ("snomed": {"load": "mmap"} in config.json) RSS includes every mapped page that has been read, which the kernel drops
|
|
157
|
+
* and re-reads as it needs to - counting them would refuse requests on a server that is fine.
|
|
158
|
+
*/
|
|
159
|
+
function measureExcludingFileBacked() {
|
|
160
|
+
state.excludeFileBacked = process.platform === 'linux';
|
|
161
|
+
state.sampleTime = 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* RssFile as last read, read again if that was FILE_SAMPLE_MS ago or more, or if the sample
|
|
166
|
+
* has been dropped (sampleTime 0: configure, measureExcludingFileBacked, a test).
|
|
167
|
+
*/
|
|
168
|
+
function fileBackedRss() {
|
|
169
|
+
const now = Date.now();
|
|
170
|
+
if (state.sampleTime === 0 || now - state.fileTime >= FILE_SAMPLE_MS) {
|
|
171
|
+
state.fileRss = rssFileBytes();
|
|
172
|
+
state.fileTime = now;
|
|
173
|
+
}
|
|
174
|
+
return state.fileRss;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function rssFileBytes() {
|
|
178
|
+
try {
|
|
179
|
+
const m = /^RssFile:\s+(\d+) kB/m.exec(fs.readFileSync('/proc/self/status', 'utf8'));
|
|
180
|
+
return m ? Number(m[1]) * 1024 : 0;
|
|
181
|
+
} catch {
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** This process's RSS, less reclaimable file-backed pages when using mapped files. */
|
|
187
|
+
function ownRss() {
|
|
188
|
+
return process.memoryUsage.rss() - (state.excludeFileBacked ? fileBackedRss() : 0);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function rss(fresh = false) {
|
|
192
|
+
const now = Date.now();
|
|
193
|
+
if (fresh || now - state.sampleTime > SAMPLE_MS) {
|
|
194
|
+
state.sampleRss = (state.group && readCgroupAnon(state.group)) || ownRss();
|
|
195
|
+
state.sampleTime = now;
|
|
196
|
+
}
|
|
197
|
+
return state.sampleRss;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** True if new requests should be refused (uses a sample at most SAMPLE_MS old). */
|
|
201
|
+
function shouldReject() {
|
|
202
|
+
return state.rejectAt > 0 && rss() > state.rejectAt;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let gcFn;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* global.gc if node was started with --expose-gc, which is how it should be started (the
|
|
209
|
+
* Dockerfile and npm start do; add it to the node command of a systemd unit). Otherwise
|
|
210
|
+
* expose it at runtime: the flag can be set late, and a fresh context then has gc - this
|
|
211
|
+
* works, but Node does not promise it will keep working. null if neither works.
|
|
212
|
+
*/
|
|
213
|
+
function getGc() {
|
|
214
|
+
if (gcFn !== undefined) {
|
|
215
|
+
return gcFn;
|
|
216
|
+
}
|
|
217
|
+
gcFn = null;
|
|
218
|
+
if (typeof global.gc === 'function') {
|
|
219
|
+
gcFn = global.gc;
|
|
220
|
+
} else {
|
|
221
|
+
try {
|
|
222
|
+
v8.setFlagsFromString('--expose-gc');
|
|
223
|
+
const fn = vm.runInNewContext('gc');
|
|
224
|
+
if (typeof fn === 'function') {
|
|
225
|
+
gcFn = fn;
|
|
226
|
+
}
|
|
227
|
+
} catch {
|
|
228
|
+
// no gc available - the guard still works, it just recovers more slowly
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return gcFn;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let aborts = 0;
|
|
235
|
+
let lastAbortLog = 0;
|
|
236
|
+
let abortLog = null;
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Current RSS in bytes if running operations should be aborted, else 0. A measure at most
|
|
240
|
+
* ABORT_SAMPLE_MS old (GROUP_SAMPLE_MS for a group; file-backed pages are read at most
|
|
241
|
+
* FILE_SAMPLE_MS ago), or fresh
|
|
242
|
+
* if the sample was dropped (configure, measureExcludingFileBacked).
|
|
243
|
+
* A trip is written to the server log (at most once a minute, with a running count) - the
|
|
244
|
+
* caller's own message only goes to the aborted operation's diagnostics.
|
|
245
|
+
* @param {string} [place] - where the check was made, for the log
|
|
246
|
+
*/
|
|
247
|
+
function overAbortThreshold(place = 'unknown') {
|
|
248
|
+
if (state.abortAt <= 0) {
|
|
249
|
+
return 0;
|
|
250
|
+
}
|
|
251
|
+
const r = rss(Date.now() - state.sampleTime > (state.group ? GROUP_SAMPLE_MS : ABORT_SAMPLE_MS));
|
|
252
|
+
if (r <= state.abortAt) {
|
|
253
|
+
return 0;
|
|
254
|
+
}
|
|
255
|
+
aborts++;
|
|
256
|
+
const now = Date.now();
|
|
257
|
+
if (now - lastAbortLog > 60000) {
|
|
258
|
+
lastAbortLog = now;
|
|
259
|
+
try {
|
|
260
|
+
if (!abortLog) {
|
|
261
|
+
abortLog = require('./logger').getInstance().child({ module: 'memory' });
|
|
262
|
+
}
|
|
263
|
+
abortLog.warn(`Memory critical: ${gb(r)} GB of ${gb(state.limit)} GB - aborting running operations (at ${place}; ${aborts} aborted so far)`);
|
|
264
|
+
} catch {
|
|
265
|
+
// logging must never stop the abort
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return r;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function gb(bytes) {
|
|
272
|
+
return (bytes / 1024 / 1024 / 1024).toFixed(1);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function describe() {
|
|
276
|
+
const group = !state.groupWanted ? '' : state.group
|
|
277
|
+
? `, for the whole cgroup ${state.group.dir} (its private memory)`
|
|
278
|
+
: ', measured as this process\'s RSS (no cgroup limit of the service\'s own to measure as a group)';
|
|
279
|
+
return `Memory limit: ${gb(state.limit)} GB (from ${state.source})${group}; ` +
|
|
280
|
+
(state.excludeFileBacked && !state.group ? 'measured as RSS less file-backed pages; ' : '') +
|
|
281
|
+
`new requests refused above ${gb(state.rejectAt)} GB (${Math.round(state.rejectFraction * 100)}%), ` +
|
|
282
|
+
`operations aborted above ${gb(state.abortAt)} GB (${Math.round(state.abortFraction * 100)}%); ` +
|
|
283
|
+
`while refusing, a forced ${state.lastResortGc ? 'last-resort ' : ''}GC` + (!state.trimAllocator ? ' (no malloc_trim: trimAllocator is off)'
|
|
284
|
+
: canTrimNativeMemory() ? ' and malloc_trim' : ' (malloc_trim not available here)');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Express middleware: 503 + Retry-After while RSS is over the reject threshold. Install it
|
|
289
|
+
* before the body parsers so a refused request costs nothing. While refusing, it looks for
|
|
290
|
+
* memory to get back now and then and re-measures - every gcInterval ms, backing off to
|
|
291
|
+
* gcMaxInterval while the looks get back little: a forced last-resort GC (where the runtime has
|
|
292
|
+
* only a plain one, not while the JS heap is too small for that to help), then malloc_trim
|
|
293
|
+
* (unless trimAllocator is false); and between those, malloc_trim alone at most every
|
|
294
|
+
* trimInterval ms, before refusing a request. It logs when refusing starts (then once a minute,
|
|
295
|
+
* with how long and how many) and when it stops.
|
|
296
|
+
* @param {object} [opts] - { log, exempt: [path prefixes never refused], retryAfter (seconds),
|
|
297
|
+
* gcInterval (ms), gcMaxInterval (ms), heapUsed (function returning bytes, for tests),
|
|
298
|
+
* gc (function, for tests: as global.gc),
|
|
299
|
+
* trim (function, for tests: as trimNativeMemory), trimInterval (ms), onReject (optional observation callback,
|
|
300
|
+
* called only when sending a 503) }
|
|
301
|
+
*/
|
|
302
|
+
function middleware(opts = {}) {
|
|
303
|
+
const exempt = opts.exempt || [];
|
|
304
|
+
const retryAfter = String(opts.retryAfter || 30);
|
|
305
|
+
const log = opts.log;
|
|
306
|
+
const gcInterval = opts.gcInterval || GC_INTERVAL_MS;
|
|
307
|
+
const gcMaxInterval = Math.max(opts.gcMaxInterval || GC_MAX_INTERVAL_MS, gcInterval);
|
|
308
|
+
const heapUsed = opts.heapUsed || (() => process.memoryUsage().heapUsed);
|
|
309
|
+
const trim = opts.trim || trimNativeMemory;
|
|
310
|
+
const trimInterval = opts.trimInterval || TRIM_INTERVAL_MS;
|
|
311
|
+
let gcWait = gcInterval; // the current wait between forced GCs (backs off, see below)
|
|
312
|
+
let refusingSince = 0; // when the current spell of refusing began (0 = not refusing)
|
|
313
|
+
let refused = 0; // refused in the current spell
|
|
314
|
+
let lastLog = 0;
|
|
315
|
+
let lastGc = 0;
|
|
316
|
+
let lastTrim = 0;
|
|
317
|
+
|
|
318
|
+
const secs = (ms) => Math.round(ms / 1000);
|
|
319
|
+
|
|
320
|
+
return (req, res, next) => {
|
|
321
|
+
if (exempt.some(p => req.path === p || req.path.startsWith(p + '/'))) {
|
|
322
|
+
return next();
|
|
323
|
+
}
|
|
324
|
+
let over = shouldReject();
|
|
325
|
+
const now = Date.now();
|
|
326
|
+
|
|
327
|
+
// Refusing starves the GC of the allocation that would normally trigger it, so while
|
|
328
|
+
// refusing, force a full collection now and then. A last-resort GC also gives back the
|
|
329
|
+
// pages V8 keeps for reuse, which no heap figure counts (0.9 GB of them under a 0.13 GB
|
|
330
|
+
// heapTotal, in one measure), so the heap's size says nothing of what it can free: it
|
|
331
|
+
// always runs, and the back-off bounds its cost. A plain gc() frees heap and nothing else,
|
|
332
|
+
// so with one, only if the JS heap is at least the amount over the threshold. Then
|
|
333
|
+
// malloc_trim: the C allocator keeps what is freed outside the JS heap (closed SQLite
|
|
334
|
+
// connections) resident until asked.
|
|
335
|
+
// Re-measure; a look that got back little doubles the wait before the next
|
|
336
|
+
if (over && now - lastGc >= gcWait) {
|
|
337
|
+
lastGc = now;
|
|
338
|
+
const before = state.sampleRss;
|
|
339
|
+
const excess = before - state.rejectAt;
|
|
340
|
+
const heap = heapUsed();
|
|
341
|
+
const gc = opts.gc || getGc();
|
|
342
|
+
// a group: every process refusing does this, so each need only free its share
|
|
343
|
+
const share = state.group ? Math.max(1, readCgroupProcessRss(state.group).size) : 1;
|
|
344
|
+
const done = [];
|
|
345
|
+
if (!state.lastResortGc && heap < excess / share) {
|
|
346
|
+
if (log) {
|
|
347
|
+
log.info(`Memory: no forced GC - the JS heap (${gb(heap)} GB) is less than the ${gb(excess)} GB over the threshold`);
|
|
348
|
+
}
|
|
349
|
+
} else if (gc) {
|
|
350
|
+
try {
|
|
351
|
+
if (state.lastResortGc) {
|
|
352
|
+
gc(LAST_RESORT_GC_ARG);
|
|
353
|
+
} else {
|
|
354
|
+
gc();
|
|
355
|
+
}
|
|
356
|
+
done.push(state.lastResortGc ? 'forced last-resort GC' : 'forced GC');
|
|
357
|
+
} catch {
|
|
358
|
+
// ignore
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (state.trimAllocator && trim() !== null) {
|
|
362
|
+
done.push('malloc_trim');
|
|
363
|
+
lastTrim = now;
|
|
364
|
+
}
|
|
365
|
+
if (done.length) {
|
|
366
|
+
const after = rss(true);
|
|
367
|
+
over = after > state.rejectAt;
|
|
368
|
+
gcWait = !over ? gcInterval : before - after >= excess / 10 ? gcWait : Math.min(gcWait * 2, gcMaxInterval);
|
|
369
|
+
if (log) {
|
|
370
|
+
log.info(`Memory: ${done.join(' and ')} while refusing - ${gb(before)} GB -> ${gb(after)} GB` +
|
|
371
|
+
(over ? `; next in ${secs(gcWait)}s` : ''));
|
|
372
|
+
}
|
|
373
|
+
} else {
|
|
374
|
+
gcWait = Math.min(gcWait * 2, gcMaxInterval);
|
|
375
|
+
}
|
|
376
|
+
} else if (over && state.trimAllocator && now - lastTrim >= trimInterval) {
|
|
377
|
+
lastTrim = now;
|
|
378
|
+
const before = state.sampleRss;
|
|
379
|
+
if (trim() !== null) {
|
|
380
|
+
const after = rss(true);
|
|
381
|
+
over = after > state.rejectAt;
|
|
382
|
+
if (log && (!over || before - after >= 0.1 * 2 ** 30)) {
|
|
383
|
+
log.info(`Memory: malloc_trim before refusing - ${gb(before)} GB -> ${gb(after)} GB`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (!over) {
|
|
389
|
+
gcWait = gcInterval;
|
|
390
|
+
if (refusingSince) {
|
|
391
|
+
if (log) {
|
|
392
|
+
log.warn(`Memory recovered: ${gb(state.sampleRss)} GB of ${gb(state.limit)} GB - accepting requests again after ${secs(now - refusingSince)}s (${refused} refused)`);
|
|
393
|
+
}
|
|
394
|
+
refusingSince = 0;
|
|
395
|
+
refused = 0;
|
|
396
|
+
}
|
|
397
|
+
return next();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (!refusingSince) {
|
|
401
|
+
refusingSince = now;
|
|
402
|
+
lastLog = 0;
|
|
403
|
+
}
|
|
404
|
+
refused++;
|
|
405
|
+
try {
|
|
406
|
+
opts.onReject?.();
|
|
407
|
+
} catch {
|
|
408
|
+
// Observability must never change whether this request is refused.
|
|
409
|
+
}
|
|
410
|
+
if (log && now - lastLog > 60000) {
|
|
411
|
+
log.warn(`Memory high: ${gb(state.sampleRss)} GB of ${gb(state.limit)} GB - refusing new requests with 503 for ${secs(now - refusingSince)}s (${refused} refused)`);
|
|
412
|
+
lastLog = now;
|
|
413
|
+
}
|
|
414
|
+
const text = `Server is under memory pressure (${gb(state.sampleRss)} GB of ${gb(state.limit)} GB); try again shortly`;
|
|
415
|
+
res.status(503);
|
|
416
|
+
res.setHeader('Retry-After', retryAfter);
|
|
417
|
+
res.setHeader('Connection', 'close');
|
|
418
|
+
res.setHeader('Content-Type', 'application/fhir+json');
|
|
419
|
+
res.end(JSON.stringify({
|
|
420
|
+
resourceType: 'OperationOutcome',
|
|
421
|
+
issue: [{ severity: 'error', code: 'transient', details: { text }, diagnostics: text }]
|
|
422
|
+
}));
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
configure();
|
|
427
|
+
|
|
428
|
+
module.exports = { thresholds, configure, measureExcludingFileBacked, shouldReject, overAbortThreshold, describe, middleware, gb, getGc, _state: state };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Return the C allocator's free memory to the OS: glibc malloc_trim(0), through the optional
|
|
3
|
+
* koffi FFI dependency that the SNOMED mapping uses (tx/sct/mmap.js).
|
|
4
|
+
*
|
|
5
|
+
* Why: SQLite connections (one per code system provider, and an operation can open thousands)
|
|
6
|
+
* are allocated by the C library's malloc in libuv's threads, in its per-thread arenas. When
|
|
7
|
+
* they close, glibc keeps the freed memory in those arenas, resident, and gives back only a
|
|
8
|
+
* free stretch at the very top of each. After a 20,000-entry LOINC $batch-validate-code was
|
|
9
|
+
* aborted, a worker held 4.7 GB of such free memory: no GC can touch it, so the memory guard
|
|
10
|
+
* stayed shut. malloc_trim(0) releases the free pages inside the arenas as well (it took that
|
|
11
|
+
* worker from 8.0 GB to 3.9 GB in half a second). It frees nothing that is in use.
|
|
12
|
+
*
|
|
13
|
+
* Linux with glibc only. Elsewhere (macOS, Windows, musl without malloc_trim), or without
|
|
14
|
+
* koffi, it does nothing and returns null.
|
|
15
|
+
*
|
|
16
|
+
* @module library/native-memory
|
|
17
|
+
*/
|
|
18
|
+
let trim; // undefined: not tried yet; null: not available
|
|
19
|
+
|
|
20
|
+
function load() {
|
|
21
|
+
if (trim === undefined) {
|
|
22
|
+
trim = null;
|
|
23
|
+
if (process.platform === 'linux') {
|
|
24
|
+
try {
|
|
25
|
+
trim = require('koffi').load(null).func('int malloc_trim(size_t pad)');
|
|
26
|
+
} catch {
|
|
27
|
+
// no koffi, or no malloc_trim in this C library
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return trim;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** @returns {boolean} whether trimNativeMemory() can do anything here */
|
|
35
|
+
function canTrimNativeMemory() {
|
|
36
|
+
return !!load();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** @returns {boolean|null} whether memory was released, or null if this can't be done here */
|
|
40
|
+
function trimNativeMemory() {
|
|
41
|
+
if (!load()) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
return trim(0) !== 0;
|
|
46
|
+
} catch {
|
|
47
|
+
trim = null;
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { trimNativeMemory, canTrimNativeMemory };
|
package/library/npm-audit.js
CHANGED
|
@@ -205,10 +205,16 @@ class NpmAudit {
|
|
|
205
205
|
* Dashboard line: this server's version, and whether there is a newer release
|
|
206
206
|
*/
|
|
207
207
|
renderRelease() {
|
|
208
|
+
const text = this.releaseText();
|
|
209
|
+
return text ? `<p>${text}</p>` : '';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** The release, as inline html ('' if the installed version isn't known) */
|
|
213
|
+
releaseText() {
|
|
208
214
|
if (!this.installedVersion) {
|
|
209
215
|
return '';
|
|
210
216
|
}
|
|
211
|
-
let html = `<
|
|
217
|
+
let html = `<strong>FHIRsmith release:</strong> this server runs ${escape(this.installedVersion)}`;
|
|
212
218
|
const cmp = compareVersions(this.installedVersion, this.latestVersion);
|
|
213
219
|
if (cmp === null) {
|
|
214
220
|
html += ' (the latest release could not be checked)';
|
|
@@ -218,7 +224,7 @@ class NpmAudit {
|
|
|
218
224
|
} else {
|
|
219
225
|
html += ', the latest release';
|
|
220
226
|
}
|
|
221
|
-
return html
|
|
227
|
+
return html;
|
|
222
228
|
}
|
|
223
229
|
|
|
224
230
|
/**
|
|
@@ -414,35 +420,55 @@ class NpmAudit {
|
|
|
414
420
|
/**
|
|
415
421
|
* Dashboard: always shown, so a check that is failing or switched off is visible too
|
|
416
422
|
*/
|
|
417
|
-
|
|
418
|
-
|
|
423
|
+
/**
|
|
424
|
+
* The release and the audit, as one line of inline html (the top of /dashboard): the advice
|
|
425
|
+
* and the findings are in renderFindings
|
|
426
|
+
*/
|
|
427
|
+
renderLine(now = Date.now()) {
|
|
419
428
|
if (!this.enabled) {
|
|
420
|
-
return
|
|
429
|
+
return '<strong>npm audit:</strong> disabled (npmAudit.enabled = false)';
|
|
430
|
+
}
|
|
431
|
+
const parts = [];
|
|
432
|
+
const release = this.releaseText();
|
|
433
|
+
if (release) {
|
|
434
|
+
parts.push(release);
|
|
421
435
|
}
|
|
422
|
-
html += this.renderRelease();
|
|
423
436
|
if (!this.checkedAt) {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
437
|
+
parts.push('<strong>npm audit:</strong> ' +
|
|
438
|
+
(this.lastError ? `<span style="color:#b00">could not be run: ${escape(this.lastError)}</span>` : 'not run yet'));
|
|
439
|
+
} else {
|
|
440
|
+
const colour = this.findings.length === 0 ? '#070' : (this.isSerious() ? '#b00' : '#b60');
|
|
441
|
+
let audit = `<strong>npm audit:</strong> <span style="color:${colour}">${escape(this.summary())}</span>` +
|
|
442
|
+
` in ${this.packageCount} packages, checked ${escape(describeAgo(this.checkedAt.getTime(), now))}`;
|
|
443
|
+
if (this.lastError) {
|
|
444
|
+
audit += ` <span style="color:#b00">(the latest check failed: ${escape(this.lastError)})</span>`;
|
|
445
|
+
}
|
|
446
|
+
parts.push(audit);
|
|
427
447
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
448
|
+
return parts.join(' · ');
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** The advice and the table of findings, when there are any ('' otherwise) */
|
|
452
|
+
renderFindings() {
|
|
453
|
+
if (!this.enabled || !this.checkedAt || this.findings.length === 0) {
|
|
454
|
+
return '';
|
|
433
455
|
}
|
|
434
|
-
html
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
html += `<tr><td>${escape(f.severity)}</td><td>${escape(f.name)}</td><td>${escape(f.installed.join(', '))}</td>` +
|
|
441
|
-
`<td>${escape(f.range)}</td><td>${link}</td></tr>`;
|
|
442
|
-
}
|
|
443
|
-
html += '</table>';
|
|
456
|
+
let html = `<p>${escape(this.advice())}</p>`;
|
|
457
|
+
html += '<table class="grid"><tr><th>Severity</th><th>Package</th><th>Installed</th><th>Vulnerable</th><th>Advisory</th></tr>';
|
|
458
|
+
for (const f of this.findings) {
|
|
459
|
+
const link = /^https:\/\//.test(f.url) ? `<a href="${escape(f.url)}">${escape(f.title || f.url)}</a>` : escape(f.title);
|
|
460
|
+
html += `<tr><td>${escape(f.severity)}</td><td>${escape(f.name)}</td><td>${escape(f.installed.join(', '))}</td>` +
|
|
461
|
+
`<td>${escape(f.range)}</td><td>${link}</td></tr>`;
|
|
444
462
|
}
|
|
445
|
-
return html + '</
|
|
463
|
+
return html + '</table>';
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* The dashboard's npm audit: one line (renderLine), and only when there are findings, the advice
|
|
468
|
+
* and the table. The home page's banner links here (#npm-audit)
|
|
469
|
+
*/
|
|
470
|
+
renderDashboard(now = Date.now()) {
|
|
471
|
+
return `<div id="npm-audit"><p>${this.renderLine(now)}</p>${this.renderFindings()}</div>`;
|
|
446
472
|
}
|
|
447
473
|
}
|
|
448
474
|
|
package/library/openapi-doc.js
CHANGED
|
@@ -429,10 +429,12 @@ const PAGE_STYLE = `<style>
|
|
|
429
429
|
.openapi-ref .try-body { max-height: 30em; overflow: auto; }
|
|
430
430
|
</style>`;
|
|
431
431
|
|
|
432
|
+
// Headings: the page this goes into is titled by the module's template with an <h2>, so the
|
|
433
|
+
// reference starts at <h3> (Endpoints, Schemas) - <h4> per operation and per schema, <h5> for
|
|
434
|
+
// the parts of an operation - and the API's title is not a heading of its own
|
|
432
435
|
function buildHtml(spec, BASE_PATH) {
|
|
433
436
|
let html = PAGE_STYLE + '<div class="openapi-ref">';
|
|
434
|
-
html += `<
|
|
435
|
-
html += `<p class="lead">${escape(spec.info.summary || '')}</p>`;
|
|
437
|
+
html += `<p class="lead"><strong>${escape(spec.info.title)} API</strong>${spec.info.summary ? ': ' + escape(spec.info.summary) : ''}</p>`;
|
|
436
438
|
html += `<p>Machine-readable description (OpenAPI ${escape(spec.openapi)}): ` +
|
|
437
439
|
`<a href="${BASE_PATH}/openapi.json">openapi.json</a> · ` +
|
|
438
440
|
`<a href="${BASE_PATH}/openapi.yaml">openapi.yaml</a></p>`;
|
|
@@ -441,7 +443,7 @@ function buildHtml(spec, BASE_PATH) {
|
|
|
441
443
|
html += `<p>See also: <a href="${escape(spec.externalDocs.url)}">${escape(spec.externalDocs.description || spec.externalDocs.url)}</a></p>`;
|
|
442
444
|
}
|
|
443
445
|
|
|
444
|
-
html += '<
|
|
446
|
+
html += '<h3>Endpoints</h3>';
|
|
445
447
|
// an index, when there are enough of them to need one
|
|
446
448
|
const all = Object.entries(spec.paths).flatMap(([p, item]) => METHODS.filter(m => item[m]).map(m => [p, m, item[m]]));
|
|
447
449
|
if (all.length > 12) {
|
|
@@ -464,7 +466,7 @@ function buildHtml(spec, BASE_PATH) {
|
|
|
464
466
|
}
|
|
465
467
|
first = false;
|
|
466
468
|
html += `<div class="panel panel-default" id="${escape(op.operationId || '')}"><div class="panel-heading">`;
|
|
467
|
-
html += `<span class="label label-primary">${method.toUpperCase()}</span> <code>${escape(BASE_PATH + p)}</code> — ${escape(op.summary || '')}
|
|
469
|
+
html += `<h4 class="panel-title"><span class="label label-primary">${method.toUpperCase()}</span> <code>${escape(BASE_PATH + p)}</code> — ${escape(op.summary || '')}</h4>`;
|
|
468
470
|
html += '</div><div class="panel-body">';
|
|
469
471
|
html += markdown(op.description);
|
|
470
472
|
html += renderParameters(spec, [...(item.parameters || []), ...(op.parameters || [])]);
|
|
@@ -474,7 +476,7 @@ function buildHtml(spec, BASE_PATH) {
|
|
|
474
476
|
}
|
|
475
477
|
}
|
|
476
478
|
|
|
477
|
-
html += '<hr/><
|
|
479
|
+
html += '<hr/><h3>Schemas</h3>';
|
|
478
480
|
for (const [name, schema] of Object.entries((spec.components && spec.components.schemas) || {})) {
|
|
479
481
|
html += renderSchema(name, schema);
|
|
480
482
|
}
|
|
@@ -351,6 +351,11 @@ class CIBuildClient {
|
|
|
351
351
|
|
|
352
352
|
class PackageManager {
|
|
353
353
|
totalDownloaded = 0;
|
|
354
|
+
/**
|
|
355
|
+
* {Set<string>} the package folders (id#version, in the cache folder) that fetch and
|
|
356
|
+
* fetchUrl have returned - what has been used, for pruning the rest (Library.pruneUnusedCaches)
|
|
357
|
+
*/
|
|
358
|
+
usedPackages = new Set();
|
|
354
359
|
|
|
355
360
|
/**
|
|
356
361
|
* @param {string[]} packageServers - Ordered list of package server URLs
|
|
@@ -377,6 +382,7 @@ class PackageManager {
|
|
|
377
382
|
// Check cache first
|
|
378
383
|
const cachedPath = await this.checkCache(packageId, resolvedVersion);
|
|
379
384
|
if (cachedPath) {
|
|
385
|
+
this.usedPackages.add(cachedPath);
|
|
380
386
|
return cachedPath;
|
|
381
387
|
}
|
|
382
388
|
|
|
@@ -386,6 +392,7 @@ class PackageManager {
|
|
|
386
392
|
this.totalDownloaded = this.totalDownloaded + packageData.length;
|
|
387
393
|
// Extract to cache
|
|
388
394
|
const extractedPath = await this.extractToCache(packageId, resolvedVersion, packageData);
|
|
395
|
+
this.usedPackages.add(extractedPath);
|
|
389
396
|
|
|
390
397
|
return extractedPath;
|
|
391
398
|
}
|
|
@@ -629,6 +636,7 @@ class PackageManager {
|
|
|
629
636
|
}
|
|
630
637
|
|
|
631
638
|
this.totalDownloaded = this.totalDownloaded + packageData.length;
|
|
639
|
+
this.usedPackages.add(finalName);
|
|
632
640
|
return finalName;
|
|
633
641
|
} catch (error) {
|
|
634
642
|
console.error(`Error fetching package from URL ${url}: ${error.message}`);
|