From ebacb4069a3b6fe515eb2d0b8edb57362bd74a24 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Mon, 29 Jun 2026 07:43:31 +1000 Subject: [PATCH 1/3] fixing missing await statements --- tx/library.js | 6 +++++- tx/library/renderer.js | 16 ++++++++-------- tx/workers/validate.js | 2 +- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/tx/library.js b/tx/library.js index c3325d06..0b5d2a56 100644 --- a/tx/library.js +++ b/tx/library.js @@ -481,7 +481,11 @@ class Library { throw new Error("Unable to load VSAC provider unless vsacCfg is provided in the configuration"); } let vsac = new VSACValueSetProvider(this.vsacCfg, this.stats); - vsac.initialize(); + // Intentionally not awaited: initialize() might run a full VSAC sync on first load, + // which can take minutes - we don't want to block startup. But attach a handler + // so a failed background sync surfaces as a log error rather than an unhandled + // promise rejection. + vsac.initialize().catch((err) => this.log.error(`VSAC initialization failed: ${err.message}`)); this.valueSetProviders.push(vsac); this.externalSources.push(vsac); //const mem = process.memoryUsage(); diff --git a/tx/library/renderer.js b/tx/library/renderer.js index 6653cfb0..c7dfb317 100644 --- a/tx/library/renderer.js +++ b/tx/library/renderer.js @@ -157,8 +157,8 @@ class Renderer { async renderMetadataTable(res, tbl, sourcePackage) { this.renderMetadataVersion(res, tbl); await this.renderMetadataProfiles(res, tbl); - this.renderMetadataTags(res, tbl); - this.renderMetadataLabels(res, tbl); + await this.renderMetadataTags(res, tbl); + await this.renderMetadataLabels(res, tbl); this.renderMetadataLastUpdated(res, tbl); this.renderMetadataSource(res, tbl); this.renderProperty(tbl, 'TEST_PLAN_LANG', res.language); @@ -221,32 +221,32 @@ class Renderer { } } - renderMetadataTags(res, tbl) { + async renderMetadataTags(res, tbl) { if (res.meta?.tag) { let tr = tbl.tr(); tr.td().b().tx(this.translate('GENERAL_PROF')); if (res.meta.tag.length > 1) { let ul = tr.td(); for (let u of res.meta.tag) { - this.renderCoding(ul.li(), u); + await this.renderCoding(ul.li(), u); } } else { - this.renderCoding(tr.td(), res.meta.tag[0]); + await this.renderCoding(tr.td(), res.meta.tag[0]); } } } - renderMetadataLabels(res, tbl) { + async renderMetadataLabels(res, tbl) { if (res.meta?.label) { let tr = tbl.tr(); tr.td().b().tx(this.translate('GENERAL_PROF')); if (res.meta.label.length > 1) { let ul = tr.td(); for (let u of res.meta.label) { - this.renderCodin(ul.li(), u); + await this.renderCoding(ul.li(), u); } } else { - this.renderCoding(tr.td(), res.meta.label[0]); + await this.renderCoding(tr.td(), res.meta.label[0]); } } } diff --git a/tx/workers/validate.js b/tx/workers/validate.js index 9bd91538..9fdd77f0 100644 --- a/tx/workers/validate.js +++ b/tx/workers/validate.js @@ -1203,7 +1203,7 @@ class ValueSetChecker { } } } else { - this.checkCanonicalStatusCS(path, op, prov, this.valueSet); + await this.checkCanonicalStatusCS(path, op, prov, this.valueSet); let ctxt = await prov.locate(c.code); if (!ctxt.context) { // message can never be populated in pascal? From 98a499cfb88a2fa529b2b04866b200eb8485d811 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Mon, 29 Jun 2026 09:02:33 +1000 Subject: [PATCH 2/3] fix unhandled promises --- .eslintrc.js | 27 ++++++++++++++++++++++++++- npmprojector/watcher.js | 2 +- packages/packages.js | 2 +- publisher/publisher.js | 16 ++++++++-------- registry/registry.js | 4 ++-- server.js | 5 ++++- token/token.js | 2 +- tsconfig.eslint.json | 9 +++++++++ xig/xig.js | 4 ++-- 9 files changed, 54 insertions(+), 17 deletions(-) create mode 100644 tsconfig.eslint.json diff --git a/.eslintrc.js b/.eslintrc.js index 5e96fb49..ffc427b9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,4 +1,5 @@ module.exports = { + root: true, env: { browser: true, commonjs: true, @@ -27,5 +28,29 @@ module.exports = { 'build/', 'coverage/', '*.min.js' + ], + // Type-aware rules to catch a forgotten `await`. They need TypeScript type info, so + // they run via @typescript-eslint with parserOptions.project, scoped to the source + // files in tsconfig.eslint.json. Test files and other excluded paths keep the plain + // parser so linting never errors on a file outside the TS program. + overrides: [ + { + files: ['**/*.js'], + excludedFiles: ['**/*.test.js', 'tests/**', 'static/**', 'coverage/**'], + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + project: './tsconfig.eslint.json' + }, + plugins: ['@typescript-eslint'], + rules: { + // A promise-returning call left unhandled is almost always a forgotten await. + '@typescript-eslint/no-floating-promises': 'error', + // A promise used where a sync value is expected (e.g. `if (asyncThing())`). + // Warn, not error: there are intentional fire-and-forget callbacks. + '@typescript-eslint/no-misused-promises': 'warn' + } + } ] -}; \ No newline at end of file +}; diff --git a/npmprojector/watcher.js b/npmprojector/watcher.js index cff65a9d..3f704e5d 100644 --- a/npmprojector/watcher.js +++ b/npmprojector/watcher.js @@ -313,7 +313,7 @@ class PackageWatcher { */ stop() { if (this.watcher) { - this.watcher.close(); + this.watcher.close().catch((err) => this.log.error('Error closing watcher: ' + err.message)); this.watcher = null; } if (this.debounceTimer) { diff --git a/packages/packages.js b/packages/packages.js index 842a4822..40052002 100644 --- a/packages/packages.js +++ b/packages/packages.js @@ -1712,7 +1712,7 @@ class PackagesModule { // Update download counts after successful response // Do this asynchronously to not delay the response setImmediate(() => { - this.incrementDownloadCounts(packageData.PackageVersionKey, id); + void this.incrementDownloadCounts(packageData.PackageVersionKey, id); // fire-and-forget; errors handled internally }); } catch (error) { diff --git a/publisher/publisher.js b/publisher/publisher.js index 236a90cc..4b8b222c 100644 --- a/publisher/publisher.js +++ b/publisher/publisher.js @@ -2665,14 +2665,14 @@ class PublisherModule { return colors[status] || 'secondary'; } - async logUserAction(userId, action, targetId, ipAddress) { - return new Promise((resolve) => { - this.db.run( - 'INSERT INTO user_actions (user_id, action, target_id, ip_address) VALUES (?, ?, ?, ?)', - [userId, action, targetId, ipAddress], - () => resolve() // Don't fail if logging fails - ); - }); + // Fire-and-forget audit logging: callers don't await this, and it must never fail a + // request, so it returns void and swallows any DB error in the callback. + logUserAction(userId, action, targetId, ipAddress) { + this.db.run( + 'INSERT INTO user_actions (user_id, action, target_id, ip_address) VALUES (?, ?, ?, ?)', + [userId, action, targetId, ipAddress], + () => {} // don't fail (or block) the request if audit logging fails + ); } getStatus() { diff --git a/registry/registry.js b/registry/registry.js index 0006b13f..ad5c190f 100644 --- a/registry/registry.js +++ b/registry/registry.js @@ -116,13 +116,13 @@ class RegistryModule { // Run initial crawl after a short delay setTimeout(() => { - this.performCrawl(); + this.performCrawl().catch((err) => this.logger.error('Registry crawl failed: ' + err.message)); }, 5000); // Set up periodic crawling this.stats.addTask("TxRegistry", `${intervalMinutes} min`); this.crawlInterval = setInterval(() => { - this.performCrawl(); + this.performCrawl().catch((err) => this.logger.error('Registry crawl failed: ' + err.message)); }, intervalMs); this.logger.info(`Started periodic crawl every ${intervalMinutes} minutes`); diff --git a/server.js b/server.js index 52626e38..6fde58aa 100644 --- a/server.js +++ b/server.js @@ -860,4 +860,7 @@ async function serveFhirsmithHome(req, res) { } // Start the server -startServer(); \ No newline at end of file +startServer().catch((err) => { + serverLog.error('Fatal error during startup:', err); + process.exit(1); +}); \ No newline at end of file diff --git a/token/token.js b/token/token.js index bb72717a..56194a72 100644 --- a/token/token.js +++ b/token/token.js @@ -528,7 +528,7 @@ class TokenModule { } if (userId) { - this.logSecurityEvent(userId, 'logout', req.ip, req.get('User-Agent'), {}); + void this.logSecurityEvent(userId, 'logout', req.ip, req.get('User-Agent'), {}); // fire-and-forget audit; errors handled internally } res.redirect('/token/login'); diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json new file mode 100644 index 00000000..a120da19 --- /dev/null +++ b/tsconfig.eslint.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "allowJs": true, "checkJs": false, "noEmit": true, + "moduleResolution": "node", "target": "es2022", "module": "commonjs", + "strict": false, "skipLibCheck": true + }, + "include": ["**/*.js"], + "exclude": ["node_modules", "static", "**/*.test.js", "tests", "coverage"] +} diff --git a/xig/xig.js b/xig/xig.js index 37dbd773..e3ebfcaf 100644 --- a/xig/xig.js +++ b/xig/xig.js @@ -3156,7 +3156,7 @@ async function initializeXigModule(stats, xigConfig) { if (!fs.existsSync(XIG_DB_PATH)) { xigLog.info('No existing XIG database found, triggering initial download'); setTimeout(() => { - updateXigDatabase(); + updateXigDatabase().catch((err) => xigLog.error('XIG database update failed: ' + err.message)); }, 5000); } @@ -3167,7 +3167,7 @@ async function initializeXigModule(stats, xigConfig) { // Note: This assumes we're called only when XIG is enabled if (xigConfig?.autoUpdate !== false) { cron.schedule('0 2 * * *', () => { - updateXigDatabase(); + updateXigDatabase().catch((err) => xigLog.error('XIG database update failed: ' + err.message)); }); } From 03fc3643ec04199a8e2e5f37fde15d6c1207ad7a Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Tue, 30 Jun 2026 03:04:59 +1000 Subject: [PATCH 3/3] lint fix --- xig/xig.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xig/xig.js b/xig/xig.js index e3ebfcaf..27c4fe31 100644 --- a/xig/xig.js +++ b/xig/xig.js @@ -977,7 +977,7 @@ async function fetchResourceRows(queryParams, offset = 0, limit = 200) { } function rowToObject(row, queryParams) { - const { ver, realm, auth, type, rt } = queryParams; + const { type } = queryParams; const packageObj = getPackage(row.PackageKey); const obj = {