From 1eee68f6c8de2196810de9835c921fbee413e419 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Sat, 16 May 2026 10:40:47 +0200 Subject: [PATCH 1/9] replace re2-wasm with re2js --- CHANGELOG.md | 11 ++++++++ library/regex-utilities.js | 44 +++++++++++++++++++++++------ package-lock.json | 17 +++++------ package.json | 2 +- test-scripts/repro-re2-wasm-leak.js | 15 +++++----- 5 files changed, 62 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8416d813..86a5ad63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [v0.9.6] - 2026-XX-XX + +### Fixed + +- Replace re2-wasm with re2js in library/regex-utilities.js to eliminate the underlying WASM-heap leak + +### Tx Conformance Statement + +XXX + ## [v0.9.5] - 2026-05-16 ### Fixed - Workaround for memory leak in re2-wasm library that reduces it's severity +- Replace re2-wasm with re2js in library/regex-utilities.js to eliminate the underlying WASM-heap leak ### Tx Conformance Statement diff --git a/library/regex-utilities.js b/library/regex-utilities.js index c289e4d5..2592cbf5 100644 --- a/library/regex-utilities.js +++ b/library/regex-utilities.js @@ -1,4 +1,32 @@ -const { RE2 } = require('re2-wasm'); +const { RE2JS } = require('re2js'); + +// Translate a JS-style flags string ("i", "im", "is", etc.) into the bit-flag +// integer that RE2JS.compile() expects. Only the flags actually used by callers +// (and their natural counterparts) are mapped; anything else is ignored. +function toRE2JSFlags(flags) { + if (!flags) return 0; + let bits = 0; + if (flags.includes('i')) bits |= RE2JS.CASE_INSENSITIVE; + if (flags.includes('m')) bits |= RE2JS.MULTILINE; + if (flags.includes('s')) bits |= RE2JS.DOTALL; + // The 'u' flag was a re2-wasm workaround; re2js is pure JS and handles + // unicode natively, so it's a no-op here. + return bits; +} + +// Thin wrapper that exposes the only method the rest of the codebase uses +// against compiled regexes: a JS-RegExp-style `.test(input)` that returns +// true if the pattern is found anywhere in `input`. +class CompiledRegex { + constructor(pattern, flags) { + this._pattern = RE2JS.compile(pattern, toRE2JSFlags(flags)); + } + + test(input) { + if (input == null) return false; + return this._pattern.matcher(String(input)).find(); + } +} class RegExUtilities { @@ -7,15 +35,14 @@ class RegExUtilities { } compile(pattern, flags) { - // re2-wasm has a fixed 16 MB WASM heap with no real free(): every - // new RE2(...) permanently consumes a few KB. Cache by (pattern, flags) - // so the same regex compiles at most once. - // TODO: replace re2-wasm with native re2 to eliminate the underlying leak. - const re2Flags = flags && flags.includes('u') ? flags : (flags || '') + 'u'; - const key = pattern + '|' + re2Flags; + // re2js doesn't have re2-wasm's WASM-heap leak, so the cache here is + // purely a performance optimisation: identical (pattern, flags) pairs + // reuse the same compiled regex instead of paying the compile cost + // every call. + const key = pattern + '|' + (flags || ''); let compiled = this._cache.get(key); if (!compiled) { - compiled = new RE2(pattern, re2Flags); + compiled = new CompiledRegex(pattern, flags); this._cache.set(key, compiled); } return compiled; @@ -24,4 +51,3 @@ class RegExUtilities { } module.exports = new RegExUtilities(); - \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9e2e7092..639b550f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "fhirsmith", - "version": "0.9.4", + "version": "0.9.5", "license": "BSD-3", "dependencies": { "axios": "^1.13.4", @@ -40,7 +40,7 @@ "passport-github2": "^0.1.12", "passport-google-oauth20": "^2.0.0", "properties-file": "^3.6.4", - "re2-wasm": "^1.0.2", + "re2js": "^2.8.0", "rimraf": "^5.0.10", "sqlite3": "^5.1.7", "tar": "^7.5.7", @@ -8060,14 +8060,11 @@ "node": ">=0.10.0" } }, - "node_modules/re2-wasm": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/re2-wasm/-/re2-wasm-1.0.2.tgz", - "integrity": "sha512-VXUdgSiUrE/WZXn6gUIVVIsg0+Hp6VPZPOaHCay+OuFKy6u/8ktmeNEf+U5qSA8jzGGFsg8jrDNu1BeHpz2pJA==", - "license": "Apache-2.0", - "engines": { - "node": ">=10" - } + "node_modules/re2js": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/re2js/-/re2js-2.8.0.tgz", + "integrity": "sha512-HCjH9S3vdPyoYqv6++dZGa0RtG9xr7cAV00I0ymNHaKgiYOISsfTOMAj2UAP9miRWP+786AO/a4bIvtzTFOI1Q==", + "license": "MIT" }, "node_modules/react-is": { "version": "18.3.1", diff --git a/package.json b/package.json index 9afb5dda..0feecaea 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "passport-github2": "^0.1.12", "passport-google-oauth20": "^2.0.0", "properties-file": "^3.6.4", - "re2-wasm": "^1.0.2", + "re2js": "^2.8.0", "rimraf": "^5.0.10", "sqlite3": "^5.1.7", "tar": "^7.5.7", diff --git a/test-scripts/repro-re2-wasm-leak.js b/test-scripts/repro-re2-wasm-leak.js index 4742e2fe..5ac77e0e 100644 --- a/test-scripts/repro-re2-wasm-leak.js +++ b/test-scripts/repro-re2-wasm-leak.js @@ -1,14 +1,15 @@ -// Manual reproducer for the re2-wasm WASM heap leak. -// See library/regex-utilities.js for the workaround. +// Historical reproducer for the re2-wasm WASM heap leak. +// regex-utilities.js now sits on top of re2js (a pure-JS RE2 port), so the +// underlying leak is gone. Kept as a stress test for the compile cache: // // node test-scripts/repro-re2-wasm-leak.js # same-pattern stress // node test-scripts/repro-re2-wasm-leak.js --unique # unique-pattern stress // -// Without the cache, same-pattern OOMs at ~2965 iterations. -// With the cache, same-pattern runs indefinitely. -// Unique-pattern still OOMs (each pattern is a real compile and the underlying -// re2-wasm heap leak still applies). A proper fix is to replace re2-wasm with -// the native `re2` package. +// Background (re2-wasm era): without the cache, same-pattern OOMed at ~2965 +// iterations; with the cache, same-pattern ran indefinitely but unique-pattern +// still OOMed because every distinct pattern was a real compile and re2-wasm +// could not free its WASM heap. Under re2js both modes now run indefinitely +// (unique-pattern is bounded only by ordinary V8 heap growth from the cache). const re = require('../library/regex-utilities'); From ce1997873ed37ec38c0e72f9107de71c87ac457d Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Sat, 16 May 2026 12:02:12 +0200 Subject: [PATCH 2/9] fix leak --- tx/cs/cs-loinc.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tx/cs/cs-loinc.js b/tx/cs/cs-loinc.js index bf187c25..d6615d2c 100644 --- a/tx/cs/cs-loinc.js +++ b/tx/cs/cs-loinc.js @@ -1429,8 +1429,8 @@ class LoincServicesFactory extends CodeSystemFactoryProvider { * @returns {Promise} Array of {code, display} objects */ async #getAnswerListConcepts(sourceKey) { - return new Promise((resolve, reject) => { - let db = new sqlite3.Database(this.dbPath); + const db = new sqlite3.Database(this.dbPath, sqlite3.OPEN_READONLY); + try { const sql = ` SELECT Code, Description FROM Relationships, Codes @@ -1439,17 +1439,17 @@ class LoincServicesFactory extends CodeSystemFactoryProvider { AND Relationships.TargetKey = Codes.CodeKey `; - db.all(sql, [sourceKey], (err, rows) => { - if (err) { - reject(err); - } else { - const concepts = rows.map(row => ({ - code: row.Code - })); - resolve(concepts); - } + const rows = await new Promise((resolve, reject) => { + db.all(sql, [sourceKey], (err, result) => { + if (err) reject(err); + else resolve(result); + }); }); - }); + + return rows.map(row => ({ code: row.Code })); + } finally { + await new Promise((resolve) => db.close(() => resolve())); + } } id() { From 0d51a0a8726c00aee45ea1741312e46103f4a8e2 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Sat, 16 May 2026 13:24:54 +0200 Subject: [PATCH 3/9] fix async problem loading OMOP --- tests/cs/cs-omop.test.js | 4 ++-- tx/cs/cs-omop.js | 47 ++++++++++++++++++++-------------------- tx/cs/cs-unii.js | 22 +++++++++---------- 3 files changed, 37 insertions(+), 36 deletions(-) diff --git a/tests/cs/cs-omop.test.js b/tests/cs/cs-omop.test.js index 9bf71efc..b05cb996 100644 --- a/tests/cs/cs-omop.test.js +++ b/tests/cs/cs-omop.test.js @@ -94,8 +94,8 @@ describe('OMOP Provider', () => { expect(provider).toBeInstanceOf(OMOPServices); }); - test('should check database status', () => { - const status = OMOPServicesFactory.checkDB(testDbPath); + test('should check database status', async () => { + const status = await OMOPServicesFactory.checkDB(testDbPath); expect(status).toContain('OK'); }); diff --git a/tx/cs/cs-omop.js b/tx/cs/cs-omop.js index 866281c7..125cf153 100644 --- a/tx/cs/cs-omop.js +++ b/tx/cs/cs-omop.js @@ -906,41 +906,42 @@ class OMOPServicesFactory extends CodeSystemFactoryProvider { return new OMOPServices(opContext, supplements, db, this._sharedData); } - static checkDB(dbPath) { + static async checkDB(dbPath) { + const fs = require('fs'); try { - const fs = require('fs'); - - // Check if file exists if (!fs.existsSync(dbPath)) { return 'Database file not found'; } - - // Check file size const stats = fs.statSync(dbPath); if (stats.size < 1024) { return 'Database file too small'; } + } catch (e) { + return `Database error: ${e.message}`; + } - // Try to open database and check for required tables - const db = new sqlite3.Database(dbPath); - - try { - // Simple count query to verify database integrity - db.get('SELECT COUNT(*) as count FROM Concepts', (err) => { - if (err) { - db.close(); - return 'Missing Tables - needs re-importing (by java)'; - } - }); - - db.close(); - return 'OK (check via provider for count)'; - } catch (e) { - return 'Missing Tables - needs re-importing (by java)'; - } + let db; + try { + db = new sqlite3.Database(dbPath, sqlite3.OPEN_READONLY); } catch (e) { return `Database error: ${e.message}`; } + + try { + // Simple count query to verify database integrity. If the Concepts + // table is missing, db.get rejects and we fall through to the catch. + const row = await new Promise((resolve, reject) => { + db.get('SELECT COUNT(*) as count FROM Concepts', (err, result) => { + if (err) reject(err); + else resolve(result); + }); + }); + return `OK (${row && row.count != null ? row.count : 0} Concepts)`; + } catch (_e) { + return 'Missing Tables - needs re-importing (by java)'; + } finally { + await new Promise((resolve) => db.close(() => resolve())); + } } diff --git a/tx/cs/cs-unii.js b/tx/cs/cs-unii.js index e714be9c..26129810 100644 --- a/tx/cs/cs-unii.js +++ b/tx/cs/cs-unii.js @@ -210,18 +210,18 @@ class UniiServicesFactory extends CodeSystemFactoryProvider { } async load() { - let db = new sqlite3.Database(this.dbPath); - - return new Promise((resolve, reject) => { - db.get('SELECT Version FROM UniiVersion', (err, row) => { - if (err) { - reject(new Error(err)); - } else { - this._version = row ? row.Version : 'unknown'; - resolve(); // This resolves the Promise - } + const db = new sqlite3.Database(this.dbPath, sqlite3.OPEN_READONLY); + try { + const row = await new Promise((resolve, reject) => { + db.get('SELECT Version FROM UniiVersion', (err, result) => { + if (err) reject(new Error(err)); + else resolve(result); + }); }); - }); + this._version = row ? row.Version : 'unknown'; + } finally { + await new Promise((resolve) => db.close(() => resolve())); + } } defaultVersion() { From 48e869812f268354b2e609c2d28933965a2642d3 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Sat, 16 May 2026 14:59:58 +0200 Subject: [PATCH 4/9] don't leak connections --- tx/operation-context.js | 39 +++++++++++++++++++++++++++++++++++++++ tx/provider.js | 1 + tx/tx.js | 12 ++++++++++++ tx/workers/translate.js | 1 + tx/workers/worker.js | 4 ++++ 5 files changed, 57 insertions(+) diff --git a/tx/operation-context.js b/tx/operation-context.js index 5ce14df0..b37f4f14 100644 --- a/tx/operation-context.js +++ b/tx/operation-context.js @@ -448,6 +448,11 @@ class OperationContext { this.resourceCache = resourceCache; this.expansionCache = expansionCache; this.debugging = isDebugging(); + // Providers opened during this operation that need their underlying + // resources (sqlite connections, etc.) released when the operation ends. + // Shared by reference with copy()'d contexts so a sub-operation's + // providers are cleaned up by the parent request's closeProviders(). + this._openProviders = []; this.timeTracker.step('tx-op'); } @@ -476,6 +481,9 @@ class OperationContext { newContext.logEntries = [...this.logEntries]; newContext.debugging = this.debugging; newContext.usageTracker = this.usageTracker; + // Share the same provider-cleanup list so providers opened by the copy + // are released when the parent operation ends. + newContext._openProviders = this._openProviders; return newContext; } @@ -624,6 +632,37 @@ class OperationContext { return this.id; } + /** + * Register a code-system provider whose resources (typically a sqlite + * connection opened by factory.build()) should be released when the + * operation ends. Providers without a close() method are ignored. + * @param {Object} provider - The provider returned from factory.build() + */ + registerProvider(provider) { + if (provider && typeof provider.close === 'function') { + this._openProviders.push(provider); + } + } + + /** + * Close every provider registered during this operation. Safe to call + * multiple times — the list is cleared after the first call. Errors + * from individual close() calls are swallowed so one bad provider can't + * prevent the others from releasing their resources. + */ + async closeProviders() { + if (!this._openProviders || this._openProviders.length === 0) return; + const providers = this._openProviders; + this._openProviders = []; + for (const p of providers) { + try { + await p.close(); + } catch (_e) { + // Swallow — provider cleanup is best-effort. + } + } + } + /** * @type {Languages} languages specified in request */ diff --git a/tx/provider.js b/tx/provider.js index 3aa9fc9f..162cabc5 100644 --- a/tx/provider.js +++ b/tx/provider.js @@ -419,6 +419,7 @@ class Provider { } if (factory != null) { const csp = await factory.build(opContext, []); + opContext.registerProvider(csp); const c = csp ? csp.locate(code) : null; if (c) { if (factory.iteratable()) { diff --git a/tx/tx.js b/tx/tx.js index 2f1964e9..28994f83 100644 --- a/tx/tx.js +++ b/tx/tx.js @@ -306,6 +306,18 @@ class TXModule { req.txI18n = this.i18n; req.txLog = this.log; + // Release any code-system providers that opened sqlite connections + // during this request. closeProviders() is idempotent so it's safe + // for both events to fire. Listeners are sync; the close itself + // runs fire-and-forget on the event loop. + const releaseProviders = () => { + opContext.closeProviders().catch((err) => { + try { this.log.warn(`closeProviders failed: ${err && err.message}`); } catch (_) { /* ignore */ } + }); + }; + res.on('finish', releaseProviders); + res.on('close', releaseProviders); + // Add X-Request-Id header to response res.setHeader('X-Request-Id', requestId); diff --git a/tx/workers/translate.js b/tx/workers/translate.js index f038f5cb..d7547a3c 100644 --- a/tx/workers/translate.js +++ b/tx/workers/translate.js @@ -495,6 +495,7 @@ class TranslateWorker extends TerminologyWorker { let result = false; const factory = cm.jsonObj.internalSource; let prov = await factory.build(this.opContext, []); + this.opContext.registerProvider(prov); output.push({ name: 'used-system', diff --git a/tx/workers/worker.js b/tx/workers/worker.js index a9676c34..5df09aaa 100644 --- a/tx/workers/worker.js +++ b/tx/workers/worker.js @@ -184,6 +184,10 @@ class TerminologyWorker { if (checkVer) { this.checkVersion(url, provider.version(), params, provider.versionAlgorithm(), op); } + // Track for lifecycle cleanup at end of request. Providers built from + // a factory open a fresh sqlite connection that needs releasing; + // resource-built providers and others without close() are no-ops. + this.opContext.registerProvider(provider); } return provider; From b2b4492e7f5a9e762b014d8220554367ac907500 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Thu, 21 May 2026 06:11:24 +0200 Subject: [PATCH 5/9] change regex provider for stability --- library/regex-utilities.js | 27 +++++++++++++++++++-------- tx/problems.js | 4 ---- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/library/regex-utilities.js b/library/regex-utilities.js index 2592cbf5..f8772811 100644 --- a/library/regex-utilities.js +++ b/library/regex-utilities.js @@ -30,21 +30,32 @@ class CompiledRegex { class RegExUtilities { + // re2js doesn't have re2-wasm's WASM-heap leak, so the cache here is + // purely a performance optimisation. We cap it with a simple LRU so a + // workload with many distinct regex patterns can't grow it without bound. + static MAX_CACHE_SIZE = 1000; + constructor() { this._cache = new Map(); } compile(pattern, flags) { - // re2js doesn't have re2-wasm's WASM-heap leak, so the cache here is - // purely a performance optimisation: identical (pattern, flags) pairs - // reuse the same compiled regex instead of paying the compile cost - // every call. const key = pattern + '|' + (flags || ''); - let compiled = this._cache.get(key); - if (!compiled) { - compiled = new CompiledRegex(pattern, flags); - this._cache.set(key, compiled); + const cached = this._cache.get(key); + if (cached) { + // Move to most-recently-used position by re-inserting. + this._cache.delete(key); + this._cache.set(key, cached); + return cached; + } + const compiled = new CompiledRegex(pattern, flags); + if (this._cache.size >= RegExUtilities.MAX_CACHE_SIZE) { + // Evict the oldest entry. Map iteration order is insertion order, so + // the first key is the least-recently-used. + const oldest = this._cache.keys().next().value; + this._cache.delete(oldest); } + this._cache.set(key, compiled); return compiled; } diff --git a/tx/problems.js b/tx/problems.js index 07ce10e4..455cce8e 100644 --- a/tx/problems.js +++ b/tx/problems.js @@ -2,10 +2,6 @@ const escape = require('escape-html'); class ProblemFinder { - constructor() { - this.map = new Map(); - } - async scanValueSets(provider) { let unknownVersions = {}; // system -> Set of versions not known to the server for (let vsp of provider.valueSetProviders) { From c186788bbc2b39a930ecad376077efa7e902af6e Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Thu, 21 May 2026 06:11:47 +0200 Subject: [PATCH 6/9] add test for parsing ucum [pH] --- tests/data/UcumFunctionalTests.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/data/UcumFunctionalTests.xml b/tests/data/UcumFunctionalTests.xml index 76a7fb03..dc4a0e10 100644 --- a/tests/data/UcumFunctionalTests.xml +++ b/tests/data/UcumFunctionalTests.xml @@ -383,6 +383,7 @@ + From e4136ff22ba148cf74aeece14f8dddf31e70fba8 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Thu, 21 May 2026 06:12:09 +0200 Subject: [PATCH 7/9] increase timeout when publishing IGs (for US Core) --- config-template.json | 3 ++- publisher/publisher.js | 7 ++++--- publisher/task-draft.js | 9 +++++---- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/config-template.json b/config-template.json index 7aba822a..802d2632 100644 --- a/config-template.json +++ b/config-template.json @@ -63,7 +63,8 @@ "enabled": true, "database": "/absolute/path/to/publisher.db", "sessionSecret": "change-this-to-a-secure-random-string", - "workspaceRoot": "/absolute/path/to/workspaces" + "workspaceRoot": "/absolute/path/to/workspaces", + "igPublisherTimeoutMinutes": 60 }, "npmprojector": { "enabled": true, diff --git a/publisher/publisher.js b/publisher/publisher.js index 415807c4..a0089a86 100644 --- a/publisher/publisher.js +++ b/publisher/publisher.js @@ -649,13 +649,14 @@ class PublisherModule { reject(error); }); - // Timeout after 30 minutes + // Timeout configurable via publisher.igPublisherTimeoutMinutes (default: 60 minutes) + const timeoutMinutes = this.config.igPublisherTimeoutMinutes || 60; const timeout = setTimeout(async () => { java.kill(); logStream.end(); - await this.logTaskMessage(taskId, 'error', 'IG Publisher timed out after 30 minutes'); + await this.logTaskMessage(taskId, 'error', 'IG Publisher timed out after ' + timeoutMinutes + ' minutes'); reject(new Error('IG Publisher timed out')); - }, 30 * 60 * 1000); + }, timeoutMinutes * 60 * 1000); java.on('close', () => { clearTimeout(timeout); diff --git a/publisher/task-draft.js b/publisher/task-draft.js index 1a717ac0..0d385ae4 100644 --- a/publisher/task-draft.js +++ b/publisher/task-draft.js @@ -433,7 +433,8 @@ class DraftTaskProcessor { reject(error); }); - // Timeout after 30 minutes + // Timeout configurable via publisher.igPublisherTimeoutMinutes (default: 60 minutes) + const timeoutMinutes = this.config.igPublisherTimeoutMinutes || 60; const timeout = setTimeout(async () => { java.kill('SIGTERM'); // Try graceful shutdown first @@ -442,11 +443,11 @@ class DraftTaskProcessor { java.kill('SIGKILL'); }, 10000); - logStream.write('\nTIMEOUT: Process killed after 30 minutes\n'); + logStream.write('\nTIMEOUT: Process killed after ' + timeoutMinutes + ' minutes\n'); logStream.end(); - await this.logTaskMessage(taskId, 'error', 'IG Publisher timed out after 30 minutes'); + await this.logTaskMessage(taskId, 'error', 'IG Publisher timed out after ' + timeoutMinutes + ' minutes'); reject(new Error('IG Publisher timed out')); - }, 30 * 60 * 1000); + }, timeoutMinutes * 60 * 1000); java.on('close', () => { clearTimeout(timeout); From 7c8647e77cef954230e6b523c066dcaaaaa5fd33 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Thu, 21 May 2026 06:34:08 +0200 Subject: [PATCH 8/9] fix regex test --- tests/cs/cs-cs.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/cs/cs-cs.test.js b/tests/cs/cs-cs.test.js index a7140321..e6e98f28 100644 --- a/tests/cs/cs-cs.test.js +++ b/tests/cs/cs-cs.test.js @@ -1520,7 +1520,7 @@ describe('FHIR CodeSystem Provider', () => { test('should handle invalid regex gracefully', async () => { await expect( simpleProvider.filter(filterContext, true, 'code', 'regex', '[invalid') - ).rejects.toThrow('The regex \'[invalid\' is not valid: Invalid regular expression: /^[invalid$/u: missing ]: [invalid$'); + ).rejects.toThrow('The regex \'[invalid\' is not valid: error parsing regexp: missing closing ]: `[invalid$`'); }); }); From 552565e4c9002b05337970fd52c0aafd9f3d8869 Mon Sep 17 00:00:00 2001 From: Grahame Grieve Date: Thu, 21 May 2026 06:50:06 +0200 Subject: [PATCH 9/9] lint fixes --- publisher/task-draft.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/publisher/task-draft.js b/publisher/task-draft.js index 0d385ae4..2cabcc3f 100644 --- a/publisher/task-draft.js +++ b/publisher/task-draft.js @@ -191,6 +191,7 @@ class DraftTaskProcessor { stdio: ['pipe', 'pipe', 'pipe'] }); + // eslint-disable-next-line no-unused-vars let stdout = ''; let stderr = ''; @@ -255,6 +256,7 @@ class DraftTaskProcessor { stdio: ['pipe', 'pipe', 'pipe'] }); + // eslint-disable-next-line no-unused-vars let stdout = ''; let stderr = ''; @@ -316,6 +318,7 @@ class DraftTaskProcessor { }); let stdout = ''; + // eslint-disable-next-line no-unused-vars let stderr = ''; sushi.stdout.on('data', (data) => { @@ -382,6 +385,7 @@ class DraftTaskProcessor { logStream.write('Working Directory: ' + draftDir + '\n'); logStream.write('=====================================\n\n'); + // eslint-disable-next-line no-unused-vars let hasOutput = false; let lastProgressUpdate = Date.now();