fhirsmith 0.9.5 → 0.9.6
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/CHANGELOG.md +18 -0
- package/config-template.json +2 -1
- package/library/regex-utilities.js +49 -12
- package/package.json +2 -2
- package/publisher/publisher.js +4 -3
- package/publisher/task-draft.js +9 -4
- package/test-scripts/repro-re2-wasm-leak.js +8 -7
- package/tx/cs/cs-loinc.js +12 -12
- package/tx/cs/cs-omop.js +24 -23
- package/tx/cs/cs-unii.js +11 -11
- package/tx/operation-context.js +39 -0
- package/tx/problems.js +0 -4
- package/tx/provider.js +1 -0
- package/tx/tx.js +12 -0
- package/tx/workers/translate.js +1 -0
- package/tx/workers/worker.js +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,11 +6,29 @@ 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.6] - 2026-05-21
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- increase timeout when publishing IGs (for US Core)
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Replace re2-wasm with re2js in library/regex-utilities.js to eliminate the underlying WASM-heap leak
|
|
18
|
+
- Fix async problem loading OMOP
|
|
19
|
+
- Fix memory leaks
|
|
20
|
+
- Don't leak database connections
|
|
21
|
+
|
|
22
|
+
### Tx Conformance Statement
|
|
23
|
+
|
|
24
|
+
FHIRsmith passed all 1649 HL7 terminology service tests (modes tx.fhir.org+omop+general+snomed, tests v1.9.1, runner v6.9.7)
|
|
25
|
+
|
|
9
26
|
## [v0.9.5] - 2026-05-16
|
|
10
27
|
|
|
11
28
|
### Fixed
|
|
12
29
|
|
|
13
30
|
- Workaround for memory leak in re2-wasm library that reduces it's severity
|
|
31
|
+
- Replace re2-wasm with re2js in library/regex-utilities.js to eliminate the underlying WASM-heap leak
|
|
14
32
|
|
|
15
33
|
### Tx Conformance Statement
|
|
16
34
|
|
package/config-template.json
CHANGED
|
@@ -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,
|
|
@@ -1,27 +1,64 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fhirsmith",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.6",
|
|
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
|
-
"
|
|
64
|
+
"re2js": "^2.8.0",
|
|
65
65
|
"rimraf": "^5.0.10",
|
|
66
66
|
"sqlite3": "^5.1.7",
|
|
67
67
|
"tar": "^7.5.7",
|
package/publisher/publisher.js
CHANGED
|
@@ -649,13 +649,14 @@ class PublisherModule {
|
|
|
649
649
|
reject(error);
|
|
650
650
|
});
|
|
651
651
|
|
|
652
|
-
// Timeout
|
|
652
|
+
// Timeout configurable via publisher.igPublisherTimeoutMinutes (default: 60 minutes)
|
|
653
|
+
const timeoutMinutes = this.config.igPublisherTimeoutMinutes || 60;
|
|
653
654
|
const timeout = setTimeout(async () => {
|
|
654
655
|
java.kill();
|
|
655
656
|
logStream.end();
|
|
656
|
-
await this.logTaskMessage(taskId, 'error', 'IG Publisher timed out after
|
|
657
|
+
await this.logTaskMessage(taskId, 'error', 'IG Publisher timed out after ' + timeoutMinutes + ' minutes');
|
|
657
658
|
reject(new Error('IG Publisher timed out'));
|
|
658
|
-
},
|
|
659
|
+
}, timeoutMinutes * 60 * 1000);
|
|
659
660
|
|
|
660
661
|
java.on('close', () => {
|
|
661
662
|
clearTimeout(timeout);
|
package/publisher/task-draft.js
CHANGED
|
@@ -191,6 +191,7 @@ class DraftTaskProcessor {
|
|
|
191
191
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
192
192
|
});
|
|
193
193
|
|
|
194
|
+
// eslint-disable-next-line no-unused-vars
|
|
194
195
|
let stdout = '';
|
|
195
196
|
let stderr = '';
|
|
196
197
|
|
|
@@ -255,6 +256,7 @@ class DraftTaskProcessor {
|
|
|
255
256
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
256
257
|
});
|
|
257
258
|
|
|
259
|
+
// eslint-disable-next-line no-unused-vars
|
|
258
260
|
let stdout = '';
|
|
259
261
|
let stderr = '';
|
|
260
262
|
|
|
@@ -316,6 +318,7 @@ class DraftTaskProcessor {
|
|
|
316
318
|
});
|
|
317
319
|
|
|
318
320
|
let stdout = '';
|
|
321
|
+
// eslint-disable-next-line no-unused-vars
|
|
319
322
|
let stderr = '';
|
|
320
323
|
|
|
321
324
|
sushi.stdout.on('data', (data) => {
|
|
@@ -382,6 +385,7 @@ class DraftTaskProcessor {
|
|
|
382
385
|
logStream.write('Working Directory: ' + draftDir + '\n');
|
|
383
386
|
logStream.write('=====================================\n\n');
|
|
384
387
|
|
|
388
|
+
// eslint-disable-next-line no-unused-vars
|
|
385
389
|
let hasOutput = false;
|
|
386
390
|
let lastProgressUpdate = Date.now();
|
|
387
391
|
|
|
@@ -433,7 +437,8 @@ class DraftTaskProcessor {
|
|
|
433
437
|
reject(error);
|
|
434
438
|
});
|
|
435
439
|
|
|
436
|
-
// Timeout
|
|
440
|
+
// Timeout configurable via publisher.igPublisherTimeoutMinutes (default: 60 minutes)
|
|
441
|
+
const timeoutMinutes = this.config.igPublisherTimeoutMinutes || 60;
|
|
437
442
|
const timeout = setTimeout(async () => {
|
|
438
443
|
java.kill('SIGTERM'); // Try graceful shutdown first
|
|
439
444
|
|
|
@@ -442,11 +447,11 @@ class DraftTaskProcessor {
|
|
|
442
447
|
java.kill('SIGKILL');
|
|
443
448
|
}, 10000);
|
|
444
449
|
|
|
445
|
-
logStream.write('\nTIMEOUT: Process killed after
|
|
450
|
+
logStream.write('\nTIMEOUT: Process killed after ' + timeoutMinutes + ' minutes\n');
|
|
446
451
|
logStream.end();
|
|
447
|
-
await this.logTaskMessage(taskId, 'error', 'IG Publisher timed out after
|
|
452
|
+
await this.logTaskMessage(taskId, 'error', 'IG Publisher timed out after ' + timeoutMinutes + ' minutes');
|
|
448
453
|
reject(new Error('IG Publisher timed out'));
|
|
449
|
-
},
|
|
454
|
+
}, timeoutMinutes * 60 * 1000);
|
|
450
455
|
|
|
451
456
|
java.on('close', () => {
|
|
452
457
|
clearTimeout(timeout);
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
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
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
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
|
|
package/tx/cs/cs-loinc.js
CHANGED
|
@@ -1429,8 +1429,8 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
|
|
|
1429
1429
|
* @returns {Promise<Array>} Array of {code, display} objects
|
|
1430
1430
|
*/
|
|
1431
1431
|
async #getAnswerListConcepts(sourceKey) {
|
|
1432
|
-
|
|
1433
|
-
|
|
1432
|
+
const db = new sqlite3.Database(this.dbPath, sqlite3.OPEN_READONLY);
|
|
1433
|
+
try {
|
|
1434
1434
|
const sql = `
|
|
1435
1435
|
SELECT Code, Description
|
|
1436
1436
|
FROM Relationships, Codes
|
|
@@ -1439,17 +1439,17 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
|
|
|
1439
1439
|
AND Relationships.TargetKey = Codes.CodeKey
|
|
1440
1440
|
`;
|
|
1441
1441
|
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
reject(err);
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
code: row.Code
|
|
1448
|
-
}));
|
|
1449
|
-
resolve(concepts);
|
|
1450
|
-
}
|
|
1442
|
+
const rows = await new Promise((resolve, reject) => {
|
|
1443
|
+
db.all(sql, [sourceKey], (err, result) => {
|
|
1444
|
+
if (err) reject(err);
|
|
1445
|
+
else resolve(result);
|
|
1446
|
+
});
|
|
1451
1447
|
});
|
|
1452
|
-
|
|
1448
|
+
|
|
1449
|
+
return rows.map(row => ({ code: row.Code }));
|
|
1450
|
+
} finally {
|
|
1451
|
+
await new Promise((resolve) => db.close(() => resolve()));
|
|
1452
|
+
}
|
|
1453
1453
|
}
|
|
1454
1454
|
|
|
1455
1455
|
id() {
|
package/tx/cs/cs-omop.js
CHANGED
|
@@ -906,41 +906,42 @@ class OMOPServicesFactory extends CodeSystemFactoryProvider {
|
|
|
906
906
|
return new OMOPServices(opContext, supplements, db, this._sharedData);
|
|
907
907
|
}
|
|
908
908
|
|
|
909
|
-
static checkDB(dbPath) {
|
|
909
|
+
static async checkDB(dbPath) {
|
|
910
|
+
const fs = require('fs');
|
|
910
911
|
try {
|
|
911
|
-
const fs = require('fs');
|
|
912
|
-
|
|
913
|
-
// Check if file exists
|
|
914
912
|
if (!fs.existsSync(dbPath)) {
|
|
915
913
|
return 'Database file not found';
|
|
916
914
|
}
|
|
917
|
-
|
|
918
|
-
// Check file size
|
|
919
915
|
const stats = fs.statSync(dbPath);
|
|
920
916
|
if (stats.size < 1024) {
|
|
921
917
|
return 'Database file too small';
|
|
922
918
|
}
|
|
919
|
+
} catch (e) {
|
|
920
|
+
return `Database error: ${e.message}`;
|
|
921
|
+
}
|
|
923
922
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
try {
|
|
928
|
-
// Simple count query to verify database integrity
|
|
929
|
-
db.get('SELECT COUNT(*) as count FROM Concepts', (err) => {
|
|
930
|
-
if (err) {
|
|
931
|
-
db.close();
|
|
932
|
-
return 'Missing Tables - needs re-importing (by java)';
|
|
933
|
-
}
|
|
934
|
-
});
|
|
935
|
-
|
|
936
|
-
db.close();
|
|
937
|
-
return 'OK (check via provider for count)';
|
|
938
|
-
} catch (e) {
|
|
939
|
-
return 'Missing Tables - needs re-importing (by java)';
|
|
940
|
-
}
|
|
923
|
+
let db;
|
|
924
|
+
try {
|
|
925
|
+
db = new sqlite3.Database(dbPath, sqlite3.OPEN_READONLY);
|
|
941
926
|
} catch (e) {
|
|
942
927
|
return `Database error: ${e.message}`;
|
|
943
928
|
}
|
|
929
|
+
|
|
930
|
+
try {
|
|
931
|
+
// Simple count query to verify database integrity. If the Concepts
|
|
932
|
+
// table is missing, db.get rejects and we fall through to the catch.
|
|
933
|
+
const row = await new Promise((resolve, reject) => {
|
|
934
|
+
db.get('SELECT COUNT(*) as count FROM Concepts', (err, result) => {
|
|
935
|
+
if (err) reject(err);
|
|
936
|
+
else resolve(result);
|
|
937
|
+
});
|
|
938
|
+
});
|
|
939
|
+
return `OK (${row && row.count != null ? row.count : 0} Concepts)`;
|
|
940
|
+
} catch (_e) {
|
|
941
|
+
return 'Missing Tables - needs re-importing (by java)';
|
|
942
|
+
} finally {
|
|
943
|
+
await new Promise((resolve) => db.close(() => resolve()));
|
|
944
|
+
}
|
|
944
945
|
}
|
|
945
946
|
|
|
946
947
|
|
package/tx/cs/cs-unii.js
CHANGED
|
@@ -210,18 +210,18 @@ class UniiServicesFactory extends CodeSystemFactoryProvider {
|
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
async load() {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
this._version = row ? row.Version : 'unknown';
|
|
221
|
-
resolve(); // This resolves the Promise
|
|
222
|
-
}
|
|
213
|
+
const db = new sqlite3.Database(this.dbPath, sqlite3.OPEN_READONLY);
|
|
214
|
+
try {
|
|
215
|
+
const row = await new Promise((resolve, reject) => {
|
|
216
|
+
db.get('SELECT Version FROM UniiVersion', (err, result) => {
|
|
217
|
+
if (err) reject(new Error(err));
|
|
218
|
+
else resolve(result);
|
|
219
|
+
});
|
|
223
220
|
});
|
|
224
|
-
|
|
221
|
+
this._version = row ? row.Version : 'unknown';
|
|
222
|
+
} finally {
|
|
223
|
+
await new Promise((resolve) => db.close(() => resolve()));
|
|
224
|
+
}
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
defaultVersion() {
|
package/tx/operation-context.js
CHANGED
|
@@ -448,6 +448,11 @@ class OperationContext {
|
|
|
448
448
|
this.resourceCache = resourceCache;
|
|
449
449
|
this.expansionCache = expansionCache;
|
|
450
450
|
this.debugging = isDebugging();
|
|
451
|
+
// Providers opened during this operation that need their underlying
|
|
452
|
+
// resources (sqlite connections, etc.) released when the operation ends.
|
|
453
|
+
// Shared by reference with copy()'d contexts so a sub-operation's
|
|
454
|
+
// providers are cleaned up by the parent request's closeProviders().
|
|
455
|
+
this._openProviders = [];
|
|
451
456
|
|
|
452
457
|
this.timeTracker.step('tx-op');
|
|
453
458
|
}
|
|
@@ -476,6 +481,9 @@ class OperationContext {
|
|
|
476
481
|
newContext.logEntries = [...this.logEntries];
|
|
477
482
|
newContext.debugging = this.debugging;
|
|
478
483
|
newContext.usageTracker = this.usageTracker;
|
|
484
|
+
// Share the same provider-cleanup list so providers opened by the copy
|
|
485
|
+
// are released when the parent operation ends.
|
|
486
|
+
newContext._openProviders = this._openProviders;
|
|
479
487
|
return newContext;
|
|
480
488
|
}
|
|
481
489
|
|
|
@@ -624,6 +632,37 @@ class OperationContext {
|
|
|
624
632
|
return this.id;
|
|
625
633
|
}
|
|
626
634
|
|
|
635
|
+
/**
|
|
636
|
+
* Register a code-system provider whose resources (typically a sqlite
|
|
637
|
+
* connection opened by factory.build()) should be released when the
|
|
638
|
+
* operation ends. Providers without a close() method are ignored.
|
|
639
|
+
* @param {Object} provider - The provider returned from factory.build()
|
|
640
|
+
*/
|
|
641
|
+
registerProvider(provider) {
|
|
642
|
+
if (provider && typeof provider.close === 'function') {
|
|
643
|
+
this._openProviders.push(provider);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Close every provider registered during this operation. Safe to call
|
|
649
|
+
* multiple times — the list is cleared after the first call. Errors
|
|
650
|
+
* from individual close() calls are swallowed so one bad provider can't
|
|
651
|
+
* prevent the others from releasing their resources.
|
|
652
|
+
*/
|
|
653
|
+
async closeProviders() {
|
|
654
|
+
if (!this._openProviders || this._openProviders.length === 0) return;
|
|
655
|
+
const providers = this._openProviders;
|
|
656
|
+
this._openProviders = [];
|
|
657
|
+
for (const p of providers) {
|
|
658
|
+
try {
|
|
659
|
+
await p.close();
|
|
660
|
+
} catch (_e) {
|
|
661
|
+
// Swallow — provider cleanup is best-effort.
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
627
666
|
/**
|
|
628
667
|
* @type {Languages} languages specified in request
|
|
629
668
|
*/
|
package/tx/problems.js
CHANGED
|
@@ -2,10 +2,6 @@ const escape = require('escape-html');
|
|
|
2
2
|
|
|
3
3
|
class ProblemFinder {
|
|
4
4
|
|
|
5
|
-
constructor() {
|
|
6
|
-
this.map = new Map();
|
|
7
|
-
}
|
|
8
|
-
|
|
9
5
|
async scanValueSets(provider) {
|
|
10
6
|
let unknownVersions = {}; // system -> Set of versions not known to the server
|
|
11
7
|
for (let vsp of provider.valueSetProviders) {
|
package/tx/provider.js
CHANGED
package/tx/tx.js
CHANGED
|
@@ -306,6 +306,18 @@ class TXModule {
|
|
|
306
306
|
req.txI18n = this.i18n;
|
|
307
307
|
req.txLog = this.log;
|
|
308
308
|
|
|
309
|
+
// Release any code-system providers that opened sqlite connections
|
|
310
|
+
// during this request. closeProviders() is idempotent so it's safe
|
|
311
|
+
// for both events to fire. Listeners are sync; the close itself
|
|
312
|
+
// runs fire-and-forget on the event loop.
|
|
313
|
+
const releaseProviders = () => {
|
|
314
|
+
opContext.closeProviders().catch((err) => {
|
|
315
|
+
try { this.log.warn(`closeProviders failed: ${err && err.message}`); } catch (_) { /* ignore */ }
|
|
316
|
+
});
|
|
317
|
+
};
|
|
318
|
+
res.on('finish', releaseProviders);
|
|
319
|
+
res.on('close', releaseProviders);
|
|
320
|
+
|
|
309
321
|
// Add X-Request-Id header to response
|
|
310
322
|
res.setHeader('X-Request-Id', requestId);
|
|
311
323
|
|
package/tx/workers/translate.js
CHANGED
|
@@ -495,6 +495,7 @@ class TranslateWorker extends TerminologyWorker {
|
|
|
495
495
|
let result = false;
|
|
496
496
|
const factory = cm.jsonObj.internalSource;
|
|
497
497
|
let prov = await factory.build(this.opContext, []);
|
|
498
|
+
this.opContext.registerProvider(prov);
|
|
498
499
|
|
|
499
500
|
output.push({
|
|
500
501
|
name: 'used-system',
|
package/tx/workers/worker.js
CHANGED
|
@@ -184,6 +184,10 @@ class TerminologyWorker {
|
|
|
184
184
|
if (checkVer) {
|
|
185
185
|
this.checkVersion(url, provider.version(), params, provider.versionAlgorithm(), op);
|
|
186
186
|
}
|
|
187
|
+
// Track for lifecycle cleanup at end of request. Providers built from
|
|
188
|
+
// a factory open a fresh sqlite connection that needs releasing;
|
|
189
|
+
// resource-built providers and others without close() are no-ops.
|
|
190
|
+
this.opContext.registerProvider(provider);
|
|
187
191
|
}
|
|
188
192
|
|
|
189
193
|
return provider;
|