From 778e36f6ccd6580376d23775ec9f7dbacf03b028 Mon Sep 17 00:00:00 2001 From: Alan Garny Date: Mon, 5 Oct 2026 23:06:34 +1300 Subject: [PATCH 1/8] New version. --- package.json | 2 +- src/renderer/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c65bf6d99..5b06fe459 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "url": "git+https://github.com/opencor/webapp.git" }, "type": "module", - "version": "1.20261005.1", + "version": "1.20261007.0", "engines": { "bun": ">=1.2.0" }, diff --git a/src/renderer/package.json b/src/renderer/package.json index e867220ff..2c9c3b161 100644 --- a/src/renderer/package.json +++ b/src/renderer/package.json @@ -42,7 +42,7 @@ }, "./style.css": "./dist/opencor.css" }, - "version": "1.20261005.1", + "version": "1.20261007.0", "libopencorVersion": "1.20260803.0", "scripts": { "build": "vite build && bun scripts/generate.version.ts", From 1669486ddc728142f66c1548a545adf7df35d97c Mon Sep 17 00:00:00 2001 From: Alan Garny Date: Tue, 6 Oct 2026 20:22:23 +1300 Subject: [PATCH 2/8] Updated our dependencies. --- bun.lock | 18 +++++++----------- package.json | 2 +- src/renderer/bun.lock | 12 ++++++------ src/renderer/package.json | 4 ++-- 4 files changed, 16 insertions(+), 20 deletions(-) diff --git a/bun.lock b/bun.lock index 3ba2c0b19..e842da079 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,7 @@ "name": "@opencor/opencor", "dependencies": { "@napi-rs/keyring": "^2.1.0", - "@opencor/libopencor-types": "^1.20260803.0", + "@opencor/libopencor-types": "^1.20261006.1", "@primeuix/themes": "2.0.3", "@primevue/auto-import-resolver": "4.2.5", "@vueuse/core": "12.8.2", @@ -126,7 +126,7 @@ "@csstools/selector-resolve-nested": ["@csstools/selector-resolve-nested@4.0.2", "", { "peerDependencies": { "postcss-selector-parser": "^7.1.1" } }, "sha512-JpjNBJFA+4OnAHKRK4AMp8xrYyLhI9XapOeBlP2PhRU/IylSw+1ep3BFNTksBJnrTS3KT6kqKNVfpzLLwZUW5Q=="], - "@csstools/selector-specificity": ["@csstools/selector-specificity@6.0.0", "", { "peerDependencies": { "postcss-selector-parser": "^7.1.1" } }, "sha512-4sSgl78OtOXEX/2d++8A83zHNTgwCJMaR24FvsYL7Uf/VS8HZk9PTwR51elTbGqMuwH3szLvvOXEaVnqn0Z3zA=="], + "@csstools/selector-specificity": ["@csstools/selector-specificity@6.0.1", "", { "peerDependencies": { "postcss-selector-parser": "^7.1.1" } }, "sha512-MBuqFIIvqk4S593MGAskTkPpEUUzZNvGzh8mMxEwl3oHwjQlXmqHEDxNbN3Y0Tsh+xjmem4GZjeg7I0qkwBivg=="], "@electron-internal/extract-zip": ["@electron-internal/extract-zip@1.0.5", "", {}, "sha512-+bqFCP98pLI0Tt0XQo1TmlXtwjWchISndDOxCkEcIuUgXWpBnLyRI+2DU+mesvnMMX6L1XDqYNA0lXNDHd/yiA=="], @@ -258,7 +258,7 @@ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - "@opencor/libopencor-types": ["@opencor/libopencor-types@1.20260803.0", "", {}, "sha512-K5b4dcrB3ZfJHBL7O72c52NeuurGK3ObmhbJziPRPKap1Wz9YjU7TIkYu2JMXaSSGxKxXE4Ck0Z/62CBSICrkQ=="], + "@opencor/libopencor-types": ["@opencor/libopencor-types@1.20261006.1", "", {}, "sha512-yRcQS23PC5n1r886sYyq9S31rF4LO8oHyeRKKVijRNRj/us8lh1Ugoq0n0cRiezzUt4f8MRYMik3by0+WJsM5Q=="], "@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.10.0", "", { "dependencies": { "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-GhokD41lV4gQrrLYm3wCkHfBOnJrnhDMgt4XeMW8gzfE1UdJqIuSwsE+ggf82XBjUXRJylcm+KIGQFa4utIVLw=="], @@ -476,7 +476,7 @@ "abbrev": ["abbrev@4.0.0", "", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="], - "acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="], + "acorn": ["acorn@8.19.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw=="], "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], @@ -646,7 +646,7 @@ "electron-publish": ["electron-publish@26.15.3", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "aws4": "^1.13.2", "builder-util": "26.15.3", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q=="], - "electron-to-chromium": ["electron-to-chromium@1.5.444", "", {}, "sha512-5ss/uJfoDYDHT0lfJzT6FbcskIzROIOPf0BbbFkGcvDzoJU7i//9GDrwwIHQVmIsrAGiF3ihpADBRIsrEFt1rQ=="], + "electron-to-chromium": ["electron-to-chromium@1.5.445", "", {}, "sha512-iX7o1nF+qNb3Bv5mLYgqfgF9Znh9e/8MKXdmbb8Gv2Q47OKIzTwEPD1oda1HrgEwYatTLJj0wQjHvIFt8Db53Q=="], "electron-updater": ["electron-updater@6.8.9", "", { "dependencies": { "builder-util-runtime": "9.7.0", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", "lodash.escaperegexp": "^4.1.2", "lodash.isequal": "^4.5.0", "semver": "~7.7.3", "tiny-typed-emitter": "^2.1.0" } }, "sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig=="], @@ -930,7 +930,7 @@ "muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="], - "nanoid": ["nanoid@3.3.19", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug=="], + "nanoid": ["nanoid@3.3.20", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ=="], "node-abi": ["node-abi@4.36.0", "", { "dependencies": { "semver": "^7.6.3" } }, "sha512-zrvE1Qy5ElbZVqmJFSiNfwzNZzUCepasgp/4dw0Z0fsn40LJkAk9FA9oMh2SKMeNFRRe/3IfeOKvWwWr8u4SmQ=="], @@ -1070,7 +1070,7 @@ "sax": ["sax@1.6.1", "", {}, "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q=="], - "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], @@ -1272,8 +1272,6 @@ "app-builder-lib/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], - "app-builder-lib/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "app-builder-lib/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], "builder-util/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], @@ -1302,8 +1300,6 @@ "electron-updater/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], - "electron-updater/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "electron-vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], "electron-winstaller/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], diff --git a/package.json b/package.json index 5b06fe459..9f4783b00 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ }, "dependencies": { "@napi-rs/keyring": "^2.1.0", - "@opencor/libopencor-types": "^1.20260803.0", + "@opencor/libopencor-types": "^1.20261006.1", "@primeuix/themes": "2.0.3", "@primevue/auto-import-resolver": "4.2.5", "@vueuse/core": "12.8.2", diff --git a/src/renderer/bun.lock b/src/renderer/bun.lock index 4fc3d4854..cdc244f03 100644 --- a/src/renderer/bun.lock +++ b/src/renderer/bun.lock @@ -6,7 +6,7 @@ "name": "@opencor/opencor", "dependencies": { "@napi-rs/keyring": "^2.1.0", - "@opencor/libopencor-types": "^1.20260803.0", + "@opencor/libopencor-types": "^1.20261006.1", "@primeuix/themes": "2.0.3", "@primevue/auto-import-resolver": "4.2.5", "@vueuse/core": "12.8.2", @@ -89,7 +89,7 @@ "@csstools/selector-resolve-nested": ["@csstools/selector-resolve-nested@4.0.2", "", { "peerDependencies": { "postcss-selector-parser": "^7.1.1" } }, "sha512-JpjNBJFA+4OnAHKRK4AMp8xrYyLhI9XapOeBlP2PhRU/IylSw+1ep3BFNTksBJnrTS3KT6kqKNVfpzLLwZUW5Q=="], - "@csstools/selector-specificity": ["@csstools/selector-specificity@6.0.0", "", { "peerDependencies": { "postcss-selector-parser": "^7.1.1" } }, "sha512-4sSgl78OtOXEX/2d++8A83zHNTgwCJMaR24FvsYL7Uf/VS8HZk9PTwR51elTbGqMuwH3szLvvOXEaVnqn0Z3zA=="], + "@csstools/selector-specificity": ["@csstools/selector-specificity@6.0.1", "", { "peerDependencies": { "postcss-selector-parser": "^7.1.1" } }, "sha512-MBuqFIIvqk4S593MGAskTkPpEUUzZNvGzh8mMxEwl3oHwjQlXmqHEDxNbN3Y0Tsh+xjmem4GZjeg7I0qkwBivg=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="], @@ -191,7 +191,7 @@ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - "@opencor/libopencor-types": ["@opencor/libopencor-types@1.20260803.0", "", {}, "sha512-K5b4dcrB3ZfJHBL7O72c52NeuurGK3ObmhbJziPRPKap1Wz9YjU7TIkYu2JMXaSSGxKxXE4Ck0Z/62CBSICrkQ=="], + "@opencor/libopencor-types": ["@opencor/libopencor-types@1.20261006.1", "", {}, "sha512-yRcQS23PC5n1r886sYyq9S31rF4LO8oHyeRKKVijRNRj/us8lh1Ugoq0n0cRiezzUt4f8MRYMik3by0+WJsM5Q=="], "@primeuix/styled": ["@primeuix/styled@0.7.4", "", { "dependencies": { "@primeuix/utils": "^0.6.1" } }, "sha512-QSO/NpOQg8e9BONWRBx9y8VGMCMYz0J/uKfNJEya/RGEu7ARx0oYW0ugI1N3/KB1AAvyGxzKBzGImbwg0KUiOQ=="], @@ -377,7 +377,7 @@ "@vueuse/shared": ["@vueuse/shared@12.8.2", "", { "dependencies": { "vue": "^3.5.13" } }, "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w=="], - "acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="], + "acorn": ["acorn@8.19.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw=="], "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], @@ -439,7 +439,7 @@ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "electron-to-chromium": ["electron-to-chromium@1.5.444", "", {}, "sha512-5ss/uJfoDYDHT0lfJzT6FbcskIzROIOPf0BbbFkGcvDzoJU7i//9GDrwwIHQVmIsrAGiF3ihpADBRIsrEFt1rQ=="], + "electron-to-chromium": ["electron-to-chromium@1.5.445", "", {}, "sha512-iX7o1nF+qNb3Bv5mLYgqfgF9Znh9e/8MKXdmbb8Gv2Q47OKIzTwEPD1oda1HrgEwYatTLJj0wQjHvIFt8Db53Q=="], "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -599,7 +599,7 @@ "muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="], - "nanoid": ["nanoid@3.3.19", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug=="], + "nanoid": ["nanoid@3.3.20", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ=="], "node-releases": ["node-releases@2.0.57", "", {}, "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw=="], diff --git a/src/renderer/package.json b/src/renderer/package.json index 2c9c3b161..bc899acbb 100644 --- a/src/renderer/package.json +++ b/src/renderer/package.json @@ -43,7 +43,7 @@ "./style.css": "./dist/opencor.css" }, "version": "1.20261007.0", - "libopencorVersion": "1.20260803.0", + "libopencorVersion": "1.20261006.1", "scripts": { "build": "vite build && bun scripts/generate.version.ts", "build:lib": "vite build --config vite.lib.config.ts && bun --bun scripts/type.check.ts --project tsconfig.lib.types.json", @@ -68,7 +68,7 @@ }, "dependencies": { "@napi-rs/keyring": "^2.1.0", - "@opencor/libopencor-types": "^1.20260803.0", + "@opencor/libopencor-types": "^1.20261006.1", "@primeuix/themes": "2.0.3", "@primevue/auto-import-resolver": "4.2.5", "@vueuse/core": "12.8.2", From f59fd73cd492701e7181e90e0469646536074bbd Mon Sep 17 00:00:00 2001 From: Alan Garny Date: Mon, 5 Oct 2026 11:51:33 +1300 Subject: [PATCH 3/8] Make sure that our SED-ML instances get released (to prevent memory leaks). --- src/preload/index.ts | 1 + .../SimulationExperimentInteractiveView.vue | 34 ++++++++++++++++ src/renderer/src/libopencor/locApi.ts | 1 + src/renderer/src/libopencor/locSedApi.ts | 39 ++++++++++++++++++- src/renderer/src/libopencor/src/main.cpp | 1 + src/renderer/src/libopencor/src/sed.cpp | 5 +++ src/renderer/src/libopencor/src/sed.h | 1 + 7 files changed, 81 insertions(+), 1 deletion(-) diff --git a/src/preload/index.ts b/src/preload/index.ts index e3fd9009e..3de2b1ce9 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -277,6 +277,7 @@ electron.contextBridge.exposeInMainWorld('locApi', { sedInstancePauseRun: (instanceId: number) => loc.sedInstancePauseRun(instanceId), sedInstanceResumeRun: (instanceId: number) => loc.sedInstanceResumeRun(instanceId), sedInstanceStopRun: (instanceId: number) => loc.sedInstanceStopRun(instanceId), + sedInstanceRelease: (instanceId: number) => loc.sedInstanceRelease(instanceId), // SedInstanceTask API. diff --git a/src/renderer/src/components/views/SimulationExperimentInteractiveView.vue b/src/renderer/src/components/views/SimulationExperimentInteractiveView.vue index 314124ead..3ae7a9ef3 100644 --- a/src/renderer/src/components/views/SimulationExperimentInteractiveView.vue +++ b/src/renderer/src/components/views/SimulationExperimentInteractiveView.vue @@ -1037,9 +1037,20 @@ const externalDataValues = (voi: math.FloatArray, externalDataMapping: IExternal return res; }; +// The instances that are currently being run. + +const runningInstances = new Set(); + // A helper function to reinstantiate our instance. const reinstantiateInstance = (): locApi.SedInstance => { + // Release our previous instance, unless a simulation run is still waiting on it, in which case it will be released by + // that simulation run once it is done with it. + + if (instance && !runningInstances.has(instance)) { + instance.release(); + } + instance = document.instantiate(); instanceTask = instance.task(0); @@ -1169,8 +1180,21 @@ const updateSimulation = async (): Promise => { return; } + runningInstances.add(crtInstance); + await vueCommon.waitWhileRunning(crtInstance).promise; + runningInstances.delete(crtInstance); + + // Release our instance if it has been replaced by a newer one while the simulation was running, in which case our + // results are stale anyway. + + if (crtInstance !== instance) { + crtInstance.release(); + + return; + } + // Check if we have been superseded by a newer call while the simulation was running. if (currentSimulationGeneration !== simulationGeneration) { @@ -1776,6 +1800,16 @@ vue.onBeforeUnmount(() => { if (instance?.status() !== locSedApi.ESedInstanceStatus.IDLE) { instance?.stopRun(); } + + // Release our instance, unless a simulation run is still waiting on it, in which case it will be released by that + // simulation run once it is done with it. + + if (instance && !runningInstances.has(instance)) { + instance.release(); + } + + instance = null; + instanceTask = null; }); // Various things that need to be done once we are mounted. diff --git a/src/renderer/src/libopencor/locApi.ts b/src/renderer/src/libopencor/locApi.ts index 69b163547..675705570 100644 --- a/src/renderer/src/libopencor/locApi.ts +++ b/src/renderer/src/libopencor/locApi.ts @@ -70,6 +70,7 @@ export interface ICppLocApi { sedInstancePauseRun: (instanceId: number) => void; sedInstanceResumeRun: (instanceId: number) => void; sedInstanceStopRun: (instanceId: number) => void; + sedInstanceRelease: (instanceId: number) => void; // SedInstanceTask API. diff --git a/src/renderer/src/libopencor/locSedApi.ts b/src/renderer/src/libopencor/locSedApi.ts index 19420ae4f..1e5317bb4 100644 --- a/src/renderer/src/libopencor/locSedApi.ts +++ b/src/renderer/src/libopencor/locSedApi.ts @@ -396,6 +396,8 @@ export enum ESedInstanceStatus { export class SedInstance { private _cppInstanceId: number = -1; private _wasmSedInstance: IWasmSedInstance = {} as IWasmSedInstance; + private _tasks: SedInstanceTask[] = []; + private _released = false; constructor(cppDocumentId: number, wasmSedDocument: IWasmSedDocument) { if (cppVersion()) { @@ -416,7 +418,11 @@ export class SedInstance { } task(index: number): SedInstanceTask { - return new SedInstanceTask(this._cppInstanceId, index, this._wasmSedInstance); + const task = new SedInstanceTask(this._cppInstanceId, index, this._wasmSedInstance); + + this._tasks.push(task); + + return task; } status(): ESedInstanceStatus { @@ -458,6 +464,31 @@ export class SedInstance { this._wasmSedInstance.stopRun(); } } + + // Release the resources held by the instance (and its tasks). + // Note: this must only be done once the instance is idle and no longer used. Not releasing an instance means that it + // (and its tasks) never gets freed, which with the WASM version of libOpenCOR eventually results in an + // out-of-memory error. + + release(): void { + if (this._released) { + return; + } + + this._released = true; + + for (const task of this._tasks) { + task.release(); + } + + this._tasks = []; + + if (cppVersion()) { + _cppLocApi.sedInstanceRelease(this._cppInstanceId); + } else { + this._wasmSedInstance.delete(); + } + } } export class SedInstanceTask extends SedIndex { @@ -474,6 +505,12 @@ export class SedInstanceTask extends SedIndex { } } + release(): void { + if (wasmVersion()) { + this._wasmSedInstanceTask.delete(); + } + } + voiName(): string { return cppVersion() ? _cppLocApi.sedInstanceTaskVoiName(this._cppInstanceId, this._index) diff --git a/src/renderer/src/libopencor/src/main.cpp b/src/renderer/src/libopencor/src/main.cpp index 8731e975e..93b0bf576 100644 --- a/src/renderer/src/libopencor/src/main.cpp +++ b/src/renderer/src/libopencor/src/main.cpp @@ -70,6 +70,7 @@ Napi::Object init(Napi::Env pEnv, Napi::Object pExports) pExports.Set(Napi::String::New(pEnv, "sedInstancePauseRun"), Napi::Function::New(pEnv, sedInstancePauseRun)); pExports.Set(Napi::String::New(pEnv, "sedInstanceResumeRun"), Napi::Function::New(pEnv, sedInstanceResumeRun)); pExports.Set(Napi::String::New(pEnv, "sedInstanceStopRun"), Napi::Function::New(pEnv, sedInstanceStopRun)); + pExports.Set(Napi::String::New(pEnv, "sedInstanceRelease"), Napi::Function::New(pEnv, sedInstanceRelease)); // SedInstanceTask API. diff --git a/src/renderer/src/libopencor/src/sed.cpp b/src/renderer/src/libopencor/src/sed.cpp index 93939e8e3..868c6a4ae 100644 --- a/src/renderer/src/libopencor/src/sed.cpp +++ b/src/renderer/src/libopencor/src/sed.cpp @@ -273,6 +273,11 @@ void sedInstanceStopRun(const Napi::CallbackInfo &pInfo) sedInstance->stopRun(); } +void sedInstanceRelease(const Napi::CallbackInfo &pInfo) +{ + sedInstances.erase(toSizeT(pInfo[0])); +} + // SedInstanceTask API. napi_value sedInstanceTaskVoiName(const Napi::CallbackInfo &pInfo) diff --git a/src/renderer/src/libopencor/src/sed.h b/src/renderer/src/libopencor/src/sed.h index 7e3120dcf..bd3a57883 100644 --- a/src/renderer/src/libopencor/src/sed.h +++ b/src/renderer/src/libopencor/src/sed.h @@ -50,6 +50,7 @@ napi_value sedInstanceWaitForRun(const Napi::CallbackInfo &pInfo); void sedInstancePauseRun(const Napi::CallbackInfo &pInfo); void sedInstanceResumeRun(const Napi::CallbackInfo &pInfo); void sedInstanceStopRun(const Napi::CallbackInfo &pInfo); +void sedInstanceRelease(const Napi::CallbackInfo &pInfo); // SedInstanceTask API. From e5a402c2f90e1ee9a4d76a517071acff380fb739 Mon Sep 17 00:00:00 2001 From: Alan Garny Date: Mon, 5 Oct 2026 13:13:16 +1300 Subject: [PATCH 4/8] Fixed various memory leaks. --- src/preload/index.ts | 1 + .../src/components/ContentsComponent.vue | 21 ++- src/renderer/src/components/OpenCOR.vue | 16 ++- .../SimulationExperimentInteractiveView.vue | 17 ++- .../SimulationExperimentStandardView.vue | 44 +++++- src/renderer/src/libopencor/locApi.ts | 1 + src/renderer/src/libopencor/locFileApi.ts | 80 +++++++++-- src/renderer/src/libopencor/locLoggerApi.ts | 8 ++ src/renderer/src/libopencor/locSedApi.ts | 126 +++++++++++++++++- src/renderer/src/libopencor/src/common.cpp | 11 +- src/renderer/src/libopencor/src/main.cpp | 1 + src/renderer/src/libopencor/src/sed.cpp | 5 + src/renderer/src/libopencor/src/sed.h | 1 + 13 files changed, 306 insertions(+), 26 deletions(-) diff --git a/src/preload/index.ts b/src/preload/index.ts index 3de2b1ce9..a0e574ff4 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -227,6 +227,7 @@ electron.contextBridge.exposeInMainWorld('locApi', { sedDocumentCreate: (path: string) => loc.sedDocumentCreate(path), sedDocumentInstantiate: (documentId: number) => loc.sedDocumentInstantiate(documentId), + sedDocumentRelease: (documentId: number) => loc.sedDocumentRelease(documentId), sedDocumentIssues: (documentId: number) => loc.sedDocumentIssues(documentId), sedDocumentModelCount: (documentId: number) => loc.sedDocumentModelCount(documentId), sedDocumentSimulationCount: (documentId: number) => loc.sedDocumentSimulationCount(documentId), diff --git a/src/renderer/src/components/ContentsComponent.vue b/src/renderer/src/components/ContentsComponent.vue index 32c7b403f..ff4da7e18 100644 --- a/src/renderer/src/components/ContentsComponent.vue +++ b/src/renderer/src/components/ContentsComponent.vue @@ -271,8 +271,17 @@ const closeFile = async (filePath: string): Promise => { locApi.fileManager.unmanage(filePath); const fileTabIndex = fileTabs.value.findIndex((fileTab) => fileTab.file.path() === filePath); + const fileTab = fileTabs.value[fileTabIndex]; - fileTabs.value.splice(fileTabIndex, 1); + if (fileTab) { + fileTabs.value.splice(fileTabIndex, 1); + + // Release the file once its views have been unmounted (and have therefore released their own resources). + + vue.nextTick(() => { + fileTab.file.release(); + }); + } if (activeFile.value === filePath && fileTabs.value.length) { const nextFileTab = fileTabs.value[Math.min(fileTabIndex, fileTabs.value.length - 1)]; @@ -550,6 +559,16 @@ defineExpose({ simulationData }); +// Release the files that are still open once we (and therefore their views) have been unmounted. + +vue.onUnmounted(() => { + for (const fileTab of fileTabs.value) { + locApi.fileManager.unmanage(fileTab.file.path()); + + fileTab.file.release(); + } +}); + // Some watchers to let people know about changes to the opened files and the selected file. vue.watch(filePaths, (newFilePaths: string[]) => { diff --git a/src/renderer/src/components/OpenCOR.vue b/src/renderer/src/components/OpenCOR.vue index 1ea8ee070..a20854a29 100644 --- a/src/renderer/src/components/OpenCOR.vue +++ b/src/renderer/src/components/OpenCOR.vue @@ -782,6 +782,8 @@ const processFile = async (fileFilePathOrFileContents: string | Uint8Array | Fil emit('file', { type: 'issue', filePath, issues: [issueMessage] }); + file.release(); + return null; } @@ -821,6 +823,16 @@ const processFile = async (fileFilePathOrFileContents: string | Uint8Array | Fil } }; +const openFileInContents = async (file: locApi.File, wait: boolean = false): Promise => { + // Open the given file in our contents component or release it if we can't (e.g., we got unmounted in the meantime). + + if (contentsRef.value) { + await contentsRef.value.openFile(file, wait); + } else { + file.release(); + } +}; + const openFile = (fileFilePathOrFileContents: string | Uint8Array | File): void => { processFile(fileFilePathOrFileContents).then(async (fileInfo) => { if (!fileInfo) { @@ -834,7 +846,7 @@ const openFile = (fileFilePathOrFileContents: string | Uint8Array | File): void } if (fileInfo.file) { - await contentsRef.value?.openFile(fileInfo.file); + await openFileInContents(fileInfo.file); } }); }; @@ -862,7 +874,7 @@ const openFiles = (filesFilePathsOrFileContents: (string | Uint8Array | File)[]) } if (currentFileInfo.file) { - await contentsRef.value?.openFile(currentFileInfo.file, true); + await openFileInContents(currentFileInfo.file, true); } }, Promise.resolve()); }; diff --git a/src/renderer/src/components/views/SimulationExperimentInteractiveView.vue b/src/renderer/src/components/views/SimulationExperimentInteractiveView.vue index 3ae7a9ef3..649ed1d15 100644 --- a/src/renderer/src/components/views/SimulationExperimentInteractiveView.vue +++ b/src/renderer/src/components/views/SimulationExperimentInteractiveView.vue @@ -809,7 +809,15 @@ const simulationData = (modelParameters: string[]): Promise { ); jsZip.file('model.cellml', modelFile.contents()); jsZip.file('document.sedml', document.serialise().replace(modelFile.path(), 'model.cellml')); + + modelFile.release(); + jsZip.file('simulation.json', JSON.stringify(actualUiJson.value, locApi.uiJsonReplacer, 2)); jsZip @@ -1810,6 +1821,10 @@ vue.onBeforeUnmount(() => { instance = null; instanceTask = null; + + // Release our document (and its model and simulation). + + document.release(); }); // Various things that need to be done once we are mounted. diff --git a/src/renderer/src/components/views/SimulationExperimentStandardView.vue b/src/renderer/src/components/views/SimulationExperimentStandardView.vue index 94e495c91..384dd6f26 100644 --- a/src/renderer/src/components/views/SimulationExperimentStandardView.vue +++ b/src/renderer/src/components/views/SimulationExperimentStandardView.vue @@ -205,6 +205,26 @@ const updatePlot = (dataSize: number = 0): void => { }; }; +// Release our instance (once it is idle) and our document. +// Note: an instance must not be released while it is running, hence we stop it and wait for it to be idle first. + +let isUnmounted = false; +let isWaitingOnRun = false; + +const releaseResources = async (): Promise => { + if (instance) { + if (instance.status() !== locSedApi.ESedInstanceStatus.IDLE) { + instance.stopRun(); + + await vueCommon.waitWhileRunning(instance).promise; + } + + instance.release(); + } + + document.release(); +}; + // Event handlers. const onRunPause = async (): Promise => { @@ -279,10 +299,22 @@ const onRunPause = async (): Promise => { progressResetCancel = runCancel; + isWaitingOnRun = true; + await runPromise; + isWaitingOnRun = false; + progressResetCancel = undefined; + // Release our resources if we got unmounted while the simulation was running (see onUnmounted() below). + + if (isUnmounted) { + await releaseResources(); + + return; + } + // Update the console with any issues that occurred during the simulation, or display the simulation time if it // completed successfully. @@ -350,12 +382,22 @@ vue.onMounted(() => { updatePlot(); }); -// Cancel any pending progress reset timers to avoid writing to stale refs after the component is torn down. +// Cancel any pending progress reset timers to avoid writing to stale refs after the component is torn down, and release +// our resources. vue.onUnmounted(() => { + isUnmounted = true; + progressResetCancel?.(); clearTimeout(abortProgressTimer); + + // Release our resources, unless a simulation run is still waiting on our instance, in which case they will be released + // by that simulation run once it is done with our instance. + + if (!isWaitingOnRun) { + releaseResources(); + } }); // Track whether the view is the currently active view for keyboard shortcut handling. diff --git a/src/renderer/src/libopencor/locApi.ts b/src/renderer/src/libopencor/locApi.ts index 675705570..36f838fc2 100644 --- a/src/renderer/src/libopencor/locApi.ts +++ b/src/renderer/src/libopencor/locApi.ts @@ -20,6 +20,7 @@ export interface ICppLocApi { sedDocumentCreate: (path: string) => number; sedDocumentInstantiate: (documentId: number) => number; + sedDocumentRelease: (documentId: number) => void; sedDocumentIssues: (documentId: number) => IIssue[]; sedDocumentModelCount: (documentId: number) => number; sedDocumentSimulationCount: (documentId: number) => number; diff --git a/src/renderer/src/libopencor/locFileApi.ts b/src/renderer/src/libopencor/locFileApi.ts index 4cec3b221..9a260d7bd 100644 --- a/src/renderer/src/libopencor/locFileApi.ts +++ b/src/renderer/src/libopencor/locFileApi.ts @@ -36,30 +36,34 @@ class FileManager { } file(path: string): File | null { - if (cppVersion()) { - const contents = _cppLocApi.fileContents(path); - - if (contents) { - return new File(path, contents); - } + // Note: the file we are after is already managed, so we just wrap it rather than create (and therefore own) it. - return null; + if (cppVersion()) { + return _cppLocApi.fileContents(path) ? new File(path, undefined, {}) : null; } + // Note: every access to a WASM vector (and to its elements) yields a new handle that must be deleted, except for + // the handle of the file we are after, which our File object takes ownership of. + const fileManager = this.fileManager(); const files = fileManager.files; + let res: File | null = null; for (const file of files) { if (!file) { continue; } - if (file.path === path) { - return new File(path, file.contents()); + if (!res && file.path === path) { + res = new File(path, undefined, { wasmFile: file }); + } else { + file.delete(); } } - return null; + files.delete(); + + return res; } unmanage(path: string): void { @@ -76,10 +80,12 @@ class FileManager { if (file.path === path) { fileManager.unmanage(file); - - break; } + + file.delete(); } + + files.delete(); } } } @@ -96,20 +102,43 @@ export enum EFileType { IRRETRIEVABLE_FILE } +// Information about a file that is already managed by libOpenCOR, in which case a File object only wraps it, i.e. it +// doesn't create (and therefore doesn't own) it. +// Note: with the WASM version of libOpenCOR, the File object takes ownership of the given WASM file handle. + +export interface IManagedFile { + wasmFile?: IWasmFile; +} + export class File { _path: string; _wasmFile: IWasmFile = {} as IWasmFile; _issues: IIssue[] = []; + private _isOwner = true; + private _hasWasmFile = false; + private _released = false; - constructor(path: string, contents: Uint8Array | undefined = undefined) { + constructor(path: string, contents: Uint8Array | undefined = undefined, managedFile?: IManagedFile) { this._path = path; - if (cppVersion()) { + if (managedFile) { + this._isOwner = false; + + if (cppVersion()) { + this._issues = _cppLocApi.fileIssues(path); + } else if (managedFile.wasmFile) { + this._wasmFile = vue.markRaw(managedFile.wasmFile); + this._hasWasmFile = true; + + this._issues = wasmIssuesToIssues(this._wasmFile.issues); + } + } else if (cppVersion()) { _cppLocApi.fileCreate(path, contents); this._issues = _cppLocApi.fileIssues(path); } else if (contents) { this._wasmFile = vue.markRaw(new _wasmLocApi.File(path)); + this._hasWasmFile = true; this._wasmFile.setContents(contents); @@ -124,6 +153,27 @@ export class File { } } + // Release the resources held by the file. + // Note: this must only be done once the file is no longer used. With the WASM version of libOpenCOR, our WASM file + // handle is what keeps the file alive (libOpenCOR's file manager only keeps a weak reference to it) while, with + // the C++ version of libOpenCOR, it is our list of tracked files, but only if we created (i.e. own) the file. + + release(): void { + if (this._released) { + return; + } + + this._released = true; + + if (cppVersion()) { + if (this._isOwner) { + _cppLocApi.fileManagerUnmanage(this._path); + } + } else if (this._hasWasmFile) { + this._wasmFile.delete(); + } + } + type(): EFileType { return cppVersion() ? _cppLocApi.fileType(this._path) : this._wasmFile.type.value; } @@ -161,6 +211,8 @@ export class File { } uiJsonContents = uiJson.contents(); + + uiJson.delete(); } const decoder = new TextDecoder(); diff --git a/src/renderer/src/libopencor/locLoggerApi.ts b/src/renderer/src/libopencor/locLoggerApi.ts index 274eadb88..be793106f 100644 --- a/src/renderer/src/libopencor/locLoggerApi.ts +++ b/src/renderer/src/libopencor/locLoggerApi.ts @@ -13,6 +13,10 @@ export interface IIssue { description: string; } +// Convert some WASM issues to our issues. +// Note: we take ownership of the given WASM issues, i.e. we release them (and each of the WASM issues they contain) once +// converted since every access to a WASM vector (and to its elements) yields a new handle that must be deleted. + export const wasmIssuesToIssues = (wasmIssues: IWasmIssues): IIssue[] => { const res: IIssue[] = []; @@ -25,7 +29,11 @@ export const wasmIssuesToIssues = (wasmIssues: IWasmIssues): IIssue[] => { type: wasmIssue.type.value, description: wasmIssue.description }); + + wasmIssue.delete(); } + wasmIssues.delete(); + return res; }; diff --git a/src/renderer/src/libopencor/locSedApi.ts b/src/renderer/src/libopencor/locSedApi.ts index 1e5317bb4..894fd9a5a 100644 --- a/src/renderer/src/libopencor/locSedApi.ts +++ b/src/renderer/src/libopencor/locSedApi.ts @@ -37,6 +37,9 @@ export class SedDocument { private _cppDocumentId: number = -1; private _wasmSedDocument: IWasmSedDocument = {} as IWasmSedDocument; private _issues: IIssue[] = []; + private _models = new Map(); + private _simulations = new Map(); + private _released = false; constructor(filePath: string, wasmFile: IWasmFile, fileIssues: IIssue[]) { // If the file has some errors (e.g., an invalid CellML file), then report its issues rather than those of the @@ -136,6 +139,36 @@ export class SedDocument { } } + release(): void { + // Release the resources held by the document (and its models and simulations). + // Note: this must only be done once the document is no longer used. Instances created from the document don't need + // it to be around since they hold their own references to whatever they need from it. + + if (this._released) { + return; + } + + this._released = true; + + for (const model of this._models.values()) { + model.release(); + } + + this._models.clear(); + + for (const simulation of this._simulations.values()) { + simulation.release(); + } + + this._simulations.clear(); + + if (cppVersion()) { + _cppLocApi.sedDocumentRelease(this._cppDocumentId); + } else { + this._wasmSedDocument.delete(); + } + } + issues(): IIssue[] { return this._issues; } @@ -145,7 +178,17 @@ export class SedDocument { } model(index: number): SedModel { - return new SedModel(this._cppDocumentId, this._wasmSedDocument, index); + // Note: we cache our models so that we can release them when releasing the document. + + let res = this._models.get(index); + + if (!res) { + res = new SedModel(this._cppDocumentId, this._wasmSedDocument, index); + + this._models.set(index, res); + } + + return res; } simulationCount(): number { @@ -155,12 +198,28 @@ export class SedDocument { } simulation(index: number): SedSimulation { + // Note: we cache our simulations so that we can release them when releasing the document. + + let res = this._simulations.get(index); + + if (!res) { + res = this.createSimulation(index); + + this._simulations.set(index, res); + } + + return res; + } + + private createSimulation(index: number): SedSimulation { let type: ESedSimulationType; if (cppVersion()) { type = _cppLocApi.sedDocumentSimulationType(this._cppDocumentId, index); } else { - switch (this._wasmSedDocument.simulation(index)?.constructor.name) { + const wasmSedSimulation = this._wasmSedDocument.simulation(index); + + switch (wasmSedSimulation?.constructor.name) { case 'SedAnalysis': type = ESedSimulationType.ANALYSIS; @@ -176,6 +235,8 @@ export class SedDocument { default: // 'SedUniformTimeCourse'. type = ESedSimulationType.UNIFORM_TIME_COURSE; } + + wasmSedSimulation?.delete(); } if (type === ESedSimulationType.ANALYSIS) { @@ -216,19 +277,43 @@ export class SedModel extends SedIndex { } } + release(): void { + if (wasmVersion()) { + this._wasmSedModel?.delete(); + } + } + file(): File | null { + // Note: the caller is responsible for releasing the returned file. + if (cppVersion()) { return fileManager.file(_cppLocApi.sedModelFilePath(this._cppDocumentId, this._index)); } - return this._wasmSedModel.file?.path != null ? fileManager.file(this._wasmSedModel.file.path) : null; + const wasmFile = this._wasmSedModel.file; + + if (!wasmFile) { + return null; + } + + const wasmFilePath = wasmFile.path; + + wasmFile.delete(); + + return fileManager.file(wasmFilePath); } addChange(componentName: string, variableName: string, newValue: string): void { if (cppVersion()) { _cppLocApi.sedModelAddChange(this._cppDocumentId, this._index, componentName, variableName, newValue); } else { - this._wasmSedModel.addChange(new _wasmLocApi.SedChangeAttribute(componentName, variableName, newValue)); + // Note: the model holds its own reference to the change, so we can (and must) delete our handle to it. + + const wasmSedChangeAttribute = new _wasmLocApi.SedChangeAttribute(componentName, variableName, newValue); + + this._wasmSedModel.addChange(wasmSedChangeAttribute); + + wasmSedChangeAttribute.delete(); } } @@ -259,6 +344,10 @@ export class SedSimulation extends SedIndex { this._type = type; } + release(): void { + // Nothing to release by default. + } + type(): ESedSimulationType { return this._type; } @@ -279,6 +368,12 @@ export class SedOneStep extends SedSimulation { } } + override release(): void { + if (wasmVersion()) { + this._wasmSedOneStep?.delete(); + } + } + step(): number { return cppVersion() ? _cppLocApi.sedOneStepStep(this._cppDocumentId, this._index) : this._wasmSedOneStep.step; } @@ -286,6 +381,7 @@ export class SedOneStep extends SedSimulation { export class SedUniformTimeCourse extends SedSimulation { private _wasmSedUniformTimeCourse: IWasmSedUniformTimeCourse = {} as IWasmSedUniformTimeCourse; + private _cvode: SolverCvode | null = null; constructor(cppDocumentId: number, wasmSedDocument: IWasmSedDocument, index: number, type: ESedSimulationType) { super(cppDocumentId, wasmSedDocument, index, type); @@ -295,6 +391,16 @@ export class SedUniformTimeCourse extends SedSimulation { } } + override release(): void { + this._cvode?.release(); + + this._cvode = null; + + if (wasmVersion()) { + this._wasmSedUniformTimeCourse?.delete(); + } + } + initialTime(): number { return cppVersion() ? _cppLocApi.sedUniformTimeCourseInitialTime(this._cppDocumentId, this._index) @@ -352,7 +458,11 @@ export class SedUniformTimeCourse extends SedSimulation { } cvode(): SolverCvode { - return new SolverCvode(this._cppDocumentId, this._wasmSedUniformTimeCourse, this._index); + // Note: we cache our solver so that we can release it when releasing the simulation. + + this._cvode ??= new SolverCvode(this._cppDocumentId, this._wasmSedUniformTimeCourse, this._index); + + return this._cvode; } } @@ -370,6 +480,12 @@ export class SolverCvode extends SedIndex { } } + release(): void { + if (wasmVersion()) { + this._wasmSolverCvode?.delete(); + } + } + // TODO: this is only temporary until we have full support for our different solvers. maximumStep(): number { diff --git a/src/renderer/src/libopencor/src/common.cpp b/src/renderer/src/libopencor/src/common.cpp index c93ebe475..ec28702f5 100644 --- a/src/renderer/src/libopencor/src/common.cpp +++ b/src/renderer/src/libopencor/src/common.cpp @@ -10,14 +10,21 @@ libOpenCOR::FilePtr toFile(const Napi::Value &pValue) return fileManager.file(pValue.ToString().Utf8Value()); } +// Note: we use find() rather than operator[] since the latter would (re)insert an entry for an unknown (e.g., released) +// ID. + libOpenCOR::SedDocumentPtr toSedDocument(size_t pId) { - return sedDocuments[pId]; + auto iter = sedDocuments.find(pId); + + return (iter != sedDocuments.end()) ? iter->second : nullptr; } libOpenCOR::SedInstancePtr toSedInstance(size_t pId) { - return sedInstances[pId]; + auto iter = sedInstances.find(pId); + + return (iter != sedInstances.end()) ? iter->second : nullptr; } size_t toSizeT(const Napi::Value &pValue) diff --git a/src/renderer/src/libopencor/src/main.cpp b/src/renderer/src/libopencor/src/main.cpp index 93b0bf576..b594710e8 100644 --- a/src/renderer/src/libopencor/src/main.cpp +++ b/src/renderer/src/libopencor/src/main.cpp @@ -26,6 +26,7 @@ Napi::Object init(Napi::Env pEnv, Napi::Object pExports) pExports.Set(Napi::String::New(pEnv, "sedDocumentCreate"), Napi::Function::New(pEnv, sedDocumentCreate)); pExports.Set(Napi::String::New(pEnv, "sedDocumentInstantiate"), Napi::Function::New(pEnv, sedDocumentInstantiate)); + pExports.Set(Napi::String::New(pEnv, "sedDocumentRelease"), Napi::Function::New(pEnv, sedDocumentRelease)); pExports.Set(Napi::String::New(pEnv, "sedDocumentIssues"), Napi::Function::New(pEnv, sedDocumentIssues)); pExports.Set(Napi::String::New(pEnv, "sedDocumentModelCount"), Napi::Function::New(pEnv, sedDocumentModelCount)); pExports.Set(Napi::String::New(pEnv, "sedDocumentSimulationCount"), Napi::Function::New(pEnv, sedDocumentSimulationCount)); diff --git a/src/renderer/src/libopencor/src/sed.cpp b/src/renderer/src/libopencor/src/sed.cpp index 868c6a4ae..cce8bb70e 100644 --- a/src/renderer/src/libopencor/src/sed.cpp +++ b/src/renderer/src/libopencor/src/sed.cpp @@ -31,6 +31,11 @@ napi_value sedDocumentInstantiate(const Napi::CallbackInfo &pInfo) return Napi::Number::New(pInfo.Env(), static_cast(id)); } +void sedDocumentRelease(const Napi::CallbackInfo &pInfo) +{ + sedDocuments.erase(toSizeT(pInfo[0])); +} + napi_value sedDocumentIssues(const Napi::CallbackInfo &pInfo) { return issues(pInfo, toSedDocument(toSizeT(pInfo[0]))->issues()); diff --git a/src/renderer/src/libopencor/src/sed.h b/src/renderer/src/libopencor/src/sed.h index bd3a57883..82e3e9dda 100644 --- a/src/renderer/src/libopencor/src/sed.h +++ b/src/renderer/src/libopencor/src/sed.h @@ -6,6 +6,7 @@ napi_value sedDocumentCreate(const Napi::CallbackInfo &pInfo); napi_value sedDocumentInstantiate(const Napi::CallbackInfo &pInfo); +void sedDocumentRelease(const Napi::CallbackInfo &pInfo); napi_value sedDocumentIssues(const Napi::CallbackInfo &pInfo); napi_value sedDocumentModelCount(const Napi::CallbackInfo &pInfo); napi_value sedDocumentSimulationCount(const Napi::CallbackInfo &pInfo); From dd2f2cc39205a1530dda239283f312b5953480cb Mon Sep 17 00:00:00 2001 From: Alan Garny Date: Tue, 6 Oct 2026 14:26:52 +1300 Subject: [PATCH 5/8] Guard the WASM's SedDocument cleanup. --- src/renderer/src/libopencor/locSedApi.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/libopencor/locSedApi.ts b/src/renderer/src/libopencor/locSedApi.ts index 894fd9a5a..ed324c157 100644 --- a/src/renderer/src/libopencor/locSedApi.ts +++ b/src/renderer/src/libopencor/locSedApi.ts @@ -39,6 +39,7 @@ export class SedDocument { private _issues: IIssue[] = []; private _models = new Map(); private _simulations = new Map(); + private _hasWasmSedDocument = false; private _released = false; constructor(filePath: string, wasmFile: IWasmFile, fileIssues: IIssue[]) { @@ -57,6 +58,7 @@ export class SedDocument { this._cppDocumentId = _cppLocApi.sedDocumentCreate(filePath); } else { this._wasmSedDocument = new _wasmLocApi.SedDocument(wasmFile); + this._hasWasmSedDocument = true; } // Retrieve the issues. @@ -164,7 +166,7 @@ export class SedDocument { if (cppVersion()) { _cppLocApi.sedDocumentRelease(this._cppDocumentId); - } else { + } else if (this._hasWasmSedDocument) { this._wasmSedDocument.delete(); } } From 2c50daaf330faca495758600f85e7c9a4f0607d3 Mon Sep 17 00:00:00 2001 From: Alan Garny Date: Tue, 6 Oct 2026 14:28:29 +1300 Subject: [PATCH 6/8] Ensure that a model file is always released. --- .../views/SimulationExperimentInteractiveView.vue | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/renderer/src/components/views/SimulationExperimentInteractiveView.vue b/src/renderer/src/components/views/SimulationExperimentInteractiveView.vue index 649ed1d15..6f24856df 100644 --- a/src/renderer/src/components/views/SimulationExperimentInteractiveView.vue +++ b/src/renderer/src/components/views/SimulationExperimentInteractiveView.vue @@ -1700,10 +1700,16 @@ const onDownloadCombineArchive = (): void => { ` ); - jsZip.file('model.cellml', modelFile.contents()); - jsZip.file('document.sedml', document.serialise().replace(modelFile.path(), 'model.cellml')); - modelFile.release(); + try { + // Note: we release our model file even if something goes wrong while retrieving its contents or serialising our + // document. + + jsZip.file('model.cellml', modelFile.contents()); + jsZip.file('document.sedml', document.serialise().replace(modelFile.path(), 'model.cellml')); + } finally { + modelFile.release(); + } jsZip.file('simulation.json', JSON.stringify(actualUiJson.value, locApi.uiJsonReplacer, 2)); From ada8548dd5038df99a799a23b97183c5f1b3a76b Mon Sep 17 00:00:00 2001 From: Alan Garny Date: Tue, 6 Oct 2026 14:30:19 +1300 Subject: [PATCH 7/8] Stop a stale run when re-instantiating an instance. --- .../SimulationExperimentInteractiveView.vue | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/renderer/src/components/views/SimulationExperimentInteractiveView.vue b/src/renderer/src/components/views/SimulationExperimentInteractiveView.vue index 6f24856df..b6fb42ac6 100644 --- a/src/renderer/src/components/views/SimulationExperimentInteractiveView.vue +++ b/src/renderer/src/components/views/SimulationExperimentInteractiveView.vue @@ -1052,11 +1052,18 @@ const runningInstances = new Set(); // A helper function to reinstantiate our instance. const reinstantiateInstance = (): locApi.SedInstance => { - // Release our previous instance, unless a simulation run is still waiting on it, in which case it will be released by - // that simulation run once it is done with it. - - if (instance && !runningInstances.has(instance)) { - instance.release(); + // Release our previous instance, unless a simulation run is still waiting on it, in which case we stop that + // simulation run (since its results are going to be stale anyway) and let it release our previous instance once it is + // done with it. + + if (instance) { + if (runningInstances.has(instance)) { + if (instance.status() !== locSedApi.ESedInstanceStatus.IDLE) { + instance.stopRun(); + } + } else { + instance.release(); + } } instance = document.instantiate(); From d1409d17a32c0413bfb0e54fbd7acc49993b8412 Mon Sep 17 00:00:00 2001 From: Alan Garny Date: Tue, 6 Oct 2026 14:36:42 +1300 Subject: [PATCH 8/8] Striped woff2 PrimeIcons assets from Vite bundles. --- electron.vite.config.ts | 2 +- src/renderer/vite.config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 65729718f..2d3305b17 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -42,7 +42,7 @@ export default electronVite.defineConfig({ name: 'strip-unneeded-primeicons-files', generateBundle(_options, bundle) { for (const fileName of Object.keys(bundle)) { - if (fileName.includes('assets/primeicons') && /\.(eot|svg|ttf|woff)$/.test(fileName)) { + if (fileName.includes('assets/primeicons') && /\.(eot|svg|ttf|woff2?)$/.test(fileName)) { delete bundle[fileName]; } } diff --git a/src/renderer/vite.config.ts b/src/renderer/vite.config.ts index f46854ca8..65442b4d0 100644 --- a/src/renderer/vite.config.ts +++ b/src/renderer/vite.config.ts @@ -44,7 +44,7 @@ export default vite.defineConfig({ name: 'strip-unneeded-primeicons-files', generateBundle(_options, bundle) { for (const fileName of Object.keys(bundle)) { - if (fileName.includes('assets/primeicons') && /\.(eot|svg|ttf|woff)$/.test(fileName)) { + if (fileName.includes('assets/primeicons') && /\.(eot|svg|ttf|woff2?)$/.test(fileName)) { delete bundle[fileName]; } }